New upstream version 3.17.0
This commit is contained in:
commit
36bc1e1299
|
@ -0,0 +1,13 @@
|
|||
# EditorConfig file: http://EditorConfig.org
|
||||
|
||||
# Top-most EditorConfig file.
|
||||
root = true
|
||||
|
||||
# Unix-style, newlines, indent style of 4 spaces, with a newline ending every file.
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
|
@ -0,0 +1,322 @@
|
|||
# Contributing to Rebar3
|
||||
|
||||
1. [License](#license)
|
||||
2. [Submitting a bug](#submitting-a-bug)
|
||||
3. [Requesting or implementing a feature](#requesting-or-implementing-a-feature)
|
||||
4. [Project Structure](#project-structure)
|
||||
5. [Tests](#tests)
|
||||
6. [Submitting your changes](#submitting-your-changes)
|
||||
1. [Code Style](#code-style)
|
||||
2. [Committing your changes](#committing-your-changes)
|
||||
3. [Pull Requests and Branching](#pull-requests-and-branching)
|
||||
4. [Credit](#credit)
|
||||
|
||||
## License ##
|
||||
|
||||
Rebar3 is licensed under the [Apache License 2.0](LICENSE) for all new code.
|
||||
However, since it is built from older code bases, some files still hold other
|
||||
free licenses (such as BSD). Where it is the case, the license is added in
|
||||
comments.
|
||||
|
||||
All files without specific headers can safely be assumed to be under Apache
|
||||
2.0.
|
||||
|
||||
## Submitting a Bug
|
||||
|
||||
Bugs can be submitted to the [Github issue page](https://github.com/erlang/rebar3/issues).
|
||||
|
||||
Rebar3 is not perfect software and will be buggy. When submitting a bug, be
|
||||
careful to know the following:
|
||||
|
||||
- The Erlang version you are running
|
||||
- The Rebar3 version you are using
|
||||
- The command you were attempting to run
|
||||
|
||||
This information can be automatically generated to put into your bug report
|
||||
by calling `rebar3 report "my command"`.
|
||||
|
||||
You may be asked for further information regarding:
|
||||
|
||||
- Your environment, including the Erlang version used to compile rebar3,
|
||||
details about your operating system, where your copy of Erlang was installed
|
||||
from, and so on;
|
||||
- Your project, including its structure, and possibly to remove build
|
||||
artifacts to start from a fresh build
|
||||
- What it is you are trying to do exactly; we may provide alternative
|
||||
means to do so.
|
||||
|
||||
If you can provide an example code base to reproduce the issue on, we will
|
||||
generally be able to provide more help, and faster.
|
||||
|
||||
All contributors and rebar3 maintainers are generally unpaid developers
|
||||
working on the project in their own free time with limited resources. We
|
||||
ask for respect and understanding and will try to provide the same back.
|
||||
|
||||
## Requesting or implementing a feature
|
||||
|
||||
Before requesting or implementing a new feature, please do the following:
|
||||
|
||||
- Take a look at our [list of plugins](https://rebar3.org/docs/configuration/plugins#recommended-plugins)
|
||||
to know if the feature isn't already supported by the community.
|
||||
- Verify in existing [tickets](https://github.com/erlang/rebar3/issues) whether
|
||||
the feature might already is in the works, has been moved to a plugin, or
|
||||
has already been rejected.
|
||||
|
||||
If this is done, open up a ticket. Tell us what is the feature you want,
|
||||
why you need it, and why you think it should be in rebar3 itself.
|
||||
|
||||
We may discuss details with you regarding the implementation, its inclusion
|
||||
within the project or as a plugin. Depending on the feature, we may provide
|
||||
full support for it, or ask you to help implement and/or commit to maintaining
|
||||
it in the future. We're dedicated to providing a stable build tool, and may
|
||||
also ask features to exist as a plugin before being included in core rebar3 --
|
||||
the migration path from one to the other is fairly simple and little to no code
|
||||
needs rewriting.
|
||||
|
||||
## Project Structure
|
||||
|
||||
Rebar3 is an escript built around the concept of providers. Providers are the
|
||||
modules that do the work to fulfill a user's command. They are documented in
|
||||
[the official documentation website](http://www.rebar3.org/docs/plugins#section-provider-interface).
|
||||
|
||||
Example provider:
|
||||
|
||||
```erlang
|
||||
-module(rebar_prv_something).
|
||||
|
||||
-behaviour(rebar_provider).
|
||||
|
||||
-export([init/1,
|
||||
do/1,
|
||||
format_error/1]).
|
||||
|
||||
-define(PROVIDER, something).
|
||||
-define(DEPS, []).
|
||||
|
||||
%% ===================================================================
|
||||
%% Public API
|
||||
%% ===================================================================
|
||||
|
||||
-spec init(rebar_state:state()) -> {ok, rebar_state:state()}.
|
||||
init(State) ->
|
||||
State1 = rebar_state:add_provider(State, rebar_provider:create([
|
||||
{name, ?PROVIDER},
|
||||
{module, ?MODULE},
|
||||
{bare, true},
|
||||
{deps, ?DEPS},
|
||||
{example, "rebar dummy"},
|
||||
{short_desc, "dummy plugin."},
|
||||
{desc, ""},
|
||||
{opts, []}
|
||||
])),
|
||||
{ok, State1}.
|
||||
|
||||
-spec do(rebar_state:state()) -> {ok, rebar_state:state()}.
|
||||
do(State) ->
|
||||
%% Do something
|
||||
{ok, State}.
|
||||
|
||||
-spec format_error(any()) -> iolist().
|
||||
format_error(Reason) ->
|
||||
io_lib:format("~p", [Reason]).
|
||||
```
|
||||
|
||||
Providers are then listed in `rebar.app.src`, and can be called from
|
||||
the command line or as a programmatical API.
|
||||
|
||||
All commands are therefore implemented in standalone modules. If you call
|
||||
`rebar3 <task>`, the module in charge of it is likely located in
|
||||
`src/rebar_prv_<task>.erl`.
|
||||
|
||||
Templates are included in `priv/templates/`
|
||||
|
||||
The official test suite is Common Test, and tests are located in `test/`.
|
||||
|
||||
Useful modules include:
|
||||
- `rebar_api`, providing an interface for plugins to call into core rebar3
|
||||
functionality
|
||||
- `rebar_core`, for initial boot and setup of a project
|
||||
- `rebar_config`, handling the configuration of each project.
|
||||
- `rebar_app_info`, giving access to the metadata of a specific OTP application
|
||||
in a project.
|
||||
- `rebar_base_compiler`, giving a uniform interface to compile `.erl` files.
|
||||
- `rebar_dir` for directory handling and management
|
||||
- `rebar_file_util` for cross-platform file handling
|
||||
- `rebar_state`, the glue holding together a specific build or task run;
|
||||
includes canonical versions of the configuration, profiles, applications,
|
||||
dependencies, and so on.
|
||||
- `rebar_utils` for generic tasks and functionality required across
|
||||
multiple providers or modules.
|
||||
|
||||
## Tests
|
||||
|
||||
Rebar3 tries to have as many of its features tested as possible. Everything
|
||||
that a user can do and should be repeatable in any way should be tested.
|
||||
|
||||
Tests are written using the Common Test framework. Tests for rebar3 can be run
|
||||
by calling:
|
||||
|
||||
```bash
|
||||
$ rebar3 escriptize # or bootstrap
|
||||
$ ./rebar3 ct
|
||||
```
|
||||
|
||||
Most tests are named according to their module name followed by the `_SUITE`
|
||||
suffix. Providers are made shorter, such that `rebar_prv_new` is tested in
|
||||
`rebar_new_SUITE`.
|
||||
|
||||
Most tests in the test suite will rely on calling Rebar3 in its API form,
|
||||
then investigating the build output. Because most tests have similar
|
||||
requirements, the `test/rebar_test_utils` file contains common code
|
||||
to set up test projects, run tasks, and verify artifacts at once.
|
||||
|
||||
A basic example can look like:
|
||||
|
||||
```erlang
|
||||
-module(rebar_some_SUITE).
|
||||
-compile(export_all).
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
all() -> [checks_success, checks_failure].
|
||||
|
||||
init_per_testcase(Case, Config0) ->
|
||||
%% Create a project directory in the test run's priv_dir
|
||||
Config = rebar_test_utils:init_rebar_state(Config0),
|
||||
%% Create toy applications
|
||||
AppDir = ?config(apps, Config),
|
||||
Name = rebar_test_utils:create_random_name("app1_"++atom_to_list(Case)),
|
||||
Vsn = rebar_test_utils:create_random_vsn(),
|
||||
rebar_test_utils:create_app(AppDir, Name, Vsn, [kernel, stdlib]),
|
||||
%% Add the data to the test config
|
||||
[{name, Name} | Config].
|
||||
|
||||
end_per_testcase(_, Config) ->
|
||||
Config.
|
||||
|
||||
checks_success(Config) ->
|
||||
%% Validates that the application in `name' is successfully compiled
|
||||
Name = ?config(name, Config),
|
||||
rebar_test_utils:run_and_check(Config, [],
|
||||
["compile"],
|
||||
{ok, [{app, Name}]}).
|
||||
|
||||
checks_failure(Config) ->
|
||||
%% Checks that a result fails
|
||||
Command = ["fakecommand", "fake-arg"],
|
||||
rebar_test_utils:run_and_check(
|
||||
Config, [], Command,
|
||||
{error, io_lib:format("Command ~p not found", [fakecommand])}
|
||||
).
|
||||
```
|
||||
|
||||
The general interface to `rebar_test_utils:run_and_check` is
|
||||
`run_and_check(CTConfig, RebarConfig, Command, Expect)` where `Expect` can
|
||||
be any of:
|
||||
|
||||
```erlang
|
||||
{ok, OKRes}
|
||||
{ok, OKRes, ProfilesUsed}
|
||||
{error, Reason}
|
||||
|
||||
% where:
|
||||
ProfilesUsed :: string() % matching the profiles to validate (defaults to "*")
|
||||
OKRes :: {app, Name} % name of an app that is in the build directory
|
||||
| {app, Name, valid} % name of an app that is in the build directory and compiled properly
|
||||
| {app, Name, invalid} % name of an app that didn't compile properly
|
||||
| {dep, Name} % name of a dependency in the build directory
|
||||
| {dep, Name, Vsn} % name of a dependency in the build directory with a specific version
|
||||
| {dep_not_exist, Name} % name of a dependency missing from the build directory
|
||||
| {checkout, Name} % name of an app that is a checkout dependency
|
||||
| {plugin, Name} % name of a plugin in the build directory
|
||||
| {plugin, Name, Vsn} % name of a plugin in the build directory with a specific version
|
||||
| {global_plugin, Name} % name of a global plugin in the build directory
|
||||
| {global_plugin, Name, Vsn} % name of a global plugin in the build directory with a specific version
|
||||
| {lock, Name} % name of a locked dependency
|
||||
| {lock, Name, Vsn} % name of a locked dependency of a specific version
|
||||
| {lock, pkg, Name, Vsn}% name of a locked package of a specific version
|
||||
| {lock, src, Name, Vsn}% name of a locked source dependency of a specific version
|
||||
| {release, Name, Vsn, ExpectedDevMode} % validates a release
|
||||
| {tar, Name, Vsn} % validates a tarball's existence
|
||||
| {file, Filename} % validates the presence of a given file
|
||||
| {dir, Dirname} % validates the presence of a given directory
|
||||
Reason :: term() % the exception thrown by rebar3
|
||||
```
|
||||
|
||||
This generally lets most features be tested fine. Ask for help if you cannot
|
||||
figure out how to write tests for your feature or patch.
|
||||
|
||||
## Submitting your changes
|
||||
|
||||
While we're not too formal when it comes to pull requests to the project,
|
||||
we do appreciate users taking the time to conform to the guidelines that
|
||||
follow.
|
||||
|
||||
We do expect all pull requests submitted to come with [tests](#tests) before
|
||||
they are merged. If you cannot figure out how to write your tests properly, ask
|
||||
in the pull request for guidance.
|
||||
|
||||
### Code Style
|
||||
|
||||
* Do not introduce trailing whitespace
|
||||
* Indentation is 4 spaces wide, no tabs.
|
||||
* Try not to introduce lines longer than 80 characters
|
||||
* Write small functions whenever possible, and use descriptive names for
|
||||
functions and variables.
|
||||
* Avoid having too many clauses containing clauses containing clauses.
|
||||
Basically, avoid deeply nested `case ... of` or `try ... catch` expressions.
|
||||
Break them out into functions if possible.
|
||||
* Comment tricky or non-obvious decisions made to explain their rationale.
|
||||
|
||||
### Committing your changes
|
||||
|
||||
It helps if your commits are structured as follows:
|
||||
|
||||
- Fixing a bug is one commit.
|
||||
- Adding a feature is one commit.
|
||||
- Adding two features is two commits.
|
||||
- Two unrelated changes is two commits (and likely two Pull requests)
|
||||
|
||||
If you fix a (buggy) commit, squash (`git rebase -i`) the changes as a fixup
|
||||
commit into the original commit, unless the patch was following a
|
||||
maintainer's code review. In such cases, it helps to have separate commits.
|
||||
|
||||
The reviewer may ask you to later squash the commits together to provide
|
||||
a clean commit history before merging in the feature.
|
||||
|
||||
It's important to write a proper commit title and description. The commit title
|
||||
should be no more than 50 characters; it is the first line of the commit text. The
|
||||
second line of the commit text must be left blank. The third line and beyond is
|
||||
the commit message. You should write a commit message. If you do, wrap all
|
||||
lines at 72 characters. You should explain what the commit does, what
|
||||
references you used, and any other information that helps understanding your
|
||||
changes.
|
||||
|
||||
### Pull Requests and Branching
|
||||
|
||||
All fixes to rebar end up requiring a +1 from one or more of the project's
|
||||
maintainers. When opening a pull request, explain what the patch is doing
|
||||
and if it makes sense, why the proposed implementation was chosen.
|
||||
|
||||
Try to use well-defined commits (one feature per commit) so that reading
|
||||
them and testing them is easier for reviewers and while bisecting the code
|
||||
base for issues.
|
||||
|
||||
During the review process, you may be asked to correct or edit a few things
|
||||
before a final rebase to merge things. Do send edits as individual commits
|
||||
to allow for gradual and partial reviews to be done by reviewers. Once the +1s
|
||||
are given, rebasing is appreciated but not mandatory.
|
||||
|
||||
Please work in feature branches, and do not commit to `master` in your fork.
|
||||
|
||||
Provide a clean branch without merge commits.
|
||||
|
||||
If you can, pick a descriptive title for your pull request. When we generate
|
||||
changelogs before cutting a release, a script uses the pull request names
|
||||
to populate the entries.
|
||||
|
||||
|
||||
### Credit
|
||||
|
||||
To give everyone proper credit in addition to the git history, please feel free to append
|
||||
your name to `THANKS` in your first contribution.
|
|
@ -0,0 +1,43 @@
|
|||
# https://docs.docker.com/engine/reference/builder/#from
|
||||
# "The FROM instruction initializes a new build stage and sets the
|
||||
# Base Image for subsequent instructions."
|
||||
FROM erlang:20.3.8.1-alpine as builder
|
||||
# https://docs.docker.com/engine/reference/builder/#label
|
||||
# "The LABEL instruction adds metadata to an image."
|
||||
LABEL stage=builder
|
||||
|
||||
# Install git for fetching non-hex depenencies. Also allows rebar3
|
||||
# to find it's own git version.
|
||||
# Add any other Alpine libraries needed to compile the project here.
|
||||
# See https://wiki.alpinelinux.org/wiki/Local_APK_cache for details
|
||||
# on the local cache and need for the symlink
|
||||
RUN ln -s /var/cache/apk /etc/apk/cache && \
|
||||
apk update && \
|
||||
apk add --update openssh-client git
|
||||
|
||||
# WORKDIR is located in the image
|
||||
# https://docs.docker.com/engine/reference/builder/#workdir
|
||||
WORKDIR /root/rebar3
|
||||
|
||||
# copy the entire src over and build
|
||||
COPY . .
|
||||
RUN ./bootstrap
|
||||
|
||||
# this is the final runner layer, notice how it diverges from the original erlang
|
||||
# alpine layer, this means this layer won't have any of the other stuff that was
|
||||
# generated previously (deps, build, etc)
|
||||
FROM erlang:20.3.8.1-alpine as runner
|
||||
|
||||
# copy the generated `rebar3` binary over here
|
||||
COPY --from=builder /root/rebar3/_build/prod/bin/rebar3 .
|
||||
|
||||
# and install it
|
||||
RUN HOME=/opt ./rebar3 local install \
|
||||
&& rm -f /usr/local/bin/rebar3 \
|
||||
&& ln /opt/.cache/rebar3/bin/rebar3 /usr/local/bin/rebar3 \
|
||||
&& rm -rf rebar3
|
||||
|
||||
# simply print out the version for visibility
|
||||
ENTRYPOINT ["/usr/local/bin/rebar3"]
|
||||
CMD ["--version"]
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
# Rebar3
|
||||
|
||||
[![Build Status](https://github.com/erlang/rebar3/workflows/Common%20Test/badge.svg)](https://github.com/erlang/rebar3/actions?query=branch%3Amaster+workflow%3A"Common+Test") [![Erlang Versions](https://img.shields.io/badge/Supported%20Erlang%2FOTP-22.0%20to%2024.0-blue)](http://www.erlang.org)
|
||||
|
||||
1. [What is Rebar3?](#what-is-rebar3)
|
||||
2. [Why Rebar3?](#why-rebar3)
|
||||
3. [Should I Use Rebar3?](#should-i-use-rebar3)
|
||||
4. [Getting Started](#getting-started)
|
||||
5. [Documentation](#documentation)
|
||||
6. [Features](#features)
|
||||
7. [Migrating from rebar2](#migrating-from-rebar2)
|
||||
8. [Additional Resources](#additional-resources)
|
||||
|
||||
## What is Rebar3
|
||||
|
||||
Rebar3 is an Erlang tool that makes it easy to create, develop, and
|
||||
release Erlang libraries, applications, and systems in a repeatable manner.
|
||||
|
||||
Rebar3 will:
|
||||
- respect and enforce standard Erlang/OTP conventions for project
|
||||
structure so they are easily reusable by the community;
|
||||
- manage source dependencies and Erlang [packages](https://hex.pm)
|
||||
while ensuring repeatable builds;
|
||||
- handle build artifacts, paths, and libraries such that standard
|
||||
development tools can be used without a headache;
|
||||
- adapt to projects of all sizes on almost any platform;
|
||||
- treat [documentation](https://rebar3.org/docs/) as a feature,
|
||||
and errors or lack of documentation as a bug.
|
||||
|
||||
Rebar3 is also a self-contained Erlang script. It is easy to distribute or
|
||||
embed directly in a project. Tasks or behaviours can be modified or expanded
|
||||
with a [plugin system](https://rebar3.org/docs/configuration/plugins)
|
||||
[flexible enough](https://github.com/lfe-rebar3/rebar3_lfe) that even other languages
|
||||
on the Erlang VM will use it as a build tool.
|
||||
|
||||
## Why Rebar3
|
||||
|
||||
Rebar3 is the spiritual successor to [rebar
|
||||
2.x](https://github.com/rebar/rebar), which was the first usable build tool
|
||||
for Erlang that ended up seeing widespread community adoption. It however
|
||||
had several shortcomings that made it difficult to use with larger projects
|
||||
or with teams with users new to Erlang.
|
||||
|
||||
Rebar3 was our attempt at improving over the legacy of Rebar 2.x, providing the
|
||||
features we felt it was missing, and to provide a better environment in which
|
||||
newcomers joining our teams could develop.
|
||||
|
||||
## Should I use Rebar3?
|
||||
|
||||
If your main language for your system is Erlang, that you value repeatable builds
|
||||
and want your various tools to integrate together, we do believe Rebar3 is the
|
||||
best experience you can get.
|
||||
|
||||
## Getting Started
|
||||
|
||||
A [getting started guide is maintained on the official documentation website](https://rebar3.org/docs/getting-started),
|
||||
but installing rebar3 can be done by any of the ways described below
|
||||
|
||||
Latest stable compiled version:
|
||||
```bash
|
||||
$ wget https://s3.amazonaws.com/rebar3/rebar3 && chmod +x rebar3
|
||||
```
|
||||
|
||||
From Source (assuming you have a full Erlang install):
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/erlang/rebar3.git
|
||||
$ cd rebar3
|
||||
$ ./bootstrap
|
||||
```
|
||||
|
||||
Stable versions can also be obtained from the [releases page](https://github.com/erlang/rebar3/releases).
|
||||
|
||||
The rebar3 escript can also extract itself with a run script under the user's home directory:
|
||||
|
||||
```bash
|
||||
$ ./rebar3 local install
|
||||
===> Extracting rebar3 libs to ~/.cache/rebar3/lib...
|
||||
===> Writing rebar3 run script ~/.cache/rebar3/bin/rebar3...
|
||||
===> Add to $PATH for use: export PATH=~/.cache/rebar3/bin:$PATH
|
||||
```
|
||||
|
||||
To keep it up to date after you've installed rebar3 this way you can use `rebar3 local upgrade` which
|
||||
fetches the latest stable release and extracts to the same place as above. A [nightly version can
|
||||
also be obtained](https://s3.amazonaws.com/rebar3-nightly/rebar3) if desired.
|
||||
|
||||
Rebar3 may also be available on various OS-specific package managers such as
|
||||
FreeBSD Ports. Those are maintained by the community and Rebar3 maintainers
|
||||
themselves are generally not involved in that process.
|
||||
|
||||
If you do not have a full Erlang install, we recommend using [erln8](https://erln8.github.io/erln8/)
|
||||
or [kerl](https://github.com/yrashk/kerl). For binary packages, use those provided
|
||||
by [Erlang Solutions](https://www.erlang-solutions.com/resources/download.html),
|
||||
but be sure to choose the "Standard" download option or you'll have issues building
|
||||
projects.
|
||||
|
||||
Do note that if you are planning to work with multiple Erlang versions on the same machine, you will want to build Rebar3 with the oldest one of them. The 3 newest major Erlang releases are supported at any given time: if the newest version is OTP-24, building with versions as old as OTP-22 will be supported, and produce an executable that will work with those that follow.
|
||||
|
||||
## Documentation
|
||||
|
||||
Rebar3 documentation is maintained on [https://rebar3.org/docs](https://rebar3.org/docs)
|
||||
|
||||
## Features
|
||||
|
||||
Rebar3 supports the following features or tools by default, and may provide many
|
||||
others via the plugin ecosystem:
|
||||
|
||||
| features | Description |
|
||||
|--------------------- |------------ |
|
||||
| Command composition | Rebar3 allows multiple commands to be run in sequence by calling `rebar3 do <task1>,<task2>,...,<taskN>`. |
|
||||
| Command dependencies | Rebar3 commands know their own dependencies. If a test run needs to fetch dependencies and build them, it will do so. |
|
||||
| Command namespaces | Allows multiple tools or commands to share the same name. |
|
||||
| Compiling | Build the project, including fetching all of its dependencies by calling `rebar3 compile` |
|
||||
| Clean up artifacts | Remove the compiled beam files from a project with `rebar3 clean` or just remove the `_build` directory to remove *all* compilation artifacts |
|
||||
| Code Coverage | Various commands can be instrumented to accumulate code coverage data (such as `eunit` or `ct`). Reports can be generated with `rebar3 cover` |
|
||||
| Common Test | The test framework can be run by calling `rebar3 ct` |
|
||||
| Dependencies | Rebar3 maintains local copies of dependencies on a per-project basis. They are fetched deterministically, can be locked, upgraded, fetched from source, packages, or from local directories. See [Dependencies on the documentation website](https://rebar3.org/docs/configuration/dependencies/). Call `rebar3 tree` to show the whole dependency tree. |
|
||||
| Documentation | Print help for rebar3 itself (`rebar3 help`) or for a specific task (`rebar3 help <task>`). Full reference at [rebar3.org](https://rebar3.org/docs). |
|
||||
| Dialyzer | Run the Dialyzer analyzer on the project with `rebar3 dialyzer`. Base PLTs for each version of the language will be cached and reused for faster analysis |
|
||||
| Edoc | Generate documentation using edoc with `rebar3 edoc` |
|
||||
| Escript generation | Rebar3 can be used to generate [escripts](http://www.erlang.org/doc/man/escript.html) providing an easy way to run all your applications on a system where Erlang is installed |
|
||||
| Eunit | The test framework can be run by calling `rebar3 eunit` |
|
||||
| Locked dependencies | Dependencies are going to be automatically locked to ensure repeatable builds. Versions can be changed with `rebar3 upgrade` or `rebar3 upgrade <app>`, or locks can be released altogether with `rebar3 unlock`. |
|
||||
| Packages | A given [Hex package](https://hex.pm) can be inspected `rebar3 pkgs <name>`. This will output its description and available versions |
|
||||
| Path | While paths are managed automatically, you can print paths to the current build directories with `rebar3 path`. |
|
||||
| Plugins | Rebar3 can be fully extended with [plugins](https://rebar3.org/docs/configuration/plugins/). List or upgrade plugins by using the plugin namespace (`rebar3 plugins`). |
|
||||
| Profiles | Rebar3 can have subconfiguration options for different profiles, such as `test` or `prod`. These allow specific dependencies or compile options to be used in specific contexts. See [Profiles](https://rebar3.org/docs/configuration/profiles) in the docs. |
|
||||
| Releases | Rebar3 supports [building releases](https://rebar3.org/docs/deployment/releases) with the `relx` tool, providing a way to ship fully self-contained Erlang systems. Release update scripts for live code updates can also be generated. |
|
||||
| Shell | A full shell with your applications available can be started with `rebar3 shell`. From there, call tasks as `r3:do(compile)` to automatically recompile and reload the code without interruption |
|
||||
| Tarballs | Releases can be packaged into tarballs ready to be deployed. |
|
||||
| Templates | Configurable templates ship out of the box (try `rebar3 new` for a list or `rebar3 new help <template>` for a specific one). [Custom templates](https://rebar3.org/docs/tutorials/templates) are also supported, and plugins can also add their own. |
|
||||
| Xref | Run cross-reference analysis on the project with [xref](http://www.erlang.org/doc/apps/tools/xref_chapter.html) by calling `rebar3 xref`. |
|
||||
|
||||
## Migrating From rebar2
|
||||
|
||||
The grievances we had with Rebar 2.x were not fixable without breaking
|
||||
compatibility in some very important ways.
|
||||
|
||||
A full guide titled [From Rebar 2.x to Rebar3](https://rebar3.org/docs/tutorials/from_rebar2_to_rebar3/)
|
||||
is provided on the documentation website.
|
||||
|
||||
Notable modifications include mandating a more standard set of directory
|
||||
structures, changing the handling of dependencies, moving some compilers (such
|
||||
as C, Diameter, ErlyDTL, or ProtoBuffs) to
|
||||
[plugins](https://rebar3.org/docs/configuration/plugins) rather than
|
||||
maintaining them in core rebar, and moving release builds from reltool to
|
||||
relx.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
In the case of problems that cannot be solved through documentation or examples, you
|
||||
may want to try to contact members of the community for help. The community is
|
||||
also where you want to go for questions about how to extend rebar, fill in bug
|
||||
reports, and so on.
|
||||
|
||||
If you need
|
||||
quick feedback, you can try the #rebar channel on
|
||||
[irc.freenode.net](https://freenode.net) or the #rebar3 channel on
|
||||
[erlanger.slack.com](https://erlanger.slack.com/). Be sure to check the
|
||||
[documentation](https://rebar3.org/docs) first, just to be sure you're not
|
||||
asking about things with well-known answers.
|
||||
|
||||
For bug reports, roadmaps, and issues, visit the [github issues
|
||||
page](https://github.com/erlang/rebar3/issues).
|
||||
|
||||
General rebar community resources and links can be found at
|
||||
[rebar3.org/docs/about/about-us/#community](https://rebar3.org/docs/about/about-us/#community)
|
||||
|
||||
To contribute to rebar3, please refer to [CONTRIBUTING](CONTRIBUTING.md).
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
The following people have contributed to rebar:
|
||||
|
||||
Dave Smith
|
||||
Jon Meredith
|
||||
Tim Dysinger
|
||||
Bryan Fink
|
||||
Tuncer Ayaz
|
||||
Ian Wilkinson
|
||||
Juan Jose Comellas
|
||||
Tom Preston-Werner
|
||||
OJ Reeves
|
||||
Ruslan Babayev
|
||||
Ryan Tilder
|
||||
Kevin Smith
|
||||
David Reid
|
||||
Cliff Moon
|
||||
Chris Bernard
|
||||
Jeremy Raymond
|
||||
Bob Ippolito
|
||||
Alex Songe
|
||||
Andrew Thompson
|
||||
Russell Brown
|
||||
Chris Chew
|
||||
Klas Johansson
|
||||
Geoff Cant
|
||||
Kostis Sagonas
|
||||
Essien Ita Essien
|
||||
Manuel Duran Aguete
|
||||
Daniel Neri
|
||||
Misha Gorodnitzky
|
||||
Adam Kocoloski
|
||||
Joseph Wayne Norton
|
||||
Mihai Balea
|
||||
Matthew Batema
|
||||
Alexey Romanov
|
||||
Benjamin Nortier
|
||||
Magnus Klaar
|
||||
Anthony Ramine
|
||||
Charles McKnight
|
||||
Andrew Tunnell-Jones
|
||||
Joe Williams
|
||||
Daniel Reverri
|
||||
Jesper Louis Andersen
|
||||
Richard Jones
|
||||
Tim Watson
|
||||
Anders 'andekar'
|
||||
Christopher Brown
|
||||
Jordi Chacon
|
||||
Shunichi Shinohara
|
||||
Mickael Remond
|
||||
Evax Software
|
||||
Piotr Usewicz
|
||||
Anthony Molinaro
|
||||
Andrew Gopienko
|
||||
Steve Vinoski
|
||||
Evan Miller
|
||||
Jared Morrow
|
||||
Jan Kloetzke
|
||||
Mathias Meyer
|
||||
Steven Gravell
|
||||
Alexis Sellier
|
||||
Mattias Holmlund
|
||||
Tino Breddin
|
||||
David Nonnenmacher
|
||||
Anders Nygren
|
||||
Scott Lystig Fritchie
|
||||
Uwe Dauernheim
|
||||
Yurii Rashkovskii
|
||||
Alfonso De Gregorio
|
||||
Matt Campbell
|
||||
Benjamin Plee
|
||||
Ben Ellis
|
||||
Ignas Vysniauskas
|
||||
Anton Lavrik
|
||||
Jan Vincent Liwanag
|
||||
Przemyslaw Dabek
|
||||
Fabian Linzberger
|
||||
Smith Winston
|
||||
Jesse Gumm
|
||||
Torbjorn Tornkvist
|
||||
Ali Sabil
|
||||
Tomas Abrahamsson
|
||||
Francis Joanis
|
||||
fisher@yun.io
|
||||
Slava Yurin
|
||||
Phillip Toland
|
||||
Mike Lazar
|
||||
Loic Hoguin
|
||||
Ali Yakout
|
||||
Adam Schepis
|
||||
Amit Kapoor
|
||||
Ulf Wiger
|
||||
Nick Vatamaniuc
|
||||
Daniel Luna
|
||||
Motiejus Jakstys
|
||||
Eric B Merritt
|
||||
Fred Hebert
|
||||
Kresten Krab Thorup
|
||||
David Aaberg
|
||||
Pedram Nimreezi
|
||||
Edwin Fine
|
||||
Lev Walkin
|
||||
Roberto Ostinelli
|
||||
Joe DeVivo
|
||||
Markus Nasman
|
||||
Dmitriy Kargapolov
|
||||
Ryan Zezeski
|
||||
Daniel White
|
||||
Martin Schut
|
||||
Serge Aleynikov
|
||||
Magnus Henoch
|
||||
Artem Teslenko
|
||||
Jeremie Lasalle Ratelle
|
||||
Jose Valim
|
||||
Krzysztof Rutka
|
||||
Mats Cronqvist
|
||||
Matthew Conway
|
||||
Giacomo Olgeni
|
||||
Pedram Nimreezi
|
||||
Sylvain Benner
|
||||
Oliver Ferrigni
|
||||
Dave Thomas
|
||||
Evgeniy Khramtsov
|
||||
YeJun Su
|
||||
Yuki Ito
|
||||
alisdair sullivan
|
||||
Alexander Verbitsky
|
||||
Andras Horvath
|
||||
Drew Varner
|
||||
Omar Yasin
|
||||
Tristan Sloughter
|
||||
Kelly McLaughlin
|
||||
Martin Karlsson
|
||||
Pierre Fenoll
|
||||
David Kubecka
|
||||
Stefan Grundmann
|
||||
Carlos Eduardo de Paula
|
||||
Derek Brown
|
||||
Heinz N. Gies
|
||||
Roberto Aloi
|
||||
Andrew McRobb
|
||||
Drew Varner
|
||||
Niklas Johansson
|
||||
Bryan Paxton
|
||||
Justin Wood
|
||||
Guilherme Andrade
|
||||
Manas Chaudhari
|
||||
Luís Rascão
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Hinagiku Soranoba
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
bbmustache
|
||||
===========
|
||||
[![Build Status](https://travis-ci.org/soranoba/bbmustache.svg?branch=master)](https://travis-ci.org/soranoba/bbmustache)
|
||||
[![hex.pm version](https://img.shields.io/hexpm/v/bbmustache.svg)](https://hex.pm/packages/bbmustache)
|
||||
|
||||
Binary pattern match Based Mustache template engine for Erlang/OTP.
|
||||
|
||||
## Overview
|
||||
- Binary pattern match based mustache template engine for Erlang/OTP.
|
||||
- It means do not use regular expressions.
|
||||
- Support maps and associative arrays.
|
||||
- Officially support is OTP17 or later.
|
||||
|
||||
### What is Mustache ?
|
||||
A logic-less templates.
|
||||
- [{{mustache}}](http://mustache.github.io/)
|
||||
|
||||
## Usage
|
||||
### Quick start
|
||||
|
||||
```bash
|
||||
$ git clone git://github.com/soranoba/bbmustache.git
|
||||
$ cd bbmustache
|
||||
$ make start
|
||||
Erlang/OTP 17 [erts-6.3] [source-f9282c6] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:true]
|
||||
|
||||
Eshell V6.3 (abort with ^G)
|
||||
1> bbmustache:render(<<"{{name}}">>, #{"name" => "hoge"}).
|
||||
<<"hoge">>
|
||||
2> bbmustache:render(<<"{{name}}">>, [{"name", "hoge"}]).
|
||||
<<"hoge">>
|
||||
```
|
||||
|
||||
### Use as a library
|
||||
Add the following settings.
|
||||
|
||||
```erlang
|
||||
%% rebar (rebar.config)
|
||||
|
||||
{deps,
|
||||
[
|
||||
{bbmustache, ".*", {git, "git://github.com/soranoba/bbmustache.git", {branch, "master"}}}
|
||||
]}.
|
||||
|
||||
%% rebar3 (rebar.config)
|
||||
|
||||
{deps, [bbmustache]}.
|
||||
```
|
||||
|
||||
### How to use simple Mustache
|
||||
|
||||
Map
|
||||
```erlang
|
||||
1> bbmustache:render(<<"{{name}}">>, #{"name" => "hoge"}).
|
||||
<<"hoge">>
|
||||
|
||||
2> Template1 = bbmustache:parse_binary(<<"{{name}}">>).
|
||||
...
|
||||
3> bbmustache:compile(Template1, #{"name" => "hoge"}).
|
||||
<<"hoge">>
|
||||
|
||||
4> Template2 = bbmustache:parse_file(<<"./hoge.mustache">>).
|
||||
...
|
||||
5> bbmustache:compile(Template2, #{"name" => "hoge"}).
|
||||
<<"hoge">>
|
||||
```
|
||||
|
||||
Associative array
|
||||
```erlang
|
||||
1> bbmustache:render(<<"{{name}}">>, [{"name", "hoge"}]).
|
||||
<<"hoge">>
|
||||
|
||||
2> Template1 = bbmustache:parse_binary(<<"{{name}}">>).
|
||||
...
|
||||
3> bbmustache:compile(Template1, [{"name", "hoge"}]).
|
||||
<<"hoge">>
|
||||
|
||||
4> Template2 = bbmustache:parse_file(<<"./hoge.mustache">>).
|
||||
...
|
||||
5> bbmustache:compile(Template2, [{"name", "hoge"}]).
|
||||
<<"hoge">>
|
||||
```
|
||||
|
||||
### Use as a command-line tool
|
||||
|
||||
```bash
|
||||
make escriptize
|
||||
echo '{"name", "hoge"}.' > vars.config
|
||||
echo '{{name}}' > template.mustache
|
||||
./bbmustache -d vars.config template.mustache
|
||||
hoge
|
||||
```
|
||||
|
||||
Data files (-d) support a single assoc list, a single map, and [consult](https://erlang.org/doc/man/file.html#consult-1) format.<br>
|
||||
Note: the behind term has a high priority in all cases. it is a result of supporting to allow for embedding relative file paths as in [config](http://erlang.org/doc/man/config.html).
|
||||
|
||||
### More information
|
||||
- For the alias of mustache, Please refer to [ManPage](http://mustache.github.io/mustache.5.html) and [Specification](https://github.com/mustache/spec)
|
||||
- For the options of this library, please see [doc](doc)
|
||||
- For the functions supported by this library, please see [here](benchmarks/README.md)
|
||||
|
||||
## FAQ
|
||||
|
||||
### Avoid http escaping
|
||||
|
||||
```erlang
|
||||
%% Please use `{{{tag}}}`
|
||||
1> bbmustache:render(<<"<h1>{{{title}}}</h1>">>, #{"title" => "I like Erlang & mustache"}).
|
||||
<<"<h1>I like Erlang & mustache</h1>">>
|
||||
|
||||
%% If you should not want to use `{{{tag}}}`, escape_fun can be use.
|
||||
1> bbmustache:render(<<"<h1>{{title}}</h1>">>, #{"title" => "I like Erlang & mustache"}, [{escape_fun, fun(X) -> X end}]).
|
||||
<<"<h1>I like Erlang & mustache</h1>">>
|
||||
```
|
||||
|
||||
### Already used `{` and `}` for other uses (like escript)
|
||||
|
||||
```erlang
|
||||
1> io:format(bbmustache:render(<<"
|
||||
1> {{=<< >>=}}
|
||||
1> {deps, [
|
||||
1> <<#deps>>
|
||||
1> {<<name>>, \"<<version>>\"}<<^last?>>,<</last?>>
|
||||
1> <</deps>>
|
||||
1> ]}.
|
||||
1> ">>, #{"deps" => [
|
||||
1> #{"name" => "bbmustache", "version" => "1.6.0"},
|
||||
1> #{"name" => "jsone", "version" => "1.4.6", "last?" => true}
|
||||
1> ]})).
|
||||
|
||||
{deps, [
|
||||
{bbmustache, "1.6.0"},
|
||||
{jsone, "1.4.6"}
|
||||
]}.
|
||||
ok
|
||||
```
|
||||
|
||||
### Want to use something other than string for key
|
||||
|
||||
```erlang
|
||||
1> bbmustache:render(<<"<h1>{{{title}}}</h1>">>, #{title => "I like Erlang & mustache"}, [{key_type, atom}]).
|
||||
<<"<h1>I like Erlang & mustache</h1>">>
|
||||
|
||||
2> bbmustache:render(<<"<h1>{{{title}}}</h1>">>, #{<<"title">> => "I like Erlang & mustache"}, [{key_type, binary}]).
|
||||
<<"<h1>I like Erlang & mustache</h1>">>
|
||||
```
|
||||
|
||||
### Want to provide a custom serializer for Erlang Terms
|
||||
|
||||
```erlang
|
||||
1> bbmustache:render(<<"<h1>{{title}}</h1>">>, #{title => "I like Erlang & mustache"}, [{key_type, atom}, {value_serializer, fun(X) -> X end}]).
|
||||
<<"<h1>I like Erlang & mustache</h1>">>
|
||||
|
||||
2> bbmustache:render(<<"<h1>{{{title}}}</h1>">>, #{<<"title">> => "I like Erlang & mustache"}, [{key_type, binary}, {value_serializer, fun(X) -> <<"replaced">> end}]).
|
||||
<<"<h1>replaced</h1>">>
|
||||
|
||||
3> bbmustache:render(<<"<h1>{{{title}}}</h1>">>, #{<<"title">> => #{<<"nested">> => <<"value">>}}, [{key_type, binary}, {value_serializer, fun(X) -> jsone:encode(X) end}]).
|
||||
<<"<h1>{\"nested\": \"value\"}</h1>">>
|
||||
|
||||
4> bbmustache:render(<<"<h1>{{title}}</h1>">>, #{<<"title">> => #{<<"nested">> => <<"value">>}}, [{key_type, binary}, {value_serializer, fun(X) -> jsone:encode(X) end}]).
|
||||
<<"<h1>{"nested":"value"}</h1>">>
|
||||
```
|
||||
|
||||
## Attention
|
||||
- Lambda expression is included wasted processing.
|
||||
- Because it is optimized to `parse_binary/1` + `compile/2`.
|
||||
|
||||
## Comparison with other libraries
|
||||
[Benchmarks and check the reference implementation](benchmarks/README.md)
|
||||
|
||||
## Contribute
|
||||
Pull request is welcome =D
|
||||
|
||||
## License
|
||||
[MIT License](LICENSE)
|
|
@ -0,0 +1,63 @@
|
|||
%% vim: set filetype=erlang : -*- erlang -*-
|
||||
|
||||
{erl_opts, [
|
||||
{platform_define, "^[0-9]+", namespaced_types},
|
||||
warnings_as_errors,
|
||||
warn_export_all,
|
||||
warn_untyped_record
|
||||
]}.
|
||||
|
||||
{xref_checks, [
|
||||
fail_on_warning,
|
||||
undefined_function_calls
|
||||
]}.
|
||||
|
||||
{cover_enabled, true}.
|
||||
|
||||
{edoc_opts, [
|
||||
{doclet, edown_doclet},
|
||||
{dialyzer_specs, all},
|
||||
{report_missing_type, true},
|
||||
{report_type_mismatch, true},
|
||||
{pretty_print, erl_pp},
|
||||
{preprocess, true}
|
||||
]}.
|
||||
{validate_app_modules, true}.
|
||||
|
||||
{ct_opts, [{dir, "ct"}]}.
|
||||
|
||||
{git_vsn, [{env_key, git_vsn},
|
||||
{describe_opt, "--tags --abbrev=10"},
|
||||
{separate, true}]}.
|
||||
|
||||
{escript_name, bbmustache}.
|
||||
{escript_incl_apps, [getopt]}.
|
||||
{escript_comment, "%% https://github.com/soranoba/bbmustache \n"}.
|
||||
|
||||
{profiles, [{test, [{erl_opts, [export_all]},
|
||||
{deps,
|
||||
[
|
||||
{jsone, "1.4.6"},
|
||||
{mustache_spec, ".*", {git, "git://github.com/soranoba/spec.git", {tag, "v1.1.3-erl"}}}
|
||||
]},
|
||||
{plugins, [rebar3_raw_deps]}
|
||||
]},
|
||||
{dev, [{erl_opts, [{d, bbmustache_escriptize}]},
|
||||
{deps,
|
||||
[
|
||||
{getopt, "1.0.1"}
|
||||
]},
|
||||
{plugins, [rebar3_git_vsn]},
|
||||
{provider_hooks, [{post, [{compile, git_vsn}]}]}
|
||||
]},
|
||||
{doc, [{deps,
|
||||
[
|
||||
{edown, ".*", {git, "git://github.com/uwiger/edown.git", {branch, "master"}}}
|
||||
]}
|
||||
]},
|
||||
{bench, [{deps,
|
||||
[
|
||||
{mustache, ".*", {git, "git://github.com/mojombo/mustache.erl", {tag, "v0.1.1"}}}
|
||||
]}
|
||||
]}
|
||||
]}.
|
|
@ -0,0 +1 @@
|
|||
[].
|
|
@ -0,0 +1,9 @@
|
|||
{application,bbmustache,
|
||||
[{description,"Binary pattern match Based Mustache template engine for Erlang/OTP"},
|
||||
{vsn,"1.10.0"},
|
||||
{registered,[]},
|
||||
{applications,[kernel,stdlib]},
|
||||
{maintainers,["Hinagiku Soranoba"]},
|
||||
{licenses,["MIT"]},
|
||||
{links,[{"GitHub","https://github.com/soranoba/bbmustache"}]},
|
||||
{env,[]}]}.
|
|
@ -0,0 +1,799 @@
|
|||
%% @copyright 2015 Hinagiku Soranoba All Rights Reserved.
|
||||
%%
|
||||
%% @doc Binary pattern match Based Mustach template engine for Erlang/OTP.
|
||||
%%
|
||||
%% Please refer to [the man page](http://mustache.github.io/mustache.5.html) and [the spec](https://github.com/mustache/spec) of mustache as the need arises.<br />
|
||||
%%
|
||||
%% Please see [this](../benchmarks/README.md) for a list of features that bbmustache supports.
|
||||
%%
|
||||
|
||||
-module(bbmustache).
|
||||
|
||||
%%----------------------------------------------------------------------------------------------------------------------
|
||||
%% Exported API
|
||||
%%----------------------------------------------------------------------------------------------------------------------
|
||||
-export([
|
||||
render/2,
|
||||
render/3,
|
||||
parse_binary/1,
|
||||
parse_binary/2,
|
||||
parse_file/1,
|
||||
parse_file/2,
|
||||
compile/2,
|
||||
compile/3,
|
||||
default_value_serializer/1,
|
||||
default_partial_file_reader/2
|
||||
]).
|
||||
|
||||
-ifdef(bbmustache_escriptize).
|
||||
-export([main/1]).
|
||||
-endif.
|
||||
|
||||
-export_type([
|
||||
key/0,
|
||||
template/0,
|
||||
data/0,
|
||||
recursive_data/0,
|
||||
option/0, % deprecated
|
||||
compile_option/0,
|
||||
parse_option/0,
|
||||
render_option/0
|
||||
]).
|
||||
|
||||
%%----------------------------------------------------------------------------------------------------------------------
|
||||
%% Defines & Records & Types
|
||||
%%----------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-define(PARSE_ERROR, incorrect_format).
|
||||
-define(FILE_ERROR, file_not_found).
|
||||
-define(CONTEXT_MISSING_ERROR(Msg), {context_missing, Msg}).
|
||||
|
||||
-define(IIF(Cond, TValue, FValue),
|
||||
case Cond of true -> TValue; false -> FValue end).
|
||||
|
||||
-define(ADD(X, Y), ?IIF(X =:= <<>>, Y, [X | Y])).
|
||||
-define(START_TAG, <<"{{">>).
|
||||
-define(STOP_TAG, <<"}}">>).
|
||||
|
||||
-define(RAISE_ON_CONTEXT_MISS_ENABLED(Options),
|
||||
proplists:get_bool(raise_on_context_miss, Options)).
|
||||
-define(RAISE_ON_PARTIAL_MISS_ENABLED(Options),
|
||||
proplists:get_bool(raise_on_partial_miss, Options)).
|
||||
|
||||
-define(PARSE_OPTIONS, [
|
||||
partial_file_reader,
|
||||
raise_on_partial_miss
|
||||
]).
|
||||
|
||||
-type key() :: binary().
|
||||
%% Key MUST be a non-whitespace character sequence NOT containing the current closing delimiter. <br />
|
||||
%%
|
||||
%% In addition, `.' have a special meaning. <br />
|
||||
%% (1) `parent.child' ... find the child in the parent. <br />
|
||||
%% (2) `.' ... It means this. However, the type of correspond is only `[integer() | float() | binary() | string() | atom()]'. Otherwise, the behavior is undefined.
|
||||
%%
|
||||
|
||||
-type source() :: binary().
|
||||
%% If you use lamda expressions, the original text is necessary.
|
||||
%%
|
||||
%% ```
|
||||
%% e.g.
|
||||
%% template:
|
||||
%% {{#lamda}}a{{b}}c{{/lamda}}
|
||||
%% parse result:
|
||||
%% {'#', <<"lamda">>, [<<"a">>, {'n', <<"b">>}, <<"c">>], <<"a{{b}}c">>}
|
||||
%% '''
|
||||
%%
|
||||
%% NOTE:
|
||||
%% Since the binary reference is used internally, it is not a capacitively large waste.
|
||||
%% However, the greater the number of tags used, it should use the wasted memory.
|
||||
|
||||
-type tag() :: {n, [key()]}
|
||||
| {'&', [key()]}
|
||||
| {'#', [key()], [tag()], source()}
|
||||
| {'^', [key()], [tag()]}
|
||||
| {'>', key(), Indent :: source()}
|
||||
| binary(). % plain text
|
||||
|
||||
-record(?MODULE,
|
||||
{
|
||||
data :: [tag()],
|
||||
|
||||
partials = [] :: [{key(), [tag()]} | key()],
|
||||
%% The `{key(), [tag()]}` indicates that `key()` already parsed and `[tag()]` is the result of parsing.
|
||||
%% The `key()` indicates that the file did not exist.
|
||||
|
||||
options = [] :: [compile_option()],
|
||||
indents = [] :: [binary()],
|
||||
context_stack = [] :: [data()]
|
||||
}).
|
||||
|
||||
-opaque template() :: #?MODULE{}.
|
||||
%% @see parse_binary/1
|
||||
%% @see parse_file/1
|
||||
|
||||
-record(state,
|
||||
{
|
||||
dirname = <<>> :: file:filename_all(),
|
||||
start = ?START_TAG :: binary(),
|
||||
stop = ?STOP_TAG :: binary(),
|
||||
partials = [] :: [key()],
|
||||
standalone = true :: boolean()
|
||||
}).
|
||||
-type state() :: #state{}.
|
||||
|
||||
-type parse_option() :: {partial_file_reader, fun((Dirname :: binary(), key()) -> Data :: binary())}
|
||||
| raise_on_partial_miss.
|
||||
%% |key |description |
|
||||
%% |:-- |:---------- |
|
||||
%% |partial_file_reader | When you specify this, it delegate reading of file to the function by `partial'.<br/> This can be used when you want to read from files other than local files.|
|
||||
%% |raise_on_partial_miss| If the template used in partials does not found, it will throw an exception (error). |
|
||||
|
||||
-type compile_option() :: {key_type, atom | binary | string}
|
||||
| raise_on_context_miss
|
||||
| {escape_fun, fun((binary()) -> binary())}
|
||||
| {value_serializer, fun((any()) -> iodata())}.
|
||||
%% |key |description |
|
||||
%% |:-- |:---------- |
|
||||
%% |key_type | Specify the type of the key in {@link data/0}. Default value is `string'. |
|
||||
%% |raise_on_context_miss| If key exists in template does not exist in data, it will throw an exception (error).|
|
||||
%% |escape_fun | Specify your own escape function. |
|
||||
%% |value_serializer | specify how terms are converted to iodata when templating. |
|
||||
|
||||
-type render_option() :: compile_option() | parse_option().
|
||||
%% @see compile_option/0
|
||||
%% @see parse_option/0
|
||||
|
||||
-type option() :: compile_option().
|
||||
%% This type has been deprecated since 1.6.0. It will remove in 2.0.0.
|
||||
%% @see compile_option/0
|
||||
|
||||
-type data() :: term().
|
||||
%% Beginners should consider {@link data/0} as {@link recursive_data/0}.
|
||||
%% By specifying options, the type are greatly relaxed and equal to `term/0'.
|
||||
%%
|
||||
%% @see render/2
|
||||
%% @see compile/2
|
||||
|
||||
-type data_key() :: atom() | binary() | string().
|
||||
%% You can choose one from these as the type of key in {@link recursive_data/0}.
|
||||
%% The default is `string/0'.
|
||||
%% If you want to change this, you need to specify `key_type' in {@link compile_option/0}.
|
||||
|
||||
-ifdef(namespaced_types).
|
||||
-type recursive_data() :: #{data_key() => term()} | [{data_key(), term()}].
|
||||
-else.
|
||||
-type recursive_data() :: [{data_key(), term()}].
|
||||
-endif.
|
||||
%% It is a part of {@link data/0} that can have child elements.
|
||||
|
||||
-type endtag() :: {endtag, {state(), [key()], LastTagSize :: non_neg_integer(), Rest :: binary(), Result :: [tag()]}}.
|
||||
|
||||
%%----------------------------------------------------------------------------------------------------------------------
|
||||
%% Exported Functions
|
||||
%%----------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
%% @equiv render(Bin, Data, [])
|
||||
-spec render(binary(), data()) -> binary().
|
||||
render(Bin, Data) ->
|
||||
render(Bin, Data, []).
|
||||
|
||||
%% @equiv compile(parse_binary(Bin), Data, Options)
|
||||
-spec render(binary(), data(), [render_option()]) -> binary().
|
||||
render(Bin, Data, Options) ->
|
||||
{ParseOptions, CompileOptions}
|
||||
= lists:partition(fun(X) ->
|
||||
lists:member(?IIF(is_tuple(X), element(1, X), X), ?PARSE_OPTIONS)
|
||||
end, Options),
|
||||
compile(parse_binary(Bin, ParseOptions), Data, CompileOptions).
|
||||
|
||||
%% @equiv parse_binary(Bin, [])
|
||||
-spec parse_binary(binary()) -> template().
|
||||
parse_binary(Bin) when is_binary(Bin) ->
|
||||
parse_binary(Bin, []).
|
||||
|
||||
%% @doc Create a {@link template/0} from a binary.
|
||||
-spec parse_binary(binary(), [parse_option()]) -> template().
|
||||
parse_binary(Bin, Options) ->
|
||||
{State, Data} = parse(#state{}, Bin),
|
||||
parse_remaining_partials(State, #?MODULE{data = Data}, Options).
|
||||
|
||||
%% @equiv parse_file(Filename, [])
|
||||
-spec parse_file(file:filename_all()) -> template().
|
||||
parse_file(Filename) ->
|
||||
parse_file(Filename, []).
|
||||
|
||||
%% @doc Create a {@link template/0} from a file.
|
||||
-spec parse_file(file:filename_all(), [parse_option()]) -> template().
|
||||
parse_file(Filename, Options) ->
|
||||
State = #state{dirname = filename:dirname(Filename)},
|
||||
case file:read_file(Filename) of
|
||||
{ok, Bin} ->
|
||||
{State1, Data} = parse(State, Bin),
|
||||
Template = case to_binary(filename:extension(Filename)) of
|
||||
<<".mustache">> = Ext -> #?MODULE{partials = [{filename:basename(Filename, Ext), Data}], data = Data};
|
||||
_ -> #?MODULE{data = Data}
|
||||
end,
|
||||
parse_remaining_partials(State1, Template, Options);
|
||||
_ ->
|
||||
error(?FILE_ERROR, [Filename, Options])
|
||||
end.
|
||||
|
||||
%% @equiv compile(Template, Data, [])
|
||||
-spec compile(template(), data()) -> binary().
|
||||
compile(Template, Data) ->
|
||||
compile(Template, Data, []).
|
||||
|
||||
%% @doc Embed the data in the template.
|
||||
%%
|
||||
%% ```
|
||||
%% 1> Template = bbmustache:parse_binary(<<"{{name}}">>).
|
||||
%% 2> bbmustache:compile(Template, #{"name" => "Alice"}).
|
||||
%% <<"Alice">>
|
||||
%% '''
|
||||
%% Data support an associative array or a map. <br />
|
||||
%% All keys MUST be same type.
|
||||
-spec compile(template(), data(), [compile_option()]) -> binary().
|
||||
compile(#?MODULE{data = Tags} = T, Data, Options) ->
|
||||
Ret = compile_impl(Tags, Data, [], T#?MODULE{options = Options, data = []}),
|
||||
iolist_to_binary(lists:reverse(Ret)).
|
||||
|
||||
%% @doc Default value serializer for templtated values
|
||||
-spec default_value_serializer(number() | binary() | string() | atom()) -> iodata().
|
||||
default_value_serializer(Integer) when is_integer(Integer) ->
|
||||
list_to_binary(integer_to_list(Integer));
|
||||
default_value_serializer(Float) when is_float(Float) ->
|
||||
%% NOTE: It is the same behaviour as io_lib:format("~p", [Float]), but it is fast than.
|
||||
%% http://www.cs.indiana.edu/~dyb/pubs/FP-Printing-PLDI96.pdf
|
||||
io_lib_format:fwrite_g(Float);
|
||||
default_value_serializer(Atom) when is_atom(Atom) ->
|
||||
list_to_binary(atom_to_list(Atom));
|
||||
default_value_serializer(X) when is_map(X); is_tuple(X) ->
|
||||
error(unsupported_term, [X]);
|
||||
default_value_serializer(X) ->
|
||||
X.
|
||||
|
||||
%% @doc Default partial file reader
|
||||
-spec default_partial_file_reader(binary(), binary()) -> {ok, binary()} | {error, Reason :: term()}.
|
||||
default_partial_file_reader(Dirname, Key) ->
|
||||
Filename0 = <<Key/binary, ".mustache">>,
|
||||
Filename = ?IIF(Dirname =:= <<>>, Filename0, filename:join([Dirname, Filename0])),
|
||||
file:read_file(Filename).
|
||||
|
||||
%%----------------------------------------------------------------------------------------------------------------------
|
||||
%% Internal Function
|
||||
%%----------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
%% @doc {@link compile/2}
|
||||
%%
|
||||
%% ATTENTION: The result is a list that is inverted.
|
||||
-spec compile_impl(Template :: [tag()], data(), Result :: iodata(), template()) -> iodata().
|
||||
compile_impl([], _, Result, _) ->
|
||||
Result;
|
||||
compile_impl([{n, Keys} | T], Data, Result, State) ->
|
||||
ValueSerializer = proplists:get_value(value_serializer, State#?MODULE.options, fun default_value_serializer/1),
|
||||
Value = iolist_to_binary(ValueSerializer(get_data_recursive(Keys, Data, <<>>, State))),
|
||||
EscapeFun = proplists:get_value(escape_fun, State#?MODULE.options, fun escape/1),
|
||||
compile_impl(T, Data, ?ADD(EscapeFun(Value), Result), State);
|
||||
compile_impl([{'&', Keys} | T], Data, Result, State) ->
|
||||
ValueSerializer = proplists:get_value(value_serializer, State#?MODULE.options, fun default_value_serializer/1),
|
||||
compile_impl(T, Data, ?ADD(ValueSerializer(get_data_recursive(Keys, Data, <<>>, State)), Result), State);
|
||||
compile_impl([{'#', Keys, Tags, Source} | T], Data, Result, State) ->
|
||||
Value = get_data_recursive(Keys, Data, false, State),
|
||||
NestedState = State#?MODULE{context_stack = [Data | State#?MODULE.context_stack]},
|
||||
case is_recursive_data(Value) of
|
||||
true ->
|
||||
compile_impl(T, Data, compile_impl(Tags, Value, Result, NestedState), State);
|
||||
_ when is_list(Value) ->
|
||||
compile_impl(T, Data, lists:foldl(fun(X, Acc) -> compile_impl(Tags, X, Acc, NestedState) end,
|
||||
Result, Value), State);
|
||||
_ when Value =:= false ->
|
||||
compile_impl(T, Data, Result, State);
|
||||
_ when is_function(Value, 2) ->
|
||||
Ret = Value(Source, fun(Text) -> render(Text, Data, State#?MODULE.options) end),
|
||||
compile_impl(T, Data, ?ADD(Ret, Result), State);
|
||||
_ ->
|
||||
compile_impl(T, Data, compile_impl(Tags, Data, Result, State), State)
|
||||
end;
|
||||
compile_impl([{'^', Keys, Tags} | T], Data, Result, State) ->
|
||||
Value = get_data_recursive(Keys, Data, false, State),
|
||||
case Value =:= [] orelse Value =:= false of
|
||||
true -> compile_impl(T, Data, compile_impl(Tags, Data, Result, State), State);
|
||||
false -> compile_impl(T, Data, Result, State)
|
||||
end;
|
||||
compile_impl([{'>', Key, Indent} | T], Data, Result0, #?MODULE{partials = Partials} = State) ->
|
||||
case proplists:get_value(Key, Partials) of
|
||||
undefined ->
|
||||
case ?RAISE_ON_CONTEXT_MISS_ENABLED(State#?MODULE.options) of
|
||||
true -> error(?CONTEXT_MISSING_ERROR({?FILE_ERROR, Key}));
|
||||
false -> compile_impl(T, Data, Result0, State)
|
||||
end;
|
||||
PartialT ->
|
||||
Indents = State#?MODULE.indents ++ [Indent],
|
||||
Result1 = compile_impl(PartialT, Data, [Indent | Result0], State#?MODULE{indents = Indents}),
|
||||
compile_impl(T, Data, Result1, State)
|
||||
end;
|
||||
compile_impl([B1 | [_|_] = T], Data, Result, #?MODULE{indents = Indents} = State) when Indents =/= [] ->
|
||||
%% NOTE: indent of partials
|
||||
case byte_size(B1) > 0 andalso binary:last(B1) of
|
||||
$\n -> compile_impl(T, Data, [Indents, B1 | Result], State);
|
||||
_ -> compile_impl(T, Data, [B1 | Result], State)
|
||||
end;
|
||||
compile_impl([Bin | T], Data, Result, State) ->
|
||||
compile_impl(T, Data, [Bin | Result], State).
|
||||
|
||||
%% @doc Parse remaining partials in State. It returns {@link template/0}.
|
||||
-spec parse_remaining_partials(state(), template(), [parse_option()]) -> template().
|
||||
parse_remaining_partials(#state{partials = []}, Template = #?MODULE{}, _Options) ->
|
||||
Template;
|
||||
parse_remaining_partials(State = #state{partials = [P | PartialKeys]}, Template = #?MODULE{partials = Partials}, Options) ->
|
||||
case proplists:is_defined(P, Partials) of
|
||||
true -> parse_remaining_partials(State#state{partials = PartialKeys}, Template, Options);
|
||||
false ->
|
||||
FileReader = proplists:get_value(partial_file_reader, Options, fun default_partial_file_reader/2),
|
||||
Dirname = State#state.dirname,
|
||||
case FileReader(Dirname, P) of
|
||||
{ok, Input} ->
|
||||
{State1, Data} = parse(State, Input),
|
||||
parse_remaining_partials(State1, Template#?MODULE{partials = [{P, Data} | Partials]}, Options);
|
||||
{error, Reason} ->
|
||||
case ?RAISE_ON_PARTIAL_MISS_ENABLED(Options) of
|
||||
true -> error({?FILE_ERROR, P, Reason});
|
||||
false -> parse_remaining_partials(State#state{partials = PartialKeys},
|
||||
Template#?MODULE{partials = [P | Partials]}, Options)
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
%% @doc Analyze the syntax of the mustache.
|
||||
-spec parse(state(), binary()) -> {#state{}, [tag()]}.
|
||||
parse(State0, Bin) ->
|
||||
case parse1(State0, Bin, []) of
|
||||
{endtag, {_, Keys, _, _, _}} ->
|
||||
error({?PARSE_ERROR, {section_is_incorrect, binary_join(Keys, <<".">>)}});
|
||||
{#state{partials = Partials} = State, Tags} ->
|
||||
{State#state{partials = lists:usort(Partials), start = ?START_TAG, stop = ?STOP_TAG},
|
||||
lists:reverse(Tags)}
|
||||
end.
|
||||
|
||||
%% @doc Part of the `parse/1'
|
||||
%%
|
||||
%% ATTENTION: The result is a list that is inverted.
|
||||
-spec parse1(state(), Input :: binary(), Result :: [tag()]) -> {state(), [tag()]} | endtag().
|
||||
parse1(#state{start = Start} = State, Bin, Result) ->
|
||||
case binary:match(Bin, [Start, <<"\n">>]) of
|
||||
nomatch -> {State, ?ADD(Bin, Result)};
|
||||
{S, L} ->
|
||||
Pos = S + L,
|
||||
B2 = binary:part(Bin, Pos, byte_size(Bin) - Pos),
|
||||
case binary:at(Bin, S) of
|
||||
$\n -> parse1(State#state{standalone = true}, B2, ?ADD(binary:part(Bin, 0, Pos), Result)); % \n
|
||||
_ -> parse2(State, split_tag(State, Bin), Result)
|
||||
end
|
||||
end.
|
||||
|
||||
%% @doc Part of the `parse/1'
|
||||
%%
|
||||
%% ATTENTION: The result is a list that is inverted.
|
||||
-spec parse2(state(), iolist(), Result :: [tag()]) -> {state(), [tag()]} | endtag().
|
||||
parse2(State, [B1, B2, B3], Result) ->
|
||||
case remove_space_from_head(B2) of
|
||||
<<T, Tag/binary>> when T =:= $&; T =:= ${ ->
|
||||
parse1(State#state{standalone = false}, B3, [{'&', keys(Tag)} | ?ADD(B1, Result)]);
|
||||
<<T, Tag/binary>> when T =:= $#; T =:= $^ ->
|
||||
parse_loop(State, ?IIF(T =:= $#, '#', '^'), keys(Tag), B3, [B1 | Result]);
|
||||
<<"=", Tag0/binary>> ->
|
||||
Tag1 = remove_space_from_tail(Tag0),
|
||||
Size = byte_size(Tag1) - 1,
|
||||
case Size >= 0 andalso Tag1 of
|
||||
<<Tag2:Size/binary, "=">> -> parse_delimiter(State, Tag2, B3, [B1 | Result]);
|
||||
_ -> error({?PARSE_ERROR, {unsupported_tag, <<"=", Tag0/binary>>}})
|
||||
end;
|
||||
<<"!", _/binary>> ->
|
||||
parse3(State, B3, [B1 | Result]);
|
||||
<<"/", Tag/binary>> ->
|
||||
EndTagSize = byte_size(B2) + byte_size(State#state.start) + byte_size(State#state.stop),
|
||||
{endtag, {State, keys(Tag), EndTagSize, B3, [B1 | Result]}};
|
||||
<<">", Tag/binary>> ->
|
||||
parse_jump(State, filename_key(Tag), B3, [B1 | Result]);
|
||||
Tag ->
|
||||
parse1(State#state{standalone = false}, B3, [{n, keys(Tag)} | ?ADD(B1, Result)])
|
||||
end;
|
||||
parse2(_, _, _) ->
|
||||
error({?PARSE_ERROR, unclosed_tag}).
|
||||
|
||||
%% @doc Part of the `parse/1'
|
||||
%%
|
||||
%% it is end processing of tag that need to be considered the standalone.
|
||||
-spec parse3(#state{}, binary(), [tag()]) -> {state(), [tag()]} | endtag().
|
||||
parse3(State0, Post0, [Tag | Result0]) when is_tuple(Tag) ->
|
||||
{State1, _, Post1, Result1} = standalone(State0, Post0, Result0),
|
||||
parse1(State1, Post1, [Tag | Result1]);
|
||||
parse3(State0, Post0, Result0) ->
|
||||
{State1, _, Post1, Result1} = standalone(State0, Post0, Result0),
|
||||
parse1(State1, Post1, Result1).
|
||||
|
||||
%% @doc Loop processing part of the `parse/1'
|
||||
%%
|
||||
%% `{{# Tag}}' or `{{^ Tag}}' corresponds to this.
|
||||
-spec parse_loop(state(), '#' | '^', [key()], Input :: binary(), Result :: [tag()]) -> {state(), [tag()]} | endtag().
|
||||
parse_loop(State0, Mark, Keys, Input0, Result0) ->
|
||||
{State1, _, Input1, Result1} = standalone(State0, Input0, Result0),
|
||||
case parse1(State1, Input1, []) of
|
||||
{endtag, {State2, Keys, LastTagSize, Rest0, LoopResult0}} ->
|
||||
{State3, _, Rest1, LoopResult1} = standalone(State2, Rest0, LoopResult0),
|
||||
case Mark of
|
||||
'#' -> Source = binary:part(Input1, 0, byte_size(Input1) - byte_size(Rest0) - LastTagSize),
|
||||
parse1(State3, Rest1, [{'#', Keys, lists:reverse(LoopResult1), Source} | Result1]);
|
||||
'^' -> parse1(State3, Rest1, [{'^', Keys, lists:reverse(LoopResult1)} | Result1])
|
||||
end;
|
||||
{endtag, {_, OtherKeys, _, _, _}} ->
|
||||
error({?PARSE_ERROR, {section_is_incorrect, binary_join(OtherKeys, <<".">>)}});
|
||||
_ ->
|
||||
error({?PARSE_ERROR, {section_end_tag_not_found, <<"/", (binary_join(Keys, <<".">>))/binary>>}})
|
||||
end.
|
||||
|
||||
%% @doc Endtag part of the `parse/1'
|
||||
-spec parse_jump(state(), Tag :: binary(), NextBin :: binary(), Result :: [tag()]) -> {state(), [tag()]} | endtag().
|
||||
parse_jump(State0, Tag, NextBin0, Result0) ->
|
||||
{State1, Indent, NextBin1, Result1} = standalone(State0, NextBin0, Result0),
|
||||
State2 = State1#state{partials = [Tag | State1#state.partials]},
|
||||
parse1(State2, NextBin1, [{'>', Tag, Indent} | Result1]).
|
||||
|
||||
%% @doc Update delimiter part of the `parse/1'
|
||||
%%
|
||||
%% ParseDelimiterBin :: e.g. `{{=%% %%=}}' -> `%% %%'
|
||||
-spec parse_delimiter(state(), ParseDelimiterBin :: binary(), NextBin :: binary(), Result :: [tag()]) -> {state(), [tag()]} | endtag().
|
||||
parse_delimiter(State0, ParseDelimiterBin, NextBin, Result) ->
|
||||
case binary:match(ParseDelimiterBin, <<"=">>) of
|
||||
nomatch ->
|
||||
case [X || X <- binary:split(ParseDelimiterBin, <<" ">>, [global]), X =/= <<>>] of
|
||||
[Start, Stop] -> parse3(State0#state{start = Start, stop = Stop}, NextBin, Result);
|
||||
_ -> error({?PARSE_ERROR, delimiters_may_not_contain_whitespaces})
|
||||
end;
|
||||
_ ->
|
||||
error({?PARSE_ERROR, delimiters_may_not_contain_equals})
|
||||
end.
|
||||
|
||||
%% @doc Split by the tag, it returns a list of the split binary.
|
||||
%%
|
||||
%% e.g.
|
||||
%% ```
|
||||
%% 1> split_tag(State, <<"...{{hoge}}...">>).
|
||||
%% [<<"...">>, <<"hoge">>, <<"...">>]
|
||||
%%
|
||||
%% 2> split_tag(State, <<"...{{hoge ...">>).
|
||||
%% [<<"...">>, <<"hoge ...">>]
|
||||
%%
|
||||
%% 3> split_tag(State, <<"...">>)
|
||||
%% [<<"...">>]
|
||||
%% '''
|
||||
-spec split_tag(state(), binary()) -> [binary(), ...].
|
||||
split_tag(#state{start = StartDelimiter, stop = StopDelimiter}, Bin) ->
|
||||
case binary:match(Bin, StartDelimiter) of
|
||||
nomatch ->
|
||||
[Bin];
|
||||
{StartPos, StartDelimiterLen} ->
|
||||
PosLimit = byte_size(Bin) - StartDelimiterLen,
|
||||
ShiftNum = while({true, StartPos + 1},
|
||||
fun(Pos) ->
|
||||
?IIF(Pos =< PosLimit
|
||||
andalso binary:part(Bin, Pos, StartDelimiterLen) =:= StartDelimiter,
|
||||
{true, Pos + 1}, {false, Pos})
|
||||
end) - StartPos - 1,
|
||||
{PreTag, X} = split_binary(Bin, StartPos + ShiftNum),
|
||||
Tag0 = part(X, StartDelimiterLen, 0),
|
||||
case binary:split(Tag0, StopDelimiter) of
|
||||
[_] -> [PreTag, Tag0]; % not found.
|
||||
[Tag, Rest] ->
|
||||
IncludeStartDelimiterTag = binary:part(X, 0, byte_size(Tag) + StartDelimiterLen),
|
||||
E = ?IIF(repeatedly_binary(StopDelimiter, $}),
|
||||
?IIF(byte_size(Rest) > 0 andalso binary:first(Rest) =:= $}, 1, 0),
|
||||
?IIF(byte_size(Tag) > 0 andalso binary:last(Tag) =:= $}, -1, 0)),
|
||||
S = ?IIF(repeatedly_binary(StartDelimiter, ${),
|
||||
?IIF(ShiftNum > 0, -1, 0),
|
||||
?IIF(byte_size(Tag) > 0 andalso binary:first(Tag) =:= ${, 1, 0)),
|
||||
case E =:= 0 orelse S =:= 0 of
|
||||
true -> % {{ ... }}
|
||||
[PreTag, Tag, Rest];
|
||||
false -> % {{{ ... }}}
|
||||
[part(PreTag, 0, min(0, S)),
|
||||
part(IncludeStartDelimiterTag, max(0, S) + StartDelimiterLen - 1, min(0, E)),
|
||||
part(Rest, max(0, E), 0)]
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
%% @doc if it is standalone line, remove spaces from edge.
|
||||
-spec standalone(#state{}, binary(), [tag()]) -> {#state{}, StashPre :: binary(), Post :: binary(), [tag()]}.
|
||||
standalone(#state{standalone = false} = State, Post, [Pre | Result]) ->
|
||||
{State, <<>>, Post, ?ADD(Pre, Result)};
|
||||
standalone(#state{standalone = false} = State, Post, Result) ->
|
||||
{State, <<>>, Post, Result};
|
||||
standalone(State, Post0, Result0) ->
|
||||
{Pre, Result1} = case Result0 =/= [] andalso hd(Result0) of
|
||||
Pre0 when is_binary(Pre0) -> {Pre0, tl(Result0)};
|
||||
_ -> {<<>>, Result0}
|
||||
end,
|
||||
case remove_indent_from_head(Pre) =:= <<>> andalso remove_indent_from_head(Post0) of
|
||||
<<"\r\n", Post1/binary>> ->
|
||||
{State, Pre, Post1, Result1};
|
||||
<<"\n", Post1/binary>> ->
|
||||
{State, Pre, Post1, Result1};
|
||||
<<>> ->
|
||||
{State, Pre, <<>>, Result1};
|
||||
_ ->
|
||||
{State#state{standalone = false}, <<>>, Post0, ?ADD(Pre, Result1)}
|
||||
end.
|
||||
|
||||
%% @doc If the binary is repeatedly the character, return true. Otherwise, return false.
|
||||
-spec repeatedly_binary(binary(), byte()) -> boolean().
|
||||
repeatedly_binary(<<X, Rest/binary>>, X) ->
|
||||
repeatedly_binary(Rest, X);
|
||||
repeatedly_binary(<<>>, _) ->
|
||||
true;
|
||||
repeatedly_binary(_, _) ->
|
||||
false.
|
||||
|
||||
%% @doc During the first element of the tuple is true, to perform the function repeatedly.
|
||||
-spec while({boolean(), term()}, fun((term()) -> {boolean(), term()})) -> term().
|
||||
while({true, Value}, Fun) ->
|
||||
while(Fun(Value), Fun);
|
||||
while({false, Value}, _Fun) ->
|
||||
Value.
|
||||
|
||||
%% @equiv binary:part(X, Start, byte_size(X) - Start + End)
|
||||
-spec part(binary(), non_neg_integer(), 0 | neg_integer()) -> binary().
|
||||
part(X, Start, End) when End =< 0 ->
|
||||
binary:part(X, Start, byte_size(X) - Start + End).
|
||||
|
||||
%% @doc binary to keys
|
||||
-spec keys(binary()) -> [key()].
|
||||
keys(Bin0) ->
|
||||
Bin1 = << <<X:8>> || <<X:8>> <= Bin0, X =/= $ >>,
|
||||
case Bin1 =:= <<>> orelse Bin1 =:= <<".">> of
|
||||
true -> [Bin1];
|
||||
false -> [X || X <- binary:split(Bin1, <<".">>, [global]), X =/= <<>>]
|
||||
end.
|
||||
|
||||
%% @doc binary to filename key
|
||||
-spec filename_key(binary()) -> key().
|
||||
filename_key(Bin) ->
|
||||
remove_space_from_tail(remove_space_from_head(Bin)).
|
||||
|
||||
%% @doc Function for binary like the `string:join/2'.
|
||||
-spec binary_join(BinaryList :: [binary()], Separator :: binary()) -> binary().
|
||||
binary_join([], _) ->
|
||||
<<>>;
|
||||
binary_join(Bins, Sep) ->
|
||||
[Hd | Tl] = [ [Sep, B] || B <- Bins ],
|
||||
iolist_to_binary([tl(Hd) | Tl]).
|
||||
|
||||
%% @doc Remove the space from the head.
|
||||
-spec remove_space_from_head(binary()) -> binary().
|
||||
remove_space_from_head(<<" ", Rest/binary>>) -> remove_space_from_head(Rest);
|
||||
remove_space_from_head(Bin) -> Bin.
|
||||
|
||||
%% @doc Remove the indent from the head.
|
||||
-spec remove_indent_from_head(binary()) -> binary().
|
||||
remove_indent_from_head(<<X:8, Rest/binary>>) when X =:= $\t; X =:= $ ->
|
||||
remove_indent_from_head(Rest);
|
||||
remove_indent_from_head(Bin) ->
|
||||
Bin.
|
||||
|
||||
%% @doc Remove the space from the tail.
|
||||
-spec remove_space_from_tail(binary()) -> binary().
|
||||
remove_space_from_tail(<<>>) -> <<>>;
|
||||
remove_space_from_tail(Bin) ->
|
||||
PosList = binary:matches(Bin, <<" ">>),
|
||||
LastPos = remove_space_from_tail_impl(lists:reverse(PosList), byte_size(Bin)),
|
||||
binary:part(Bin, 0, LastPos).
|
||||
|
||||
%% @see remove_space_from_tail/1
|
||||
-spec remove_space_from_tail_impl([{non_neg_integer(), pos_integer()}], non_neg_integer()) -> non_neg_integer().
|
||||
remove_space_from_tail_impl([{X, Y} | T], Size) when Size =:= X + Y ->
|
||||
remove_space_from_tail_impl(T, X);
|
||||
remove_space_from_tail_impl(_, Size) ->
|
||||
Size.
|
||||
|
||||
%% @doc string or binary to binary
|
||||
-spec to_binary(binary() | [byte()]) -> binary().
|
||||
to_binary(Bin) when is_binary(Bin) ->
|
||||
Bin;
|
||||
to_binary(Bytes) when is_list(Bytes) ->
|
||||
list_to_binary(Bytes).
|
||||
|
||||
%% @doc HTML Escape
|
||||
-spec escape(binary()) -> binary().
|
||||
escape(Bin) ->
|
||||
<< <<(escape_char(X))/binary>> || <<X:8>> <= Bin >>.
|
||||
|
||||
%% @doc escape a character if needed.
|
||||
-spec escape_char(byte()) -> <<_:8, _:_*8>>.
|
||||
escape_char($<) -> <<"<">>;
|
||||
escape_char($>) -> <<">">>;
|
||||
escape_char($&) -> <<"&">>;
|
||||
escape_char($") -> <<""">>;
|
||||
escape_char(C) -> <<C:8>>.
|
||||
|
||||
%% @doc convert to {@link data_key/0} from binary.
|
||||
-spec convert_keytype(key(), template()) -> data_key().
|
||||
convert_keytype(KeyBin, #?MODULE{options = Options}) ->
|
||||
case proplists:get_value(key_type, Options, string) of
|
||||
atom ->
|
||||
try binary_to_existing_atom(KeyBin, utf8) of
|
||||
Atom -> Atom
|
||||
catch
|
||||
_:_ -> <<" ">> % It is not always present in data/0
|
||||
end;
|
||||
string -> binary_to_list(KeyBin);
|
||||
binary -> KeyBin
|
||||
end.
|
||||
|
||||
%% @doc fetch the value of the specified `Keys' from {@link data/0}
|
||||
%%
|
||||
%% - If `Keys' is `[<<".">>]', it returns `Data'.
|
||||
%% - If raise_on_context_miss enabled, it raise an exception when missing `Keys'. Otherwise, it returns `Default'.
|
||||
-spec get_data_recursive([key()], data(), Default :: term(), template()) -> term().
|
||||
get_data_recursive(Keys, Data, Default, Template) ->
|
||||
case get_data_recursive_impl(Keys, Data, Template) of
|
||||
{ok, Term} -> Term;
|
||||
error ->
|
||||
case ?RAISE_ON_CONTEXT_MISS_ENABLED(Template#?MODULE.options) of
|
||||
true -> error(?CONTEXT_MISSING_ERROR({key, binary_join(Keys, <<".">>)}));
|
||||
false -> Default
|
||||
end
|
||||
end.
|
||||
|
||||
%% @see get_data_recursive/4
|
||||
-spec get_data_recursive_impl([key()], data(), template()) -> {ok, term()} | error.
|
||||
get_data_recursive_impl([], Data, _) ->
|
||||
{ok, Data};
|
||||
get_data_recursive_impl([<<".">>], Data, _) ->
|
||||
{ok, Data};
|
||||
get_data_recursive_impl([Key | RestKey] = Keys, Data, #?MODULE{context_stack = Stack} = State) ->
|
||||
case is_recursive_data(Data) andalso find_data(convert_keytype(Key, State), Data) of
|
||||
{ok, ChildData} ->
|
||||
get_data_recursive_impl(RestKey, ChildData, State#?MODULE{context_stack = []});
|
||||
_ when Stack =:= [] ->
|
||||
error;
|
||||
_ ->
|
||||
get_data_recursive_impl(Keys, hd(Stack), State#?MODULE{context_stack = tl(Stack)})
|
||||
end.
|
||||
|
||||
%% @doc find the value of the specified key from {@link recursive_data/0}
|
||||
-spec find_data(data_key(), recursive_data() | term()) -> {ok, Value :: term()} | error.
|
||||
-ifdef(namespaced_types).
|
||||
find_data(Key, Map) when is_map(Map) ->
|
||||
maps:find(Key, Map);
|
||||
find_data(Key, AssocList) when is_list(AssocList) ->
|
||||
case proplists:lookup(Key, AssocList) of
|
||||
none -> error;
|
||||
{_, V} -> {ok, V}
|
||||
end;
|
||||
find_data(_, _) ->
|
||||
error.
|
||||
-else.
|
||||
find_data(Key, AssocList) ->
|
||||
case proplists:lookup(Key, AssocList) of
|
||||
none -> error;
|
||||
{_, V} -> {ok, V}
|
||||
end;
|
||||
find_data(_, _) ->
|
||||
error.
|
||||
-endif.
|
||||
|
||||
%% @doc When the value is {@link recursive_data/0}, it returns true. Otherwise it returns false.
|
||||
-spec is_recursive_data(recursive_data() | term()) -> boolean().
|
||||
-ifdef(namespaced_types).
|
||||
is_recursive_data([Tuple | _]) when is_tuple(Tuple) -> true;
|
||||
is_recursive_data(V) when is_map(V) -> true;
|
||||
is_recursive_data(_) -> false.
|
||||
-else.
|
||||
is_recursive_data([Tuple | _]) when is_tuple(Tuple) -> true;
|
||||
is_recursive_data(_) -> false.
|
||||
-endif.
|
||||
|
||||
%%----------------------------------------------------------------------------------------------------------------------
|
||||
%% Escriptize
|
||||
%%----------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-ifdef(bbmustache_escriptize).
|
||||
|
||||
%% escript entry point
|
||||
-spec main([string()]) -> ok.
|
||||
main(Args) ->
|
||||
%% Load the application to be able to access its information
|
||||
%% (e.g. --version option)
|
||||
_ = application:load(bbmustache),
|
||||
try case getopt:parse(option_spec_list(), Args) of
|
||||
{ok, {Options, Commands}} -> process_commands(Options, Commands);
|
||||
{error, Reason} -> throw(getopt:format_error(option_spec_list(), Reason))
|
||||
end catch
|
||||
throw:ThrowReason ->
|
||||
ok = io:format(standard_error, "ERROR: ~s~n", [ThrowReason]),
|
||||
halt(1)
|
||||
end.
|
||||
|
||||
%% Processes command-line commands
|
||||
-spec process_commands([getopt:option()], [string()]) -> ok.
|
||||
process_commands(Opts, Cmds) ->
|
||||
HasHelp = proplists:is_defined(help, Opts),
|
||||
HasVersion = proplists:is_defined(version, Opts),
|
||||
if
|
||||
HasHelp -> print_help(standard_io);
|
||||
HasVersion -> print_version();
|
||||
Opts =:= [], Cmds =:= [] -> print_help(standard_error);
|
||||
true -> process_render(Opts, Cmds)
|
||||
end.
|
||||
|
||||
%% Returns command-line options.
|
||||
-spec option_spec_list() -> [getopt:option_spec()].
|
||||
option_spec_list() ->
|
||||
[
|
||||
%% {Name, ShortOpt, LongOpt, ArgSpec, HelpMsg}
|
||||
{help, $h, "help", undefined, "Show this help information."},
|
||||
{version, $v, "version", undefined, "Output the current bbmustache version."},
|
||||
{key_type, $k, "key-type", atom, "Key type (atom | binary | string)."},
|
||||
{data_file, $d, "data-file", string, "Erlang terms file."}
|
||||
].
|
||||
|
||||
%% Processes render
|
||||
-spec process_render([getopt:option()], [string()]) -> ok.
|
||||
process_render(Opts, TemplateFileNames) ->
|
||||
DataFileNames = proplists:get_all_values(data_file, Opts),
|
||||
Data = lists:foldl(fun(Filename, Acc) -> read_data_files(Filename) ++ Acc end, [], DataFileNames),
|
||||
KeyType = proplists:get_value(key_type, Opts, string),
|
||||
RenderOpts = [{key_type, KeyType}],
|
||||
lists:foreach(fun(TemplateFileName) ->
|
||||
try parse_file(TemplateFileName) of
|
||||
Template -> io:format(compile(Template, Data, RenderOpts))
|
||||
catch
|
||||
error:?FILE_ERROR ->
|
||||
throw(io_lib:format("~s is unable to read.", [TemplateFileName]))
|
||||
end
|
||||
end, TemplateFileNames).
|
||||
|
||||
%% Prints usage/help.
|
||||
-spec print_help(getopt:output_stream()) -> ok.
|
||||
print_help(OutputStream) ->
|
||||
getopt:usage(option_spec_list(), escript:script_name(), "template_files ...", OutputStream).
|
||||
|
||||
%% Prints version.
|
||||
-spec print_version() -> ok.
|
||||
print_version() ->
|
||||
Vsn = case application:get_key(bbmustache, vsn) of
|
||||
undefined -> throw("vsn can not read from bbmustache.app");
|
||||
{ok, Vsn0} -> Vsn0
|
||||
end,
|
||||
AdditionalVsn = case application:get_env(bbmustache, git_vsn) of
|
||||
{ok, {_Tag, Count, [$g | GitHash]}} -> "+build." ++ Count ++ ".ref" ++ GitHash;
|
||||
_ -> ""
|
||||
end,
|
||||
%% e.g. bbmustache v1.9.0+build.5.ref90a0afd4f2
|
||||
io:format("bbmustache v~s~s~n", [Vsn, AdditionalVsn]).
|
||||
|
||||
%% Read the data-file and return terms.
|
||||
-spec read_data_files(file:filename_all()) -> [term()].
|
||||
read_data_files(Filename) ->
|
||||
case file:consult(Filename) of
|
||||
{ok, [Map]} when is_map(Map) ->
|
||||
maps:to_list(Map);
|
||||
{ok, Terms0} when is_list(Terms0) ->
|
||||
Terms = case Terms0 of
|
||||
[Term] when is_list(Term) -> Term;
|
||||
_ -> Terms0
|
||||
end,
|
||||
lists:foldl(fun(Term, Acc) when is_tuple(Term) ->
|
||||
[Term | Acc];
|
||||
(InclusionFilename, Acc) when is_list(InclusionFilename) ->
|
||||
Path = filename:join(filename:dirname(Filename), InclusionFilename),
|
||||
read_data_files(Path) ++ Acc;
|
||||
(Term, _Acc) ->
|
||||
throw(io_lib:format("~s have unsupported format terms. (~p)", [Filename, Term]))
|
||||
end, [], Terms);
|
||||
{error, Reason} ->
|
||||
throw(io_lib:format("~s is unable to read. (~p)", [Filename, Reason]))
|
||||
end.
|
||||
|
||||
-endif.
|
|
@ -0,0 +1,29 @@
|
|||
Copyright (c) 2015, Benoit Chesneau <bchesneau@gmail.com>.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
* The names of its contributors may not be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,33 @@
|
|||
# [certifi](https://github.com/certifi/erlang-certifi)
|
||||
|
||||
[![Build Status](https://github.com/certifi/erlang-certifi/workflows/build/badge.svg)](https://github.com/certifi/erlang-certifi)
|
||||
|
||||
This Erlang library contains a CA bundle that you can reference in your Erlang
|
||||
application. This is useful for systems that do not have CA bundles that
|
||||
Erlang can find itself, or where a uniform set of CAs is valuable.
|
||||
|
||||
This an Erlang specific port of [certifi](https://certifi.io/). The CA bundle
|
||||
is derived from Mozilla's canonical set.
|
||||
|
||||
## Usage
|
||||
|
||||
```erlang
|
||||
CaCerts = certifi:cacerts(),
|
||||
SslOptions = [{verify, verify_peer},
|
||||
{depth, 99},
|
||||
{cacerts, CaCerts}],
|
||||
ssl:connect( "example.com", 443, SslOptions ).
|
||||
```
|
||||
|
||||
|
||||
You can also retrieve the path to the file and load it by yourself if needed:
|
||||
|
||||
```erlang
|
||||
Path = certifi:cacertfile().
|
||||
```
|
||||
|
||||
## Build & test
|
||||
|
||||
```shell
|
||||
$ rebar3 eunit
|
||||
```
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
{erl_first_files, ["src/certifi_pt.erl"]}.
|
||||
|
||||
{erl_opts, [deterministic %% Added since OTP 20
|
||||
,{platform_define, "^2", 'OTP_20_AND_ABOVE'}
|
||||
]}.
|
|
@ -0,0 +1,11 @@
|
|||
{application,certifi,
|
||||
[{description,"CA bundle adapted from Mozilla by https://certifi.io"},
|
||||
{vsn,"2.6.1"},
|
||||
{registered,[]},
|
||||
{applications,[kernel,stdlib]},
|
||||
{env,[]},
|
||||
{modules,[]},
|
||||
{licenses,["BSD"]},
|
||||
{links,[{"Github","https://github.com/certifi/erlang-certifi"}]},
|
||||
{files,["src","priv","test","rebar.config","README.md",
|
||||
"LICENSE"]}]}.
|
|
@ -0,0 +1,27 @@
|
|||
-module(certifi).
|
||||
-compile({parse_transform, certifi_pt}).
|
||||
|
||||
-export([cacertfile/0,
|
||||
cacerts/0]).
|
||||
|
||||
%% @doc CACertFile gives the path to the file with an X.509 certificate list
|
||||
%% containing the Mozilla CA Certificate that can then be used via the
|
||||
%% cacertfile setting in ssl options passed to the connect function.
|
||||
cacertfile() ->
|
||||
PrivDir = case code:priv_dir(certifi) of
|
||||
{error, _} ->
|
||||
%% try to get relative priv dir. useful for tests.
|
||||
AppDir = filename:dirname(
|
||||
filename:dirname(code:which(?MODULE))
|
||||
),
|
||||
filename:join(AppDir, "priv");
|
||||
Dir -> Dir
|
||||
end,
|
||||
filename:join(PrivDir, "cacerts.pem").
|
||||
|
||||
%% @doc CACerts builds an X.509 certificate list containing the Mozilla CA
|
||||
%% Certificate that can then be used via the cacerts setting in ssl options
|
||||
%% passed to the connect function.
|
||||
-spec cacerts() -> [binary(),...].
|
||||
cacerts() ->
|
||||
ok.
|
|
@ -0,0 +1,25 @@
|
|||
-module(certifi_pt).
|
||||
-export([parse_transform/2]).
|
||||
|
||||
parse_transform(Forms, _Opts) ->
|
||||
[replace_cacerts(Form) || Form <- Forms].
|
||||
|
||||
replace_cacerts({function, Ann, cacerts, 0, [_]}) ->
|
||||
{ok, Binary} = file:read_file(cert_file() ),
|
||||
Pems = public_key:pem_decode(Binary),
|
||||
Cacerts = [Der || {'Certificate', Der, _} <- Pems],
|
||||
Body = lists:foldl(fun(Cert, Acc) ->
|
||||
{cons, 0, cert_to_bin_ast(Cert), Acc}
|
||||
end, {nil, 0}, Cacerts),
|
||||
{function, Ann, cacerts, 0, [{clause, Ann, [], [], [Body]}]};
|
||||
replace_cacerts(Other) ->
|
||||
Other.
|
||||
|
||||
cert_file() ->
|
||||
AppDir = filename:dirname(
|
||||
filename:dirname(code:which(?MODULE))
|
||||
),
|
||||
filename:join([AppDir, "priv", "cacerts.pem"]).
|
||||
|
||||
cert_to_bin_ast(Cert) ->
|
||||
{bin, 0, [{bin_element, 0, {string, 0, binary_to_list(Cert)}, default, default}]}.
|
|
@ -0,0 +1,18 @@
|
|||
-module(certifi_tests).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-ifdef('OTP_20_AND_ABOVE').
|
||||
reproducible_module_test() ->
|
||||
%% When compiled with +deterministic, only version is left out.
|
||||
?assertMatch([{version,[_|_]}], certifi:module_info(compile)).
|
||||
-endif.
|
||||
|
||||
cacerts_test_() ->
|
||||
Certs = [Cert1, Cert2, Cert3 | _] = certifi:cacerts(),
|
||||
[?_assertEqual(127, length(Certs))
|
||||
,?_assertMatch(<<48,130,2,11,48,130,1,145,160,3,2,1,2,2,18,17,210,187,186,51,_/binary>>, Cert1)
|
||||
,?_assertMatch(<<48,130,5,90,48,130,3,66,160,3,2,1,2,2,18,17,210,187,185,215,_/binary>>, Cert2)
|
||||
,?_assertMatch(<< 48,130,2,110,48,130,1,243,160,3,2,1,2,2,16,98,246,50,108, _ / binary>>, Cert3)
|
||||
,?_assertMatch(<<48,130,3,117,48,130,2,93,160,3,2,1,2,2,11,4,0,0,0,0,1,21,75,90,195,148,48,13,6,_/binary>>, lists:last(Certs))
|
||||
].
|
|
@ -0,0 +1,29 @@
|
|||
Copyright (c) 2015, Project-FiFo UG
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
* The names of its contributors may not be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,40 @@
|
|||
cf
|
||||
=====
|
||||
|
||||
A helper library for termial colour printing extending the io:format
|
||||
syntax to add colours.
|
||||
|
||||
```erlang
|
||||
%% Effectively the same as io:format just takes the additional color
|
||||
%% console text colour can be set by ~!**<colour>**. ~#**<colour>**
|
||||
%% will change the background. Both ~# only work with lowercase colours.
|
||||
%% An uppercase letersindicate bold colours.
|
||||
%%
|
||||
%% The colour can be one of:
|
||||
%%
|
||||
%% ! - resets the output
|
||||
%% ^ - bold (no colour change)
|
||||
%% __ - (two _) makes text underlined (no colour change)
|
||||
%% x,X - black
|
||||
%% r,R - red
|
||||
%% g,G - greeen
|
||||
%% y,Y - yellow
|
||||
%% b,B - blue
|
||||
%% m,M - magenta
|
||||
%% c,C - cyan
|
||||
%% w,W - white
|
||||
%%
|
||||
%% The function will disable colours on non x term termials
|
||||
```
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
$ rebar3 compile
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
`cf:format/[1,2]` - an equivalent to `io_lib:format/[1,2]`.
|
||||
`cf:print/[1,2]` - an equivalent to `io:format/[1,2]`.
|
|
@ -0,0 +1,8 @@
|
|||
{erl_opts, [debug_info]}.
|
||||
{deps, []}.
|
||||
|
||||
{profiles, [
|
||||
{shell, [
|
||||
{deps, [sync]}
|
||||
]}
|
||||
]}.
|
|
@ -0,0 +1 @@
|
|||
[].
|
|
@ -0,0 +1,10 @@
|
|||
{application,cf,
|
||||
[{description,"Terminal colour helper"},
|
||||
{vsn,"0.3.1"},
|
||||
{registered,[]},
|
||||
{applications,[kernel,stdlib]},
|
||||
{env,[]},
|
||||
{modules,[]},
|
||||
{maintainers,["Heinz N. Gies <heinz@project-fifo.net>"]},
|
||||
{licenses,["MIT"]},
|
||||
{links,[{"github","https://github.com/project-fifo/cf"}]}]}.
|
|
@ -0,0 +1,196 @@
|
|||
%%%-------------------------------------------------------------------
|
||||
%%% @author Heinz Nikolaus Gies <heinz@project-fifo.net>
|
||||
%%% @copyright (C) 2015, Project-FiFo UG
|
||||
%%% @doc
|
||||
%%% Printing library for coloured console output, extends the format
|
||||
%%% strings by adding ~! (forground) ~# (background) and ~_ (underline)
|
||||
%%% terminal colours.
|
||||
%%% @end
|
||||
%%% Created : 22 Sep 2015 by Heinz Nikolaus Gies <heinz@licenser.net>
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(cf).
|
||||
|
||||
|
||||
%% API exports
|
||||
-export([format/1, format/2]).
|
||||
-export([print/1, print/2]).
|
||||
|
||||
%%====================================================================
|
||||
%% API functions
|
||||
%%====================================================================
|
||||
%% @doc Prints a coloured string.
|
||||
%% Effectively the same as io:format just takes the additional color
|
||||
%% console text colour can be set by ~!**<colour>**. ~#**<colour>**
|
||||
%% will change the background. Both ~# only work with lowercase colours.
|
||||
%% An uppercase letersindicate bold colours.
|
||||
%% A `_` can be added after the ~! to make the text underlined.
|
||||
%%
|
||||
%% The colour can be one of:
|
||||
%%
|
||||
%% ! - resets the output
|
||||
%% ^ - makes text bold
|
||||
%% x,X - black
|
||||
%% r,R - red
|
||||
%% g,G - greeen
|
||||
%% y,Y - yellow
|
||||
%% b,B - blue
|
||||
%% m,M - magenta
|
||||
%% c,C - cyan
|
||||
%% w,W - white
|
||||
%%
|
||||
%% true color is supported by using
|
||||
%% ~!#<rr><gg><bb> as each as hex values so ~!#ff6402
|
||||
%%
|
||||
%% the same can be done for the background by yusign ~##
|
||||
%%
|
||||
%% The function will disable colours on non x term termials
|
||||
%% @end
|
||||
print(Fmt, Args) ->
|
||||
io:format(cfmt(Fmt), Args).
|
||||
|
||||
%% @doc Formates a coloured string
|
||||
%% Arguments are the same as for print/2, just returns a string as
|
||||
%% io_lib:format/2 does instead of printing it to stdout.
|
||||
%% @end
|
||||
format(Fmt, Args) ->
|
||||
io_lib:format(cfmt(Fmt), Args).
|
||||
|
||||
|
||||
print(Fmt) ->
|
||||
print(Fmt, []).
|
||||
format(Fmt) ->
|
||||
format(Fmt, []).
|
||||
|
||||
%%====================================================================
|
||||
%% Internal functions
|
||||
%%====================================================================
|
||||
|
||||
|
||||
-define(NX, "\033[0;30m").
|
||||
-define(NR, "\033[0;31m").
|
||||
-define(NG, "\033[0;32m").
|
||||
-define(NY, "\033[0;33m").
|
||||
-define(NB, "\033[0;34m").
|
||||
-define(NM, "\033[0;35m").
|
||||
-define(NC, "\033[0;36m").
|
||||
-define(NW, "\033[0;37m").
|
||||
-define(BX, "\033[1;30m").
|
||||
-define(BR, "\033[1;31m").
|
||||
-define(BG, "\033[1;32m").
|
||||
-define(BY, "\033[1;33m").
|
||||
-define(BB, "\033[1;34m").
|
||||
-define(BM, "\033[1;35m").
|
||||
-define(BC, "\033[1;36m").
|
||||
-define(BW, "\033[1;37m").
|
||||
-define(U, "\033[4m").
|
||||
-define(B, "\033[1m").
|
||||
-define(BGX, "\033[40m").
|
||||
-define(BGR, "\033[41m").
|
||||
-define(BGG, "\033[42m").
|
||||
-define(BGY, "\033[43m").
|
||||
-define(BGB, "\033[44m").
|
||||
-define(BGM, "\033[45m").
|
||||
-define(BGC, "\033[46m").
|
||||
-define(BGW, "\033[47m").
|
||||
-define(R, "\033[0m").
|
||||
-define(CFMT(Char, Colour),
|
||||
cfmt_([$~, $!, Char | S], Enabled) -> [Colour | cfmt_(S, Enabled)];
|
||||
cfmt_([$~, $!, $_, Char | S], Enabled) -> [Colour, ?U | cfmt_(S, Enabled)]).
|
||||
-define(CFMT_BG(Char, Colour),
|
||||
cfmt_([$~, $#, Char | S], Enabled) -> [Colour | cfmt_(S, Enabled)]).
|
||||
-define(CFMT_U(Char, Colour),
|
||||
cfmt_([$~, $_, Char | S], Enabled) -> [Colour | cfmt_(S, Enabled)]).
|
||||
|
||||
colour_term() ->
|
||||
case application:get_env(cf, colour_term) of
|
||||
{ok, V} ->
|
||||
V;
|
||||
undefined ->
|
||||
Term = os:getenv("TERM"),
|
||||
V = cf_term:has_color(Term),
|
||||
application:set_env(cf, colour_term, V),
|
||||
V
|
||||
end.
|
||||
|
||||
cfmt(S) ->
|
||||
cfmt(S, colour_term()).
|
||||
|
||||
cfmt(S, Enabled) ->
|
||||
lists:flatten(cfmt_(S, Enabled)).
|
||||
|
||||
cfmt_([$~, $!, $#, _R1, _R2, _G1, _G2, _B1, _B2 | S], false) ->
|
||||
cfmt_(S, false);
|
||||
cfmt_([$~, $#, $#, _R1, _R2, _G1, _G2, _B1, _B2 | S], false) ->
|
||||
cfmt_(S, false);
|
||||
|
||||
cfmt_([$~, $!, $_, _C | S], false) ->
|
||||
cfmt_(S, false);
|
||||
cfmt_([$~, $#, _C | S], false) ->
|
||||
cfmt_(S, false);
|
||||
cfmt_([$~, $!, _C | S], false) ->
|
||||
cfmt_(S, false);
|
||||
|
||||
cfmt_([$~, $!, $#, R1, R2, G1, G2, B1, B2 | S], Enabled) ->
|
||||
R = list_to_integer([R1, R2], 16),
|
||||
G = list_to_integer([G1, G2], 16),
|
||||
B = list_to_integer([B1, B2], 16),
|
||||
["\033[38;2;",
|
||||
integer_to_list(R), $;,
|
||||
integer_to_list(G), $;,
|
||||
integer_to_list(B), $m |
|
||||
cfmt_(S, Enabled)];
|
||||
|
||||
cfmt_([$~, $#, $#, R1, R2, G1, G2, B1, B2 | S], Enabled) ->
|
||||
R = list_to_integer([R1, R2], 16),
|
||||
G = list_to_integer([G1, G2], 16),
|
||||
B = list_to_integer([B1, B2], 16),
|
||||
["\033[48;2;",
|
||||
integer_to_list(R), $;,
|
||||
integer_to_list(G), $;,
|
||||
integer_to_list(B), $m |
|
||||
cfmt_(S, Enabled)];
|
||||
|
||||
cfmt_([$~, $!, $_, $_ | S], Enabled) ->
|
||||
[?U |cfmt_(S, Enabled)];
|
||||
cfmt_([$~,$!, $^ | S], Enabled) ->
|
||||
[?B | cfmt_(S, Enabled)];
|
||||
cfmt_([$~,$!, $_, $^ | S], Enabled) ->
|
||||
[?U, ?B | cfmt_(S, Enabled)];
|
||||
|
||||
?CFMT($!, ?R);
|
||||
?CFMT($x, ?NX);
|
||||
?CFMT($X, ?BX);
|
||||
?CFMT($r, ?NR);
|
||||
?CFMT($R, ?BR);
|
||||
?CFMT($g, ?NG);
|
||||
?CFMT($G, ?BG);
|
||||
?CFMT($y, ?NY);
|
||||
?CFMT($Y, ?BY);
|
||||
?CFMT($b, ?NB);
|
||||
?CFMT($B, ?BB);
|
||||
?CFMT($m, ?NM);
|
||||
?CFMT($M, ?BM);
|
||||
?CFMT($c, ?NC);
|
||||
?CFMT($C, ?BC);
|
||||
?CFMT($w, ?NW);
|
||||
?CFMT($W, ?BW);
|
||||
|
||||
?CFMT_BG($x, ?BGX);
|
||||
?CFMT_BG($r, ?BGR);
|
||||
?CFMT_BG($g, ?BGG);
|
||||
?CFMT_BG($y, ?BGY);
|
||||
?CFMT_BG($b, ?BGB);
|
||||
?CFMT_BG($m, ?BGM);
|
||||
?CFMT_BG($c, ?BGC);
|
||||
?CFMT_BG($w, ?BGW);
|
||||
|
||||
cfmt_([$~,$~ | S], Enabled) ->
|
||||
[$~,$~ | cfmt_(S, Enabled)];
|
||||
|
||||
cfmt_([C | S], Enabled) ->
|
||||
[C | cfmt_(S, Enabled)];
|
||||
|
||||
cfmt_([], false) ->
|
||||
"";
|
||||
cfmt_([], _Enabled) ->
|
||||
?R.
|
|
@ -0,0 +1,370 @@
|
|||
-module(cf_term).
|
||||
-export([has_color/1]).
|
||||
has_color("Eterm") -> true;
|
||||
has_color("Eterm-256color") -> true;
|
||||
has_color("Eterm-88color") -> true;
|
||||
has_color("aixterm") -> true;
|
||||
has_color("aixterm-16color") -> true;
|
||||
has_color("amiga-vnc") -> true;
|
||||
has_color("ansi") -> true;
|
||||
has_color("ansi-color-2-emx") -> true;
|
||||
has_color("ansi-color-3-emx") -> true;
|
||||
has_color("ansi-emx") -> true;
|
||||
has_color("ansi.sys") -> true;
|
||||
has_color("ansi.sys-old") -> true;
|
||||
has_color("ansi.sysk") -> true;
|
||||
has_color("arm100") -> true;
|
||||
has_color("arm100-w") -> true;
|
||||
has_color("aterm") -> true;
|
||||
has_color("att6386") -> true;
|
||||
has_color("beterm") -> true;
|
||||
has_color("bsdos-pc") -> true;
|
||||
has_color("bsdos-pc-nobold") -> true;
|
||||
has_color("bsdos-ppc") -> true;
|
||||
has_color("bterm") -> true;
|
||||
has_color("color_xterm") -> true;
|
||||
has_color("cons25") -> true;
|
||||
has_color("cons25-debian") -> true;
|
||||
has_color("cons25-m") -> true;
|
||||
has_color("cons25l1") -> true;
|
||||
has_color("cons25l1-m") -> true;
|
||||
has_color("cons25r") -> true;
|
||||
has_color("cons25r-m") -> true;
|
||||
has_color("cons25w") -> true;
|
||||
has_color("cons30") -> true;
|
||||
has_color("cons30-m") -> true;
|
||||
has_color("cons43") -> true;
|
||||
has_color("cons43-m") -> true;
|
||||
has_color("cons50") -> true;
|
||||
has_color("cons50-m") -> true;
|
||||
has_color("cons50l1") -> true;
|
||||
has_color("cons50l1-m") -> true;
|
||||
has_color("cons50r") -> true;
|
||||
has_color("cons50r-m") -> true;
|
||||
has_color("cons60") -> true;
|
||||
has_color("cons60-m") -> true;
|
||||
has_color("cons60l1") -> true;
|
||||
has_color("cons60l1-m") -> true;
|
||||
has_color("cons60r") -> true;
|
||||
has_color("cons60r-m") -> true;
|
||||
has_color("crt") -> true;
|
||||
has_color("ctrm") -> true;
|
||||
has_color("cygwin") -> true;
|
||||
has_color("cygwinB19") -> true;
|
||||
has_color("cygwinDBG") -> true;
|
||||
has_color("d220") -> true;
|
||||
has_color("d220-7b") -> true;
|
||||
has_color("d220-dg") -> true;
|
||||
has_color("d230c") -> true;
|
||||
has_color("d230c-dg") -> true;
|
||||
has_color("d430c-dg") -> true;
|
||||
has_color("d430c-dg-ccc") -> true;
|
||||
has_color("d430c-unix") -> true;
|
||||
has_color("d430c-unix-25") -> true;
|
||||
has_color("d430c-unix-25-ccc") -> true;
|
||||
has_color("d430c-unix-ccc") -> true;
|
||||
has_color("d430c-unix-s") -> true;
|
||||
has_color("d430c-unix-s-ccc") -> true;
|
||||
has_color("d430c-unix-sr") -> true;
|
||||
has_color("d430c-unix-sr-ccc") -> true;
|
||||
has_color("d430c-unix-w") -> true;
|
||||
has_color("d430c-unix-w-ccc") -> true;
|
||||
has_color("d470c") -> true;
|
||||
has_color("d470c-7b") -> true;
|
||||
has_color("d470c-dg") -> true;
|
||||
has_color("decansi") -> true;
|
||||
has_color("dg+ccc") -> true;
|
||||
has_color("dg+color") -> true;
|
||||
has_color("dg+color8") -> true;
|
||||
has_color("dg+fixed") -> true;
|
||||
has_color("dgmode+color") -> true;
|
||||
has_color("dgmode+color8") -> true;
|
||||
has_color("dgunix+ccc") -> true;
|
||||
has_color("dgunix+fixed") -> true;
|
||||
has_color("djgpp") -> true;
|
||||
has_color("djgpp204") -> true;
|
||||
has_color("dtterm") -> true;
|
||||
has_color("ecma+color") -> true;
|
||||
has_color("emu") -> true;
|
||||
has_color("emx-base") -> true;
|
||||
has_color("eterm-color") -> true;
|
||||
has_color("gnome") -> true;
|
||||
has_color("gnome-2007") -> true;
|
||||
has_color("gnome-2008") -> true;
|
||||
has_color("gnome-2012") -> true;
|
||||
has_color("gnome-256color") -> true;
|
||||
has_color("gnome-fc5") -> true;
|
||||
has_color("gnome-rh62") -> true;
|
||||
has_color("gnome-rh72") -> true;
|
||||
has_color("gnome-rh80") -> true;
|
||||
has_color("gnome-rh90") -> true;
|
||||
has_color("gs6300") -> true;
|
||||
has_color("hft-c") -> true;
|
||||
has_color("hft-c-old") -> true;
|
||||
has_color("hft-old") -> true;
|
||||
has_color("hp+color") -> true;
|
||||
has_color("hp2397a") -> true;
|
||||
has_color("hpterm-color") -> true;
|
||||
has_color("hurd") -> true;
|
||||
has_color("iTerm.app") -> true;
|
||||
has_color("ibm+16color") -> true;
|
||||
has_color("ibm+color") -> true;
|
||||
has_color("ibm3164") -> true;
|
||||
has_color("ibm5081") -> true;
|
||||
has_color("ibm5154") -> true;
|
||||
has_color("ibm6154") -> true;
|
||||
has_color("ibm8503") -> true;
|
||||
has_color("ibm8512") -> true;
|
||||
has_color("ibmpc3") -> true;
|
||||
has_color("interix") -> true;
|
||||
has_color("iris-color") -> true;
|
||||
has_color("jaixterm") -> true;
|
||||
has_color("klone+color") -> true;
|
||||
has_color("kon") -> true;
|
||||
has_color("konsole") -> true;
|
||||
has_color("konsole-16color") -> true;
|
||||
has_color("konsole-256color") -> true;
|
||||
has_color("konsole-base") -> true;
|
||||
has_color("konsole-linux") -> true;
|
||||
has_color("konsole-solaris") -> true;
|
||||
has_color("konsole-vt100") -> true;
|
||||
has_color("konsole-vt420pc") -> true;
|
||||
has_color("konsole-xf3x") -> true;
|
||||
has_color("konsole-xf4x") -> true;
|
||||
has_color("kterm") -> true;
|
||||
has_color("kterm-color") -> true;
|
||||
has_color("kvt") -> true;
|
||||
has_color("linux") -> true;
|
||||
has_color("linux-16color") -> true;
|
||||
has_color("linux-basic") -> true;
|
||||
has_color("linux-c") -> true;
|
||||
has_color("linux-c-nc") -> true;
|
||||
has_color("linux-koi8") -> true;
|
||||
has_color("linux-koi8r") -> true;
|
||||
has_color("linux-lat") -> true;
|
||||
has_color("linux-m") -> true;
|
||||
has_color("linux-nic") -> true;
|
||||
has_color("linux-vt") -> true;
|
||||
has_color("linux2.2") -> true;
|
||||
has_color("linux2.6") -> true;
|
||||
has_color("linux2.6.26") -> true;
|
||||
has_color("linux3.0") -> true;
|
||||
has_color("mach-color") -> true;
|
||||
has_color("mach-gnu-color") -> true;
|
||||
has_color("mgt") -> true;
|
||||
has_color("mgterm") -> true;
|
||||
has_color("minitel1") -> true;
|
||||
has_color("minitel1b") -> true;
|
||||
has_color("minitel1b-80") -> true;
|
||||
has_color("minix") -> true;
|
||||
has_color("minix-3.0") -> true;
|
||||
has_color("mlterm") -> true;
|
||||
has_color("mlterm-256color") -> true;
|
||||
has_color("mlterm2") -> true;
|
||||
has_color("mlterm3") -> true;
|
||||
has_color("mrxvt") -> true;
|
||||
has_color("mrxvt-256color") -> true;
|
||||
has_color("ms-vt-utf8") -> true;
|
||||
has_color("ms-vt100+") -> true;
|
||||
has_color("ms-vt100-color") -> true;
|
||||
has_color("mvterm") -> true;
|
||||
has_color("nansi.sys") -> true;
|
||||
has_color("nansi.sysk") -> true;
|
||||
has_color("ncr260intan") -> true;
|
||||
has_color("ncr260intpp") -> true;
|
||||
has_color("ncr260intwan") -> true;
|
||||
has_color("ncr260intwpp") -> true;
|
||||
has_color("ncr260wy325pp") -> true;
|
||||
has_color("ncr260wy325wpp") -> true;
|
||||
has_color("ncr260wy350pp") -> true;
|
||||
has_color("ncr260wy350wpp") -> true;
|
||||
has_color("ncsa") -> true;
|
||||
has_color("ncsa-ns") -> true;
|
||||
has_color("ncsa-vt220") -> true;
|
||||
has_color("netbsd6") -> true;
|
||||
has_color("nsterm") -> true;
|
||||
has_color("nsterm+c") -> true;
|
||||
has_color("nsterm+c41") -> true;
|
||||
has_color("nsterm-16color") -> true;
|
||||
has_color("nsterm-256color") -> true;
|
||||
has_color("nsterm-7") -> true;
|
||||
has_color("nsterm-7-c") -> true;
|
||||
has_color("nsterm-acs") -> true;
|
||||
has_color("nsterm-bce") -> true;
|
||||
has_color("nsterm-build326") -> true;
|
||||
has_color("nsterm-build343") -> true;
|
||||
has_color("nsterm-c") -> true;
|
||||
has_color("nsterm-c-acs") -> true;
|
||||
has_color("nsterm-c-s") -> true;
|
||||
has_color("nsterm-c-s-7") -> true;
|
||||
has_color("nsterm-c-s-acs") -> true;
|
||||
has_color("nsterm-old") -> true;
|
||||
has_color("nsterm-s") -> true;
|
||||
has_color("nsterm-s-7") -> true;
|
||||
has_color("nsterm-s-acs") -> true;
|
||||
has_color("pc-minix") -> true;
|
||||
has_color("pc3") -> true;
|
||||
has_color("pcansi") -> true;
|
||||
has_color("pcansi-25") -> true;
|
||||
has_color("pcansi-33") -> true;
|
||||
has_color("pcansi-43") -> true;
|
||||
has_color("pccon") -> true;
|
||||
has_color("pccon+colors") -> true;
|
||||
has_color("pccon0") -> true;
|
||||
has_color("pcvt25-color") -> true;
|
||||
has_color("putty") -> true;
|
||||
has_color("putty-256color") -> true;
|
||||
has_color("putty-sco") -> true;
|
||||
has_color("putty-vt100") -> true;
|
||||
has_color("qansi") -> true;
|
||||
has_color("qansi-g") -> true;
|
||||
has_color("qansi-m") -> true;
|
||||
has_color("qansi-t") -> true;
|
||||
has_color("qansi-w") -> true;
|
||||
has_color("qnx") -> true;
|
||||
has_color("rcons-color") -> true;
|
||||
has_color("rxvt") -> true;
|
||||
has_color("rxvt-16color") -> true;
|
||||
has_color("rxvt-256color") -> true;
|
||||
has_color("rxvt-88color") -> true;
|
||||
has_color("rxvt-color") -> true;
|
||||
has_color("rxvt-cygwin") -> true;
|
||||
has_color("rxvt-cygwin-native") -> true;
|
||||
has_color("rxvt-unicode") -> true;
|
||||
has_color("rxvt-unicode-256color") -> true;
|
||||
has_color("rxvt-xpm") -> true;
|
||||
has_color("scoansi") -> true;
|
||||
has_color("scoansi-new") -> true;
|
||||
has_color("scoansi-old") -> true;
|
||||
has_color("screen") -> true;
|
||||
has_color("screen-16color") -> true;
|
||||
has_color("screen-16color-bce") -> true;
|
||||
has_color("screen-16color-bce-s") -> true;
|
||||
has_color("screen-16color-s") -> true;
|
||||
has_color("screen-256color") -> true;
|
||||
has_color("screen-256color-bce") -> true;
|
||||
has_color("screen-256color-bce-s") -> true;
|
||||
has_color("screen-256color-s") -> true;
|
||||
has_color("screen-bce") -> true;
|
||||
has_color("screen-bce.Eterm") -> true;
|
||||
has_color("screen-bce.gnome") -> true;
|
||||
has_color("screen-bce.konsole") -> true;
|
||||
has_color("screen-bce.linux") -> true;
|
||||
has_color("screen-bce.mrxvt") -> true;
|
||||
has_color("screen-bce.rxvt") -> true;
|
||||
has_color("screen-s") -> true;
|
||||
has_color("screen-w") -> true;
|
||||
has_color("screen.Eterm") -> true;
|
||||
has_color("screen.gnome") -> true;
|
||||
has_color("screen.konsole") -> true;
|
||||
has_color("screen.konsole-256color") -> true;
|
||||
has_color("screen.linux") -> true;
|
||||
has_color("screen.mlterm") -> true;
|
||||
has_color("screen.mlterm-256color") -> true;
|
||||
has_color("screen.mrxvt") -> true;
|
||||
has_color("screen.putty") -> true;
|
||||
has_color("screen.putty-256color") -> true;
|
||||
has_color("screen.rxvt") -> true;
|
||||
has_color("screen.teraterm") -> true;
|
||||
has_color("screen.vte") -> true;
|
||||
has_color("screen.vte-256color") -> true;
|
||||
has_color("screen.xterm-256color") -> true;
|
||||
has_color("screen.xterm-xfree86") -> true;
|
||||
has_color("simpleterm") -> true;
|
||||
has_color("st") -> true;
|
||||
has_color("st-16color") -> true;
|
||||
has_color("st-256color") -> true;
|
||||
has_color("st52-color") -> true;
|
||||
has_color("sun-color") -> true;
|
||||
has_color("tek4205") -> true;
|
||||
has_color("teken") -> true;
|
||||
has_color("teraterm") -> true;
|
||||
has_color("teraterm2.3") -> true;
|
||||
has_color("teraterm4.59") -> true;
|
||||
has_color("terminator") -> true;
|
||||
has_color("terminology") -> true;
|
||||
has_color("ti928") -> true;
|
||||
has_color("ti928-8") -> true;
|
||||
has_color("ti_ansi") -> true;
|
||||
has_color("tmux") -> true;
|
||||
has_color("tmux-256color") -> true;
|
||||
has_color("tw100") -> true;
|
||||
has_color("tw52") -> true;
|
||||
has_color("uwin") -> true;
|
||||
has_color("vte") -> true;
|
||||
has_color("vte-2007") -> true;
|
||||
has_color("vte-2008") -> true;
|
||||
has_color("vte-2012") -> true;
|
||||
has_color("vte-2014") -> true;
|
||||
has_color("vte-256color") -> true;
|
||||
has_color("vwmterm") -> true;
|
||||
has_color("wsvt25") -> true;
|
||||
has_color("wsvt25m") -> true;
|
||||
has_color("wy350") -> true;
|
||||
has_color("wy350-vb") -> true;
|
||||
has_color("wy350-w") -> true;
|
||||
has_color("wy350-wvb") -> true;
|
||||
has_color("wy370") -> true;
|
||||
has_color("wy370-105k") -> true;
|
||||
has_color("wy370-EPC") -> true;
|
||||
has_color("wy370-nk") -> true;
|
||||
has_color("wy370-rv") -> true;
|
||||
has_color("wy370-vb") -> true;
|
||||
has_color("wy370-w") -> true;
|
||||
has_color("wy370-wvb") -> true;
|
||||
has_color("xfce") -> true;
|
||||
has_color("xiterm") -> true;
|
||||
has_color("xnuppc") -> true;
|
||||
has_color("xnuppc+c") -> true;
|
||||
has_color("xnuppc-100x37") -> true;
|
||||
has_color("xnuppc-112x37") -> true;
|
||||
has_color("xnuppc-128x40") -> true;
|
||||
has_color("xnuppc-128x48") -> true;
|
||||
has_color("xnuppc-144x48") -> true;
|
||||
has_color("xnuppc-160x64") -> true;
|
||||
has_color("xnuppc-200x64") -> true;
|
||||
has_color("xnuppc-200x75") -> true;
|
||||
has_color("xnuppc-256x96") -> true;
|
||||
has_color("xnuppc-80x25") -> true;
|
||||
has_color("xnuppc-80x30") -> true;
|
||||
has_color("xnuppc-90x30") -> true;
|
||||
has_color("xnuppc-b") -> true;
|
||||
has_color("xnuppc-f") -> true;
|
||||
has_color("xnuppc-f2") -> true;
|
||||
has_color("xterm") -> true;
|
||||
has_color("xterm+256color") -> true;
|
||||
has_color("xterm+256setaf") -> true;
|
||||
has_color("xterm+88color") -> true;
|
||||
has_color("xterm-1002") -> true;
|
||||
has_color("xterm-1003") -> true;
|
||||
has_color("xterm-1005") -> true;
|
||||
has_color("xterm-1006") -> true;
|
||||
has_color("xterm-16color") -> true;
|
||||
has_color("xterm-256color") -> true;
|
||||
has_color("xterm-88color") -> true;
|
||||
has_color("xterm-8bit") -> true;
|
||||
has_color("xterm-basic") -> true;
|
||||
has_color("xterm-color") -> true;
|
||||
has_color("xterm-hp") -> true;
|
||||
has_color("xterm-new") -> true;
|
||||
has_color("xterm-nic") -> true;
|
||||
has_color("xterm-noapp") -> true;
|
||||
has_color("xterm-sco") -> true;
|
||||
has_color("xterm-sun") -> true;
|
||||
has_color("xterm-utf8") -> true;
|
||||
has_color("xterm-vt220") -> true;
|
||||
has_color("xterm-x10mouse") -> true;
|
||||
has_color("xterm-x11hilite") -> true;
|
||||
has_color("xterm-x11mouse") -> true;
|
||||
has_color("xterm-xf86-v32") -> true;
|
||||
has_color("xterm-xf86-v33") -> true;
|
||||
has_color("xterm-xf86-v333") -> true;
|
||||
has_color("xterm-xf86-v40") -> true;
|
||||
has_color("xterm-xf86-v43") -> true;
|
||||
has_color("xterm-xf86-v44") -> true;
|
||||
has_color("xterm-xfree86") -> true;
|
||||
has_color("xterm-xi") -> true;
|
||||
has_color("xterm1") -> true;
|
||||
has_color("xtermc") -> true;
|
||||
has_color("xterms-sun") -> true;
|
||||
has_color(_) -> false.
|
|
@ -0,0 +1,29 @@
|
|||
Copyright (c) 2015, Fred Hebert <mononcqc@ferd.ca>.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
* The names of its contributors may not be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,159 @@
|
|||
# `cth_readable`
|
||||
|
||||
An OTP library to be used for CT log outputs you want to be readable
|
||||
around all that noise they contain.
|
||||
|
||||
There are currently the following hooks:
|
||||
|
||||
1. `cth_readable_shell`, which shows failure stacktraces in the shell and
|
||||
otherwise shows successes properly, in color.
|
||||
2. `cth_readable_compact_shell`, which is similar to the previous ones, but
|
||||
only ouputs a period (`.`) for each successful test
|
||||
3. `cth_readable_failonly`, which only outputs error and SASL logs to the
|
||||
shell in case of failures. It also provides `cthr:pal/1-4` functions,
|
||||
working like `ct:pal/1-4`, but being silenceable by that hook. A parse
|
||||
transform exists to automatically convert `ct:pal/1-3` into `cthr:pal/1-3`.
|
||||
Also automatically handles lager. This hook buffers the IO/logging events,
|
||||
and the buffer size can be limited with the `max_events` config option. The
|
||||
default value is `inf` which means that all events are buffered.
|
||||
4. `cth_readable_nosasl`, which disables all SASL logging. It however requires
|
||||
to be run *before* `cth_readable_failonly` to work.
|
||||
|
||||
## What it looks like
|
||||
|
||||
![example](http://i.imgur.com/dDFNxZr.png)
|
||||
![example](http://i.imgur.com/RXZBG7H.png)
|
||||
|
||||
## Usage with rebar3
|
||||
|
||||
Supported and enabled by default.
|
||||
|
||||
## Usage with rebar2.x
|
||||
|
||||
Add the following to your `rebar.config`:
|
||||
|
||||
```erlang
|
||||
{deps, [
|
||||
{cth_readable, {git, "https://github.com/ferd/cth_readable.git", {tag, "v1.5.1"}}}
|
||||
]}.
|
||||
|
||||
{ct_compile_opts, [{parse_transform, cth_readable_transform}]}.
|
||||
{ct_opts, [{ct_hooks, [cth_readable_failonly, cth_readable_shell]}]}.
|
||||
%% Or add limitations to how many messages are buffered with:
|
||||
%% {ct_opts, [{ct_hooks, [{cth_readable_failonly, [{max_events, 1000}]}, cth_readable_shell]}]}.
|
||||
```
|
||||
|
||||
## Usage with lager
|
||||
|
||||
If your lager handler has a custom formatter and you want that formatter
|
||||
to take effect, rather than using a configuration such as:
|
||||
|
||||
```erlang
|
||||
{lager, [
|
||||
{handlers, [{lager_console_backend,
|
||||
[info, {custom_formatter, [{app, "some-val"}]}]}
|
||||
]}
|
||||
]}.
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```erlang
|
||||
{lager, [
|
||||
{handlers, [{cth_readable_lager_backend,
|
||||
[info, {custom_formatter, [{app, "some-val"}]}]}
|
||||
]}
|
||||
]}.
|
||||
```
|
||||
|
||||
It will let you have both proper formatting and support for arbitrary
|
||||
configurations.
|
||||
|
||||
## Changelog
|
||||
1.5.1:
|
||||
- Adding support for `cthr:pal/5` (thanks @ashleyjlive)
|
||||
|
||||
1.5.0:
|
||||
- Adding an optional bound buffer in `cth_readable_failonly` (thanks @TheGeorge)
|
||||
- (published to hex but never to github, ended up with a messy commit tree)
|
||||
|
||||
1.4.9:
|
||||
- No change, re-pushing the hex.pm package since it had an untracked dependency somehow
|
||||
|
||||
1.4.8:
|
||||
- Fixed handling of comments in EUnit macros
|
||||
|
||||
1.4.7:
|
||||
- Fixed handling of the result of an `?assertNot()` macro
|
||||
|
||||
1.4.6:
|
||||
- Reloading formatter config for logs after each test where the information needs to be printed
|
||||
|
||||
1.4.5:
|
||||
- Restoring proper logs for Lager in OTP-21+. A problem existed when `error_logger` was no longer registered by default and lager log lines would silently get lost.
|
||||
|
||||
1.4.4:
|
||||
- Better interactions with Lager; since newer releases, it removes the Logger default interface when starting, which could cause crashes when this happened before the CT hooks would start (i.e. a eunit suite)
|
||||
|
||||
1.4.3:
|
||||
- OTP-21.2 support (Logger interface); importing a function that was de-exported by OTP team
|
||||
|
||||
1.4.2:
|
||||
- OTP-21.0 support (Logger interface)
|
||||
|
||||
1.4.1:
|
||||
- OTP-21-rc2 support (Logger interface); dropping rc1 support.
|
||||
|
||||
1.4.0:
|
||||
- OTP-21-rc1 support (Logger interface)
|
||||
- Add compact shell output handler
|
||||
|
||||
1.3.4:
|
||||
- Restore proper eunit assertion formatting
|
||||
|
||||
1.3.3:
|
||||
- More fixes due to lager old default config formats
|
||||
|
||||
1.3.2:
|
||||
- Fix deprecation warning on newer lagers due to old default config format
|
||||
|
||||
1.3.1:
|
||||
- Unicode support and OTP-21 readiness.
|
||||
|
||||
1.3.0:
|
||||
- display groups in test output. Thanks to @egobrain for the contribution
|
||||
|
||||
1.2.6:
|
||||
- report `end_per_testcase` errors as a non-critical failure when the test case passes
|
||||
- add in a (voluntarily failing) test suite to demo multiple output cases required
|
||||
|
||||
1.2.5:
|
||||
- support for `on_tc_skip/4` to fully prevent misreporting of skipped suites
|
||||
|
||||
1.2.4:
|
||||
- unset suite name at the end of hooks run to prevent misreporting
|
||||
|
||||
1.2.3:
|
||||
- correct `syntax_lib` to `syntax_tools` as an app dependency
|
||||
|
||||
1.2.2:
|
||||
- fix output for assertions
|
||||
|
||||
1.2.1:
|
||||
- handle failures of parse transforms by just ignoring the culprit files.
|
||||
|
||||
1.2.0:
|
||||
- move to `cf` library for color output, adding support for 'dumb' terminals
|
||||
|
||||
1.1.1:
|
||||
- fix typo of `poplist -> proplist`, thanks to @egobrain
|
||||
|
||||
1.1.0:
|
||||
- support for better looking EUnit logs
|
||||
- support for lager backends logging to HTML files
|
||||
|
||||
1.0.1:
|
||||
- support for CT versions in Erlang copies older than R16
|
||||
|
||||
1.0.0:
|
||||
- initial stable release
|
|
@ -0,0 +1,20 @@
|
|||
{deps, [cf]}.
|
||||
|
||||
{ct_opts, [
|
||||
{ct_hooks, [cth_readable_failonly, cth_readable_shell]}
|
||||
]}.
|
||||
|
||||
{ct_compile_opts, [
|
||||
{parse_transform, cth_readable_transform}
|
||||
]}.
|
||||
{eunit_compile_opts, [ % to avoid 'do eunit, ct' eating up the parse transform
|
||||
{parse_transform, cth_readable_transform}
|
||||
]}.
|
||||
|
||||
{erl_opts, [{platform_define, "^(R|1|20)", no_logger_hrl}]}.
|
||||
|
||||
{profiles, [
|
||||
{test, [
|
||||
{deps, [{lager, "3.6.10"}]}
|
||||
]}
|
||||
]}.
|
|
@ -0,0 +1,10 @@
|
|||
case erlang:function_exported(rebar3, main, 1) of
|
||||
true -> % rebar3
|
||||
CONFIG;
|
||||
false -> % rebar 2.x or older
|
||||
%% Rebuild deps, possibly including those that have been moved to
|
||||
%% profiles
|
||||
[{deps, [
|
||||
{cf, ".*", {git, "https://github.com/project-fifo/cf.git", "a6b3957"}}
|
||||
]} | lists:keydelete(deps, 1, CONFIG)]
|
||||
end.
|
|
@ -0,0 +1,8 @@
|
|||
{"1.2.0",
|
||||
[{<<"cf">>,{pkg,<<"cf">>,<<"0.2.1">>},0}]}.
|
||||
[
|
||||
{pkg_hash,[
|
||||
{<<"cf">>, <<"69D0B1349FD4D7D4DC55B7F407D29D7A840BF9A1EF5AF529F1EBE0CE153FC2AB">>}]},
|
||||
{pkg_hash_ext,[
|
||||
{<<"cf">>, <<"BAEE9AA7EC2DFA3CB4486B67211177CAA293F876780F0B313B45718EDEF6A0A5">>}]}
|
||||
].
|
|
@ -0,0 +1,9 @@
|
|||
{application,cth_readable,
|
||||
[{description,"Common Test hooks for more readable logs"},
|
||||
{vsn,"1.5.1"},
|
||||
{registered,[cth_readable_failonly,cth_readable_logger]},
|
||||
{applications,[kernel,stdlib,syntax_tools,cf]},
|
||||
{env,[]},
|
||||
{modules,[]},
|
||||
{licenses,["BSD"]},
|
||||
{links,[{"Github","https://github.com/ferd/cth_readable"}]}]}.
|
|
@ -0,0 +1,140 @@
|
|||
-module(cth_readable_compact_shell).
|
||||
-import(cth_readable_helpers, [format_path/2, colorize/2, maybe_eunit_format/1]).
|
||||
|
||||
-define(OKC, green).
|
||||
-define(FAILC, red).
|
||||
-define(SKIPC, magenta).
|
||||
|
||||
-define(OK(Suite, CasePat, CaseArgs),
|
||||
?CASE(Suite, CasePat, ?OKC, "OK", CaseArgs)).
|
||||
-define(SKIP(Suite, CasePat, CaseArgs, Reason),
|
||||
?STACK(Suite, CasePat, CaseArgs, Reason, ?SKIPC, "SKIPPED")).
|
||||
-define(FAIL(Suite, CasePat, CaseArgs, Reason),
|
||||
?STACK(Suite, CasePat, CaseArgs, Reason, ?FAILC, "FAILED")).
|
||||
-define(STACK(Suite, CasePat, CaseArgs, Reason, Color, Label),
|
||||
begin
|
||||
?CASE(Suite, CasePat, Color, Label, CaseArgs),
|
||||
io:format(user, "%%% ~p ==> "++colorize(Color, maybe_eunit_format(Reason))++"~n", [Suite])
|
||||
end).
|
||||
-define(CASE(Suite, CasePat, Color, Res, Args),
|
||||
case Res of
|
||||
"OK" -> io:format(user, colorize(Color, "."), []);
|
||||
_ -> io:format(user, "~n%%% ~p ==> "++CasePat++": "++colorize(Color, Res)++"~n", [Suite | Args])
|
||||
end).
|
||||
|
||||
%% Callbacks
|
||||
-export([id/1]).
|
||||
-export([init/2]).
|
||||
|
||||
-export([pre_init_per_suite/3]).
|
||||
-export([post_init_per_suite/4]).
|
||||
-export([pre_end_per_suite/3]).
|
||||
-export([post_end_per_suite/4]).
|
||||
|
||||
-export([pre_init_per_group/3]).
|
||||
-export([post_init_per_group/4]).
|
||||
-export([pre_end_per_group/3]).
|
||||
-export([post_end_per_group/4]).
|
||||
|
||||
-export([pre_init_per_testcase/3]).
|
||||
-export([post_end_per_testcase/4]).
|
||||
|
||||
-export([on_tc_fail/3]).
|
||||
-export([on_tc_skip/3, on_tc_skip/4]).
|
||||
|
||||
-export([terminate/1]).
|
||||
|
||||
-record(state, {id, suite, groups}).
|
||||
|
||||
%% @doc Return a unique id for this CTH.
|
||||
id(_Opts) ->
|
||||
{?MODULE, make_ref()}.
|
||||
|
||||
%% @doc Always called before any other callback function. Use this to initiate
|
||||
%% any common state.
|
||||
init(Id, _Opts) ->
|
||||
{ok, #state{id=Id}}.
|
||||
|
||||
%% @doc Called before init_per_suite is called.
|
||||
pre_init_per_suite(Suite,Config,State) ->
|
||||
io:format(user, "%%% ~p: ", [Suite]),
|
||||
{Config, State#state{suite=Suite, groups=[]}}.
|
||||
|
||||
%% @doc Called after init_per_suite.
|
||||
post_init_per_suite(_Suite,_Config,Return,State) ->
|
||||
{Return, State}.
|
||||
|
||||
%% @doc Called before end_per_suite.
|
||||
pre_end_per_suite(_Suite,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after end_per_suite.
|
||||
post_end_per_suite(_Suite,_Config,Return,State) ->
|
||||
io:format(user, "~n", []),
|
||||
{Return, State#state{suite=undefined, groups=[]}}.
|
||||
|
||||
%% @doc Called before each init_per_group.
|
||||
pre_init_per_group(_Group,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after each init_per_group.
|
||||
post_init_per_group(Group,_Config,Return, State=#state{groups=Groups}) ->
|
||||
{Return, State#state{groups=[Group|Groups]}}.
|
||||
|
||||
%% @doc Called after each end_per_group.
|
||||
pre_end_per_group(_Group,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after each end_per_group.
|
||||
post_end_per_group(_Group,_Config,Return, State=#state{groups=Groups}) ->
|
||||
{Return, State#state{groups=tl(Groups)}}.
|
||||
|
||||
%% @doc Called before each test case.
|
||||
pre_init_per_testcase(_TC,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after each test case.
|
||||
post_end_per_testcase(TC,_Config,ok,State=#state{suite=Suite, groups=Groups}) ->
|
||||
?OK(Suite, "~s", [format_path(TC,Groups)]),
|
||||
{ok, State};
|
||||
post_end_per_testcase(TC,Config,Error,State=#state{suite=Suite, groups=Groups}) ->
|
||||
case lists:keyfind(tc_status, 1, Config) of
|
||||
{tc_status, ok} ->
|
||||
%% Test case passed, but we still ended in an error
|
||||
?STACK(Suite, "~s", [format_path(TC,Groups)], Error, ?SKIPC, "end_per_testcase FAILED");
|
||||
_ ->
|
||||
%% Test case failed, in which case on_tc_fail already reports it
|
||||
ok
|
||||
end,
|
||||
{Error, State}.
|
||||
|
||||
%% @doc Called after post_init_per_suite, post_end_per_suite, post_init_per_group,
|
||||
%% post_end_per_group and post_end_per_testcase if the suite, group or test case failed.
|
||||
on_tc_fail({TC,_Group}, Reason, State=#state{suite=Suite, groups=Groups}) ->
|
||||
?FAIL(Suite, "~s", [format_path(TC,Groups)], Reason),
|
||||
State;
|
||||
on_tc_fail(TC, Reason, State=#state{suite=Suite, groups=Groups}) ->
|
||||
?FAIL(Suite, "~s", [format_path(TC,Groups)], Reason),
|
||||
State.
|
||||
|
||||
%% @doc Called when a test case is skipped by either user action
|
||||
%% or due to an init function failing. (>= 19.3)
|
||||
on_tc_skip(Suite, {TC,_Group}, Reason, State=#state{groups=Groups}) ->
|
||||
?SKIP(Suite, "~s", [format_path(TC,Groups)], Reason),
|
||||
State#state{suite=Suite};
|
||||
on_tc_skip(Suite, TC, Reason, State=#state{groups=Groups}) ->
|
||||
?SKIP(Suite, "~s", [format_path(TC,Groups)], Reason),
|
||||
State#state{suite=Suite}.
|
||||
|
||||
%% @doc Called when a test case is skipped by either user action
|
||||
%% or due to an init function failing. (Pre-19.3)
|
||||
on_tc_skip({TC,Group}, Reason, State=#state{suite=Suite}) ->
|
||||
?SKIP(Suite, "~p (group ~p)", [TC, Group], Reason),
|
||||
State;
|
||||
on_tc_skip(TC, Reason, State=#state{suite=Suite}) ->
|
||||
?SKIP(Suite, "~p", [TC], Reason),
|
||||
State.
|
||||
|
||||
%% @doc Called when the scope of the CTH is done
|
||||
terminate(_State) ->
|
||||
ok.
|
|
@ -0,0 +1,499 @@
|
|||
-module(cth_readable_failonly).
|
||||
|
||||
-record(state, {id,
|
||||
sasl_reset,
|
||||
lager_reset,
|
||||
handlers=[],
|
||||
named,
|
||||
has_logger}).
|
||||
-record(eh_state, {buf=queue:new(),
|
||||
sasl=false,
|
||||
max_events = inf,
|
||||
stored_events = 0,
|
||||
dropped_events = 0}).
|
||||
|
||||
%% Callbacks
|
||||
-export([id/1]).
|
||||
-export([init/2]).
|
||||
|
||||
-export([pre_init_per_suite/3]).
|
||||
-export([post_init_per_suite/4]).
|
||||
-export([pre_end_per_suite/3]).
|
||||
-export([post_end_per_suite/4]).
|
||||
|
||||
-export([pre_init_per_group/3]).
|
||||
-export([post_init_per_group/4]).
|
||||
-export([pre_end_per_group/3]).
|
||||
-export([post_end_per_group/4]).
|
||||
|
||||
-export([pre_init_per_testcase/3]).
|
||||
-export([post_end_per_testcase/4]).
|
||||
|
||||
-export([on_tc_fail/3]).
|
||||
-export([on_tc_skip/3, on_tc_skip/4]).
|
||||
|
||||
-export([terminate/1]).
|
||||
|
||||
%% Error Logger Handler API
|
||||
-export([init/1,
|
||||
handle_event/2, handle_call/2, handle_info/2,
|
||||
terminate/2, code_change/3]).
|
||||
|
||||
%% Logger API
|
||||
-export([log/2,
|
||||
adding_handler/1, removing_handler/1]).
|
||||
|
||||
-define(DEFAULT_LAGER_SINK, lager_event).
|
||||
-define(DEFAULT_LAGER_HANDLER_CONF,
|
||||
[{lager_console_backend, [{level, info}]},
|
||||
{lager_file_backend,
|
||||
[{file, "log/error.log"}, {level, error},
|
||||
{size, 10485760}, {date, "$D0"}, {count, 5}]
|
||||
},
|
||||
{lager_file_backend,
|
||||
[{file, "log/console.log"}, {level, info},
|
||||
{size, 10485760}, {date, "$D0"}, {count, 5}]
|
||||
}
|
||||
]).
|
||||
|
||||
-ifndef(LOCATION).
|
||||
%% imported from kernel/include/logger.hrl but with replaced unsupported macros
|
||||
-define(LOCATION,#{mfa=>{?MODULE,log_to_binary,2},
|
||||
line=>?LINE,
|
||||
file=>?FILE}).
|
||||
-endif.
|
||||
%% imported from logger_internal.hrl
|
||||
-define(DEFAULT_FORMATTER, logger_formatter).
|
||||
-define(DEFAULT_FORMAT_CONFIG, #{legacy_header => true,
|
||||
single_line => false}).
|
||||
-define(LOG_INTERNAL(Level,Report),
|
||||
case logger:allow(Level,?MODULE) of
|
||||
true ->
|
||||
%% Spawn this to avoid deadlocks
|
||||
_ = spawn(logger,macro_log,[?LOCATION,Level,Report,
|
||||
logger:add_default_metadata(#{})]),
|
||||
ok;
|
||||
false ->
|
||||
ok
|
||||
end).
|
||||
|
||||
%% @doc Return a unique id for this CTH.
|
||||
id(_Opts) ->
|
||||
{?MODULE, make_ref()}.
|
||||
|
||||
%% @doc Always called before any other callback function. Use this to initiate
|
||||
%% any common state.
|
||||
init(Id, Opts) ->
|
||||
%% ct:pal replacement needs to know if this hook is enabled -- we use a named proc for that.
|
||||
%% Use a `receive' -- if people mock `timer' or reload it, it can kill the
|
||||
%% hook and then CT as a whole.
|
||||
Named = spawn_link(fun() -> receive after infinity -> ok end end),
|
||||
register(?MODULE, Named),
|
||||
MaxEvents = proplists:get_value(max_events, Opts, inf),
|
||||
HasLogger = has_logger(), % Pre OTP-21 or not
|
||||
Cfg = maybe_steal_logger_config(),
|
||||
case HasLogger of
|
||||
false ->
|
||||
error_logger:tty(false), % TODO check if on to begin with
|
||||
application:load(sasl); % TODO do this optionally?
|
||||
true ->
|
||||
%% Assume default logger is running // TODO: check if on to begin with
|
||||
logger:add_handler_filter(default, ?MODULE, {fun(_,_) -> stop end, nostate}),
|
||||
ok
|
||||
end,
|
||||
LagerReset = setup_lager(),
|
||||
case application:get_env(sasl, sasl_error_logger) of
|
||||
{ok, tty} when not HasLogger ->
|
||||
ok = gen_event:add_handler(error_logger, ?MODULE, [sasl, {max_events, MaxEvents}]),
|
||||
application:set_env(sasl, sasl_error_logger, false),
|
||||
{ok, #state{id=Id, sasl_reset={reset, tty}, lager_reset=LagerReset,
|
||||
handlers=[?MODULE], named=Named, has_logger=HasLogger}};
|
||||
{ok, tty} when HasLogger ->
|
||||
logger:add_handler(?MODULE, ?MODULE, Cfg#{sasl => true, max_events => MaxEvents}),
|
||||
{ok, #state{id=Id, lager_reset=LagerReset, handlers=[?MODULE],
|
||||
named=Named, has_logger=HasLogger}};
|
||||
_ when HasLogger ->
|
||||
logger:add_handler(?MODULE, ?MODULE, Cfg#{sasl => false, max_events => MaxEvents}),
|
||||
{ok, #state{id=Id, lager_reset=LagerReset, handlers=[?MODULE],
|
||||
named=Named, has_logger=HasLogger}};
|
||||
_ ->
|
||||
ok = gen_event:add_handler(error_logger, ?MODULE, [{max_events, MaxEvents}]),
|
||||
{ok, #state{id=Id, lager_reset=LagerReset, handlers=[?MODULE],
|
||||
named=Named, has_logger=HasLogger}}
|
||||
end.
|
||||
|
||||
%% @doc Called before init_per_suite is called.
|
||||
pre_init_per_suite(_Suite,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after init_per_suite.
|
||||
post_init_per_suite(_Suite,_Config,Return,State) ->
|
||||
{Return, State}.
|
||||
|
||||
%% @doc Called before end_per_suite.
|
||||
pre_end_per_suite(_Suite,Config,State) ->
|
||||
call_handlers(ignore, State),
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after end_per_suite.
|
||||
post_end_per_suite(_Suite,_Config,Return,State) ->
|
||||
{Return, State}.
|
||||
|
||||
%% @doc Called before each init_per_group.
|
||||
pre_init_per_group(_Group,Config,State) ->
|
||||
call_handlers(ignore, State),
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after each init_per_group.
|
||||
post_init_per_group(_Group,_Config,Return,State) ->
|
||||
{Return, State}.
|
||||
|
||||
%% @doc Called after each end_per_group.
|
||||
pre_end_per_group(_Group,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after each end_per_group.
|
||||
post_end_per_group(_Group,_Config,Return,State) ->
|
||||
{Return, State}.
|
||||
|
||||
%% @doc Called before each test case.
|
||||
pre_init_per_testcase(_TC,Config,State) ->
|
||||
call_handlers(ignore, State),
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after each test case.
|
||||
post_end_per_testcase(_TC,_Config,ok,State=#state{}) ->
|
||||
{ok, State};
|
||||
post_end_per_testcase(_TC,_Config,Error,State) ->
|
||||
{Error, State}.
|
||||
|
||||
%% @doc Called after post_init_per_suite, post_end_per_suite, post_init_per_group,
|
||||
%% post_end_per_group and post_end_per_testcase if the suite, group or test case failed.
|
||||
on_tc_fail({_TC,_Group}, _Reason, State=#state{}) ->
|
||||
call_handlers(flush, State),
|
||||
State;
|
||||
on_tc_fail(_TC, _Reason, State=#state{}) ->
|
||||
call_handlers(flush, State),
|
||||
State.
|
||||
|
||||
%% @doc Called when a test case is skipped by either user action
|
||||
%% or due to an init function failing (>= 19.3)
|
||||
on_tc_skip(_Suite, {_TC,_Group}, _Reason, State=#state{}) ->
|
||||
call_handlers(flush, State),
|
||||
State;
|
||||
on_tc_skip(_Suite, _TC, _Reason, State=#state{}) ->
|
||||
call_handlers(flush, State),
|
||||
State.
|
||||
|
||||
%% @doc Called when a test case is skipped by either user action
|
||||
%% or due to an init function failing (pre 19.3)
|
||||
on_tc_skip({_TC,_Group}, _Reason, State=#state{}) ->
|
||||
call_handlers(flush, State),
|
||||
State;
|
||||
on_tc_skip(_TC, _Reason, State=#state{}) ->
|
||||
call_handlers(flush, State),
|
||||
State.
|
||||
|
||||
%% @doc Called when the scope of the CTH is done
|
||||
terminate(_State=#state{handlers=Handlers, sasl_reset=SReset,
|
||||
lager_reset=LReset, named=Pid, has_logger=HasLogger}) ->
|
||||
|
||||
if HasLogger ->
|
||||
logger:remove_handler(?MODULE),
|
||||
logger:remove_handler_filter(default, ?MODULE);
|
||||
not HasLogger ->
|
||||
_ = [gen_event:delete_handler(error_logger, Handler, shutdown)
|
||||
|| Handler <- Handlers]
|
||||
end,
|
||||
case SReset of
|
||||
{reset, Val} -> application:set_env(sasl, sasl_error_logger, Val);
|
||||
undefined -> ok
|
||||
end,
|
||||
not HasLogger andalso error_logger:tty(true),
|
||||
application:unload(sasl), % silently fails if running
|
||||
lager_reset(LReset),
|
||||
%% Kill the named process signifying this is running
|
||||
unlink(Pid),
|
||||
Ref = erlang:monitor(process, Pid),
|
||||
exit(Pid, shutdown),
|
||||
receive
|
||||
{'DOWN', Ref, process, Pid, shutdown} -> ok
|
||||
end.
|
||||
|
||||
%%%%%%%%%%%%% ERROR_LOGGER HANDLER %%%%%%%%%%%%
|
||||
|
||||
init(Opts) ->
|
||||
{ok,
|
||||
#eh_state{
|
||||
sasl = proplists:get_bool(sasl, Opts),
|
||||
max_events = proplists:get_value(max_events, Opts)
|
||||
}}.
|
||||
|
||||
handle_event(Event, State) ->
|
||||
NewState = case parse_event(Event) of
|
||||
ignore -> State;
|
||||
logger -> buffer_event({logger, Event}, State);
|
||||
sasl -> buffer_event({sasl, {calendar:local_time(), Event}}, State);
|
||||
error_logger -> buffer_event({error_logger, {erlang:universaltime(), Event}}, State)
|
||||
end,
|
||||
{ok, NewState}.
|
||||
|
||||
handle_info(_, State) ->
|
||||
{ok, State}.
|
||||
|
||||
handle_call({lager, _} = Event, State) ->
|
||||
%% lager events come in from our fake handler, pre-filtered.
|
||||
{ok, ok, buffer_event(Event, State)};
|
||||
handle_call({ct_pal, ignore}, S) ->
|
||||
{ok, ok, S};
|
||||
handle_call({ct_pal, _}=Event, State) ->
|
||||
{ok, ok, buffer_event(Event, State)};
|
||||
handle_call(ignore, State) ->
|
||||
{ok, ok, State#eh_state{buf=queue:new(), stored_events=0}};
|
||||
handle_call(flush, S=#eh_state{buf=Buf, dropped_events=Dropped}) ->
|
||||
Cfg = maybe_steal_logger_config(),
|
||||
ShowSASL = sasl_running() orelse sasl_ran(Buf) andalso S#eh_state.sasl,
|
||||
SASLType = get_sasl_error_logger_type(),
|
||||
not queue:is_empty(Buf) andalso io:put_chars(user, "\n"),
|
||||
flush(Buf, Cfg, ShowSASL, SASLType, Dropped),
|
||||
{ok, ok, S#eh_state{buf=queue:new(), stored_events=0}};
|
||||
handle_call(_Event, State) ->
|
||||
{ok, ok, State}.
|
||||
|
||||
code_change(_, _, State) ->
|
||||
{ok, State}.
|
||||
|
||||
terminate(_, _) ->
|
||||
ok.
|
||||
|
||||
buffer_event(Event, S=#eh_state{buf=Buf, max_events=inf}) ->
|
||||
%% unbound buffer
|
||||
S#eh_state{buf=queue:in(Event, Buf)};
|
||||
buffer_event(Event, S=#eh_state{buf=Buf, max_events=MaxEvents, stored_events=StoredEvents}) when MaxEvents > StoredEvents ->
|
||||
%% bound buffer; buffer not filled yet
|
||||
S#eh_state{buf=queue:in(Event, Buf), stored_events=StoredEvents + 1};
|
||||
buffer_event(Event, S=#eh_state{buf=Buf0, dropped_events=DroppedEvents}) ->
|
||||
%% bound buffer; buffer filled
|
||||
{_, Buf1} = queue:out(Buf0),
|
||||
S#eh_state{buf=queue:in(Event, Buf1), dropped_events=DroppedEvents + 1}.
|
||||
|
||||
flush(Buf, Cfg, ShowSASL, SASLType, Dropped) when Dropped > 0 ->
|
||||
io:format(user, "(logs are truncated, dropped ~b events)~n", [Dropped]),
|
||||
flush(Buf, Cfg, ShowSASL, SASLType);
|
||||
flush(Buf, Cfg, ShowSASL, SASLType, _) ->
|
||||
flush(Buf, Cfg, ShowSASL, SASLType).
|
||||
|
||||
flush(Buf, Cfg, ShowSASL, SASLType) ->
|
||||
case queue:out(Buf) of
|
||||
{empty, _} -> ok;
|
||||
{{value, {T, Event}}, NextBuf} ->
|
||||
case T of
|
||||
error_logger ->
|
||||
error_logger_tty_h:write_event(Event, io);
|
||||
sasl when ShowSASL ->
|
||||
sasl_report:write_report(standard_io, SASLType, Event);
|
||||
ct_pal ->
|
||||
io:format(user, Event, []);
|
||||
lager ->
|
||||
io:put_chars(user, Event);
|
||||
logger ->
|
||||
Bin = log_to_binary(Event,Cfg),
|
||||
io:put_chars(user, Bin);
|
||||
_ ->
|
||||
ignore
|
||||
end,
|
||||
flush(NextBuf, Cfg, ShowSASL, SASLType)
|
||||
end.
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%% LOGGER %%%%%%%%%%%%%%%%%%%
|
||||
adding_handler(Config = #{sasl := SASL, max_events := MaxEvents}) ->
|
||||
{ok, Pid} = gen_event:start({local, cth_readable_logger}, []),
|
||||
gen_event:add_handler(cth_readable_logger, ?MODULE, [{sasl, SASL}, {max_events, MaxEvents}]),
|
||||
{ok, Config#{cth_readable_logger => Pid}}.
|
||||
|
||||
removing_handler(#{cth_readable_logger := Pid}) ->
|
||||
try gen_event:stop(Pid, shutdown, 1000) of
|
||||
ok -> ok
|
||||
catch
|
||||
error:noproc -> ok;
|
||||
error:timeout -> at_least_we_tried
|
||||
end.
|
||||
|
||||
log(Msg, #{cth_readable_logger := Pid}) ->
|
||||
gen_event:notify(Pid, Msg).
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
has_logger() ->
|
||||
%% Module is present
|
||||
erlang:function_exported(logger, module_info, 0).
|
||||
|
||||
has_usable_logger() ->
|
||||
%% The config is set (lager didn't remove it)
|
||||
erlang:function_exported(logger, get_handler_config, 1) andalso
|
||||
logger:get_handler_config(default) =/= {error, {not_found, default}}.
|
||||
|
||||
maybe_steal_logger_config() ->
|
||||
case has_logger() andalso has_usable_logger() of
|
||||
false ->
|
||||
#{};
|
||||
true ->
|
||||
case logger:get_handler_config(default) of
|
||||
{ok, {_,Cfg}} -> %% OTP-21.0-rc2 result
|
||||
maps:with([formatter], Cfg); % only keep the essential
|
||||
{ok, Cfg} -> %% OTP-21.0 result
|
||||
maps:with([formatter], Cfg) % only keep the essential
|
||||
end
|
||||
end.
|
||||
|
||||
sasl_running() ->
|
||||
length([1 || {sasl, _, _} <- application:which_applications()]) > 0.
|
||||
|
||||
get_sasl_error_logger_type() ->
|
||||
case application:get_env(sasl, errlog_type) of
|
||||
{ok, error} -> error;
|
||||
{ok, progress} -> progress;
|
||||
{ok, all} -> all;
|
||||
{ok, Bad} -> exit({bad_config, {sasl, {errlog_type, Bad}}});
|
||||
_ -> all
|
||||
end.
|
||||
|
||||
sasl_ran(Buf) ->
|
||||
case queue:out(Buf) of
|
||||
{empty, _} -> false;
|
||||
{{value, {sasl, {_DateTime, {info_report,_,
|
||||
{_,progress, [{application,sasl},{started_at,_}|_]}}}}}, _} -> true;
|
||||
{_, Rest} -> sasl_ran(Rest)
|
||||
end.
|
||||
|
||||
call_handlers(Msg, #state{handlers=Handlers, has_logger=HasLogger}) ->
|
||||
Name = if HasLogger -> cth_readable_logger;
|
||||
not HasLogger -> error_logger
|
||||
end,
|
||||
_ = [gen_event:call(Name, Handler, Msg, 300000)
|
||||
|| Handler <- Handlers],
|
||||
ok.
|
||||
|
||||
parse_event({_, GL, _}) when node(GL) =/= node() -> ignore;
|
||||
parse_event({info_report, _GL, {_Pid, progress, _Args}}) -> sasl;
|
||||
parse_event({error_report, _GL, {_Pid, supervisor_report, _Args}}) -> sasl;
|
||||
parse_event({error_report, _GL, {_Pid, crash_report, _Args}}) -> sasl;
|
||||
parse_event({error, _GL, {_Pid, _Format, _Args}}) -> error_logger;
|
||||
parse_event({info_msg, _GL, {_Pid, _Format, _Args}}) -> error_logger;
|
||||
parse_event({warning_msg, _GL, {_Pid, _Format, _Args}}) -> error_logger;
|
||||
parse_event({error_report, _GL, {_Pid, _Format, _Args}}) -> error_logger;
|
||||
parse_event({info_report, _GL, {_Pid, _Format, _Args}}) -> error_logger;
|
||||
parse_event({warning_report, _GL, {_Pid, _Format, _Args}}) -> error_logger;
|
||||
parse_event(Map) when is_map(Map) -> logger;
|
||||
parse_event(_) -> sasl. % sasl does its own filtering
|
||||
|
||||
setup_lager() ->
|
||||
case application:load(lager) of
|
||||
{error, {"no such file or directory", _}} ->
|
||||
%% app not available
|
||||
undefined;
|
||||
_ -> % it's show time
|
||||
%% Keep lager from throwing us out
|
||||
WhiteList = application:get_env(lager, error_logger_whitelist, []),
|
||||
application:set_env(lager, error_logger_whitelist, [?MODULE|WhiteList]),
|
||||
InitConf = application:get_env(lager, handlers, ?DEFAULT_LAGER_HANDLER_CONF),
|
||||
%% Add ourselves to the config
|
||||
NewConf = case proplists:get_value(lager_console_backend, InitConf) of
|
||||
undefined -> % no console backend running
|
||||
InitConf;
|
||||
Opts ->
|
||||
[{cth_readable_lager_backend, Opts}
|
||||
| InitConf -- [{lager_console_backend, Opts}]]
|
||||
end,
|
||||
application:set_env(lager, handlers, NewConf),
|
||||
%% check if lager is running and override!
|
||||
case {whereis(lager_sup),
|
||||
proplists:get_value(cth_readable_lager_backend, NewConf)} of
|
||||
{undefined, _} ->
|
||||
InitConf;
|
||||
{_, undefined} ->
|
||||
InitConf;
|
||||
{_, LOpts} ->
|
||||
swap_lager_handlers(lager_console_backend,
|
||||
cth_readable_lager_backend, LOpts),
|
||||
InitConf
|
||||
end
|
||||
end.
|
||||
|
||||
lager_reset(undefined) ->
|
||||
ok;
|
||||
lager_reset(InitConf) ->
|
||||
%% Reset the whitelist
|
||||
WhiteList = application:get_env(lager, error_logger_whitelist, []),
|
||||
application:set_env(lager, error_logger_whitelist, WhiteList--[?MODULE]),
|
||||
%% Swap them handlers again
|
||||
Opts = proplists:get_value(lager_console_backend, InitConf),
|
||||
application:set_env(lager, handlers, InitConf),
|
||||
case {whereis(lager_sup), Opts} of
|
||||
{undefined, _} -> % not running
|
||||
ok;
|
||||
{_, undefined} -> % not scheduled
|
||||
ok;
|
||||
{_, _} ->
|
||||
swap_lager_handlers(cth_readable_lager_backend,
|
||||
lager_console_backend, Opts)
|
||||
end.
|
||||
|
||||
swap_lager_handlers(Old, New, Opts) ->
|
||||
gen_event:delete_handler(?DEFAULT_LAGER_SINK, Old, shutdown),
|
||||
lager_app:start_handler(?DEFAULT_LAGER_SINK,
|
||||
New, Opts).
|
||||
|
||||
%% Imported from Erlang/OTP -- this function used to be public in OTP-20,
|
||||
%% but was then taken public by OTP-21, which broke functionality.
|
||||
%% Original at https://raw.githubusercontent.com/erlang/otp/OTP-21.2.5/lib/kernel/src/logger_h_common.erl
|
||||
log_to_binary(#{msg:={report,_},meta:=#{report_cb:=_}}=Log,Config) ->
|
||||
do_log_to_binary(Log,Config);
|
||||
log_to_binary(#{msg:={report,_},meta:=Meta}=Log,Config) ->
|
||||
DefaultReportCb = fun logger:format_otp_report/1,
|
||||
do_log_to_binary(Log#{meta=>Meta#{report_cb=>DefaultReportCb}},Config);
|
||||
log_to_binary(Log,Config) ->
|
||||
do_log_to_binary(Log,Config).
|
||||
|
||||
do_log_to_binary(Log,Config) ->
|
||||
{Formatter,FormatterConfig} =
|
||||
maps:get(formatter,Config,{?DEFAULT_FORMATTER,?DEFAULT_FORMAT_CONFIG}),
|
||||
String = try_format(Log,Formatter,FormatterConfig),
|
||||
try string_to_binary(String)
|
||||
catch C2:R2 ->
|
||||
?LOG_INTERNAL(debug,[{formatter_error,Formatter},
|
||||
{config,FormatterConfig},
|
||||
{log_event,Log},
|
||||
{bad_return_value,String},
|
||||
{catched,{C2,R2,[]}}]),
|
||||
<<"FORMATTER ERROR: bad return value">>
|
||||
end.
|
||||
|
||||
try_format(Log,Formatter,FormatterConfig) ->
|
||||
try Formatter:format(Log,FormatterConfig)
|
||||
catch
|
||||
C:R ->
|
||||
?LOG_INTERNAL(debug,[{formatter_crashed,Formatter},
|
||||
{config,FormatterConfig},
|
||||
{log_event,Log},
|
||||
{reason,
|
||||
{C,R,[]}}]),
|
||||
case {?DEFAULT_FORMATTER,#{}} of
|
||||
{Formatter,FormatterConfig} ->
|
||||
"DEFAULT FORMATTER CRASHED";
|
||||
{DefaultFormatter,DefaultConfig} ->
|
||||
try_format(Log#{msg=>{"FORMATTER CRASH: ~tp",
|
||||
[maps:get(msg,Log)]}},
|
||||
DefaultFormatter,DefaultConfig)
|
||||
end
|
||||
end.
|
||||
|
||||
string_to_binary(String) ->
|
||||
case unicode:characters_to_binary(String) of
|
||||
Binary when is_binary(Binary) ->
|
||||
Binary;
|
||||
Error ->
|
||||
throw(Error)
|
||||
end.
|
|
@ -0,0 +1,163 @@
|
|||
-module(cth_readable_helpers).
|
||||
-export([format_path/2, colorize/2, maybe_eunit_format/1]).
|
||||
|
||||
format_path(TC, Groups) ->
|
||||
join([atom_to_list(P) || P <- lists:reverse([TC|Groups])], ".").
|
||||
|
||||
%% string:join/2 copy; string:join/2 is getting obsoleted
|
||||
%% and replaced by lists:join/2, but lists:join/2 is too new
|
||||
%% for version support (only appeared in 19.0) so it cannot be
|
||||
%% used. Instead we just adopt join/2 locally and hope it works
|
||||
%% for most unicode use cases anyway.
|
||||
join([], Sep) when is_list(Sep) ->
|
||||
[];
|
||||
join([H|T], Sep) ->
|
||||
H ++ lists:append([Sep ++ X || X <- T]).
|
||||
|
||||
colorize(red, Txt) -> cf:format("~!r~s~!!", [Txt]);
|
||||
colorize(green, Txt) -> cf:format("~!g~s~!!", [Txt]);
|
||||
colorize(magenta, Txt) -> cf:format("~!m~s~!!",[Txt]).
|
||||
|
||||
maybe_eunit_format({failed, Reason}) ->
|
||||
maybe_eunit_format(Reason);
|
||||
|
||||
maybe_eunit_format({{Type, Props}, _}) when Type =:= assert_failed
|
||||
; Type =:= assert ->
|
||||
Keys = proplists:get_keys(Props),
|
||||
HasEUnitProps = ([expression, value, line] -- Keys) =:= [],
|
||||
HasHamcrestProps = ([expected, actual, matcher, line] -- Keys) =:= [],
|
||||
if
|
||||
HasEUnitProps ->
|
||||
[io_lib:format("~nFailure/Error: ?assert(~s)~n", [proplists:get_value(expression, Props)]),
|
||||
io_lib:format(" expected: ~p~n", [proplists:get_value(expected, Props)]),
|
||||
case proplists:get_value(value, Props) of
|
||||
{not_a_boolean, V} ->
|
||||
io_lib:format(" got: ~p~n", [V]);
|
||||
V ->
|
||||
io_lib:format(" got: ~p~n", [V])
|
||||
end, io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++
|
||||
[io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]];
|
||||
HasHamcrestProps ->
|
||||
[io_lib:format("~nFailure/Error: ?assertThat(~p)~n", [proplists:get_value(matcher, Props)]),
|
||||
io_lib:format(" expected: ~p~n", [proplists:get_value(expected, Props)]),
|
||||
io_lib:format(" got: ~p~n", [proplists:get_value(actual, Props)]),
|
||||
io_lib:format(" line: ~p", [proplists:get_value(line, Props)])];
|
||||
true ->
|
||||
[io_lib:format("~nFailure/Error: unknown assert: ~p", [Props])]
|
||||
end;
|
||||
|
||||
maybe_eunit_format({{Type, Props}, _}) when Type =:= assertMatch_failed
|
||||
; Type =:= assertMatch ->
|
||||
Expr = proplists:get_value(expression, Props),
|
||||
Pattern = proplists:get_value(pattern, Props),
|
||||
Value = proplists:get_value(value, Props),
|
||||
[io_lib:format("~nFailure/Error: ?assertMatch(~s, ~s)~n", [Pattern, Expr]),
|
||||
io_lib:format(" expected: = ~s~n", [Pattern]),
|
||||
io_lib:format(" got: ~p~n", [Value]),
|
||||
io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++
|
||||
[io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]];
|
||||
|
||||
maybe_eunit_format({{Type, Props}, _}) when Type =:= assertNotMatch_failed
|
||||
; Type =:= assertNotMatch ->
|
||||
Expr = proplists:get_value(expression, Props),
|
||||
Pattern = proplists:get_value(pattern, Props),
|
||||
Value = proplists:get_value(value, Props),
|
||||
[io_lib:format("~nFailure/Error: ?assertNotMatch(~s, ~s)~n", [Pattern, Expr]),
|
||||
io_lib:format(" expected not: = ~s~n", [Pattern]),
|
||||
io_lib:format(" got: ~p~n", [Value]),
|
||||
io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++
|
||||
[io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]];
|
||||
|
||||
maybe_eunit_format({{Type, Props}, _}) when Type =:= assertEqual_failed
|
||||
; Type =:= assertEqual ->
|
||||
Expr = proplists:get_value(expression, Props),
|
||||
Expected = proplists:get_value(expected, Props),
|
||||
Value = proplists:get_value(value, Props),
|
||||
[io_lib:format("~nFailure/Error: ?assertEqual(~w, ~s)~n", [Expected,
|
||||
Expr]),
|
||||
io_lib:format(" expected: ~p~n", [Expected]),
|
||||
io_lib:format(" got: ~p~n", [Value]),
|
||||
io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++
|
||||
[io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]];
|
||||
|
||||
maybe_eunit_format({{Type, Props}, _}) when Type =:= assertNotEqual_failed
|
||||
; Type =:= assertNotEqual ->
|
||||
Expr = proplists:get_value(expression, Props),
|
||||
Value = proplists:get_value(value, Props),
|
||||
[io_lib:format("~nFailure/Error: ?assertNotEqual(~p, ~s)~n",
|
||||
[Value, Expr]),
|
||||
io_lib:format(" expected not: == ~p~n", [Value]),
|
||||
io_lib:format(" got: ~p~n", [Value]),
|
||||
io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++
|
||||
[io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]];
|
||||
|
||||
maybe_eunit_format({{Type, Props}, _}) when Type =:= assertException_failed
|
||||
; Type =:= assertException ->
|
||||
Expr = proplists:get_value(expression, Props),
|
||||
Pattern = proplists:get_value(pattern, Props),
|
||||
{Class, Term} = extract_exception_pattern(Pattern), % I hate that we have to do this, why not just give DATA
|
||||
[io_lib:format("~nFailure/Error: ?assertException(~s, ~s, ~s)~n", [Class, Term, Expr]),
|
||||
case proplists:is_defined(unexpected_success, Props) of
|
||||
true ->
|
||||
[io_lib:format(" expected: exception ~s but nothing was raised~n", [Pattern]),
|
||||
io_lib:format(" got: value ~p~n", [proplists:get_value(unexpected_success, Props)]),
|
||||
io_lib:format(" line: ~p", [proplists:get_value(line, Props)])];
|
||||
false ->
|
||||
Ex = proplists:get_value(unexpected_exception, Props),
|
||||
[io_lib:format(" expected: exception ~s~n", [Pattern]),
|
||||
io_lib:format(" got: exception ~p~n", [Ex]),
|
||||
io_lib:format(" line: ~p", [proplists:get_value(line, Props)])]
|
||||
end] ++
|
||||
[io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]];
|
||||
|
||||
maybe_eunit_format({{Type, Props}, _}) when Type =:= assertNotException_failed
|
||||
; Type =:= assertNotException ->
|
||||
Expr = proplists:get_value(expression, Props),
|
||||
Pattern = proplists:get_value(pattern, Props),
|
||||
{Class, Term} = extract_exception_pattern(Pattern), % I hate that we have to do this, why not just give DAT
|
||||
Ex = proplists:get_value(unexpected_exception, Props),
|
||||
[io_lib:format("~nFailure/Error: ?assertNotException(~s, ~s, ~s)~n", [Class, Term, Expr]),
|
||||
io_lib:format(" expected not: exception ~s~n", [Pattern]),
|
||||
io_lib:format(" got: exception ~p~n", [Ex]),
|
||||
io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++
|
||||
[io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]];
|
||||
|
||||
maybe_eunit_format({{Type, Props}, _}) when Type =:= command_failed
|
||||
; Type =:= command ->
|
||||
Cmd = proplists:get_value(command, Props),
|
||||
Expected = proplists:get_value(expected_status, Props),
|
||||
Status = proplists:get_value(status, Props),
|
||||
[io_lib:format("~nFailure/Error: ?cmdStatus(~p, ~p)~n", [Expected, Cmd]),
|
||||
io_lib:format(" expected: status ~p~n", [Expected]),
|
||||
io_lib:format(" got: status ~p~n", [Status]),
|
||||
io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++
|
||||
[io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]];
|
||||
|
||||
maybe_eunit_format({{Type, Props}, _}) when Type =:= assertCmd_failed
|
||||
; Type =:= assertCmd ->
|
||||
Cmd = proplists:get_value(command, Props),
|
||||
Expected = proplists:get_value(expected_status, Props),
|
||||
Status = proplists:get_value(status, Props),
|
||||
[io_lib:format("~nFailure/Error: ?assertCmdStatus(~p, ~p)~n", [Expected, Cmd]),
|
||||
io_lib:format(" expected: status ~p~n", [Expected]),
|
||||
io_lib:format(" got: status ~p~n", [Status]),
|
||||
io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++
|
||||
[io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]];
|
||||
|
||||
maybe_eunit_format({{Type, Props}, _}) when Type =:= assertCmdOutput_failed
|
||||
; Type =:= assertCmdOutput ->
|
||||
Cmd = proplists:get_value(command, Props),
|
||||
Expected = proplists:get_value(expected_output, Props),
|
||||
Output = proplists:get_value(output, Props),
|
||||
[io_lib:format("~nFailure/Error: ?assertCmdOutput(~p, ~p)~n", [Expected, Cmd]),
|
||||
io_lib:format(" expected: ~p~n", [Expected]),
|
||||
io_lib:format(" got: ~p~n", [Output]),
|
||||
io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++
|
||||
[io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]];
|
||||
|
||||
maybe_eunit_format(Reason) ->
|
||||
io_lib:format("~p", [Reason]).
|
||||
|
||||
extract_exception_pattern(Str) ->
|
||||
["{", Class, Term|_] = re:split(Str, "[, ]{1,2}", [unicode,{return,list}]),
|
||||
{Class, Term}.
|
|
@ -0,0 +1,147 @@
|
|||
%% Copyright (c) 2011-2012, 2014 Basho Technologies, Inc. All Rights Reserved.
|
||||
%%
|
||||
%% This file is provided to you under the Apache License,
|
||||
%% Version 2.0 (the "License"); you may not use this file
|
||||
%% except in compliance with the License. You may obtain
|
||||
%% a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing,
|
||||
%% software distributed under the License is distributed on an
|
||||
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
%% KIND, either express or implied. See the License for the
|
||||
%% specific language governing permissions and limitations
|
||||
%% under the License.
|
||||
|
||||
%% @doc Console backend for lager that mutes logs to the shell when
|
||||
%% CT runs succeed. Configured with a single option, the loglevel
|
||||
%% desired.
|
||||
|
||||
-module(cth_readable_lager_backend).
|
||||
|
||||
-behaviour(gen_event).
|
||||
|
||||
-export([init/1, handle_call/2, handle_event/2, handle_info/2, terminate/2,
|
||||
code_change/3]).
|
||||
|
||||
-record(state, {level :: {'mask', integer()},
|
||||
out = user :: user | standard_error,
|
||||
formatter :: atom(),
|
||||
format_config :: any(),
|
||||
colors=[] :: list()}).
|
||||
|
||||
%-include("lager.hrl").
|
||||
-define(TERSE_FORMAT,[time, " ", color, "[", severity,"] ", message]).
|
||||
-define(DEFAULT_FORMAT_CONFIG, ?TERSE_FORMAT ++ [eol()]).
|
||||
-define(FORMAT_CONFIG_OFF, [{eol, eol()}]).
|
||||
|
||||
%% @private
|
||||
init([Level]) when is_atom(Level) ->
|
||||
init([{level, Level}]);
|
||||
init([Level, true]) when is_atom(Level) -> % for backwards compatibility
|
||||
init([{level, Level}, {formatter_config, ?FORMAT_CONFIG_OFF}]);
|
||||
init([Level, false]) when is_atom(Level) -> % for backwards compatibility
|
||||
init([{level, Level}]);
|
||||
init(Options) when is_list(Options) ->
|
||||
Colors = case application:get_env(lager, colored) of
|
||||
{ok, true} ->
|
||||
{ok, LagerColors} = application:get_env(lager, colors),
|
||||
LagerColors;
|
||||
_ -> []
|
||||
end,
|
||||
|
||||
Level = get_option(level, Options, undefined),
|
||||
%% edited out a bunch of console detection stuff, hopefully not breaking
|
||||
try lager_util:config_to_mask(Level) of
|
||||
L ->
|
||||
[UseErr, Formatter, Config] =
|
||||
[get_option(K, Options, Default) || {K, Default} <- [{use_stderr, false},
|
||||
{formatter, lager_default_formatter},
|
||||
{formatter_config, ?DEFAULT_FORMAT_CONFIG}]
|
||||
],
|
||||
Out = case UseErr of
|
||||
false -> user;
|
||||
true -> standard_error
|
||||
end,
|
||||
{ok, #state{level=L,
|
||||
out=Out,
|
||||
formatter=Formatter,
|
||||
format_config=Config,
|
||||
colors=Colors}}
|
||||
catch
|
||||
_:_ ->
|
||||
{error, {fatal, bad_log_level}}
|
||||
end;
|
||||
init(Level) ->
|
||||
init([Level,{lager_default_formatter,?TERSE_FORMAT ++ [eol()]}]).
|
||||
|
||||
get_option(K, Options, Default) ->
|
||||
case lists:keyfind(K, 1, Options) of
|
||||
{K, V} -> V;
|
||||
false -> Default
|
||||
end.
|
||||
|
||||
%% @private
|
||||
handle_call(get_loglevel, #state{level=Level} = State) ->
|
||||
{ok, Level, State};
|
||||
handle_call({set_loglevel, Level}, State) ->
|
||||
try lager_util:config_to_mask(Level) of
|
||||
Levels ->
|
||||
{ok, ok, State#state{level=Levels}}
|
||||
catch
|
||||
_:_ ->
|
||||
{ok, {error, bad_log_level}, State}
|
||||
end;
|
||||
handle_call(_Request, State) ->
|
||||
{ok, ok, State}.
|
||||
|
||||
%% @private
|
||||
handle_event({log, Message},
|
||||
#state{level=L,formatter=Formatter,format_config=FormatConfig,colors=Colors} = State) ->
|
||||
case lager_util:is_loggable(Message, L, lager_console_backend) of
|
||||
true ->
|
||||
%% Handle multiple possible functions -- older lagers didn't
|
||||
%% support colors, and we depend on the currently running lib.
|
||||
Formatted = case erlang:function_exported(Formatter, format, 3) of
|
||||
true ->
|
||||
Formatter:format(Message,FormatConfig,Colors);
|
||||
false ->
|
||||
Formatter:format(Message,FormatConfig)
|
||||
end,
|
||||
%% lagger forwards in sync mode, and a call to error_logger makes
|
||||
%% everything deadlock, so we gotta go async on the logging call.
|
||||
%% We also need to do a call so that lager doesn't reforward the
|
||||
%% event in an infinite loop.
|
||||
Name = case erlang:function_exported(logger, module_info, 0) of
|
||||
true -> cth_readable_logger;
|
||||
false -> error_logger
|
||||
end,
|
||||
spawn(fun() -> gen_event:call(Name, cth_readable_failonly, {lager, Formatted}) end),
|
||||
ct_logs:tc_log(default, Formatted, []),
|
||||
{ok, State};
|
||||
false ->
|
||||
{ok, State}
|
||||
end;
|
||||
handle_event(_Event, State) ->
|
||||
{ok, State}.
|
||||
|
||||
%% @private
|
||||
handle_info(_Info, State) ->
|
||||
{ok, State}.
|
||||
|
||||
%% @private
|
||||
terminate(_Reason, _State) ->
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
eol() ->
|
||||
case application:get_env(lager, colored) of
|
||||
{ok, true} ->
|
||||
"\e[0m\r\n";
|
||||
_ ->
|
||||
"\r\n"
|
||||
end.
|
|
@ -0,0 +1,100 @@
|
|||
-module(cth_readable_nosasl).
|
||||
|
||||
%% Callbacks
|
||||
-export([id/1]).
|
||||
-export([init/2]).
|
||||
|
||||
-export([pre_init_per_suite/3]).
|
||||
-export([post_init_per_suite/4]).
|
||||
-export([pre_end_per_suite/3]).
|
||||
-export([post_end_per_suite/4]).
|
||||
|
||||
-export([pre_init_per_group/3]).
|
||||
-export([post_init_per_group/4]).
|
||||
-export([pre_end_per_group/3]).
|
||||
-export([post_end_per_group/4]).
|
||||
|
||||
-export([pre_init_per_testcase/3]).
|
||||
-export([post_end_per_testcase/4]).
|
||||
|
||||
-export([on_tc_fail/3]).
|
||||
-export([on_tc_skip/3, on_tc_skip/4]).
|
||||
|
||||
-export([terminate/1]).
|
||||
|
||||
%% @doc Return a unique id for this CTH.
|
||||
id(_Opts) ->
|
||||
{?MODULE, make_ref()}.
|
||||
|
||||
%% @doc Always called before any other callback function. Use this to initiate
|
||||
%% any common state.
|
||||
init(_Id, _Opts) ->
|
||||
application:load(sasl), % TODO do this optionally?
|
||||
Res = application:get_env(sasl, sasl_error_logger),
|
||||
application:set_env(sasl, sasl_error_logger, false),
|
||||
{ok, Res}.
|
||||
|
||||
%% @doc Called before init_per_suite is called.
|
||||
pre_init_per_suite(_Suite,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after init_per_suite.
|
||||
post_init_per_suite(_Suite,_Config,Return,State) ->
|
||||
{Return, State}.
|
||||
|
||||
%% @doc Called before end_per_suite.
|
||||
pre_end_per_suite(_Suite,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after end_per_suite.
|
||||
post_end_per_suite(_Suite,_Config,Return,State) ->
|
||||
{Return, State}.
|
||||
|
||||
%% @doc Called before each init_per_group.
|
||||
pre_init_per_group(_Group,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after each init_per_group.
|
||||
post_init_per_group(_Group,_Config,Return,State) ->
|
||||
{Return, State}.
|
||||
|
||||
%% @doc Called after each end_per_group.
|
||||
pre_end_per_group(_Group,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after each end_per_group.
|
||||
post_end_per_group(_Group,_Config,Return,State) ->
|
||||
{Return, State}.
|
||||
|
||||
%% @doc Called before each test case.
|
||||
pre_init_per_testcase(_TC,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after each test case.
|
||||
post_end_per_testcase(_TC,_Config,Error,State) ->
|
||||
{Error, State}.
|
||||
|
||||
%% @doc Called after post_init_per_suite, post_end_per_suite, post_init_per_group,
|
||||
%% post_end_per_group and post_end_per_testcase if the suite, group or test case failed.
|
||||
on_tc_fail(_TC, _Reason, State) ->
|
||||
State.
|
||||
|
||||
%% @doc Called when a test case is skipped by either user action
|
||||
%% or due to an init function failing. (>= 19.3)
|
||||
on_tc_skip(_Suite, _TC, _Reason, State) ->
|
||||
State.
|
||||
%% @doc Called when a test case is skipped by either user action
|
||||
%% or due to an init function failing. (Pre-19.3)
|
||||
on_tc_skip(_TC, _Reason, State) ->
|
||||
State.
|
||||
|
||||
%% @doc Called when the scope of the CTH is done
|
||||
terminate(Env) ->
|
||||
case Env of
|
||||
{ok, Val} ->
|
||||
application:set_env(sasl, sasl_error_logger, Val);
|
||||
undefined ->
|
||||
application:unset_env(sasl, sasl_error_logger)
|
||||
end,
|
||||
application:unload(sasl), % silently fails if running
|
||||
ok.
|
|
@ -0,0 +1,135 @@
|
|||
-module(cth_readable_shell).
|
||||
-import(cth_readable_helpers, [format_path/2, colorize/2, maybe_eunit_format/1]).
|
||||
|
||||
-define(OKC, green).
|
||||
-define(FAILC, red).
|
||||
-define(SKIPC, magenta).
|
||||
|
||||
-define(OK(Suite, CasePat, CaseArgs),
|
||||
?CASE(Suite, CasePat, ?OKC, "OK", CaseArgs)).
|
||||
-define(SKIP(Suite, CasePat, CaseArgs, Reason),
|
||||
?STACK(Suite, CasePat, CaseArgs, Reason, ?SKIPC, "SKIPPED")).
|
||||
-define(FAIL(Suite, CasePat, CaseArgs, Reason),
|
||||
?STACK(Suite, CasePat, CaseArgs, Reason, ?FAILC, "FAILED")).
|
||||
-define(STACK(Suite, CasePat, CaseArgs, Reason, Color, Label),
|
||||
begin
|
||||
?CASE(Suite, CasePat, Color, Label, CaseArgs),
|
||||
io:format(user, "%%% ~p ==> "++colorize(Color, maybe_eunit_format(Reason))++"~n", [Suite])
|
||||
end).
|
||||
-define(CASE(Suite, CasePat, Color, Res, Args),
|
||||
io:format(user, "%%% ~p ==> "++CasePat++": "++colorize(Color, Res)++"~n", [Suite | Args])).
|
||||
|
||||
%% Callbacks
|
||||
-export([id/1]).
|
||||
-export([init/2]).
|
||||
|
||||
-export([pre_init_per_suite/3]).
|
||||
-export([post_init_per_suite/4]).
|
||||
-export([pre_end_per_suite/3]).
|
||||
-export([post_end_per_suite/4]).
|
||||
|
||||
-export([pre_init_per_group/3]).
|
||||
-export([post_init_per_group/4]).
|
||||
-export([pre_end_per_group/3]).
|
||||
-export([post_end_per_group/4]).
|
||||
|
||||
-export([pre_init_per_testcase/3]).
|
||||
-export([post_end_per_testcase/4]).
|
||||
|
||||
-export([on_tc_fail/3]).
|
||||
-export([on_tc_skip/3, on_tc_skip/4]).
|
||||
|
||||
-export([terminate/1]).
|
||||
|
||||
-record(state, {id, suite, groups}).
|
||||
|
||||
%% @doc Return a unique id for this CTH.
|
||||
id(_Opts) ->
|
||||
{?MODULE, make_ref()}.
|
||||
|
||||
%% @doc Always called before any other callback function. Use this to initiate
|
||||
%% any common state.
|
||||
init(Id, _Opts) ->
|
||||
{ok, #state{id=Id}}.
|
||||
|
||||
%% @doc Called before init_per_suite is called.
|
||||
pre_init_per_suite(Suite,Config,State) ->
|
||||
{Config, State#state{suite=Suite, groups=[]}}.
|
||||
|
||||
%% @doc Called after init_per_suite.
|
||||
post_init_per_suite(_Suite,_Config,Return,State) ->
|
||||
{Return, State}.
|
||||
|
||||
%% @doc Called before end_per_suite.
|
||||
pre_end_per_suite(_Suite,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after end_per_suite.
|
||||
post_end_per_suite(_Suite,_Config,Return,State) ->
|
||||
{Return, State#state{suite=undefined, groups=[]}}.
|
||||
|
||||
%% @doc Called before each init_per_group.
|
||||
pre_init_per_group(_Group,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after each init_per_group.
|
||||
post_init_per_group(Group,_Config,Return, State=#state{groups=Groups}) ->
|
||||
{Return, State#state{groups=[Group|Groups]}}.
|
||||
|
||||
%% @doc Called after each end_per_group.
|
||||
pre_end_per_group(_Group,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after each end_per_group.
|
||||
post_end_per_group(_Group,_Config,Return, State=#state{groups=Groups}) ->
|
||||
{Return, State#state{groups=tl(Groups)}}.
|
||||
|
||||
%% @doc Called before each test case.
|
||||
pre_init_per_testcase(_TC,Config,State) ->
|
||||
{Config, State}.
|
||||
|
||||
%% @doc Called after each test case.
|
||||
post_end_per_testcase(TC,_Config,ok,State=#state{suite=Suite, groups=Groups}) ->
|
||||
?OK(Suite, "~s", [format_path(TC,Groups)]),
|
||||
{ok, State};
|
||||
post_end_per_testcase(TC,Config,Error,State=#state{suite=Suite, groups=Groups}) ->
|
||||
case lists:keyfind(tc_status, 1, Config) of
|
||||
{tc_status, ok} ->
|
||||
%% Test case passed, but we still ended in an error
|
||||
?STACK(Suite, "~s", [format_path(TC,Groups)], Error, ?SKIPC, "end_per_testcase FAILED");
|
||||
_ ->
|
||||
%% Test case failed, in which case on_tc_fail already reports it
|
||||
ok
|
||||
end,
|
||||
{Error, State}.
|
||||
|
||||
%% @doc Called after post_init_per_suite, post_end_per_suite, post_init_per_group,
|
||||
%% post_end_per_group and post_end_per_testcase if the suite, group or test case failed.
|
||||
on_tc_fail({TC,_Group}, Reason, State=#state{suite=Suite, groups=Groups}) ->
|
||||
?FAIL(Suite, "~s", [format_path(TC,Groups)], Reason),
|
||||
State;
|
||||
on_tc_fail(TC, Reason, State=#state{suite=Suite, groups=Groups}) ->
|
||||
?FAIL(Suite, "~s", [format_path(TC,Groups)], Reason),
|
||||
State.
|
||||
|
||||
%% @doc Called when a test case is skipped by either user action
|
||||
%% or due to an init function failing. (>= 19.3)
|
||||
on_tc_skip(Suite, {TC,_Group}, Reason, State=#state{groups=Groups}) ->
|
||||
?SKIP(Suite, "~s", [format_path(TC,Groups)], Reason),
|
||||
State#state{suite=Suite};
|
||||
on_tc_skip(Suite, TC, Reason, State=#state{groups=Groups}) ->
|
||||
?SKIP(Suite, "~s", [format_path(TC,Groups)], Reason),
|
||||
State#state{suite=Suite}.
|
||||
|
||||
%% @doc Called when a test case is skipped by either user action
|
||||
%% or due to an init function failing. (Pre-19.3)
|
||||
on_tc_skip({TC,Group}, Reason, State=#state{suite=Suite}) ->
|
||||
?SKIP(Suite, "~p (group ~p)", [TC, Group], Reason),
|
||||
State;
|
||||
on_tc_skip(TC, Reason, State=#state{suite=Suite}) ->
|
||||
?SKIP(Suite, "~p", [TC], Reason),
|
||||
State.
|
||||
|
||||
%% @doc Called when the scope of the CTH is done
|
||||
terminate(_State) ->
|
||||
ok.
|
|
@ -0,0 +1,17 @@
|
|||
-module(cth_readable_transform).
|
||||
-export([parse_transform/2]).
|
||||
|
||||
parse_transform(ASTs, _Options) ->
|
||||
try
|
||||
[erl_syntax_lib:map(fun(T) ->
|
||||
transform(erl_syntax:revert(T))
|
||||
end, AST) || AST <- ASTs]
|
||||
catch
|
||||
_:_ ->
|
||||
ASTs
|
||||
end.
|
||||
|
||||
transform({call, Line, {remote, _, {atom, _, ct}, {atom, _, pal}}, Args}) ->
|
||||
{call, Line, {remote, Line, {atom, Line, cthr}, {atom, Line, pal}}, Args};
|
||||
transform(Term) ->
|
||||
Term.
|
|
@ -0,0 +1,140 @@
|
|||
%% @doc Experimental
|
||||
-module(cthr).
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-ifndef(MAX_VERBOSITY).
|
||||
-define(R15_FALLBACK, true).
|
||||
%% the log level is used as argument to any CT logging function
|
||||
-define(MIN_IMPORTANCE, 0 ).
|
||||
-define(LOW_IMPORTANCE, 25).
|
||||
-define(STD_IMPORTANCE, 50).
|
||||
-define(HI_IMPORTANCE, 75).
|
||||
-define(MAX_IMPORTANCE, 99).
|
||||
|
||||
%% verbosity thresholds to filter out logging printouts
|
||||
-define(MIN_VERBOSITY, 0 ). %% turn logging off
|
||||
-define(LOW_VERBOSITY, 25 ).
|
||||
-define(STD_VERBOSITY, 50 ).
|
||||
-define(HI_VERBOSITY, 75 ).
|
||||
-define(MAX_VERBOSITY, 100).
|
||||
-endif.
|
||||
|
||||
-export([pal/1, pal/2, pal/3, pal/4, pal/5]).
|
||||
|
||||
pal(Format) ->
|
||||
pal(default, ?STD_IMPORTANCE, Format, []).
|
||||
|
||||
pal(X1,X2) ->
|
||||
{Category,Importance,Format,Args} =
|
||||
if is_atom(X1) -> {X1,?STD_IMPORTANCE,X2,[]};
|
||||
is_integer(X1) -> {default,X1,X2,[]};
|
||||
is_list(X1) -> {default,?STD_IMPORTANCE,X1,X2}
|
||||
end,
|
||||
pal(Category,Importance,Format,Args).
|
||||
|
||||
pal(X1,X2,X3) ->
|
||||
{Category,Importance,Format,Args} =
|
||||
if is_atom(X1), is_integer(X2) -> {X1,X2,X3,[]};
|
||||
is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3};
|
||||
is_integer(X1) -> {default,X1,X2,X3}
|
||||
end,
|
||||
pal(Category,Importance,Format,Args).
|
||||
|
||||
-ifdef(R15_FALLBACK).
|
||||
%% R15 and earlier didn't support log verbosity.
|
||||
|
||||
pal(Category,_Importance,Format,Args) ->
|
||||
case whereis(cth_readable_failonly) of
|
||||
undefined -> % hook not running, passthrough
|
||||
ct_logs:tc_pal(Category,Format,Args);
|
||||
_ -> % hook running, take over
|
||||
Name = case erlang:function_exported(logger, module_info, 0) of
|
||||
true -> cth_readable_logger;
|
||||
false -> error_logger
|
||||
end,
|
||||
gen_event:call(Name, cth_readable_failonly,
|
||||
{ct_pal, format(Category,Format,Args)}),
|
||||
%% Send to ct group leader
|
||||
ct_logs:tc_log(Category, Format, Args),
|
||||
ok
|
||||
end.
|
||||
|
||||
-else.
|
||||
|
||||
pal(Category,Importance,Format,Args) ->
|
||||
case whereis(cth_readable_failonly) of
|
||||
undefined -> % hook not running, passthrough
|
||||
ct_logs:tc_pal(Category,Importance,Format,Args);
|
||||
_ -> % hook running, take over
|
||||
%% Send to error_logger, but only our own handler
|
||||
Name = case erlang:function_exported(logger, module_info, 0) of
|
||||
true -> cth_readable_logger;
|
||||
false -> error_logger
|
||||
end,
|
||||
gen_event:call(Name, cth_readable_failonly,
|
||||
{ct_pal, format(Category,Importance,Format,Args)}),
|
||||
%% Send to ct group leader
|
||||
ct_logs:tc_log(Category, Importance, Format, Args),
|
||||
ok
|
||||
end.
|
||||
|
||||
pal(Category,Importance,Format,Args,Opts) ->
|
||||
case whereis(cth_readable_failonly) of
|
||||
undefined -> % hook not running, passthrough
|
||||
ct_logs:tc_pal(Category,Importance,Format,Args,Opts);
|
||||
_ -> % hook running, take over
|
||||
%% Send to error_logger, but only our own handler
|
||||
Name = case erlang:function_exported(logger, module_info, 0) of
|
||||
true -> cth_readable_logger;
|
||||
false -> error_logger
|
||||
end,
|
||||
gen_event:call(Name, cth_readable_failonly,
|
||||
{ct_pal, format(Category,Importance,Format,Args)}),
|
||||
%% Send to ct group leader
|
||||
ct_logs:tc_log(Category, Importance, Format, Args, Opts),
|
||||
ok
|
||||
end.
|
||||
|
||||
-endif.
|
||||
%%% Replicate CT stuff but don't output it
|
||||
format(Category, Importance, Format, Args) ->
|
||||
VLvl = try ct_util:get_verbosity(Category) of
|
||||
undefined ->
|
||||
ct_util:get_verbosity('$unspecified');
|
||||
{error,bad_invocation} ->
|
||||
?MAX_VERBOSITY;
|
||||
{error,_Failure} ->
|
||||
?MAX_VERBOSITY;
|
||||
Val ->
|
||||
Val
|
||||
catch error:undef ->
|
||||
?MAX_VERBOSITY
|
||||
end,
|
||||
if Importance >= (100-VLvl) ->
|
||||
format(Category, Format, Args);
|
||||
true ->
|
||||
ignore
|
||||
end.
|
||||
|
||||
format(Category, Format, Args) ->
|
||||
Head = get_heading(Category),
|
||||
io_lib:format(lists:concat([Head,Format,"\n\n"]), Args).
|
||||
|
||||
get_heading(default) ->
|
||||
io_lib:format("\n-----------------------------"
|
||||
"-----------------------\n~s\n",
|
||||
[log_timestamp(os:timestamp())]);
|
||||
get_heading(Category) ->
|
||||
io_lib:format("\n-----------------------------"
|
||||
"-----------------------\n~s ~w\n",
|
||||
[log_timestamp(os:timestamp()),Category]).
|
||||
|
||||
log_timestamp({MS,S,US}) ->
|
||||
put(log_timestamp, {MS,S,US}),
|
||||
{{Year,Month,Day}, {Hour,Min,Sec}} =
|
||||
calendar:now_to_local_time({MS,S,US}),
|
||||
MilliSec = trunc(US/1000),
|
||||
lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0B "
|
||||
"~2.10.0B:~2.10.0B:~2.10.0B.~3.10.0B",
|
||||
[Year,Month,Day,Hour,Min,Sec,MilliSec])).
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
Erlware Commons
|
||||
===============
|
||||
|
||||
Current Status
|
||||
--------------
|
||||
|
||||
![Tests](https://github.com/erlware/erlware_commons/workflows/EUnit/badge.svg)
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
Erlware commons can best be described as an extension to the stdlib
|
||||
application that is distributed with Erlang. These are things that we
|
||||
at Erlware have found useful for production applications but are not
|
||||
included with the distribution. We hope that as things in this library
|
||||
prove themselves useful, they will make their way into the main Erlang
|
||||
distribution. However, whether they do or not, we hope that this
|
||||
application will prove generally useful.
|
||||
|
||||
Goals for the project
|
||||
---------------------
|
||||
|
||||
* Generally Useful Code
|
||||
* High Quality
|
||||
* Well Documented
|
||||
* Well Tested
|
||||
|
||||
Licenses
|
||||
--------
|
||||
|
||||
This project contains elements licensed with Apache License, Version 2.0,
|
||||
as well as elements licensed with The MIT License.
|
||||
|
||||
You'll find license-related information in the header of specific files,
|
||||
where warranted.
|
||||
|
||||
In cases where no such information is present refer to
|
||||
[COPYING](COPYING).
|
||||
|
||||
Currently Available Modules/Systems
|
||||
------------------------------------
|
||||
|
||||
### [ec_date](https://github.com/erlware/erlware_commons/blob/master/src/ec_date.erl)
|
||||
|
||||
This module formats erlang dates in the form {{Year, Month, Day},
|
||||
{Hour, Minute, Second}} to printable strings, using (almost)
|
||||
equivalent formatting rules as http://uk.php.net/date, US vs European
|
||||
dates are disambiguated in the same way as
|
||||
http://uk.php.net/manual/en/function.strtotime.php That is, Dates in
|
||||
the m/d/y or d-m-y formats are disambiguated by looking at the
|
||||
separator between the various components: if the separator is a slash
|
||||
(/), then the American m/d/y is assumed; whereas if the separator is a
|
||||
dash (-) or a dot (.), then the European d-m-y format is assumed. To
|
||||
avoid potential ambiguity, it's best to use ISO 8601 (YYYY-MM-DD)
|
||||
dates.
|
||||
|
||||
erlang has no concept of timezone so the following formats are not
|
||||
implemented: B e I O P T Z formats c and r will also differ slightly
|
||||
|
||||
### [ec_file](https://github.com/erlware/erlware_commons/blob/master/src/ec_file.erl)
|
||||
|
||||
A set of commonly defined helper functions for files that are not
|
||||
included in stdlib.
|
||||
|
||||
### [ec_plists](https://github.com/erlware/erlware_commons/blob/master/src/ec_plists.erl)
|
||||
|
||||
plists is a drop-in replacement for module <a
|
||||
href="http://www.erlang.org/doc/man/lists.html">lists</a>, making most
|
||||
list operations parallel. It can operate on each element in parallel,
|
||||
for IO-bound operations, on sublists in parallel, for taking advantage
|
||||
of multi-core machines with CPU-bound operations, and across erlang
|
||||
nodes, for parallizing inside a cluster. It handles errors and node
|
||||
failures. It can be configured, tuned, and tweaked to get optimal
|
||||
performance while minimizing overhead.
|
||||
|
||||
Almost all the functions are identical to equivalent functions in
|
||||
lists, returning exactly the same result, and having both a form with
|
||||
an identical syntax that operates on each element in parallel and a
|
||||
form which takes an optional "malt", a specification for how to
|
||||
parallize the operation.
|
||||
|
||||
fold is the one exception, parallel fold is different from linear
|
||||
fold. This module also include a simple mapreduce implementation, and
|
||||
the function runmany. All the other functions are implemented with
|
||||
runmany, which is as a generalization of parallel list operations.
|
||||
|
||||
### [ec_semver](https://github.com/erlware/erlware_commons/blob/master/src/ec_semver.erl)
|
||||
|
||||
A complete parser for the [semver](http://semver.org/)
|
||||
standard. Including a complete set of conforming comparison functions.
|
||||
|
||||
### [ec_lists](https://github.com/erlware/erlware_commons/blob/master/src/ec_lists.erl)
|
||||
|
||||
A set of additional list manipulation functions designed to supliment
|
||||
the `lists` module in stdlib.
|
||||
|
||||
### [ec_talk](https://github.com/erlware/erlware_commons/blob/master/src/ec_talk.erl)
|
||||
|
||||
A set of simple utility functions to facilitate command line
|
||||
communication with a user.
|
||||
|
||||
Signatures
|
||||
-----------
|
||||
|
||||
Other languages, have built in support for **Interface** or
|
||||
**signature** functionality. Java has Interfaces, SML has
|
||||
Signatures. Erlang, though, doesn't currently support this model, at
|
||||
least not directly. There are a few ways you can approximate it. We
|
||||
have defined a mechnism called *signatures* and several modules that
|
||||
to serve as examples and provide a good set of *dictionary*
|
||||
signatures. More information about signatures can be found at
|
||||
[signature](https://github.com/erlware/erlware_commons/blob/master/doc/signatures.md).
|
||||
|
||||
|
||||
### [ec_dictionary](https://github.com/erlware/erlware_commons/blob/master/src/ec_dictionary.erl)
|
||||
|
||||
A signature that supports association of keys to values. A map cannot
|
||||
contain duplicate keys; each key can map to at most one value.
|
||||
|
||||
### [ec_dict](https://github.com/erlware/erlware_commons/blob/master/src/ec_dict.erl)
|
||||
|
||||
This provides an implementation of the ec_dictionary signature using
|
||||
erlang's dicts as a base. The function documentation for ec_dictionary
|
||||
applies here as well.
|
||||
|
||||
### [ec_gb_trees](https://github.com/erlware/erlware_commons/blob/master/src/ec_gb_trees.erl)
|
||||
|
||||
This provides an implementation of the ec_dictionary signature using
|
||||
erlang's gb_trees as a base. The function documentation for
|
||||
ec_dictionary applies here as well.
|
||||
|
||||
### [ec_orddict](https://github.com/erlware/erlware_commons/blob/master/src/ec_orddict.erl)
|
||||
|
||||
This provides an implementation of the ec_dictionary signature using
|
||||
erlang's orddict as a base. The function documentation for
|
||||
ec_dictionary applies here as well.
|
||||
|
||||
### [ec_rbdict](https://github.com/erlware/erlware_commons/blob/master/src/ec_rbdict.erl)
|
||||
|
||||
This provides an implementation of the ec_dictionary signature using
|
||||
Robert Virding's rbdict module as a base. The function documentation
|
||||
for ec_dictionary applies here as well.
|
|
@ -0,0 +1,24 @@
|
|||
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
|
||||
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
|
||||
%%%
|
||||
%%% This file is provided to you under the Apache License,
|
||||
%%% Version 2.0 (the "License"); you may not use this file
|
||||
%%% except in compliance with the License. You may obtain
|
||||
%%% a copy of the License at
|
||||
%%%
|
||||
%%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%%
|
||||
%%% Unless required by applicable law or agreed to in writing,
|
||||
%%% software distributed under the License is distributed on an
|
||||
%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
%%% KIND, either express or implied. See the License for the
|
||||
%%% specific language governing permissions and limitations
|
||||
%%% under the License.
|
||||
%%%---------------------------------------------------------------------------
|
||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||
%%% @copyright (C) 2012 Erlware, LLC.
|
||||
|
||||
-define(EC_ERROR, 0).
|
||||
-define(EC_WARN, 1).
|
||||
-define(EC_INFO, 2).
|
||||
-define(EC_DEBUG, 3).
|
|
@ -0,0 +1,9 @@
|
|||
semver <- major_minor_patch_min_patch ("-" alpha_part ("." alpha_part)*)? ("+" alpha_part ("." alpha_part)*)? !.
|
||||
` ec_semver:internal_parse_version(Node) ` ;
|
||||
|
||||
major_minor_patch_min_patch <- ("v"? numeric_part / alpha_part) ("." version_part)? ("." version_part)? ("." version_part)? ;
|
||||
|
||||
version_part <- numeric_part / alpha_part ;
|
||||
|
||||
numeric_part <- [0-9]+ `erlang:list_to_integer(erlang:binary_to_list(erlang:iolist_to_binary(Node)))` ;
|
||||
alpha_part <- [A-Za-z0-9]+ `erlang:iolist_to_binary(Node)` ;
|
|
@ -0,0 +1,33 @@
|
|||
%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
|
||||
|
||||
%% Dependencies ================================================================
|
||||
{deps, [
|
||||
{cf, "~>0.3"}
|
||||
]}.
|
||||
|
||||
{erl_first_files, ["ec_dictionary", "ec_vsn"]}.
|
||||
|
||||
%% Compiler Options ============================================================
|
||||
{erl_opts,
|
||||
[{platform_define, "^[0-9]+", namespaced_types},
|
||||
{platform_define, "^[0-9]+", have_callback_support},
|
||||
{platform_define, "^R1[4|5]", deprecated_crypto},
|
||||
{platform_define, "^1[8|9]", rand_module},
|
||||
{platform_define, "^2", rand_module},
|
||||
{platform_define, "^2", unicode_str},
|
||||
{platform_define, "^(R|1|20)", fun_stacktrace},
|
||||
debug_info,
|
||||
warnings_as_errors]}.
|
||||
|
||||
%% EUnit =======================================================================
|
||||
{eunit_opts, [verbose,
|
||||
{report, {eunit_surefire, [{dir, "."}]}}]}.
|
||||
|
||||
{cover_enabled, true}.
|
||||
{cover_print_enabled, true}.
|
||||
|
||||
%% Profiles ====================================================================
|
||||
{profiles, [{dev, [{deps,
|
||||
[{neotoma, "",
|
||||
{git, "https://github.com/seancribbs/neotoma.git", {branch, master}}}]}]}
|
||||
]}.
|
|
@ -0,0 +1,17 @@
|
|||
IsRebar3 = case application:get_key(rebar, vsn) of
|
||||
{ok, Vsn} ->
|
||||
[MajorVersion|_] = string:tokens(Vsn, "."),
|
||||
(list_to_integer(MajorVersion) >= 3);
|
||||
undefined ->
|
||||
false
|
||||
end,
|
||||
|
||||
Rebar2Deps = [
|
||||
{cf, ".*", {git, "https://github.com/project-fifo/cf", {tag, "0.2.2"}}}
|
||||
],
|
||||
|
||||
case IsRebar3 of
|
||||
true -> CONFIG;
|
||||
false ->
|
||||
lists:keyreplace(deps, 1, CONFIG, {deps, Rebar2Deps})
|
||||
end.
|
|
@ -0,0 +1,8 @@
|
|||
{"1.2.0",
|
||||
[{<<"cf">>,{pkg,<<"cf">>,<<"0.3.1">>},0}]}.
|
||||
[
|
||||
{pkg_hash,[
|
||||
{<<"cf">>, <<"5CB902239476E141EA70A740340233782D363A31EEA8AD37049561542E6CD641">>}]},
|
||||
{pkg_hash_ext,[
|
||||
{<<"cf">>, <<"315E8D447D3A4B02BCDBFA397AD03BBB988A6E0AA6F44D3ADD0F4E3C3BF97672">>}]}
|
||||
].
|
|
@ -0,0 +1,106 @@
|
|||
%%% vi:ts=4 sw=4 et
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||
%%% @copyright 2011 Erlware, LLC.
|
||||
%%% @doc
|
||||
%%% provides an implementation of ec_dictionary using an association
|
||||
%%% list as a basy
|
||||
%%% see ec_dictionary
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ec_assoc_list).
|
||||
|
||||
-behaviour(ec_dictionary).
|
||||
|
||||
%% API
|
||||
-export([new/0,
|
||||
has_key/2,
|
||||
get/2,
|
||||
get/3,
|
||||
add/3,
|
||||
remove/2,
|
||||
has_value/2,
|
||||
size/1,
|
||||
to_list/1,
|
||||
from_list/1,
|
||||
keys/1]).
|
||||
|
||||
-export_type([dictionary/2]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Types
|
||||
%%%===================================================================
|
||||
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||
%% however you should not rely on the internal representation here
|
||||
-type dictionary(K, V) :: {ec_assoc_list,
|
||||
[{ec_dictionary:key(K), ec_dictionary:value(V)}]}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec new() -> dictionary(_K, _V).
|
||||
new() ->
|
||||
{ec_assoc_list, []}.
|
||||
|
||||
-spec has_key(ec_dictionary:key(K), Object::dictionary(K, _V)) -> boolean().
|
||||
has_key(Key, {ec_assoc_list, Data}) ->
|
||||
lists:keymember(Key, 1, Data).
|
||||
|
||||
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
|
||||
ec_dictionary:value(V).
|
||||
get(Key, {ec_assoc_list, Data}) ->
|
||||
case lists:keyfind(Key, 1, Data) of
|
||||
{Key, Value} ->
|
||||
Value;
|
||||
false ->
|
||||
throw(not_found)
|
||||
end.
|
||||
|
||||
-spec get(ec_dictionary:key(K),
|
||||
ec_dictionary:value(V),
|
||||
Object::dictionary(K, V)) ->
|
||||
ec_dictionary:value(V).
|
||||
get(Key, Default, {ec_assoc_list, Data}) ->
|
||||
case lists:keyfind(Key, 1, Data) of
|
||||
{Key, Value} ->
|
||||
Value;
|
||||
false ->
|
||||
Default
|
||||
end.
|
||||
|
||||
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
|
||||
Object::dictionary(K, V)) ->
|
||||
dictionary(K, V).
|
||||
add(Key, Value, {ec_assoc_list, _Data}=Dict) ->
|
||||
{ec_assoc_list, Rest} = remove(Key,Dict),
|
||||
{ec_assoc_list, [{Key, Value} | Rest ]}.
|
||||
|
||||
-spec remove(ec_dictionary:key(K), Object::dictionary(K, _V)) ->
|
||||
dictionary(K, _V).
|
||||
remove(Key, {ec_assoc_list, Data}) ->
|
||||
{ec_assoc_list, lists:keydelete(Key, 1, Data)}.
|
||||
|
||||
-spec has_value(ec_dictionary:value(V), Object::dictionary(_K, V)) -> boolean().
|
||||
has_value(Value, {ec_assoc_list, Data}) ->
|
||||
lists:keymember(Value, 2, Data).
|
||||
|
||||
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
|
||||
size({ec_assoc_list, Data}) ->
|
||||
length(Data).
|
||||
|
||||
-spec to_list(dictionary(K, V)) -> [{ec_dictionary:key(K),
|
||||
ec_dictionary:value(V)}].
|
||||
to_list({ec_assoc_list, Data}) ->
|
||||
Data.
|
||||
|
||||
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
||||
dictionary(K, V).
|
||||
from_list(List) when is_list(List) ->
|
||||
{ec_assoc_list, List}.
|
||||
|
||||
-spec keys(dictionary(K, _V)) -> [ec_dictionary:key(K)].
|
||||
keys({ec_assoc_list, Data}) ->
|
||||
lists:map(fun({Key, _Value}) ->
|
||||
Key
|
||||
end, Data).
|
|
@ -0,0 +1,303 @@
|
|||
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
|
||||
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
|
||||
%%%
|
||||
%%% This file is provided to you under the Apache License,
|
||||
%%% Version 2.0 (the "License"); you may not use this file
|
||||
%%% except in compliance with the License. You may obtain
|
||||
%%% a copy of the License at
|
||||
%%%
|
||||
%%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%%
|
||||
%%% Unless required by applicable law or agreed to in writing,
|
||||
%%% software distributed under the License is distributed on an
|
||||
%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
%%% KIND, either express or implied. See the License for the
|
||||
%%% specific language governing permissions and limitations
|
||||
%%% under the License.
|
||||
%%%---------------------------------------------------------------------------
|
||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||
%%% @copyright (C) 2012 Erlware, LLC.
|
||||
%%%
|
||||
%%% @doc This provides simple output functions for command line apps. You should
|
||||
%%% use this to talk to the users if you are wrting code for the system
|
||||
-module(ec_cmd_log).
|
||||
|
||||
%% Avoid clashing with `error/3` BIF added in Erlang/OTP 24
|
||||
-compile({no_auto_import,[error/3]}).
|
||||
|
||||
-export([new/1,
|
||||
new/2,
|
||||
new/3,
|
||||
log/4,
|
||||
should/2,
|
||||
debug/2,
|
||||
debug/3,
|
||||
info/2,
|
||||
info/3,
|
||||
error/2,
|
||||
error/3,
|
||||
warn/2,
|
||||
warn/3,
|
||||
log_level/1,
|
||||
atom_log_level/1,
|
||||
format/1]).
|
||||
|
||||
-include("ec_cmd_log.hrl").
|
||||
|
||||
-define(RED, $r).
|
||||
-define(GREEN, $g).
|
||||
-define(YELLOW, $y).
|
||||
-define(BLUE, $b).
|
||||
-define(MAGENTA, $m).
|
||||
-define(CYAN, $c).
|
||||
|
||||
-define(PREFIX, "===> ").
|
||||
|
||||
-record(state_t, {log_level=0 :: int_log_level(),
|
||||
caller=api :: caller(),
|
||||
intensity=low :: none | low | high}).
|
||||
|
||||
%%============================================================================
|
||||
%% types
|
||||
%%============================================================================
|
||||
-export_type([t/0,
|
||||
int_log_level/0,
|
||||
atom_log_level/0,
|
||||
log_level/0,
|
||||
caller/0,
|
||||
log_fun/0]).
|
||||
|
||||
-type caller() :: api | command_line.
|
||||
|
||||
-type log_level() :: int_log_level() | atom_log_level().
|
||||
|
||||
-type int_log_level() :: 0..3.
|
||||
|
||||
-type atom_log_level() :: error | warn | info | debug.
|
||||
|
||||
-type intensity() :: none | low | high.
|
||||
|
||||
-type log_fun() :: fun(() -> iolist()).
|
||||
|
||||
-type color() :: char().
|
||||
|
||||
-opaque t() :: #state_t{}.
|
||||
|
||||
%%============================================================================
|
||||
%% API
|
||||
%%============================================================================
|
||||
%% @doc Create a new 'log level' for the system
|
||||
-spec new(log_level()) -> t().
|
||||
new(LogLevel) ->
|
||||
new(LogLevel, api).
|
||||
|
||||
-spec new(log_level(), caller()) -> t().
|
||||
new(LogLevel, Caller) ->
|
||||
new(LogLevel, Caller, high).
|
||||
|
||||
|
||||
-spec new(log_level(), caller(), intensity()) -> t().
|
||||
new(LogLevel, Caller, Intensity) when (Intensity =:= none orelse
|
||||
Intensity =:= low orelse
|
||||
Intensity =:= high),
|
||||
LogLevel >= 0, LogLevel =< 3 ->
|
||||
#state_t{log_level=LogLevel, caller=Caller,
|
||||
intensity=Intensity};
|
||||
new(AtomLogLevel, Caller, Intensity)
|
||||
when AtomLogLevel =:= error;
|
||||
AtomLogLevel =:= warn;
|
||||
AtomLogLevel =:= info;
|
||||
AtomLogLevel =:= debug ->
|
||||
LogLevel = case AtomLogLevel of
|
||||
error -> 0;
|
||||
warn -> 1;
|
||||
info -> 2;
|
||||
debug -> 3
|
||||
end,
|
||||
new(LogLevel, Caller, Intensity).
|
||||
|
||||
|
||||
%% @doc log at the debug level given the current log state with a string or
|
||||
%% function that returns a string
|
||||
-spec debug(t(), string() | log_fun()) -> ok.
|
||||
debug(LogState, Fun)
|
||||
when erlang:is_function(Fun) ->
|
||||
log(LogState, ?EC_DEBUG, fun() ->
|
||||
colorize(LogState, ?CYAN, false, Fun())
|
||||
end);
|
||||
debug(LogState, String) ->
|
||||
debug(LogState, "~s~n", [String]).
|
||||
|
||||
%% @doc log at the debug level given the current log state with a format string
|
||||
%% and argements @see io:format/2
|
||||
-spec debug(t(), string(), [any()]) -> ok.
|
||||
debug(LogState, FormatString, Args) ->
|
||||
log(LogState, ?EC_DEBUG, colorize(LogState, ?CYAN, false, FormatString), Args).
|
||||
|
||||
%% @doc log at the info level given the current log state with a string or
|
||||
%% function that returns a string
|
||||
-spec info(t(), string() | log_fun()) -> ok.
|
||||
info(LogState, Fun)
|
||||
when erlang:is_function(Fun) ->
|
||||
log(LogState, ?EC_INFO, fun() ->
|
||||
colorize(LogState, ?GREEN, false, Fun())
|
||||
end);
|
||||
info(LogState, String) ->
|
||||
info(LogState, "~s~n", [String]).
|
||||
|
||||
%% @doc log at the info level given the current log state with a format string
|
||||
%% and argements @see io:format/2
|
||||
-spec info(t(), string(), [any()]) -> ok.
|
||||
info(LogState, FormatString, Args) ->
|
||||
log(LogState, ?EC_INFO, colorize(LogState, ?GREEN, false, FormatString), Args).
|
||||
|
||||
%% @doc log at the error level given the current log state with a string or
|
||||
%% format string that returns a function
|
||||
-spec error(t(), string() | log_fun()) -> ok.
|
||||
error(LogState, Fun)
|
||||
when erlang:is_function(Fun) ->
|
||||
log(LogState, ?EC_ERROR, fun() ->
|
||||
colorize(LogState, ?RED, false, Fun())
|
||||
end);
|
||||
error(LogState, String) ->
|
||||
error(LogState, "~s~n", [String]).
|
||||
|
||||
%% @doc log at the error level given the current log state with a format string
|
||||
%% and argements @see io:format/2
|
||||
-spec error(t(), string(), [any()]) -> ok.
|
||||
error(LogState, FormatString, Args) ->
|
||||
log(LogState, ?EC_ERROR, colorize(LogState, ?RED, false, FormatString), Args).
|
||||
|
||||
%% @doc log at the warn level given the current log state with a string or
|
||||
%% format string that returns a function
|
||||
-spec warn(t(), string() | log_fun()) -> ok.
|
||||
warn(LogState, Fun)
|
||||
when erlang:is_function(Fun) ->
|
||||
log(LogState, ?EC_WARN, fun() -> colorize(LogState, ?MAGENTA, false, Fun()) end);
|
||||
warn(LogState, String) ->
|
||||
warn(LogState, "~s~n", [String]).
|
||||
|
||||
%% @doc log at the warn level given the current log state with a format string
|
||||
%% and argements @see io:format/2
|
||||
-spec warn(t(), string(), [any()]) -> ok.
|
||||
warn(LogState, FormatString, Args) ->
|
||||
log(LogState, ?EC_WARN, colorize(LogState, ?MAGENTA, false, FormatString), Args).
|
||||
|
||||
%% @doc Execute the fun passed in if log level is as expected.
|
||||
-spec log(t(), int_log_level(), log_fun()) -> ok.
|
||||
log(#state_t{log_level=DetailLogLevel}, LogLevel, Fun)
|
||||
when DetailLogLevel >= LogLevel ->
|
||||
io:format("~s~n", [Fun()]);
|
||||
log(_, _, _) ->
|
||||
ok.
|
||||
|
||||
%% @doc when the module log level is less then or equal to the log level for the
|
||||
%% call then write the log info out. When its not then ignore the call.
|
||||
-spec log(t(), int_log_level(), string(), [any()]) -> ok.
|
||||
log(#state_t{log_level=DetailLogLevel}, LogLevel, FormatString, Args)
|
||||
when DetailLogLevel >= LogLevel,
|
||||
erlang:is_list(Args) ->
|
||||
io:format(FormatString, Args);
|
||||
log(_, _, _, _) ->
|
||||
ok.
|
||||
|
||||
%% @doc return a boolean indicating if the system should log for the specified
|
||||
%% levelg
|
||||
-spec should(t(), int_log_level() | any()) -> boolean().
|
||||
should(#state_t{log_level=DetailLogLevel}, LogLevel)
|
||||
when DetailLogLevel >= LogLevel ->
|
||||
true;
|
||||
should(_, _) ->
|
||||
false.
|
||||
|
||||
%% @doc get the current log level as an integer
|
||||
-spec log_level(t()) -> int_log_level().
|
||||
log_level(#state_t{log_level=DetailLogLevel}) ->
|
||||
DetailLogLevel.
|
||||
|
||||
%% @doc get the current log level as an atom
|
||||
-spec atom_log_level(t()) -> atom_log_level().
|
||||
atom_log_level(#state_t{log_level=?EC_ERROR}) ->
|
||||
error;
|
||||
atom_log_level(#state_t{log_level=?EC_WARN}) ->
|
||||
warn;
|
||||
atom_log_level(#state_t{log_level=?EC_INFO}) ->
|
||||
info;
|
||||
atom_log_level(#state_t{log_level=?EC_DEBUG}) ->
|
||||
debug.
|
||||
|
||||
-spec format(t()) -> iolist().
|
||||
format(Log) ->
|
||||
[<<"(">>,
|
||||
ec_cnv:to_binary(log_level(Log)), <<":">>,
|
||||
ec_cnv:to_binary(atom_log_level(Log)),
|
||||
<<")">>].
|
||||
|
||||
-spec colorize(t(), color(), boolean(), string()) -> string().
|
||||
|
||||
-define(VALID_COLOR(C),
|
||||
C =:= $r orelse C =:= $g orelse C =:= $y orelse
|
||||
C =:= $b orelse C =:= $m orelse C =:= $c orelse
|
||||
C =:= $R orelse C =:= $G orelse C =:= $Y orelse
|
||||
C =:= $B orelse C =:= $M orelse C =:= $C).
|
||||
|
||||
colorize(#state_t{intensity=none}, _, _, Msg) ->
|
||||
Msg;
|
||||
%% When it is suposed to be bold and we already have a uppercase
|
||||
%% (bold color) we don't need to modify the color
|
||||
colorize(State, Color, true, Msg) when ?VALID_COLOR(Color),
|
||||
Color >= $A, Color =< $Z ->
|
||||
colorize(State, Color, false, Msg);
|
||||
%% We're sneaky we can substract 32 to get the uppercase character if we want
|
||||
%% bold but have a non bold color.
|
||||
colorize(State, Color, true, Msg) when ?VALID_COLOR(Color) ->
|
||||
colorize(State, Color - 32, false, Msg);
|
||||
colorize(#state_t{caller=command_line, intensity = high},
|
||||
Color, false, Msg) when ?VALID_COLOR(Color) ->
|
||||
lists:flatten(cf:format("~!" ++ [Color] ++"~s~s", [?PREFIX, Msg]));
|
||||
colorize(#state_t{caller=command_line, intensity = low},
|
||||
Color, false, Msg) when ?VALID_COLOR(Color) ->
|
||||
lists:flatten(cf:format("~!" ++ [Color] ++"~s~!!~s", [?PREFIX, Msg]));
|
||||
colorize(_LogState, _Color, _Bold, Msg) ->
|
||||
Msg.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Test Functions
|
||||
%%%===================================================================
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
should_test() ->
|
||||
ErrorLogState = new(error),
|
||||
?assertMatch(true, should(ErrorLogState, ?EC_ERROR)),
|
||||
?assertMatch(true, not should(ErrorLogState, ?EC_INFO)),
|
||||
?assertMatch(true, not should(ErrorLogState, ?EC_DEBUG)),
|
||||
?assertEqual(?EC_ERROR, log_level(ErrorLogState)),
|
||||
?assertEqual(error, atom_log_level(ErrorLogState)),
|
||||
|
||||
InfoLogState = new(info),
|
||||
?assertMatch(true, should(InfoLogState, ?EC_ERROR)),
|
||||
?assertMatch(true, should(InfoLogState, ?EC_INFO)),
|
||||
?assertMatch(true, not should(InfoLogState, ?EC_DEBUG)),
|
||||
?assertEqual(?EC_INFO, log_level(InfoLogState)),
|
||||
?assertEqual(info, atom_log_level(InfoLogState)),
|
||||
|
||||
DebugLogState = new(debug),
|
||||
?assertMatch(true, should(DebugLogState, ?EC_ERROR)),
|
||||
?assertMatch(true, should(DebugLogState, ?EC_INFO)),
|
||||
?assertMatch(true, should(DebugLogState, ?EC_DEBUG)),
|
||||
?assertEqual(?EC_DEBUG, log_level(DebugLogState)),
|
||||
?assertEqual(debug, atom_log_level(DebugLogState)).
|
||||
|
||||
|
||||
no_color_test() ->
|
||||
LogState = new(debug, command_line, none),
|
||||
?assertEqual("test",
|
||||
colorize(LogState, ?RED, true, "test")).
|
||||
|
||||
color_test() ->
|
||||
LogState = new(debug, command_line, high),
|
||||
?assertEqual("\e[1;31m===> test\e[0m",
|
||||
colorize(LogState, ?RED, true, "test")).
|
||||
-endif.
|
|
@ -0,0 +1,247 @@
|
|||
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
|
||||
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
|
||||
%%%
|
||||
%%% This file is provided to you under the Apache License,
|
||||
%%% Version 2.0 (the "License"); you may not use this file
|
||||
%%% except in compliance with the License. You may obtain
|
||||
%%% a copy of the License at
|
||||
%%%
|
||||
%%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%%
|
||||
%%% Unless required by applicable law or agreed to in writing,
|
||||
%%% software distributed under the License is distributed on an
|
||||
%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
%%% KIND, either express or implied. See the License for the
|
||||
%%% specific language governing permissions and limitations
|
||||
%%% under the License.
|
||||
%%%---------------------------------------------------------------------------
|
||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||
%%% @copyright (C) 2012 Erlware, LLC.
|
||||
%%%
|
||||
-module(ec_cnv).
|
||||
|
||||
%% API
|
||||
-export([to_integer/1,
|
||||
to_integer/2,
|
||||
to_float/1,
|
||||
to_float/2,
|
||||
to_number/1,
|
||||
to_list/1,
|
||||
to_binary/1,
|
||||
to_atom/1,
|
||||
to_boolean/1,
|
||||
is_true/1,
|
||||
is_false/1]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc
|
||||
%% Automatic conversion of a term into integer type. The conversion
|
||||
%% will round a float value if nonstrict is specified otherwise badarg
|
||||
-spec to_integer(string() | binary() | integer() | float()) ->
|
||||
integer().
|
||||
to_integer(X)->
|
||||
to_integer(X, nonstrict).
|
||||
|
||||
-spec to_integer(string() | binary() | integer() | float(),
|
||||
strict | nonstrict) ->
|
||||
integer().
|
||||
to_integer(X, strict)
|
||||
when erlang:is_float(X) ->
|
||||
erlang:error(badarg);
|
||||
to_integer(X, nonstrict)
|
||||
when erlang:is_float(X) ->
|
||||
erlang:round(X);
|
||||
to_integer(X, S)
|
||||
when erlang:is_binary(X) ->
|
||||
to_integer(erlang:binary_to_list(X), S);
|
||||
to_integer(X, S)
|
||||
when erlang:is_list(X) ->
|
||||
try erlang:list_to_integer(X) of
|
||||
Result ->
|
||||
Result
|
||||
catch
|
||||
error:badarg when S =:= nonstrict ->
|
||||
erlang:round(erlang:list_to_float(X))
|
||||
end;
|
||||
to_integer(X, _)
|
||||
when erlang:is_integer(X) ->
|
||||
X.
|
||||
|
||||
%% @doc
|
||||
%% Automatic conversion of a term into float type. badarg if strict
|
||||
%% is defined and an integer value is passed.
|
||||
-spec to_float(string() | binary() | integer() | float()) ->
|
||||
float().
|
||||
to_float(X) ->
|
||||
to_float(X, nonstrict).
|
||||
|
||||
-spec to_float(string() | binary() | integer() | float(),
|
||||
strict | nonstrict) ->
|
||||
float().
|
||||
to_float(X, S) when is_binary(X) ->
|
||||
to_float(erlang:binary_to_list(X), S);
|
||||
to_float(X, S)
|
||||
when erlang:is_list(X) ->
|
||||
try erlang:list_to_float(X) of
|
||||
Result ->
|
||||
Result
|
||||
catch
|
||||
error:badarg when S =:= nonstrict ->
|
||||
erlang:list_to_integer(X) * 1.0
|
||||
end;
|
||||
to_float(X, strict) when
|
||||
erlang:is_integer(X) ->
|
||||
erlang:error(badarg);
|
||||
to_float(X, nonstrict)
|
||||
when erlang:is_integer(X) ->
|
||||
X * 1.0;
|
||||
to_float(X, _) when erlang:is_float(X) ->
|
||||
X.
|
||||
|
||||
%% @doc
|
||||
%% Automatic conversion of a term into number type.
|
||||
-spec to_number(binary() | string() | number()) ->
|
||||
number().
|
||||
to_number(X)
|
||||
when erlang:is_number(X) ->
|
||||
X;
|
||||
to_number(X)
|
||||
when erlang:is_binary(X) ->
|
||||
to_number(to_list(X));
|
||||
to_number(X)
|
||||
when erlang:is_list(X) ->
|
||||
try list_to_integer(X) of
|
||||
Int -> Int
|
||||
catch
|
||||
error:badarg ->
|
||||
list_to_float(X)
|
||||
end.
|
||||
|
||||
%% @doc
|
||||
%% Automatic conversion of a term into Erlang list
|
||||
-spec to_list(atom() | list() | binary() | integer() | float()) ->
|
||||
list().
|
||||
to_list(X)
|
||||
when erlang:is_float(X) ->
|
||||
erlang:float_to_list(X);
|
||||
to_list(X)
|
||||
when erlang:is_integer(X) ->
|
||||
erlang:integer_to_list(X);
|
||||
to_list(X)
|
||||
when erlang:is_binary(X) ->
|
||||
erlang:binary_to_list(X);
|
||||
to_list(X)
|
||||
when erlang:is_atom(X) ->
|
||||
erlang:atom_to_list(X);
|
||||
to_list(X)
|
||||
when erlang:is_list(X) ->
|
||||
X.
|
||||
|
||||
%% @doc
|
||||
%% Known limitations:
|
||||
%% Converting [256 | _], lists with integers > 255
|
||||
-spec to_binary(atom() | string() | binary() | integer() | float()) ->
|
||||
binary().
|
||||
to_binary(X)
|
||||
when erlang:is_float(X) ->
|
||||
to_binary(to_list(X));
|
||||
to_binary(X)
|
||||
when erlang:is_integer(X) ->
|
||||
erlang:iolist_to_binary(integer_to_list(X));
|
||||
to_binary(X)
|
||||
when erlang:is_atom(X) ->
|
||||
erlang:list_to_binary(erlang:atom_to_list(X));
|
||||
to_binary(X)
|
||||
when erlang:is_list(X) ->
|
||||
erlang:iolist_to_binary(X);
|
||||
to_binary(X)
|
||||
when erlang:is_binary(X) ->
|
||||
X.
|
||||
|
||||
-spec to_boolean(binary() | string() | atom()) ->
|
||||
boolean().
|
||||
to_boolean(<<"true">>) ->
|
||||
true;
|
||||
to_boolean("true") ->
|
||||
true;
|
||||
to_boolean(true) ->
|
||||
true;
|
||||
to_boolean(<<"false">>) ->
|
||||
false;
|
||||
to_boolean("false") ->
|
||||
false;
|
||||
to_boolean(false) ->
|
||||
false.
|
||||
|
||||
-spec is_true(binary() | string() | atom()) ->
|
||||
boolean().
|
||||
is_true(<<"true">>) ->
|
||||
true;
|
||||
is_true("true") ->
|
||||
true;
|
||||
is_true(true) ->
|
||||
true;
|
||||
is_true(_) ->
|
||||
false.
|
||||
|
||||
-spec is_false(binary() | string() | atom()) ->
|
||||
boolean().
|
||||
is_false(<<"false">>) ->
|
||||
true;
|
||||
is_false("false") ->
|
||||
true;
|
||||
is_false(false) ->
|
||||
true;
|
||||
is_false(_) ->
|
||||
false.
|
||||
|
||||
%% @doc
|
||||
%% Automation conversion a term to an existing atom. badarg is
|
||||
%% returned if the atom doesn't exist. the safer version, won't let
|
||||
%% you leak atoms
|
||||
-spec to_atom(atom() | list() | binary() | integer() | float()) ->
|
||||
atom().
|
||||
to_atom(X)
|
||||
when erlang:is_atom(X) ->
|
||||
X;
|
||||
to_atom(X)
|
||||
when erlang:is_list(X) ->
|
||||
erlang:list_to_existing_atom(X);
|
||||
to_atom(X) ->
|
||||
to_atom(to_list(X)).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Tests
|
||||
%%%===================================================================
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
to_integer_test() ->
|
||||
?assertError(badarg, to_integer(1.5, strict)).
|
||||
|
||||
to_float_test() ->
|
||||
?assertError(badarg, to_float(10, strict)).
|
||||
|
||||
to_atom_test() ->
|
||||
?assertMatch(true, to_atom("true")),
|
||||
?assertMatch(true, to_atom(<<"true">>)),
|
||||
?assertMatch(false, to_atom(<<"false">>)),
|
||||
?assertMatch(false, to_atom(false)),
|
||||
?assertError(badarg, to_atom("hello_foo_bar_baz")),
|
||||
|
||||
S = erlang:list_to_atom("1"),
|
||||
?assertMatch(S, to_atom(1)).
|
||||
|
||||
to_boolean_test()->
|
||||
?assertMatch(true, to_boolean(<<"true">>)),
|
||||
?assertMatch(true, to_boolean("true")),
|
||||
?assertMatch(true, to_boolean(true)),
|
||||
?assertMatch(false, to_boolean(<<"false">>)),
|
||||
?assertMatch(false, to_boolean("false")),
|
||||
?assertMatch(false, to_boolean(false)).
|
||||
|
||||
-endif.
|
|
@ -0,0 +1,131 @@
|
|||
%%%-------------------------------------------------------------------
|
||||
%%% @author Eric Merritt <>
|
||||
%%% @copyright (C) 2011, Erlware, LLC.
|
||||
%%% @doc
|
||||
%%% These are various utility functions to help with compiling and
|
||||
%%% decompiling erlang source. They are mostly useful to the
|
||||
%%% language/parse transform implementor.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ec_compile).
|
||||
|
||||
-export([beam_to_erl_source/2,
|
||||
erl_source_to_core_ast/1,
|
||||
erl_source_to_erl_ast/1,
|
||||
erl_source_to_asm/1,
|
||||
erl_source_to_erl_syntax/1,
|
||||
erl_string_to_core_ast/1,
|
||||
erl_string_to_erl_ast/1,
|
||||
erl_string_to_asm/1,
|
||||
erl_string_to_erl_syntax/1]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc decompile a beam file that has been compiled with +debug_info
|
||||
%% into a erlang source file
|
||||
%%
|
||||
%% @param BeamFName the name of the beamfile
|
||||
%% @param ErlFName the name of the erlang file where the generated
|
||||
%% source file will be output. This should *not* be the same as the
|
||||
%% source file that created the beamfile unless you want to overwrite
|
||||
%% it.
|
||||
-spec beam_to_erl_source(string(), string()) -> ok | term().
|
||||
beam_to_erl_source(BeamFName, ErlFName) ->
|
||||
case beam_lib:chunks(BeamFName, [abstract_code]) of
|
||||
{ok, {_, [{abstract_code, {raw_abstract_v1,Forms}}]}} ->
|
||||
Src =
|
||||
erl_prettypr:format(erl_syntax:form_list(tl(Forms))),
|
||||
{ok, Fd} = file:open(ErlFName, [write]),
|
||||
io:fwrite(Fd, "~s~n", [Src]),
|
||||
file:close(Fd);
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% @doc compile an erlang source file into a Core Erlang AST
|
||||
%%
|
||||
%% @param Path - The path to the erlang source file
|
||||
-spec erl_source_to_core_ast(file:filename()) -> CoreAst::term().
|
||||
erl_source_to_core_ast(Path) ->
|
||||
{ok, Contents} = file:read_file(Path),
|
||||
erl_string_to_core_ast(binary_to_list(Contents)).
|
||||
|
||||
%% @doc compile an erlang source file into an Erlang AST
|
||||
%%
|
||||
%% @param Path - The path to the erlang source file
|
||||
-spec erl_source_to_erl_ast(file:filename()) -> ErlangAst::term().
|
||||
erl_source_to_erl_ast(Path) ->
|
||||
{ok, Contents} = file:read_file(Path),
|
||||
erl_string_to_erl_ast(binary_to_list(Contents)).
|
||||
|
||||
%% @doc compile an erlang source file into erlang terms that represent
|
||||
%% the relevant ASM
|
||||
%%
|
||||
%% @param Path - The path to the erlang source file
|
||||
-spec erl_source_to_asm(file:filename()) -> ErlangAsm::term().
|
||||
erl_source_to_asm(Path) ->
|
||||
{ok, Contents} = file:read_file(Path),
|
||||
erl_string_to_asm(binary_to_list(Contents)).
|
||||
|
||||
%% @doc compile an erlang source file to a string that displays the
|
||||
%% 'erl_syntax1 calls needed to reproduce those terms.
|
||||
%%
|
||||
%% @param Path - The path to the erlang source file
|
||||
-spec erl_source_to_erl_syntax(file:filename()) -> string().
|
||||
erl_source_to_erl_syntax(Path) ->
|
||||
{ok, Contents} = file:read_file(Path),
|
||||
erl_string_to_erl_syntax(Contents).
|
||||
|
||||
%% @doc compile a string representing an erlang expression into an
|
||||
%% Erlang AST
|
||||
%%
|
||||
%% @param StringExpr - The path to the erlang source file
|
||||
-spec erl_string_to_erl_ast(string()) -> ErlangAst::term().
|
||||
erl_string_to_erl_ast(StringExpr) ->
|
||||
Forms0 =
|
||||
lists:foldl(fun(<<>>, Acc) ->
|
||||
Acc;
|
||||
(<<"\n\n">>, Acc) ->
|
||||
Acc;
|
||||
(El, Acc) ->
|
||||
{ok, Tokens, _} =
|
||||
erl_scan:string(binary_to_list(El)
|
||||
++ "."),
|
||||
[Tokens | Acc]
|
||||
end, [], re:split(StringExpr, "\\.\n")),
|
||||
%% No need to reverse. This will rereverse for us
|
||||
lists:foldl(fun(Form, Forms) ->
|
||||
{ok, ErlAST} = erl_parse:parse_form(Form),
|
||||
[ErlAST | Forms]
|
||||
end, [], Forms0).
|
||||
|
||||
%% @doc compile a string representing an erlang expression into a
|
||||
%% Core Erlang AST
|
||||
%%
|
||||
%% @param StringExpr - The path to the erlang source file
|
||||
-spec erl_string_to_core_ast(string()) -> CoreAst::term().
|
||||
erl_string_to_core_ast(StringExpr) ->
|
||||
compile:forms(erl_string_to_erl_ast(StringExpr), [to_core]).
|
||||
|
||||
%% @doc compile a string representing an erlang expression into a term
|
||||
%% that represents the ASM
|
||||
%%
|
||||
%% @param StringExpr - The path to the erlang source file
|
||||
-spec erl_string_to_asm(string()) -> ErlangAsm::term().
|
||||
erl_string_to_asm(StringExpr) ->
|
||||
compile:forms(erl_string_to_erl_ast(StringExpr), ['S']).
|
||||
|
||||
%% @doc compile an erlang source file to a string that displays the
|
||||
%% 'erl_syntax1 calls needed to reproduce those terms.
|
||||
%%
|
||||
%% @param StringExpr - The string representing the erlang code.
|
||||
-spec erl_string_to_erl_syntax(string() | binary()) -> string().
|
||||
erl_string_to_erl_syntax(BinaryExpr)
|
||||
when erlang:is_binary(BinaryExpr) ->
|
||||
erlang:binary_to_list(BinaryExpr);
|
||||
erl_string_to_erl_syntax(StringExpr) ->
|
||||
{ok, Tokens, _} = erl_scan:string(StringExpr),
|
||||
{ok, ErlAST} = erl_parse:parse_form(Tokens),
|
||||
io:format(erl_prettypr:format(erl_syntax:meta(ErlAST))).
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,114 @@
|
|||
%%% vi:ts=4 sw=4 et
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||
%%% @copyright 2011 Erlware, LLC.
|
||||
%%% @doc
|
||||
%%% This provides an implementation of the ec_dictionary type using
|
||||
%%% erlang dicts as a base. The function documentation for
|
||||
%%% ec_dictionary applies here as well.
|
||||
%%% see ec_dictionary
|
||||
%%% see dict
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ec_dict).
|
||||
|
||||
-behaviour(ec_dictionary).
|
||||
|
||||
%% API
|
||||
-export([new/0,
|
||||
has_key/2,
|
||||
get/2,
|
||||
get/3,
|
||||
add/3,
|
||||
remove/2,
|
||||
has_value/2,
|
||||
size/1,
|
||||
to_list/1,
|
||||
from_list/1,
|
||||
keys/1]).
|
||||
|
||||
-export_type([dictionary/2]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Types
|
||||
%%%===================================================================
|
||||
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||
%% however you should not rely on the internal representation here
|
||||
-ifdef(namespaced_types).
|
||||
-type dictionary(_K, _V) :: dict:dict().
|
||||
-else.
|
||||
-type dictionary(_K, _V) :: dict().
|
||||
-endif.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec new() -> dictionary(_K, _V).
|
||||
new() ->
|
||||
dict:new().
|
||||
|
||||
-spec has_key(ec_dictionary:key(K), Object::dictionary(K, _V)) -> boolean().
|
||||
has_key(Key, Data) ->
|
||||
dict:is_key(Key, Data).
|
||||
|
||||
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
|
||||
ec_dictionary:value(V).
|
||||
get(Key, Data) ->
|
||||
case dict:find(Key, Data) of
|
||||
{ok, Value} ->
|
||||
Value;
|
||||
error ->
|
||||
throw(not_found)
|
||||
end.
|
||||
|
||||
-spec get(ec_dictionary:key(K),
|
||||
ec_dictionary:value(V),
|
||||
Object::dictionary(K, V)) ->
|
||||
ec_dictionary:value(V).
|
||||
get(Key, Default, Data) ->
|
||||
case dict:find(Key, Data) of
|
||||
{ok, Value} ->
|
||||
Value;
|
||||
error ->
|
||||
Default
|
||||
end.
|
||||
|
||||
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
|
||||
Object::dictionary(K, V)) ->
|
||||
dictionary(K, V).
|
||||
add(Key, Value, Data) ->
|
||||
dict:store(Key, Value, Data).
|
||||
|
||||
-spec remove(ec_dictionary:key(K), Object::dictionary(K, V)) ->
|
||||
dictionary(K, V).
|
||||
remove(Key, Data) ->
|
||||
dict:erase(Key, Data).
|
||||
|
||||
-spec has_value(ec_dictionary:value(V), Object::dictionary(_K, V)) -> boolean().
|
||||
has_value(Value, Data) ->
|
||||
dict:fold(fun(_, NValue, _) when NValue == Value ->
|
||||
true;
|
||||
(_, _, Acc) ->
|
||||
Acc
|
||||
end,
|
||||
false,
|
||||
Data).
|
||||
|
||||
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
|
||||
size(Data) ->
|
||||
dict:size(Data).
|
||||
|
||||
-spec to_list(dictionary(K, V)) -> [{ec_dictionary:key(K),
|
||||
ec_dictionary:value(V)}].
|
||||
to_list(Data) ->
|
||||
dict:to_list(Data).
|
||||
|
||||
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
||||
dictionary(K, V).
|
||||
from_list(List) when is_list(List) ->
|
||||
dict:from_list(List).
|
||||
|
||||
-spec keys(dictionary(K, _V)) -> [ec_dictionary:key(K)].
|
||||
keys(Dict) ->
|
||||
dict:fetch_keys(Dict).
|
|
@ -0,0 +1,176 @@
|
|||
%%% vi:ts=4 sw=4 et
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||
%%% @copyright 2011 Erlware, LLC.
|
||||
%%% @doc
|
||||
%%% A module that supports association of keys to values. A map cannot
|
||||
%%% contain duplicate keys; each key can map to at most one value.
|
||||
%%%
|
||||
%%% This interface is a member of the Erlware Commons Library.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ec_dictionary).
|
||||
|
||||
%% API
|
||||
-export([new/1,
|
||||
has_key/2,
|
||||
get/2,
|
||||
get/3,
|
||||
add/3,
|
||||
remove/2,
|
||||
has_value/2,
|
||||
size/1,
|
||||
to_list/1,
|
||||
from_list/2,
|
||||
keys/1]).
|
||||
|
||||
-export_type([dictionary/2,
|
||||
key/1,
|
||||
value/1]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Types
|
||||
%%%===================================================================
|
||||
|
||||
-record(dict_t,
|
||||
{callback,
|
||||
data}).
|
||||
|
||||
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||
%% however you should not rely on the internal representation here
|
||||
-type dictionary(_K, _V) :: #dict_t{}.
|
||||
-type key(T) :: T.
|
||||
-type value(T) :: T.
|
||||
|
||||
-ifdef(have_callback_support).
|
||||
|
||||
-callback new() -> any().
|
||||
-callback has_key(key(any()), any()) -> boolean().
|
||||
-callback get(key(any()), any()) -> any().
|
||||
-callback add(key(any()), value(any()), T) -> T.
|
||||
-callback remove(key(any()), T) -> T.
|
||||
-callback has_value(value(any()), any()) -> boolean().
|
||||
-callback size(any()) -> non_neg_integer().
|
||||
-callback to_list(any()) -> [{key(any()), value(any())}].
|
||||
-callback from_list([{key(any()), value(any())}]) -> any().
|
||||
-callback keys(any()) -> [key(any())].
|
||||
|
||||
-else.
|
||||
|
||||
%% In the case where R14 or lower is being used to compile the system
|
||||
%% we need to export a behaviour info
|
||||
-export([behaviour_info/1]).
|
||||
-spec behaviour_info(atom()) -> [{atom(), arity()}] | undefined.
|
||||
behaviour_info(callbacks) ->
|
||||
[{new, 0},
|
||||
{has_key, 2},
|
||||
{get, 2},
|
||||
{add, 3},
|
||||
{remove, 2},
|
||||
{has_value, 2},
|
||||
{size, 1},
|
||||
{to_list, 1},
|
||||
{from_list, 1},
|
||||
{keys, 1}];
|
||||
behaviour_info(_Other) ->
|
||||
undefined.
|
||||
-endif.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc create a new dictionary object from the specified module. The
|
||||
%% module should implement the dictionary behaviour.
|
||||
%%
|
||||
%% @param ModuleName The module name.
|
||||
-spec new(module()) -> dictionary(_K, _V).
|
||||
new(ModuleName) when is_atom(ModuleName) ->
|
||||
#dict_t{callback = ModuleName, data = ModuleName:new()}.
|
||||
|
||||
%% @doc check to see if the dictionary provided has the specified key.
|
||||
%%
|
||||
%% @param Dict The dictory object to check
|
||||
%% @param Key The key to check the dictionary for
|
||||
-spec has_key(key(K), dictionary(K, _V)) -> boolean().
|
||||
has_key(Key, #dict_t{callback = Mod, data = Data}) ->
|
||||
Mod:has_key(Key, Data).
|
||||
|
||||
%% @doc given a key return that key from the dictionary. If the key is
|
||||
%% not found throw a 'not_found' exception.
|
||||
%%
|
||||
%% @param Dict The dictionary object to return the value from
|
||||
%% @param Key The key requested
|
||||
%% when the key does not exist @throws not_found
|
||||
-spec get(key(K), dictionary(K, V)) -> value(V).
|
||||
get(Key, #dict_t{callback = Mod, data = Data}) ->
|
||||
Mod:get(Key, Data).
|
||||
|
||||
%% @doc given a key return that key from the dictionary. If the key is
|
||||
%% not found then the default value is returned.
|
||||
%%
|
||||
%% @param Dict The dictionary object to return the value from
|
||||
%% @param Key The key requested
|
||||
%% @param Default The value that will be returned if no value is found
|
||||
%% in the database.
|
||||
-spec get(key(K), value(V), dictionary(K, V)) -> value(V).
|
||||
get(Key, Default, #dict_t{callback = Mod, data = Data}) ->
|
||||
Mod:get(Key, Default, Data).
|
||||
|
||||
%% @doc add a new value to the existing dictionary. Return a new
|
||||
%% dictionary containing the value.
|
||||
%%
|
||||
%% @param Dict the dictionary object to add too
|
||||
%% @param Key the key to add
|
||||
%% @param Value the value to add
|
||||
-spec add(key(K), value(V), dictionary(K, V)) -> dictionary(K, V).
|
||||
add(Key, Value, #dict_t{callback = Mod, data = Data} = Dict) ->
|
||||
Dict#dict_t{data = Mod:add(Key, Value, Data)}.
|
||||
|
||||
%% @doc Remove a value from the dictionary returning a new dictionary
|
||||
%% with the value removed.
|
||||
%%
|
||||
%% @param Dict the dictionary object to remove the value from
|
||||
%% @param Key the key of the key/value pair to remove
|
||||
-spec remove(key(K), dictionary(K, V)) -> dictionary(K, V).
|
||||
remove(Key, #dict_t{callback = Mod, data = Data} = Dict) ->
|
||||
Dict#dict_t{data = Mod:remove(Key, Data)}.
|
||||
|
||||
%% @doc Check to see if the value exists in the dictionary
|
||||
%%
|
||||
%% @param Dict the dictionary object to check
|
||||
%% @param Value The value to check if exists
|
||||
-spec has_value(value(V), dictionary(_K, V)) -> boolean().
|
||||
has_value(Value, #dict_t{callback = Mod, data = Data}) ->
|
||||
Mod:has_value(Value, Data).
|
||||
|
||||
%% @doc return the current number of key value pairs in the dictionary
|
||||
%%
|
||||
%% @param Dict the object return the size for.
|
||||
-spec size(dictionary(_K, _V)) -> integer().
|
||||
size(#dict_t{callback = Mod, data = Data}) ->
|
||||
Mod:size(Data).
|
||||
|
||||
%% @doc Return the contents of this dictionary as a list of key value
|
||||
%% pairs.
|
||||
%%
|
||||
%% @param Dict the base dictionary to make use of.
|
||||
-spec to_list(Dict::dictionary(K, V)) -> [{key(K), value(V)}].
|
||||
to_list(#dict_t{callback = Mod, data = Data}) ->
|
||||
Mod:to_list(Data).
|
||||
|
||||
%% @doc Create a new dictionary, of the specified implementation using
|
||||
%% the list provided as the starting contents.
|
||||
%%
|
||||
%% @param ModuleName the type to create the dictionary from
|
||||
%% @param List The list of key value pairs to start with
|
||||
-spec from_list(module(), [{key(K), value(V)}]) -> dictionary(K, V).
|
||||
from_list(ModuleName, List) when is_list(List) ->
|
||||
#dict_t{callback = ModuleName, data = ModuleName:from_list(List)}.
|
||||
|
||||
%% @doc Return the keys of this dictionary as a list
|
||||
%%
|
||||
%% @param Dict the base dictionary to make use of.
|
||||
-spec keys(Dict::dictionary(K, _V)) -> [key(K)].
|
||||
keys(#dict_t{callback = Mod, data = Data}) ->
|
||||
Mod:keys(Data).
|
|
@ -0,0 +1,478 @@
|
|||
%%% vi:ts=4 sw=4 et
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @copyright (C) 2011, Erlware LLC
|
||||
%%% @doc
|
||||
%%% Helper functions for working with files.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ec_file).
|
||||
|
||||
-export([
|
||||
exists/1,
|
||||
copy/2,
|
||||
copy/3,
|
||||
copy_file_info/3,
|
||||
insecure_mkdtemp/0,
|
||||
mkdir_path/1,
|
||||
mkdir_p/1,
|
||||
find/2,
|
||||
is_symlink/1,
|
||||
is_dir/1,
|
||||
type/1,
|
||||
real_dir_path/1,
|
||||
remove/1,
|
||||
remove/2,
|
||||
md5sum/1,
|
||||
sha1sum/1,
|
||||
read/1,
|
||||
write/2,
|
||||
write_term/2
|
||||
]).
|
||||
|
||||
-export_type([
|
||||
option/0
|
||||
]).
|
||||
|
||||
-include_lib("kernel/include/file.hrl").
|
||||
|
||||
-define(CHECK_PERMS_MSG,
|
||||
"Try checking that you have the correct permissions and try again~n").
|
||||
|
||||
%%============================================================================
|
||||
%% Types
|
||||
%%============================================================================
|
||||
-type file_info() :: mode | time | owner | group.
|
||||
-type option() :: recursive | {file_info, [file_info()]}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
-spec exists(file:filename()) -> boolean().
|
||||
exists(Filename) ->
|
||||
case file:read_file_info(Filename) of
|
||||
{ok, _} ->
|
||||
true;
|
||||
{error, _Reason} ->
|
||||
false
|
||||
end.
|
||||
|
||||
%% @doc copy an entire directory to another location.
|
||||
-spec copy(file:name(), file:name(), Options::[option()]) -> ok | {error, Reason::term()}.
|
||||
copy(From, To, []) ->
|
||||
copy_(From, To, []);
|
||||
copy(From, To, Options) ->
|
||||
case proplists:get_value(recursive, Options, false) of
|
||||
true ->
|
||||
case is_dir(From) of
|
||||
false ->
|
||||
copy_(From, To, Options);
|
||||
true ->
|
||||
make_dir_if_dir(To),
|
||||
copy_subfiles(From, To, Options)
|
||||
end;
|
||||
false ->
|
||||
copy_(From, To, Options)
|
||||
end.
|
||||
|
||||
%% @doc copy a file including timestamps,ownership and mode etc.
|
||||
-spec copy(From::file:filename(), To::file:filename()) -> ok | {error, Reason::term()}.
|
||||
copy(From, To) ->
|
||||
copy_(From, To, [{file_info, [mode, time, owner, group]}]).
|
||||
|
||||
copy_(From, To, Options) ->
|
||||
Linked
|
||||
= case file:read_link(From) of
|
||||
{ok, Linked0} -> Linked0;
|
||||
{error, _} -> undefined
|
||||
end,
|
||||
case Linked =/= undefined orelse file:copy(From, To) of
|
||||
true ->
|
||||
file:make_symlink(Linked, To);
|
||||
{ok, _} ->
|
||||
copy_file_info(To, From, proplists:get_value(file_info, Options, []));
|
||||
{error, Error} ->
|
||||
{error, {copy_failed, Error}}
|
||||
end.
|
||||
|
||||
copy_file_info(To, From, FileInfoToKeep) ->
|
||||
case file:read_file_info(From) of
|
||||
{ok, FileInfo} ->
|
||||
case write_file_info(To, FileInfo, FileInfoToKeep) of
|
||||
[] ->
|
||||
ok;
|
||||
Errors ->
|
||||
{error, {write_file_info_failed_for, Errors}}
|
||||
end;
|
||||
{error, RFError} ->
|
||||
{error, {read_file_info_failed, RFError}}
|
||||
end.
|
||||
|
||||
write_file_info(To, FileInfo, FileInfoToKeep) ->
|
||||
WriteInfoFuns = [{mode, fun try_write_mode/2},
|
||||
{time, fun try_write_time/2},
|
||||
{group, fun try_write_group/2},
|
||||
{owner, fun try_write_owner/2}],
|
||||
lists:foldl(fun(Info, Acc) ->
|
||||
case proplists:get_value(Info, WriteInfoFuns, undefined) of
|
||||
undefined ->
|
||||
Acc;
|
||||
F ->
|
||||
case F(To, FileInfo) of
|
||||
ok ->
|
||||
Acc;
|
||||
{error, Reason} ->
|
||||
[{Info, Reason} | Acc]
|
||||
end
|
||||
end
|
||||
end, [], FileInfoToKeep).
|
||||
|
||||
|
||||
try_write_mode(To, #file_info{mode=Mode}) ->
|
||||
file:write_file_info(To, #file_info{mode=Mode}).
|
||||
|
||||
try_write_time(To, #file_info{atime=Atime, mtime=Mtime}) ->
|
||||
file:write_file_info(To, #file_info{atime=Atime, mtime=Mtime}).
|
||||
|
||||
try_write_owner(To, #file_info{uid=OwnerId}) ->
|
||||
file:write_file_info(To, #file_info{uid=OwnerId}).
|
||||
|
||||
try_write_group(To, #file_info{gid=OwnerId}) ->
|
||||
file:write_file_info(To, #file_info{gid=OwnerId}).
|
||||
|
||||
%% @doc return an md5 checksum string or a binary. Same as unix utility of
|
||||
%% same name.
|
||||
-spec md5sum(string() | binary()) -> string().
|
||||
md5sum(Value) ->
|
||||
hex(binary_to_list(erlang:md5(Value))).
|
||||
|
||||
%% @doc return an sha1sum checksum string or a binary. Same as unix utility of
|
||||
%% same name.
|
||||
-ifdef(deprecated_crypto).
|
||||
-spec sha1sum(string() | binary()) -> string().
|
||||
sha1sum(Value) ->
|
||||
hex(binary_to_list(crypto:sha(Value))).
|
||||
-else.
|
||||
-spec sha1sum(string() | binary()) -> string().
|
||||
sha1sum(Value) ->
|
||||
hex(binary_to_list(crypto:hash(sha, Value))).
|
||||
-endif.
|
||||
|
||||
%% @doc delete a file. Use the recursive option for directories.
|
||||
%% <pre>
|
||||
%% Example: remove("./tmp_dir", [recursive]).
|
||||
%% </pre>
|
||||
-spec remove(file:name(), Options::[option()]) -> ok | {error, Reason::term()}.
|
||||
remove(Path, Options) ->
|
||||
case lists:member(recursive, Options) of
|
||||
false -> file:delete(Path);
|
||||
true -> remove_recursive(Path, Options)
|
||||
end.
|
||||
|
||||
|
||||
%% @doc delete a file.
|
||||
-spec remove(file:name()) -> ok | {error, Reason::term()}.
|
||||
remove(Path) ->
|
||||
remove(Path, []).
|
||||
|
||||
%% @doc indicates witha boolean if the path supplied refers to symlink.
|
||||
-spec is_symlink(file:name()) -> boolean().
|
||||
is_symlink(Path) ->
|
||||
case file:read_link_info(Path) of
|
||||
{ok, #file_info{type = symlink}} ->
|
||||
true;
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
is_dir(Path) ->
|
||||
case file:read_file_info(Path) of
|
||||
{ok, #file_info{type = directory}} ->
|
||||
true;
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
%% @doc returns the type of the file.
|
||||
-spec type(file:name()) -> file | symlink | directory | undefined.
|
||||
type(Path) ->
|
||||
case filelib:is_regular(Path) of
|
||||
true ->
|
||||
file;
|
||||
false ->
|
||||
case is_symlink(Path) of
|
||||
true ->
|
||||
symlink;
|
||||
false ->
|
||||
case is_dir(Path) of
|
||||
true -> directory;
|
||||
false -> undefined
|
||||
end
|
||||
end
|
||||
|
||||
end.
|
||||
%% @doc gets the real path of a directory. This is mostly useful for
|
||||
%% resolving symlinks. Be aware that this temporarily changes the
|
||||
%% current working directory to figure out what the actual path
|
||||
%% is. That means that it can be quite slow.
|
||||
-spec real_dir_path(file:name()) -> file:name().
|
||||
real_dir_path(Path) ->
|
||||
{ok, CurCwd} = file:get_cwd(),
|
||||
ok = file:set_cwd(Path),
|
||||
{ok, RealPath} = file:get_cwd(),
|
||||
ok = file:set_cwd(CurCwd),
|
||||
filename:absname(RealPath).
|
||||
|
||||
%% @doc make a unique temporary directory. Similar function to BSD stdlib
|
||||
%% function of the same name.
|
||||
-spec insecure_mkdtemp() -> TmpDirPath::file:name() | {error, term()}.
|
||||
insecure_mkdtemp() ->
|
||||
UniqueNumber = erlang:integer_to_list(erlang:trunc(random_uniform() * 1000000000000)),
|
||||
TmpDirPath =
|
||||
filename:join([tmp(), lists:flatten([".tmp_dir", UniqueNumber])]),
|
||||
|
||||
case mkdir_path(TmpDirPath) of
|
||||
ok -> TmpDirPath;
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%% @doc Makes a directory including parent dirs if they are missing.
|
||||
-spec mkdir_p(file:name()) -> ok | {error, Reason::term()}.
|
||||
mkdir_p(Path) ->
|
||||
%% We are exploiting a feature of ensuredir that that creates all
|
||||
%% directories up to the last element in the filename, then ignores
|
||||
%% that last element. This way we ensure that the dir is created
|
||||
%% and not have any worries about path names
|
||||
DirName = filename:join([filename:absname(Path), "tmp"]),
|
||||
filelib:ensure_dir(DirName).
|
||||
|
||||
|
||||
%% @doc Makes a directory including parent dirs if they are missing.
|
||||
-spec mkdir_path(file:name()) -> ok | {error, Reason::term()}.
|
||||
mkdir_path(Path) ->
|
||||
mkdir_p(Path).
|
||||
|
||||
|
||||
%% @doc read a file from the file system. Provide UEX exeption on failure.
|
||||
-spec read(FilePath::file:filename()) -> {ok, binary()} | {error, Reason::term()}.
|
||||
read(FilePath) ->
|
||||
%% Now that we are moving away from exceptions again this becomes
|
||||
%% a bit redundant but we want to be backwards compatible as much
|
||||
%% as possible in the api.
|
||||
file:read_file(FilePath).
|
||||
|
||||
|
||||
%% @doc write a file to the file system. Provide UEX exeption on failure.
|
||||
-spec write(FileName::file:filename(), Contents::string()) -> ok | {error, Reason::term()}.
|
||||
write(FileName, Contents) ->
|
||||
%% Now that we are moving away from exceptions again this becomes
|
||||
%% a bit redundant but we want to be backwards compatible as much
|
||||
%% as possible in the api.
|
||||
file:write_file(FileName, Contents).
|
||||
|
||||
%% @doc write a term out to a file so that it can be consulted later.
|
||||
-spec write_term(file:filename(), term()) -> ok | {error, Reason::term()}.
|
||||
write_term(FileName, Term) ->
|
||||
write(FileName, lists:flatten(io_lib:fwrite("~p. ", [Term]))).
|
||||
|
||||
%% @doc Finds files and directories that match the regexp supplied in
|
||||
%% the TargetPattern regexp.
|
||||
-spec find(FromDir::file:name(), TargetPattern::string()) -> [file:name()].
|
||||
find([], _) ->
|
||||
[];
|
||||
find(FromDir, TargetPattern) ->
|
||||
case is_dir(FromDir) of
|
||||
false ->
|
||||
case re:run(FromDir, TargetPattern) of
|
||||
{match, _} -> [FromDir];
|
||||
_ -> []
|
||||
end;
|
||||
true ->
|
||||
FoundDir = case re:run(FromDir, TargetPattern) of
|
||||
{match, _} -> [FromDir];
|
||||
_ -> []
|
||||
end,
|
||||
List = find_in_subdirs(FromDir, TargetPattern),
|
||||
FoundDir ++ List
|
||||
end.
|
||||
%%%===================================================================
|
||||
%%% Internal Functions
|
||||
%%%===================================================================
|
||||
-spec find_in_subdirs(file:name(), string()) -> [file:name()].
|
||||
find_in_subdirs(FromDir, TargetPattern) ->
|
||||
lists:foldl(fun(CheckFromDir, Acc)
|
||||
when CheckFromDir == FromDir ->
|
||||
Acc;
|
||||
(ChildFromDir, Acc) ->
|
||||
case find(ChildFromDir, TargetPattern) of
|
||||
[] -> Acc;
|
||||
Res -> Res ++ Acc
|
||||
end
|
||||
end,
|
||||
[],
|
||||
sub_files(FromDir)).
|
||||
|
||||
|
||||
|
||||
-spec remove_recursive(file:name(), Options::list()) -> ok | {error, Reason::term()}.
|
||||
remove_recursive(Path, Options) ->
|
||||
case is_dir(Path) of
|
||||
false ->
|
||||
file:delete(Path);
|
||||
true ->
|
||||
lists:foreach(fun(ChildPath) ->
|
||||
remove_recursive(ChildPath, Options)
|
||||
end, sub_files(Path)),
|
||||
file:del_dir(Path)
|
||||
end.
|
||||
|
||||
-spec tmp() -> file:name().
|
||||
tmp() ->
|
||||
case erlang:system_info(system_architecture) of
|
||||
"win32" ->
|
||||
case os:getenv("TEMP") of
|
||||
false -> "./tmp";
|
||||
Val -> Val
|
||||
end;
|
||||
_SysArch ->
|
||||
case os:getenv("TMPDIR") of
|
||||
false -> "/tmp";
|
||||
Val -> Val
|
||||
end
|
||||
end.
|
||||
|
||||
%% Copy the subfiles of the From directory to the to directory.
|
||||
-spec copy_subfiles(file:name(), file:name(), [option()]) -> {error, Reason::term()} | ok.
|
||||
copy_subfiles(From, To, Options) ->
|
||||
Fun =
|
||||
fun(ChildFrom) ->
|
||||
ChildTo = filename:join([To, filename:basename(ChildFrom)]),
|
||||
copy(ChildFrom, ChildTo, Options)
|
||||
end,
|
||||
lists:foreach(Fun, sub_files(From)).
|
||||
|
||||
-spec make_dir_if_dir(file:name()) -> ok | {error, Reason::term()}.
|
||||
make_dir_if_dir(File) ->
|
||||
case is_dir(File) of
|
||||
true -> ok;
|
||||
false -> mkdir_path(File)
|
||||
end.
|
||||
|
||||
%% @doc convert a list of integers into hex.
|
||||
-spec hex(string() | non_neg_integer()) -> string().
|
||||
hex(L) when is_list (L) ->
|
||||
lists:flatten([hex(I) || I <- L]);
|
||||
hex(I) when I > 16#f ->
|
||||
[hex0((I band 16#f0) bsr 4), hex0((I band 16#0f))];
|
||||
hex(I) ->
|
||||
[$0, hex0(I)].
|
||||
|
||||
hex0(10) -> $a;
|
||||
hex0(11) -> $b;
|
||||
hex0(12) -> $c;
|
||||
hex0(13) -> $d;
|
||||
hex0(14) -> $e;
|
||||
hex0(15) -> $f;
|
||||
hex0(I) -> $0 + I.
|
||||
|
||||
|
||||
sub_files(From) ->
|
||||
{ok, SubFiles} = file:list_dir(From),
|
||||
[filename:join(From, SubFile) || SubFile <- SubFiles].
|
||||
|
||||
-ifdef(rand_module).
|
||||
random_uniform() ->
|
||||
rand:uniform().
|
||||
-else.
|
||||
random_uniform() ->
|
||||
random:seed(os:timestamp()),
|
||||
random:uniform().
|
||||
-endif.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Test Functions
|
||||
%%%===================================================================
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
setup_test() ->
|
||||
Dir = insecure_mkdtemp(),
|
||||
mkdir_path(Dir),
|
||||
?assertMatch(false, is_symlink(Dir)),
|
||||
?assertMatch(true, filelib:is_dir(Dir)).
|
||||
|
||||
md5sum_test() ->
|
||||
?assertMatch("cfcd208495d565ef66e7dff9f98764da", md5sum("0")).
|
||||
|
||||
sha1sum_test() ->
|
||||
?assertMatch("b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", sha1sum("0")).
|
||||
|
||||
file_test() ->
|
||||
Dir = insecure_mkdtemp(),
|
||||
TermFile = filename:join(Dir, "ec_file/dir/file.term"),
|
||||
TermFileCopy = filename:join(Dir, "ec_file/dircopy/file.term"),
|
||||
filelib:ensure_dir(TermFile),
|
||||
filelib:ensure_dir(TermFileCopy),
|
||||
write_term(TermFile, "term"),
|
||||
?assertMatch({ok, <<"\"term\". ">>}, read(TermFile)),
|
||||
copy(filename:dirname(TermFile),
|
||||
filename:dirname(TermFileCopy),
|
||||
[recursive]).
|
||||
|
||||
teardown_test() ->
|
||||
Dir = insecure_mkdtemp(),
|
||||
remove(Dir, [recursive]),
|
||||
?assertMatch(false, filelib:is_dir(Dir)).
|
||||
|
||||
setup_base_and_target() ->
|
||||
BaseDir = insecure_mkdtemp(),
|
||||
DummyContents = <<"This should be deleted">>,
|
||||
SourceDir = filename:join([BaseDir, "source"]),
|
||||
ok = file:make_dir(SourceDir),
|
||||
Name1 = filename:join([SourceDir, "fileone"]),
|
||||
Name2 = filename:join([SourceDir, "filetwo"]),
|
||||
Name3 = filename:join([SourceDir, "filethree"]),
|
||||
NoName = filename:join([SourceDir, "noname"]),
|
||||
|
||||
ok = file:write_file(Name1, DummyContents),
|
||||
ok = file:write_file(Name2, DummyContents),
|
||||
ok = file:write_file(Name3, DummyContents),
|
||||
ok = file:write_file(NoName, DummyContents),
|
||||
{BaseDir, SourceDir, {Name1, Name2, Name3, NoName}}.
|
||||
|
||||
exists_test() ->
|
||||
BaseDir = insecure_mkdtemp(),
|
||||
SourceDir = filename:join([BaseDir, "source1"]),
|
||||
NoName = filename:join([SourceDir, "noname"]),
|
||||
ok = file:make_dir(SourceDir),
|
||||
Name1 = filename:join([SourceDir, "fileone"]),
|
||||
ok = file:write_file(Name1, <<"Testn">>),
|
||||
?assertMatch(true, exists(Name1)),
|
||||
?assertMatch(false, exists(NoName)).
|
||||
|
||||
real_path_test() ->
|
||||
BaseDir = "foo",
|
||||
Dir = filename:absname(filename:join(BaseDir, "source1")),
|
||||
LinkDir = filename:join([BaseDir, "link"]),
|
||||
ok = mkdir_p(Dir),
|
||||
file:make_symlink(Dir, LinkDir),
|
||||
?assertEqual(Dir, real_dir_path(LinkDir)),
|
||||
?assertEqual(directory, type(Dir)),
|
||||
?assertEqual(symlink, type(LinkDir)),
|
||||
TermFile = filename:join(BaseDir, "test_file"),
|
||||
ok = write_term(TermFile, foo),
|
||||
?assertEqual(file, type(TermFile)),
|
||||
?assertEqual(true, is_symlink(LinkDir)),
|
||||
?assertEqual(false, is_symlink(Dir)).
|
||||
|
||||
find_test() ->
|
||||
%% Create a directory in /tmp for the test. Clean everything afterwards
|
||||
{BaseDir, _SourceDir, {Name1, Name2, Name3, _NoName}} = setup_base_and_target(),
|
||||
Result = find(BaseDir, "file[a-z]+\$"),
|
||||
?assertMatch(3, erlang:length(Result)),
|
||||
?assertEqual(true, lists:member(Name1, Result)),
|
||||
?assertEqual(true, lists:member(Name2, Result)),
|
||||
?assertEqual(true, lists:member(Name3, Result)),
|
||||
remove(BaseDir, [recursive]).
|
||||
|
||||
-endif.
|
|
@ -0,0 +1,213 @@
|
|||
%%% vi:ts=4 sw=4 et
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||
%%% @copyright 2011 Erlware, LLC.
|
||||
%%% @doc
|
||||
%%% This provides an implementation of the type ec_dictionary using
|
||||
%%% gb_trees as a backin
|
||||
%%% see ec_dictionary
|
||||
%%% see gb_trees
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ec_gb_trees).
|
||||
|
||||
-behaviour(ec_dictionary).
|
||||
|
||||
%% API
|
||||
-export([new/0,
|
||||
has_key/2,
|
||||
get/2,
|
||||
get/3,
|
||||
add/3,
|
||||
remove/2,
|
||||
has_value/2,
|
||||
size/1,
|
||||
to_list/1,
|
||||
from_list/1,
|
||||
keys/1]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc create a new dictionary object from the specified module. The
|
||||
%% module should implement the dictionary behaviour. In the clause
|
||||
%% where an existing object is passed in new empty dictionary of the
|
||||
%% same implementation is created and returned.
|
||||
%%
|
||||
%% @param ModuleName|Object The module name or existing dictionary object.
|
||||
-spec new() -> gb_trees:tree(_K, _V).
|
||||
new() ->
|
||||
gb_trees:empty().
|
||||
|
||||
%% @doc check to see if the dictionary provided has the specified key.
|
||||
%%
|
||||
%% @param Object The dictory object to check
|
||||
%% @param Key The key to check the dictionary for
|
||||
-spec has_key(ec_dictionary:key(K), Object::gb_trees:tree(K, _V)) -> boolean().
|
||||
has_key(Key, Data) ->
|
||||
case gb_trees:lookup(Key, Data) of
|
||||
{value, _Val} ->
|
||||
true;
|
||||
none ->
|
||||
false
|
||||
end.
|
||||
|
||||
%% @doc given a key return that key from the dictionary. If the key is
|
||||
%% not found throw a 'not_found' exception.
|
||||
%%
|
||||
%% @param Object The dictionary object to return the value from
|
||||
%% @param Key The key requested
|
||||
%% when the key does not exist @throws not_found
|
||||
-spec get(ec_dictionary:key(K), Object::gb_trees:tree(K, V)) ->
|
||||
ec_dictionary:value(V).
|
||||
get(Key, Data) ->
|
||||
case gb_trees:lookup(Key, Data) of
|
||||
{value, Value} ->
|
||||
Value;
|
||||
none ->
|
||||
throw(not_found)
|
||||
end.
|
||||
|
||||
-spec get(ec_dictionary:key(K),
|
||||
ec_dictionary:value(V),
|
||||
Object::gb_trees:tree(K, V)) ->
|
||||
ec_dictionary:value(V).
|
||||
get(Key, Default, Data) ->
|
||||
case gb_trees:lookup(Key, Data) of
|
||||
{value, Value} ->
|
||||
Value;
|
||||
none ->
|
||||
Default
|
||||
end.
|
||||
|
||||
%% @doc add a new value to the existing dictionary. Return a new
|
||||
%% dictionary containing the value.
|
||||
%%
|
||||
%% @param Object the dictionary object to add too
|
||||
%% @param Key the key to add
|
||||
%% @param Value the value to add
|
||||
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
|
||||
Object::gb_trees:tree(K, V)) ->
|
||||
gb_trees:tree(K, V).
|
||||
add(Key, Value, Data) ->
|
||||
gb_trees:enter(Key, Value, Data).
|
||||
|
||||
%% @doc Remove a value from the dictionary returning a new dictionary
|
||||
%% with the value removed.
|
||||
%%
|
||||
%% @param Object the dictionary object to remove the value from
|
||||
%% @param Key the key of the key/value pair to remove
|
||||
-spec remove(ec_dictionary:key(K), Object::gb_trees:tree(K, V)) ->
|
||||
gb_trees:tree(K, V).
|
||||
remove(Key, Data) ->
|
||||
gb_trees:delete_any(Key, Data).
|
||||
|
||||
%% @doc Check to see if the value exists in the dictionary
|
||||
%%
|
||||
%% @param Object the dictionary object to check
|
||||
%% @param Value The value to check if exists
|
||||
-spec has_value(ec_dictionary:value(V), Object::gb_trees:tree(_K, V)) -> boolean().
|
||||
has_value(Value, Data) ->
|
||||
lists:member(Value, gb_trees:values(Data)).
|
||||
|
||||
%% @doc return the current number of key value pairs in the dictionary
|
||||
%%
|
||||
%% @param Object the object return the size for.
|
||||
-spec size(Object::gb_trees:tree(_K, _V)) -> non_neg_integer().
|
||||
size(Data) ->
|
||||
gb_trees:size(Data).
|
||||
|
||||
-spec to_list(gb_trees:tree(K, V)) -> [{ec_dictionary:key(K),
|
||||
ec_dictionary:value(V)}].
|
||||
to_list(Data) ->
|
||||
gb_trees:to_list(Data).
|
||||
|
||||
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
||||
gb_trees:tree(K, V).
|
||||
from_list(List) when is_list(List) ->
|
||||
lists:foldl(fun({Key, Value}, Dict) ->
|
||||
gb_trees:enter(Key, Value, Dict)
|
||||
end,
|
||||
gb_trees:empty(),
|
||||
List).
|
||||
|
||||
-spec keys(gb_trees:tree(K,_V)) -> [ec_dictionary:key(K)].
|
||||
keys(Data) ->
|
||||
gb_trees:keys(Data).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Tests
|
||||
%%%===================================================================
|
||||
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
%% For me unit testing initially is about covering the obvious case. A
|
||||
%% check to make sure that what you expect the tested functionality to
|
||||
%% do, it actually does. As time goes on and people detect bugs you
|
||||
%% add tests for those specific problems to the unit test suit.
|
||||
%%
|
||||
%% However, when getting started you can only test your basic
|
||||
%% expectations. So here are the expectations I have for the add
|
||||
%% functionality.
|
||||
%%
|
||||
%% 1) I can put arbitrary terms into the dictionary as keys
|
||||
%% 2) I can put arbitrary terms into the dictionary as values
|
||||
%% 3) When I put a value in the dictionary by a key, I can retrieve
|
||||
%% that same value
|
||||
%% 4) When I put a different value in the dictionary by key it does
|
||||
%% not change other key value pairs.
|
||||
%% 5) When I update a value the new value in available by the new key
|
||||
%% 6) When a value does not exist a not found exception is created
|
||||
|
||||
add_test() ->
|
||||
Dict0 = ec_dictionary:new(ec_gb_trees),
|
||||
|
||||
Key1 = foo,
|
||||
Key2 = [1, 3],
|
||||
Key3 = {"super"},
|
||||
Key4 = <<"fabulous">>,
|
||||
Key5 = {"Sona", 2, <<"Zuper">>},
|
||||
|
||||
Value1 = Key5,
|
||||
Value2 = Key4,
|
||||
Value3 = Key2,
|
||||
Value4 = Key3,
|
||||
Value5 = Key1,
|
||||
|
||||
Dict01 = ec_dictionary:add(Key1, Value1, Dict0),
|
||||
Dict02 = ec_dictionary:add(Key3, Value3,
|
||||
ec_dictionary:add(Key2, Value2,
|
||||
Dict01)),
|
||||
Dict1 =
|
||||
ec_dictionary:add(Key5, Value5,
|
||||
ec_dictionary:add(Key4, Value4,
|
||||
Dict02)),
|
||||
|
||||
?assertMatch(Value1, ec_dictionary:get(Key1, Dict1)),
|
||||
?assertMatch(Value2, ec_dictionary:get(Key2, Dict1)),
|
||||
?assertMatch(Value3, ec_dictionary:get(Key3, Dict1)),
|
||||
?assertMatch(Value4, ec_dictionary:get(Key4, Dict1)),
|
||||
?assertMatch(Value5, ec_dictionary:get(Key5, Dict1)),
|
||||
|
||||
|
||||
Dict2 = ec_dictionary:add(Key3, Value5,
|
||||
ec_dictionary:add(Key2, Value4, Dict1)),
|
||||
|
||||
|
||||
?assertMatch(Value1, ec_dictionary:get(Key1, Dict2)),
|
||||
?assertMatch(Value4, ec_dictionary:get(Key2, Dict2)),
|
||||
?assertMatch(Value5, ec_dictionary:get(Key3, Dict2)),
|
||||
?assertMatch(Value4, ec_dictionary:get(Key4, Dict2)),
|
||||
?assertMatch(Value5, ec_dictionary:get(Key5, Dict2)),
|
||||
|
||||
|
||||
?assertThrow(not_found, ec_dictionary:get(should_blow_up, Dict2)),
|
||||
?assertThrow(not_found, ec_dictionary:get("This should blow up too",
|
||||
Dict2)).
|
||||
|
||||
|
||||
|
||||
-endif.
|
|
@ -0,0 +1,124 @@
|
|||
%%% vi:ts=4 sw=4 et
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||
%%% @copyright 2011 Erlware, LLC.
|
||||
%%% @doc
|
||||
%%% This provides an implementation of the ec_vsn for git. That is
|
||||
%%% it is capable of returning a semver for a git repository
|
||||
%%% see ec_vsn
|
||||
%%% see ec_semver
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ec_git_vsn).
|
||||
|
||||
-behaviour(ec_vsn).
|
||||
|
||||
%% API
|
||||
-export([new/0,
|
||||
vsn/1]).
|
||||
|
||||
-export_type([t/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Types
|
||||
%%%===================================================================
|
||||
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||
%% however you should not rely on the internal representation here
|
||||
-type t() :: {}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec new() -> t().
|
||||
new() ->
|
||||
{}.
|
||||
|
||||
-spec vsn(t()|string()) -> {ok, string()} | {error, Reason::any()}.
|
||||
vsn(Data) ->
|
||||
{Vsn, RawRef, RawCount} = collect_default_refcount(Data),
|
||||
{ok, build_vsn_string(Vsn, RawRef, RawCount)}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal Functions
|
||||
%%%===================================================================
|
||||
|
||||
collect_default_refcount(Data) ->
|
||||
%% Get the tag timestamp and minimal ref from the system. The
|
||||
%% timestamp is really important from an ordering perspective.
|
||||
RawRef = os:cmd("git log -n 1 --pretty=format:'%h\n' "),
|
||||
|
||||
{Tag, TagVsn} = parse_tags(Data),
|
||||
RawCount =
|
||||
case Tag of
|
||||
undefined ->
|
||||
os:cmd("git rev-list --count HEAD");
|
||||
_ ->
|
||||
get_patch_count(Tag)
|
||||
end,
|
||||
{TagVsn, RawRef, RawCount}.
|
||||
|
||||
build_vsn_string(Vsn, RawRef, RawCount) ->
|
||||
%% Cleanup the tag and the Ref information. Basically leading 'v's and
|
||||
%% whitespace needs to go away.
|
||||
RefTag = [".ref", re:replace(RawRef, "\\s", "", [global])],
|
||||
Count = erlang:iolist_to_binary(re:replace(RawCount, "\\s", "", [global])),
|
||||
|
||||
%% Create the valid [semver](http://semver.org) version from the tag
|
||||
case Count of
|
||||
<<"0">> ->
|
||||
erlang:binary_to_list(erlang:iolist_to_binary(Vsn));
|
||||
_ ->
|
||||
erlang:binary_to_list(erlang:iolist_to_binary([Vsn, "+build.",
|
||||
Count, RefTag]))
|
||||
end.
|
||||
|
||||
get_patch_count(RawRef) ->
|
||||
Ref = re:replace(RawRef, "\\s", "", [global]),
|
||||
Cmd = io_lib:format("git rev-list --count ~s..HEAD",
|
||||
[Ref]),
|
||||
case os:cmd(Cmd) of
|
||||
"fatal: " ++ _ ->
|
||||
0;
|
||||
Count ->
|
||||
Count
|
||||
end.
|
||||
|
||||
-spec parse_tags(t()|string()) -> {string()|undefined, ec_semver:version_string()}.
|
||||
parse_tags({}) ->
|
||||
parse_tags("");
|
||||
parse_tags(Pattern) ->
|
||||
Cmd = io_lib:format("git describe --abbrev=0 --tags --match \"~s*\"", [Pattern]),
|
||||
Tag = os:cmd(Cmd),
|
||||
case Tag of
|
||||
"fatal: " ++ _ ->
|
||||
{undefined, ""};
|
||||
_ ->
|
||||
Vsn = slice(Tag, len(Pattern)),
|
||||
Vsn1 = trim(trim(Vsn, left, "v"), right, "\n"),
|
||||
{Tag, Vsn1}
|
||||
end.
|
||||
|
||||
-ifdef(unicode_str).
|
||||
len(Str) -> string:length(Str).
|
||||
trim(Str, right, Chars) -> string:trim(Str, trailing, Chars);
|
||||
trim(Str, left, Chars) -> string:trim(Str, leading, Chars).
|
||||
slice(Str, Len) -> string:slice(Str, Len).
|
||||
-else.
|
||||
len(Str) -> string:len(Str).
|
||||
trim(Str, Dir, [Chars|_]) -> string:strip(Str, Dir, Chars).
|
||||
slice(Str, Len) -> string:substr(Str, Len + 1).
|
||||
-endif.
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
parse_tags_test() ->
|
||||
?assertEqual({undefined, ""}, parse_tags("a.b.c")).
|
||||
|
||||
get_patch_count_test() ->
|
||||
?assertEqual(0, get_patch_count("a.b.c")).
|
||||
|
||||
collect_default_refcount_test() ->
|
||||
?assertMatch({"", _, _}, collect_default_refcount("a.b.c")).
|
||||
-endif.
|
|
@ -0,0 +1,246 @@
|
|||
%%% vi:ts=4 sw=4 et
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @copyright (C) 2011, Erlware LLC
|
||||
%%% @doc
|
||||
%%% Provides useful functionionality on standard lists that are
|
||||
%%% not provided in the standard library.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ec_lists).
|
||||
|
||||
%% API
|
||||
-export([find/2,
|
||||
fetch/2,
|
||||
search/2]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Search each value in the list with the specified
|
||||
%% function. When the function returns a value of {ok, term()} the
|
||||
%% search function stops and returns a tuple of {ok, term(), term()},
|
||||
%% where the second value is the term returned from the function and
|
||||
%% the third value is the element passed to the function. The purpose
|
||||
%% of this is to allow a list to be searched where some internal state
|
||||
%% is important while the input element is not.
|
||||
-spec search(fun(), list()) -> {ok, Result::term(), Element::term()} | not_found.
|
||||
search(Fun, [H|T]) ->
|
||||
case Fun(H) of
|
||||
{ok, Value} ->
|
||||
{ok, Value, H};
|
||||
not_found ->
|
||||
search(Fun, T)
|
||||
end;
|
||||
search(_, []) ->
|
||||
not_found.
|
||||
|
||||
%% @doc Find a value in the list with the specified function. If the
|
||||
%% function returns the atom true, the value is returned as {ok,
|
||||
%% term()} and processing is aborted, if the function returns false,
|
||||
%% processing continues until the end of the list. If the end is found
|
||||
%% and the function never returns true the atom error is returned.
|
||||
-spec find(fun(), list()) -> {ok, term()} | error.
|
||||
find(Fun, [Head|Tail]) when is_function(Fun) ->
|
||||
case Fun(Head) of
|
||||
true ->
|
||||
{ok, Head};
|
||||
false ->
|
||||
find(Fun, Tail)
|
||||
end;
|
||||
find(_Fun, []) ->
|
||||
error.
|
||||
|
||||
%% @doc Fetch a value from the list. If the function returns true the
|
||||
%% value is returend. If processing reaches the end of the list and
|
||||
%% the function has never returned true an exception not_found is
|
||||
%% thrown.
|
||||
-spec fetch(fun(), list()) -> term().
|
||||
fetch(Fun, List) when is_list(List), is_function(Fun) ->
|
||||
case find(Fun, List) of
|
||||
{ok, Head} ->
|
||||
Head;
|
||||
error ->
|
||||
throw(not_found)
|
||||
end.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Test Functions
|
||||
%%%===================================================================
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
find1_test() ->
|
||||
TestData = [1, 2, 3, 4, 5, 6],
|
||||
Result = find(fun(5) ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch({ok, 5}, Result),
|
||||
|
||||
Result2 = find(fun(37) ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch(error, Result2).
|
||||
|
||||
find2_test() ->
|
||||
TestData = ["one", "two", "three", "four", "five", "six"],
|
||||
Result = find(fun("five") ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch({ok, "five"}, Result),
|
||||
|
||||
Result2 = find(fun(super_duper) ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch(error, Result2).
|
||||
|
||||
|
||||
|
||||
find3_test() ->
|
||||
TestData = [{"one", 1}, {"two", 2}, {"three", 3}, {"four", 5}, {"five", 5},
|
||||
{"six", 6}],
|
||||
Result = find(fun({"one", 1}) ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch({ok, {"one", 1}}, Result),
|
||||
|
||||
Result2 = find(fun([fo, bar, baz]) ->
|
||||
true;
|
||||
({"onehundred", 100}) ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch(error, Result2).
|
||||
|
||||
|
||||
|
||||
fetch1_test() ->
|
||||
TestData = [1, 2, 3, 4, 5, 6],
|
||||
Result = fetch(fun(5) ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch(5, Result),
|
||||
|
||||
?assertThrow(not_found,
|
||||
fetch(fun(37) ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
TestData)).
|
||||
|
||||
fetch2_test() ->
|
||||
TestData = ["one", "two", "three", "four", "five", "six"],
|
||||
Result = fetch(fun("five") ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch("five", Result),
|
||||
|
||||
?assertThrow(not_found,
|
||||
fetch(fun(super_duper) ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
TestData)).
|
||||
|
||||
fetch3_test() ->
|
||||
TestData = [{"one", 1}, {"two", 2}, {"three", 3}, {"four", 5}, {"five", 5},
|
||||
{"six", 6}],
|
||||
Result = fetch(fun({"one", 1}) ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch({"one", 1}, Result),
|
||||
|
||||
?assertThrow(not_found,
|
||||
fetch(fun([fo, bar, baz]) ->
|
||||
true;
|
||||
({"onehundred", 100}) ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
TestData)).
|
||||
|
||||
search1_test() ->
|
||||
TestData = [1, 2, 3, 4, 5, 6],
|
||||
Result = search(fun(5) ->
|
||||
{ok, 5};
|
||||
(_) ->
|
||||
not_found
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch({ok, 5, 5}, Result),
|
||||
|
||||
Result2 = search(fun(37) ->
|
||||
{ok, 37};
|
||||
(_) ->
|
||||
not_found
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch(not_found, Result2).
|
||||
|
||||
search2_test() ->
|
||||
TestData = [1, 2, 3, 4, 5, 6],
|
||||
Result = search(fun(1) ->
|
||||
{ok, 10};
|
||||
(_) ->
|
||||
not_found
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch({ok, 10, 1}, Result),
|
||||
|
||||
Result2 = search(fun(6) ->
|
||||
{ok, 37};
|
||||
(_) ->
|
||||
not_found
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch({ok, 37, 6}, Result2).
|
||||
|
||||
search3_test() ->
|
||||
TestData = [1, 2, 3, 4, 5, 6],
|
||||
Result = search(fun(10) ->
|
||||
{ok, 10};
|
||||
(_) ->
|
||||
not_found
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch(not_found, Result),
|
||||
|
||||
Result2 = search(fun(-1) ->
|
||||
{ok, 37};
|
||||
(_) ->
|
||||
not_found
|
||||
end,
|
||||
TestData),
|
||||
?assertMatch(not_found, Result2).
|
||||
|
||||
-endif.
|
|
@ -0,0 +1,110 @@
|
|||
%%% vi:ts=4 sw=4 et
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||
%%% @copyright 2011 Erlware, LLC.
|
||||
%%% @doc
|
||||
%%% This provides an implementation of the ec_dictionary type using
|
||||
%%% erlang orddicts as a base. The function documentation for
|
||||
%%% ec_dictionary applies here as well.
|
||||
%%% see ec_dictionary
|
||||
%%% see orddict
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ec_orddict).
|
||||
|
||||
-behaviour(ec_dictionary).
|
||||
|
||||
%% API
|
||||
-export([new/0,
|
||||
has_key/2,
|
||||
get/2,
|
||||
get/3,
|
||||
add/3,
|
||||
remove/2,
|
||||
has_value/2,
|
||||
size/1,
|
||||
to_list/1,
|
||||
from_list/1,
|
||||
keys/1]).
|
||||
|
||||
-export_type([dictionary/2]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Types
|
||||
%%%===================================================================
|
||||
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||
%% however you should not rely on the internal representation here
|
||||
-type dictionary(K, V) :: [{K, V}].
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec new() -> dictionary(_K, _V).
|
||||
new() ->
|
||||
orddict:new().
|
||||
|
||||
-spec has_key(ec_dictionary:key(K), Object::dictionary(K, _V)) -> boolean().
|
||||
has_key(Key, Data) ->
|
||||
orddict:is_key(Key, Data).
|
||||
|
||||
-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) ->
|
||||
ec_dictionary:value(V).
|
||||
get(Key, Data) ->
|
||||
case orddict:find(Key, Data) of
|
||||
{ok, Value} ->
|
||||
Value;
|
||||
error ->
|
||||
throw(not_found)
|
||||
end.
|
||||
|
||||
-spec get(ec_dictionary:key(K),
|
||||
Default::ec_dictionary:value(V),
|
||||
Object::dictionary(K, V)) ->
|
||||
ec_dictionary:value(V).
|
||||
get(Key, Default, Data) ->
|
||||
case orddict:find(Key, Data) of
|
||||
{ok, Value} ->
|
||||
Value;
|
||||
error ->
|
||||
Default
|
||||
end.
|
||||
|
||||
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
|
||||
Object::dictionary(K, V)) ->
|
||||
dictionary(K, V).
|
||||
add(Key, Value, Data) ->
|
||||
orddict:store(Key, Value, Data).
|
||||
|
||||
-spec remove(ec_dictionary:key(K), Object::dictionary(K, V)) ->
|
||||
dictionary(K, V).
|
||||
remove(Key, Data) ->
|
||||
orddict:erase(Key, Data).
|
||||
|
||||
-spec has_value(ec_dictionary:value(V), Object::dictionary(_K, V)) -> boolean().
|
||||
has_value(Value, Data) ->
|
||||
orddict:fold(fun(_, NValue, _) when NValue == Value ->
|
||||
true;
|
||||
(_, _, Acc) ->
|
||||
Acc
|
||||
end,
|
||||
false,
|
||||
Data).
|
||||
|
||||
-spec size(Object::dictionary(_K, _V)) -> non_neg_integer().
|
||||
size(Data) ->
|
||||
orddict:size(Data).
|
||||
|
||||
-spec to_list(dictionary(K, V)) ->
|
||||
[{ec_dictionary:key(K), ec_dictionary:value(V)}].
|
||||
to_list(Data) ->
|
||||
orddict:to_list(Data).
|
||||
|
||||
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
||||
dictionary(K, V).
|
||||
from_list(List) when is_list(List) ->
|
||||
orddict:from_list(List).
|
||||
|
||||
-spec keys(dictionary(K, _V)) -> [ec_dictionary:key(K)].
|
||||
keys(Dict) ->
|
||||
orddict:fetch_keys(Dict).
|
|
@ -0,0 +1,975 @@
|
|||
%%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
|
||||
%%% vi:ts=4 sw=4 et
|
||||
%%% The MIT License
|
||||
%%%
|
||||
%%% Copyright (c) 2007 Stephen Marsh
|
||||
%%%
|
||||
%%% Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
%%% of this software and associated documentation files (the "Software"), to deal
|
||||
%%% in the Software without restriction, including without limitation the rights
|
||||
%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
%%% copies of the Software, and to permit persons to whom the Software is
|
||||
%%% furnished to do so, subject to the following conditions:
|
||||
%%%
|
||||
%%% The above copyright notice and this permission notice shall be included in
|
||||
%%% all copies or substantial portions of the Software.
|
||||
%%%
|
||||
%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
%%% THE SOFTWARE.
|
||||
%%%---------------------------------------------------------------------------
|
||||
%%% @author Stephen Marsh
|
||||
%%% @copyright 2007 Stephen Marsh freeyourmind ++ [$@|gmail.com]
|
||||
%%% @doc
|
||||
%%% plists is a drop-in replacement for module <a
|
||||
%%% href="http://www.erlang.org/doc/man/lists.html">lists</a>, making
|
||||
%%% most list operations parallel. It can operate on each element in
|
||||
%%% parallel, for IO-bound operations, on sublists in parallel, for
|
||||
%%% taking advantage of multi-core machines with CPU-bound operations,
|
||||
%%% and across erlang nodes, for parallizing inside a cluster. It
|
||||
%%% handles errors and node failures. It can be configured, tuned, and
|
||||
%%% tweaked to get optimal performance while minimizing overhead.
|
||||
%%%
|
||||
%%% Almost all the functions are identical to equivalent functions in
|
||||
%%% lists, returning exactly the same result, and having both a form
|
||||
%%% with an identical syntax that operates on each element in parallel
|
||||
%%% and a form which takes an optional "malt", a specification for how
|
||||
%%% to parallize the operation.
|
||||
%%%
|
||||
%%% fold is the one exception, parallel fold is different from linear
|
||||
%%% fold. This module also include a simple mapreduce implementation,
|
||||
%%% and the function runmany. All the other functions are implemented
|
||||
%%% with runmany, which is as a generalization of parallel list
|
||||
%%% operations.
|
||||
%%%
|
||||
%%% Malts
|
||||
%%% =====
|
||||
%%%
|
||||
%%% A malt specifies how to break a list into sublists, and can optionally
|
||||
%%% specify a timeout, which nodes to run on, and how many processes to start
|
||||
%%% per node.
|
||||
%%%
|
||||
%%% Malt = MaltComponent | [MaltComponent]
|
||||
%%% MaltComponent = SubListSize::integer() | {processes, integer()} |
|
||||
%%% {processes, schedulers} |
|
||||
%%% {timeout, Milliseconds::integer()} | {nodes, [NodeSpec]}<br/>
|
||||
%%%
|
||||
%%% NodeSpec = Node::atom() | {Node::atom(), NumProcesses::integer()} |
|
||||
%%% {Node::atom(), schedulers}
|
||||
%%%
|
||||
%%% An integer can be given to specify the exact size for sublists. 1
|
||||
%%% is a good choice for IO-bound operations and when the operation on
|
||||
%%% each list element is expensive. Larger numbers minimize overhead
|
||||
%%% and are faster for cheap operations.
|
||||
%%%
|
||||
%%% If the integer is omitted, and you have specified a `{processes,
|
||||
%%% X}`, the list is split into X sublists. This is only useful when
|
||||
%%% the time to process each element is close to identical and you
|
||||
%%% know exactly how many lines of execution are available to you.
|
||||
%%%
|
||||
%%% If neither of the above applies, the sublist size defaults to 1.
|
||||
%%%
|
||||
%%% You can use `{processes, X}` to have the list processed by `X`
|
||||
%%% processes on the local machine. A good choice for `X` is the
|
||||
%%% number of lines of execution (cores) the machine provides. This
|
||||
%%% can be done automatically with {processes, schedulers}, which sets
|
||||
%%% the number of processes to the number of schedulers in the erlang
|
||||
%%% virtual machine (probably equal to the number of cores).
|
||||
%%%
|
||||
%%% `{timeout, Milliseconds}` specifies a timeout. This is a timeout
|
||||
%%% for the entire operation, both operating on the sublists and
|
||||
%%% combining the results. exit(timeout) is evaluated if the timeout
|
||||
%%% is exceeded.
|
||||
%%%
|
||||
%%% `{nodes, NodeList}` specifies that the operation should be done
|
||||
%%% across nodes. Every element of NodeList is of the form
|
||||
%%% `{NodeName, NumProcesses}` or NodeName, which means the same as
|
||||
%%% `{NodeName, 1}`. plists runs NumProcesses processes on NodeName
|
||||
%%% concurrently. A good choice for NumProcesses is the number of
|
||||
%%% lines of execution (cores) a node provides plus one. This ensures
|
||||
%%% the node is completely busy even when fetching a new sublist. This
|
||||
%%% can be done automatically with `{NodeName, schedulers}`, in which
|
||||
%%% case plists uses a cached value if it has one, and otherwise finds
|
||||
%%% the number of schedulers in the remote node and adds one. This
|
||||
%%% will ensure at least one busy process per core (assuming the node
|
||||
%%% has a scheduler for each core).
|
||||
%%%
|
||||
%%% plists is able to recover if a node goes down. If all nodes go
|
||||
%%% down, exit(allnodescrashed) is evaluated.
|
||||
%%%
|
||||
%%% Any of the above may be used as a malt, or may be combined into a
|
||||
%%% list. `{nodes, NodeList}` and {processes, X} may not be combined.
|
||||
%%%
|
||||
%%% Examples
|
||||
%%% ========
|
||||
%%%
|
||||
%%% %%start a process for each element (1-element sublists)<
|
||||
%%% 1
|
||||
%%%
|
||||
%%% %% start a process for each ten elements (10-element sublists)
|
||||
%%% 10
|
||||
%%%
|
||||
%%% %% split the list into two sublists and process in two processes
|
||||
%%% {processes, 2}
|
||||
%%%
|
||||
%%% %% split the list into X sublists and process in X processes,
|
||||
%%% %% where X is the number of cores in the machine
|
||||
%%% {processes, schedulers}
|
||||
%%%
|
||||
%%% %% split the list into 10-element sublists and process in two processes
|
||||
%%% [10, {processes, 2}]
|
||||
%%%
|
||||
%%% %% timeout after one second. Assumes that a process should be started
|
||||
%%% %% for each element.<br/>
|
||||
%%% {timeout, 1000}
|
||||
%%%
|
||||
%%% %% Runs 3 processes at a time on apple@desktop, and 2 on orange@laptop
|
||||
%%% %% This is the best way to utilize all the CPU-power of a dual-core<br/>
|
||||
%%% %% desktop and a single-core laptop. Assumes that the list should be<br/>
|
||||
%%% %% split into 1-element sublists.<br/>
|
||||
%%% {nodes, [{apple@desktop, 3}, {orange@laptop, 2}]}
|
||||
%%%
|
||||
%%% %% Like above, but makes plists figure out how many processes to use.
|
||||
%%% {nodes, [{apple@desktop, schedulers}, {orange@laptop, schedulers}]}
|
||||
%%%
|
||||
%%% %% Gives apple and orange three seconds to process the list as<br/>
|
||||
%%% %% 100-element sublists.<br/>
|
||||
%%% [100, {timeout, 3000}, {nodes, [{apple@desktop, 3}, {orange@laptop, 2}]}]
|
||||
%%%
|
||||
%%% Aside: Why Malt?
|
||||
%%% ================
|
||||
%%%
|
||||
%%% I needed a word for this concept, so maybe my subconsciousness
|
||||
%%% gave me one by making me misspell multiply. Maybe it is an acronym
|
||||
%%% for Malt is A List Tearing Specification. Maybe it is a beer
|
||||
%%% metaphor, suggesting that code only runs in parallel if bribed
|
||||
%%% with spirits. It's jargon, learn it or you can't be part of the
|
||||
%%% in-group.
|
||||
%%%
|
||||
%%% Messages and Errors
|
||||
%%% ===================
|
||||
%%%
|
||||
%%% plists assures that no extraneous messages are left in or will
|
||||
%%% later enter the message queue. This is guaranteed even in the
|
||||
%%% event of an error.
|
||||
%%%
|
||||
%%% Errors in spawned processes are caught and propagated to the
|
||||
%%% calling process. If you invoke
|
||||
%%%
|
||||
%%% plists:map(fun (X) -> 1/X end, [1, 2, 3, 0]).
|
||||
%%%
|
||||
%%% you get a badarith error, exactly like when you use lists:map.
|
||||
%%%
|
||||
%%% plists uses monitors to watch the processes it spawns. It is not a
|
||||
%%% good idea to invoke plists when you are already monitoring
|
||||
%%% processes. If one of them does a non-normal exit, plists receives
|
||||
%%% the 'DOWN' message believing it to be from one of its own
|
||||
%%% processes. The error propagation system goes into effect, which
|
||||
%%% results in the error occuring in the calling process.
|
||||
%%%
|
||||
-module(ec_plists).
|
||||
|
||||
-export([all/2, all/3,
|
||||
any/2, any/3,
|
||||
filter/2, filter/3,
|
||||
fold/3, fold/4, fold/5,
|
||||
foreach/2, foreach/3,
|
||||
map/2, map/3,
|
||||
ftmap/2, ftmap/3,
|
||||
partition/2, partition/3,
|
||||
sort/1, sort/2, sort/3,
|
||||
usort/1, usort/2, usort/3,
|
||||
mapreduce/2, mapreduce/3, mapreduce/5,
|
||||
runmany/3, runmany/4]).
|
||||
|
||||
-export_type([malt/0, malt_component/0, node_spec/0, fuse/0, fuse_fun/0]).
|
||||
|
||||
%%============================================================================
|
||||
%% types
|
||||
%%============================================================================
|
||||
|
||||
-type malt() :: malt_component() | [malt_component()].
|
||||
|
||||
-type malt_component() :: SubListSize::integer()
|
||||
| {processes, integer()}
|
||||
| {processes, schedulers}
|
||||
| {timeout, Milliseconds::integer()}
|
||||
| {nodes, [node_spec()]}.
|
||||
|
||||
-type node_spec() :: Node::atom()
|
||||
| {Node::atom(), NumProcesses::integer()}
|
||||
| {Node::atom(), schedulers}.
|
||||
|
||||
-type fuse_fun() :: fun((term(), term()) -> term()).
|
||||
-type fuse() :: fuse_fun() | {recursive, fuse_fun()} | {reverse, fuse_fun()}.
|
||||
-type el_fun() :: fun((term()) -> term()).
|
||||
|
||||
%%============================================================================
|
||||
%% API
|
||||
%%============================================================================
|
||||
|
||||
%% Everything here is defined in terms of runmany.
|
||||
%% The following methods are convient interfaces to runmany.
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec all(el_fun(), list()) -> boolean().
|
||||
all(Fun, List) ->
|
||||
all(Fun, List, 1).
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec all(el_fun(), list(), malt()) -> boolean().
|
||||
all(Fun, List, Malt) ->
|
||||
try
|
||||
runmany(fun (L) ->
|
||||
B = lists:all(Fun, L),
|
||||
if
|
||||
B ->
|
||||
nil;
|
||||
true ->
|
||||
erlang:throw(notall)
|
||||
end
|
||||
end,
|
||||
fun (_A1, _A2) ->
|
||||
nil
|
||||
end,
|
||||
List, Malt),
|
||||
true
|
||||
catch
|
||||
throw:notall ->
|
||||
false
|
||||
end.
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec any(fun(), list()) -> boolean().
|
||||
any(Fun, List) ->
|
||||
any(Fun, List, 1).
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec any(fun(), list(), malt()) -> boolean().
|
||||
any(Fun, List, Malt) ->
|
||||
try
|
||||
runmany(fun (L) ->
|
||||
B = lists:any(Fun, L),
|
||||
if B ->
|
||||
erlang:throw(any);
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end,
|
||||
fun (_A1, _A2) ->
|
||||
nil
|
||||
end,
|
||||
List, Malt) of
|
||||
_ ->
|
||||
false
|
||||
catch throw:any ->
|
||||
true
|
||||
end.
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec filter(fun(), list()) -> list().
|
||||
filter(Fun, List) ->
|
||||
filter(Fun, List, 1).
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec filter(fun(), list(), malt()) -> list().
|
||||
filter(Fun, List, Malt) ->
|
||||
runmany(fun (L) ->
|
||||
lists:filter(Fun, L)
|
||||
end,
|
||||
{reverse, fun (A1, A2) ->
|
||||
A1 ++ A2
|
||||
end},
|
||||
List, Malt).
|
||||
|
||||
%% Note that with parallel fold there is not foldl and foldr,
|
||||
%% instead just one fold that can fuse Accumlators.
|
||||
|
||||
%% @doc Like below, but assumes 1 as the Malt. This function is almost useless,
|
||||
%% and is intended only to aid converting code from using lists to plists.
|
||||
-spec fold(fun(), InitAcc::term(), list()) -> term().
|
||||
fold(Fun, InitAcc, List) ->
|
||||
fold(Fun, Fun, InitAcc, List, 1).
|
||||
|
||||
%% @doc Like below, but uses the Fun as the Fuse by default.
|
||||
-spec fold(fun(), InitAcc::term(), list(), malt()) -> term().
|
||||
fold(Fun, InitAcc, List, Malt) ->
|
||||
fold(Fun, Fun, InitAcc, List, Malt).
|
||||
|
||||
%% @doc fold is more complex when made parallel. There is no foldl and
|
||||
%% foldr, accumulators aren't passed in any defined order. The list
|
||||
%% is split into sublists which are folded together. Fun is identical
|
||||
%% to the function passed to lists:fold[lr], it takes (an element, and
|
||||
%% the accumulator) and returns -> a new accumulator. It is used for
|
||||
%% the initial stage of folding sublists. Fuse fuses together the
|
||||
%% results, it takes (Results1, Result2) and returns -> a new result.
|
||||
%% By default sublists are fused left to right, each result of a fuse
|
||||
%% being fed into the first element of the next fuse. The result of
|
||||
%% the last fuse is the result.
|
||||
%%
|
||||
%% Fusing may also run in parallel using a recursive algorithm,
|
||||
%% by specifying the fuse as {recursive, Fuse}. See
|
||||
%% the discussion in {@link runmany/4}.
|
||||
%%
|
||||
%% Malt is the malt for the initial folding of sublists, and for the
|
||||
%% possible recursive fuse.
|
||||
-spec fold(fun(), fuse(), InitAcc::term(), list(), malt()) -> term().
|
||||
fold(Fun, Fuse, InitAcc, List, Malt) ->
|
||||
Fun2 = fun (L) ->
|
||||
lists:foldl(Fun, InitAcc, L)
|
||||
end,
|
||||
runmany(Fun2, Fuse, List, Malt).
|
||||
|
||||
%% @doc Similiar to foreach in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>
|
||||
%% except it makes no guarantee about the order it processes list elements.
|
||||
-spec foreach(fun(), list()) -> ok.
|
||||
foreach(Fun, List) ->
|
||||
foreach(Fun, List, 1).
|
||||
|
||||
%% @doc Similiar to foreach in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>
|
||||
%% except it makes no guarantee about the order it processes list elements.
|
||||
-spec foreach(fun(), list(), malt()) -> ok.
|
||||
foreach(Fun, List, Malt) ->
|
||||
runmany(fun (L) ->
|
||||
lists:foreach(Fun, L)
|
||||
end,
|
||||
fun (_A1, _A2) ->
|
||||
ok
|
||||
end,
|
||||
List, Malt).
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec map(fun(), list()) -> list().
|
||||
map(Fun, List) ->
|
||||
map(Fun, List, 1).
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec map(fun(), list(), malt()) -> list().
|
||||
map(Fun, List, Malt) ->
|
||||
runmany(fun (L) ->
|
||||
lists:map(Fun, L)
|
||||
end,
|
||||
{reverse, fun (A1, A2) ->
|
||||
A1 ++ A2
|
||||
end},
|
||||
List, Malt).
|
||||
|
||||
%% @doc values are returned as {value, term()}.
|
||||
-spec ftmap(fun(), list()) -> list().
|
||||
ftmap(Fun, List) ->
|
||||
map(fun(L) ->
|
||||
try
|
||||
{value, Fun(L)}
|
||||
catch
|
||||
Class:Type ->
|
||||
{error, {Class, Type}}
|
||||
end
|
||||
end, List).
|
||||
|
||||
%% @doc values are returned as {value, term()}.
|
||||
-spec ftmap(fun(), list(), malt()) -> list().
|
||||
ftmap(Fun, List, Malt) ->
|
||||
map(fun(L) ->
|
||||
try
|
||||
{value, Fun(L)}
|
||||
catch
|
||||
Class:Type ->
|
||||
{error, {Class, Type}}
|
||||
end
|
||||
end, List, Malt).
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec partition(fun(), list()) -> {list(), list()}.
|
||||
partition(Fun, List) ->
|
||||
partition(Fun, List, 1).
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec partition(fun(), list(), malt()) -> {list(), list()}.
|
||||
partition(Fun, List, Malt) ->
|
||||
runmany(fun (L) ->
|
||||
lists:partition(Fun, L)
|
||||
end,
|
||||
{reverse, fun ({True1, False1}, {True2, False2}) ->
|
||||
{True1 ++ True2, False1 ++ False2}
|
||||
end},
|
||||
List, Malt).
|
||||
|
||||
%% SORTMALT needs to be tuned
|
||||
-define(SORTMALT, 100).
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec sort(list()) -> list().
|
||||
sort(List) ->
|
||||
sort(fun (A, B) ->
|
||||
A =< B
|
||||
end,
|
||||
List).
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec sort(fun(), list()) -> list().
|
||||
sort(Fun, List) ->
|
||||
sort(Fun, List, ?SORTMALT).
|
||||
|
||||
%% @doc This version lets you specify your own malt for sort.
|
||||
%%
|
||||
%% sort splits the list into sublists and sorts them, and it merges the
|
||||
%% sorted lists together. These are done in parallel. Each sublist is
|
||||
%% sorted in a seperate process, and each merging of results is done in a
|
||||
%% seperate process. Malt defaults to 100, causing the list to be split into
|
||||
%% 100-element sublists.
|
||||
-spec sort(fun(), list(), malt()) -> list().
|
||||
sort(Fun, List, Malt) ->
|
||||
Fun2 = fun (L) ->
|
||||
lists:sort(Fun, L)
|
||||
end,
|
||||
Fuse = fun (A1, A2) ->
|
||||
lists:merge(Fun, A1, A2)
|
||||
end,
|
||||
runmany(Fun2, {recursive, Fuse}, List, Malt).
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec usort(list()) -> list().
|
||||
usort(List) ->
|
||||
usort(fun (A, B) ->
|
||||
A =< B
|
||||
end,
|
||||
List).
|
||||
|
||||
%% @doc Same semantics as in module
|
||||
%% <a href="http://www.erlang.org/doc/man/lists.html">lists</a>.
|
||||
-spec usort(fun(), list()) -> list().
|
||||
usort(Fun, List) ->
|
||||
usort(Fun, List, ?SORTMALT).
|
||||
|
||||
%% @doc This version lets you specify your own malt for usort.
|
||||
%%
|
||||
%% usort splits the list into sublists and sorts them, and it merges the
|
||||
%% sorted lists together. These are done in parallel. Each sublist is
|
||||
%% sorted in a seperate process, and each merging of results is done in a
|
||||
%% seperate process. Malt defaults to 100, causing the list to be split into
|
||||
%% 100-element sublists.
|
||||
%%
|
||||
%% usort removes duplicate elments while it sorts.
|
||||
-spec usort(fun(), list(), malt()) -> list().
|
||||
usort(Fun, List, Malt) ->
|
||||
Fun2 = fun (L) ->
|
||||
lists:usort(Fun, L)
|
||||
end,
|
||||
Fuse = fun (A1, A2) ->
|
||||
lists:umerge(Fun, A1, A2)
|
||||
end,
|
||||
runmany(Fun2, {recursive, Fuse}, List, Malt).
|
||||
|
||||
%% @doc Like below, assumes default MapMalt of 1.
|
||||
-ifdef(namespaced_types).
|
||||
-spec mapreduce(MapFunc, list()) -> dict:dict() when
|
||||
MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
|
||||
DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()}.
|
||||
-else.
|
||||
-spec mapreduce(MapFunc, list()) -> dict() when
|
||||
MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
|
||||
DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()}.
|
||||
-endif.
|
||||
|
||||
|
||||
mapreduce(MapFunc, List) ->
|
||||
mapreduce(MapFunc, List, 1).
|
||||
|
||||
%% Like below, but uses a default reducer that collects all
|
||||
%% {Key, Value} pairs into a
|
||||
%% <a href="http://www.erlang.org/doc/man/dict.html">dict</a>,
|
||||
%% with values {Key, [Value1, Value2...]}.
|
||||
%% This dict is returned as the result.
|
||||
mapreduce(MapFunc, List, MapMalt) ->
|
||||
mapreduce(MapFunc, List, dict:new(), fun add_key/3, MapMalt).
|
||||
|
||||
%% @doc This is a very basic mapreduce. You won't write a
|
||||
%% Google-rivaling search engine with it. It has no equivalent in
|
||||
%% lists. Each element in the list is run through the MapFunc, which
|
||||
%% produces either a {Key, Value} pair, or a lists of key value pairs,
|
||||
%% or a list of lists of key value pairs...etc. A reducer process runs
|
||||
%% in parallel with the mapping processes, collecting the key value
|
||||
%% pairs. It starts with a state given by InitState, and for each
|
||||
%% {Key, Value} pair that it receives it invokes ReduceFunc(OldState,
|
||||
%% Key, Value) to compute its new state. mapreduce returns the
|
||||
%% reducer's final state.
|
||||
%%
|
||||
%% MapMalt is the malt for the mapping operation, with a default value of 1,
|
||||
%% meaning each element of the list is mapped by a seperate process.
|
||||
%%
|
||||
%% mapreduce requires OTP R11B, or it may leave monitoring messages in the
|
||||
%% message queue.
|
||||
-ifdef(namespaced_types).
|
||||
-spec mapreduce(MapFunc, list(), InitState::term(), ReduceFunc, malt()) -> dict:dict() when
|
||||
MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
|
||||
DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()},
|
||||
ReduceFunc :: fun((OldState::term(), Key::term(), Value::term()) -> NewState::term()).
|
||||
-else.
|
||||
-spec mapreduce(MapFunc, list(), InitState::term(), ReduceFunc, malt()) -> dict() when
|
||||
MapFunc :: fun((term()) -> DeepListOfKeyValuePairs),
|
||||
DeepListOfKeyValuePairs :: [DeepListOfKeyValuePairs] | {Key::term(), Value::term()},
|
||||
ReduceFunc :: fun((OldState::term(), Key::term(), Value::term()) -> NewState::term()).
|
||||
-endif.
|
||||
mapreduce(MapFunc, List, InitState, ReduceFunc, MapMalt) ->
|
||||
Parent = self(),
|
||||
{Reducer, ReducerRef} =
|
||||
erlang:spawn_monitor(fun () ->
|
||||
reducer(Parent, 0, InitState, ReduceFunc)
|
||||
end),
|
||||
MapFunc2 = fun (L) ->
|
||||
Reducer ! lists:map(MapFunc, L),
|
||||
1
|
||||
end,
|
||||
SentMessages = try
|
||||
runmany(MapFunc2, fun (A, B) -> A+B end, List, MapMalt)
|
||||
catch
|
||||
exit:Reason ->
|
||||
erlang:demonitor(ReducerRef, [flush]),
|
||||
Reducer ! die,
|
||||
exit(Reason)
|
||||
end,
|
||||
Reducer ! {mappers, done, SentMessages},
|
||||
Results = receive
|
||||
{Reducer, Results2} ->
|
||||
Results2;
|
||||
{'DOWN', _, _, Reducer, Reason2} ->
|
||||
exit(Reason2)
|
||||
end,
|
||||
receive
|
||||
{'DOWN', _, _, Reducer, normal} ->
|
||||
nil
|
||||
end,
|
||||
Results.
|
||||
|
||||
reducer(Parent, NumReceived, State, Func) ->
|
||||
receive
|
||||
die ->
|
||||
nil;
|
||||
{mappers, done, NumReceived} ->
|
||||
Parent ! {self (), State};
|
||||
Keys ->
|
||||
reducer(Parent, NumReceived + 1, each_key(State, Func, Keys), Func)
|
||||
end.
|
||||
|
||||
each_key(State, Func, {Key, Value}) ->
|
||||
Func(State, Key, Value);
|
||||
each_key(State, Func, [List|Keys]) ->
|
||||
each_key(each_key(State, Func, List), Func, Keys);
|
||||
each_key(State, _, []) ->
|
||||
State.
|
||||
|
||||
add_key(Dict, Key, Value) ->
|
||||
case dict:is_key(Key, Dict) of
|
||||
true ->
|
||||
dict:append(Key, Value, Dict);
|
||||
false ->
|
||||
dict:store(Key, [Value], Dict)
|
||||
end.
|
||||
|
||||
%% @doc Like below, but assumes a Malt of 1,
|
||||
%% meaning each element of the list is processed by a seperate process.
|
||||
-spec runmany(fun(), fuse(), list()) -> term().
|
||||
runmany(Fun, Fuse, List) ->
|
||||
runmany(Fun, Fuse, List, 1).
|
||||
|
||||
%% Begin internal stuff (though runmany/4 is exported).
|
||||
|
||||
%% @doc All of the other functions are implemented with runmany. runmany
|
||||
%% takes a List, splits it into sublists, and starts processes to operate on
|
||||
%% each sublist, all done according to Malt. Each process passes its sublist
|
||||
%% into Fun and sends the result back.
|
||||
%%
|
||||
%% The results are then fused together to get the final result. There are two
|
||||
%% ways this can operate, lineraly and recursively. If Fuse is a function,
|
||||
%% a fuse is done linearly left-to-right on the sublists, the results
|
||||
%% of processing the first and second sublists being passed to Fuse, then
|
||||
%% the result of the first fuse and processing the third sublits, and so on. If
|
||||
%% Fuse is {reverse, FuseFunc}, then a fuse is done right-to-left, the results
|
||||
%% of processing the second-to-last and last sublists being passed to FuseFunc,
|
||||
%% then the results of processing the third-to-last sublist and
|
||||
%% the results of the first fuse, and and so forth.
|
||||
%% Both methods preserve the original order of the lists elements.
|
||||
%%
|
||||
%% To do a recursive fuse, pass Fuse as {recursive, FuseFunc}.
|
||||
%% The recursive fuse makes no guarantee about the order the results of
|
||||
%% sublists, or the results of fuses are passed to FuseFunc. It
|
||||
%% continues fusing pairs of results until it is down to one.
|
||||
%%
|
||||
%% Recursive fuse is down in parallel with processing the sublists, and a
|
||||
%% process is spawned to fuse each pair of results. It is a parallized
|
||||
%% algorithm. Linear fuse is done after all results of processing sublists
|
||||
%% have been collected, and can only run in a single process.
|
||||
%%
|
||||
%% Even if you pass {recursive, FuseFunc}, a recursive fuse is only done if
|
||||
%% the malt contains {nodes, NodeList} or {processes, X}. If this is not the
|
||||
%% case, a linear fuse is done.
|
||||
-spec runmany(fun(([term()]) -> term()), fuse(), list(), malt()) -> term().
|
||||
runmany(Fun, Fuse, List, Malt)
|
||||
when erlang:is_list(Malt) ->
|
||||
runmany(Fun, Fuse, List, local, no_split, Malt);
|
||||
runmany(Fun, Fuse, List, Malt) ->
|
||||
runmany(Fun, Fuse, List, [Malt]).
|
||||
|
||||
runmany(Fun, Fuse, List, Nodes, no_split, [MaltTerm|Malt])
|
||||
when erlang:is_integer(MaltTerm) ->
|
||||
runmany(Fun, Fuse, List, Nodes, MaltTerm, Malt);
|
||||
runmany(Fun, Fuse, List, local, Split, [{processes, schedulers}|Malt]) ->
|
||||
%% run a process for each scheduler
|
||||
S = erlang:system_info(schedulers),
|
||||
runmany(Fun, Fuse, List, local, Split, [{processes, S}|Malt]);
|
||||
runmany(Fun, Fuse, List, local, no_split, [{processes, X}|_]=Malt) ->
|
||||
%% Split the list into X sublists, where X is the number of processes
|
||||
L = erlang:length(List),
|
||||
case (L rem X) of
|
||||
0 ->
|
||||
runmany(Fun, Fuse, List, local, (L / X), Malt);
|
||||
_ ->
|
||||
runmany(Fun, Fuse, List, local, (L / X) + 1, Malt)
|
||||
end;
|
||||
runmany(Fun, Fuse, List, local, Split, [{processes, X}|Malt]) ->
|
||||
%% run X process on local machine
|
||||
Nodes = lists:duplicate(X, node()),
|
||||
runmany(Fun, Fuse, List, Nodes, Split, Malt);
|
||||
runmany(Fun, Fuse, List, Nodes, Split, [{timeout, X}|Malt]) ->
|
||||
Parent = erlang:self(),
|
||||
Timer = proc_lib:spawn(fun () ->
|
||||
receive
|
||||
stoptimer ->
|
||||
Parent ! {timerstopped, erlang:self()}
|
||||
after X ->
|
||||
Parent ! {timerrang, erlang:self()},
|
||||
receive
|
||||
stoptimer ->
|
||||
Parent ! {timerstopped, erlang:self()}
|
||||
end
|
||||
end
|
||||
end),
|
||||
Ans = try
|
||||
runmany(Fun, Fuse, List, Nodes, Split, Malt)
|
||||
catch
|
||||
%% we really just want the after block, the syntax
|
||||
%% makes this catch necessary.
|
||||
willneverhappen ->
|
||||
nil
|
||||
after
|
||||
Timer ! stoptimer,
|
||||
cleanup_timer(Timer)
|
||||
end,
|
||||
Ans;
|
||||
runmany(Fun, Fuse, List, local, Split, [{nodes, NodeList}|Malt]) ->
|
||||
Nodes = lists:foldl(fun ({Node, schedulers}, A) ->
|
||||
X = schedulers_on_node(Node) + 1,
|
||||
lists:reverse(lists:duplicate(X, Node), A);
|
||||
({Node, X}, A) ->
|
||||
lists:reverse(lists:duplicate(X, Node), A);
|
||||
(Node, A) ->
|
||||
[Node|A]
|
||||
end,
|
||||
[], NodeList),
|
||||
runmany(Fun, Fuse, List, Nodes, Split, Malt);
|
||||
runmany(Fun, {recursive, Fuse}, List, local, Split, []) ->
|
||||
%% local recursive fuse, for when we weren't invoked with {processes, X}
|
||||
%% or {nodes, NodeList}. Degenerates recursive fuse into linear fuse.
|
||||
runmany(Fun, Fuse, List, local, Split, []);
|
||||
runmany(Fun, Fuse, List, Nodes, no_split, []) ->
|
||||
%% by default, operate on each element seperately
|
||||
runmany(Fun, Fuse, List, Nodes, 1, []);
|
||||
runmany(Fun, Fuse, List, local, Split, []) ->
|
||||
List2 = splitmany(List, Split),
|
||||
local_runmany(Fun, Fuse, List2);
|
||||
runmany(Fun, Fuse, List, Nodes, Split, []) ->
|
||||
List2 = splitmany(List, Split),
|
||||
cluster_runmany(Fun, Fuse, List2, Nodes).
|
||||
|
||||
cleanup_timer(Timer) ->
|
||||
receive
|
||||
{timerrang, Timer} ->
|
||||
cleanup_timer(Timer);
|
||||
{timerstopped, Timer} ->
|
||||
nil
|
||||
end.
|
||||
|
||||
schedulers_on_node(Node) ->
|
||||
case erlang:get(ec_plists_schedulers_on_nodes) of
|
||||
undefined ->
|
||||
X = determine_schedulers(Node),
|
||||
erlang:put(ec_plists_schedulers_on_nodes,
|
||||
dict:store(Node, X, dict:new())),
|
||||
X;
|
||||
Dict ->
|
||||
case dict:is_key(Node, Dict) of
|
||||
true ->
|
||||
dict:fetch(Node, Dict);
|
||||
false ->
|
||||
X = determine_schedulers(Node),
|
||||
erlang:put(ec_plists_schedulers_on_nodes,
|
||||
dict:store(Node, X, Dict)),
|
||||
X
|
||||
end
|
||||
end.
|
||||
|
||||
determine_schedulers(Node) ->
|
||||
Parent = erlang:self(),
|
||||
Child = proc_lib:spawn(Node, fun () ->
|
||||
Parent ! {self(), erlang:system_info(schedulers)}
|
||||
end),
|
||||
erlang:monitor(process, Child),
|
||||
receive
|
||||
{Child, X} ->
|
||||
receive
|
||||
{'DOWN', _, _, Child, _Reason} ->
|
||||
nil
|
||||
end,
|
||||
X;
|
||||
{'DOWN', _, _, Child, Reason} when Reason =/= normal ->
|
||||
0
|
||||
end.
|
||||
|
||||
%% @doc local runmany, for when we weren't invoked with {processes, X}
|
||||
%% or {nodes, NodeList}. Every sublist is processed in parallel.
|
||||
local_runmany(Fun, Fuse, List) ->
|
||||
Parent = self (),
|
||||
Pids = lists:map(fun (L) ->
|
||||
F = fun () ->
|
||||
Parent ! {self (), Fun(L)}
|
||||
end,
|
||||
{Pid, _} = erlang:spawn_monitor(F),
|
||||
Pid
|
||||
end,
|
||||
List),
|
||||
Answers = try
|
||||
lists:map(fun receivefrom/1, Pids)
|
||||
catch
|
||||
throw:Message ->
|
||||
{BadPid, Reason} = Message,
|
||||
handle_error(BadPid, Reason, Pids)
|
||||
end,
|
||||
lists:foreach(fun (Pid) ->
|
||||
normal_cleanup(Pid)
|
||||
end, Pids),
|
||||
fuse(Fuse, Answers).
|
||||
|
||||
receivefrom(Pid) ->
|
||||
receive
|
||||
{Pid, R} ->
|
||||
R;
|
||||
{'DOWN', _, _, Pid, Reason} when Reason =/= normal ->
|
||||
erlang:throw({Pid, Reason});
|
||||
{timerrang, _} ->
|
||||
erlang:throw({nil, timeout})
|
||||
end.
|
||||
|
||||
%% Convert List into [{Number, Sublist}]
|
||||
cluster_runmany(Fun, Fuse, List, Nodes) ->
|
||||
{List2, _} = lists:foldl(fun (X, {L, Count}) ->
|
||||
{[{Count, X}|L], Count+1}
|
||||
end,
|
||||
{[], 0}, List),
|
||||
cluster_runmany(Fun, Fuse, List2, Nodes, [], []).
|
||||
|
||||
%% @doc Add a pair of results into the TaskList as a fusing task
|
||||
cluster_runmany(Fun, {recursive, Fuse}, [], Nodes, Running,
|
||||
[{_, R1}, {_, R2}|Results]) ->
|
||||
cluster_runmany(Fun, {recursive, Fuse}, [{fuse, R1, R2}], Nodes,
|
||||
Running, Results);
|
||||
cluster_runmany(_, {recursive, _Fuse}, [], _Nodes, [], [{_, Result}]) ->
|
||||
%% recursive fuse done, return result
|
||||
Result;
|
||||
cluster_runmany(_, {recursive, _Fuse}, [], _Nodes, [], []) ->
|
||||
%% edge case where we are asked to do nothing
|
||||
[];
|
||||
cluster_runmany(_, Fuse, [], _Nodes, [], Results) ->
|
||||
%% We're done, now we just have to [linear] fuse the results
|
||||
fuse(Fuse, lists:map(fun ({_, R}) ->
|
||||
R
|
||||
end,
|
||||
lists:sort(fun ({A, _}, {B, _}) ->
|
||||
A =< B
|
||||
end,
|
||||
lists:reverse(Results))));
|
||||
cluster_runmany(Fun, Fuse, [Task|TaskList], [N|Nodes], Running, Results) ->
|
||||
%% We have a ready node and a sublist or fuse to be processed, so we start
|
||||
%% a new process
|
||||
|
||||
Parent = erlang:self(),
|
||||
case Task of
|
||||
{Num, L2} ->
|
||||
Fun2 = fun () ->
|
||||
Parent ! {erlang:self(), Num, Fun(L2)}
|
||||
end;
|
||||
{fuse, R1, R2} ->
|
||||
{recursive, FuseFunc} = Fuse,
|
||||
Fun2 = fun () ->
|
||||
Parent ! {erlang:self(), fuse, FuseFunc(R1, R2)}
|
||||
end
|
||||
end,
|
||||
Fun3 = fun() -> runmany_wrap(Fun2, Parent) end,
|
||||
Pid = proc_lib:spawn(N, Fun3),
|
||||
erlang:monitor(process, Pid),
|
||||
cluster_runmany(Fun, Fuse, TaskList, Nodes, [{Pid, N, Task}|Running], Results);
|
||||
cluster_runmany(Fun, Fuse, TaskList, Nodes, Running, Results) when length(Running) > 0 ->
|
||||
%% We can't start a new process, but can watch over already running ones
|
||||
receive
|
||||
{_Pid, error, Reason} ->
|
||||
RunningPids = lists:map(fun ({Pid, _, _}) ->
|
||||
Pid
|
||||
end,
|
||||
Running),
|
||||
handle_error(junkvalue, Reason, RunningPids);
|
||||
{Pid, Num, Result} ->
|
||||
%% throw out the exit message, Reason should be
|
||||
%% normal, noproc, or noconnection
|
||||
receive
|
||||
{'DOWN', _, _, Pid, _Reason} ->
|
||||
nil
|
||||
end,
|
||||
{Running2, FinishedNode, _} = delete_running(Pid, Running, []),
|
||||
cluster_runmany(Fun, Fuse, TaskList,
|
||||
[FinishedNode|Nodes], Running2, [{Num, Result}|Results]);
|
||||
{timerrang, _} ->
|
||||
RunningPids = lists:map(fun ({Pid, _, _}) ->
|
||||
Pid
|
||||
end,
|
||||
Running),
|
||||
handle_error(nil, timeout, RunningPids);
|
||||
%% node failure
|
||||
{'DOWN', _, _, Pid, noconnection} ->
|
||||
{Running2, _DeadNode, Task} = delete_running(Pid, Running, []),
|
||||
cluster_runmany(Fun, Fuse, [Task|TaskList], Nodes,
|
||||
Running2, Results);
|
||||
%% could a noproc exit message come before the message from
|
||||
%% the process? we are assuming it can't.
|
||||
%% this clause is unlikely to get invoked due to cluster_runmany's
|
||||
%% spawned processes. It will still catch errors in mapreduce's
|
||||
%% reduce process, however.
|
||||
{'DOWN', _, _, BadPid, Reason} when Reason =/= normal ->
|
||||
RunningPids = lists:map(fun ({Pid, _, _}) ->
|
||||
Pid
|
||||
end,
|
||||
Running),
|
||||
handle_error(BadPid, Reason, RunningPids)
|
||||
end;
|
||||
cluster_runmany(_, _, [_Non|_Empty], []=_Nodes, []=_Running, _) ->
|
||||
%% We have data, but no nodes either available or occupied
|
||||
erlang:exit(allnodescrashed).
|
||||
|
||||
-ifdef(fun_stacktrace).
|
||||
runmany_wrap(Fun, Parent) ->
|
||||
try
|
||||
Fun
|
||||
catch
|
||||
exit:siblingdied ->
|
||||
ok;
|
||||
exit:Reason ->
|
||||
Parent ! {erlang:self(), error, Reason};
|
||||
error:R ->
|
||||
Parent ! {erlang:self(), error, {R, erlang:get_stacktrace()}};
|
||||
throw:R ->
|
||||
Parent ! {erlang:self(), error, {{nocatch, R}, erlang:get_stacktrace()}}
|
||||
end.
|
||||
-else.
|
||||
runmany_wrap(Fun, Parent) ->
|
||||
try
|
||||
Fun
|
||||
catch
|
||||
exit:siblingdied ->
|
||||
ok;
|
||||
exit:Reason ->
|
||||
Parent ! {erlang:self(), error, Reason};
|
||||
error:R:Stacktrace ->
|
||||
Parent ! {erlang:self(), error, {R, Stacktrace}};
|
||||
throw:R:Stacktrace ->
|
||||
Parent ! {erlang:self(), error, {{nocatch, R}, Stacktrace}}
|
||||
end.
|
||||
-endif.
|
||||
|
||||
delete_running(Pid, [{Pid, Node, List}|Running], Acc) ->
|
||||
{Running ++ Acc, Node, List};
|
||||
delete_running(Pid, [R|Running], Acc) ->
|
||||
delete_running(Pid, Running, [R|Acc]).
|
||||
|
||||
handle_error(BadPid, Reason, Pids) ->
|
||||
lists:foreach(fun (Pid) ->
|
||||
erlang:exit(Pid, siblingdied)
|
||||
end, Pids),
|
||||
lists:foreach(fun (Pid) ->
|
||||
error_cleanup(Pid, BadPid)
|
||||
end, Pids),
|
||||
erlang:exit(Reason).
|
||||
|
||||
error_cleanup(BadPid, BadPid) ->
|
||||
ok;
|
||||
error_cleanup(Pid, BadPid) ->
|
||||
receive
|
||||
{Pid, _} ->
|
||||
error_cleanup(Pid, BadPid);
|
||||
{Pid, _, _} ->
|
||||
error_cleanup(Pid, BadPid);
|
||||
{'DOWN', _, _, Pid, _Reason} ->
|
||||
ok
|
||||
end.
|
||||
|
||||
normal_cleanup(Pid) ->
|
||||
receive
|
||||
{'DOWN', _, _, Pid, _Reason} ->
|
||||
ok
|
||||
end.
|
||||
|
||||
%% edge case
|
||||
fuse(_, []) ->
|
||||
[];
|
||||
fuse({reverse, _}=Fuse, Results) ->
|
||||
[RL|ResultsR] = lists:reverse(Results),
|
||||
fuse(Fuse, ResultsR, RL);
|
||||
fuse(Fuse, [R1|Results]) ->
|
||||
fuse(Fuse, Results, R1).
|
||||
|
||||
fuse({reverse, FuseFunc}=Fuse, [R2|Results], R1) ->
|
||||
fuse(Fuse, Results, FuseFunc(R2, R1));
|
||||
fuse(Fuse, [R2|Results], R1) ->
|
||||
fuse(Fuse, Results, Fuse(R1, R2));
|
||||
fuse(_, [], R) ->
|
||||
R.
|
||||
|
||||
%% @doc Splits a list into a list of sublists, each of size Size,
|
||||
%% except for the last element which is less if the original list
|
||||
%% could not be evenly divided into Size-sized lists.
|
||||
splitmany(List, Size) ->
|
||||
splitmany(List, [], Size).
|
||||
|
||||
splitmany([], Acc, _) ->
|
||||
lists:reverse(Acc);
|
||||
splitmany(List, Acc, Size) ->
|
||||
{Top, NList} = split(Size, List),
|
||||
splitmany(NList, [Top|Acc], Size).
|
||||
|
||||
%% @doc Like lists:split, except it splits a list smaller than its first
|
||||
%% parameter
|
||||
split(Size, List) ->
|
||||
split(Size, List, []).
|
||||
|
||||
split(0, List, Acc) ->
|
||||
{lists:reverse(Acc), List};
|
||||
split(Size, [H|List], Acc) ->
|
||||
split(Size - 1, List, [H|Acc]);
|
||||
split(_, [], Acc) ->
|
||||
{lists:reverse(Acc), []}.
|
|
@ -0,0 +1,322 @@
|
|||
%%% vi:ts=4 sw=4 et
|
||||
%%% Copyright (c) 2008 Robert Virding. All rights reserved.
|
||||
%%%
|
||||
%%% Redistribution and use in source and binary forms, with or without
|
||||
%%% modification, are permitted provided that the following conditions
|
||||
%%% are met:
|
||||
%%%
|
||||
%%% 1. Redistributions of source code must retain the above copyright
|
||||
%%% notice, this list of conditions and the following disclaimer.
|
||||
%%% 2. Redistributions in binary form must reproduce the above copyright
|
||||
%%% notice, this list of conditions and the following disclaimer in the
|
||||
%%% documentation and/or other materials provided with the distribution.
|
||||
%%%
|
||||
%%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
%%% "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
%%% LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
%%% FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
%%% COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
%%% INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
%%% BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
%%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
%%% CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
%%% LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
%%% ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
%%% POSSIBILITY OF SUCH DAMAGE.
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @copyright 2008 Robert Verding
|
||||
%%%
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% Rbdict implements a Key - Value dictionary. An rbdict is a
|
||||
%%% representation of a dictionary, where a red-black tree is used to
|
||||
%%% store the keys and values.
|
||||
%%%
|
||||
%%% This module implents exactly the same interface as the module
|
||||
%%% ec_dictionary but with a defined representation. One difference is
|
||||
%%% that while dict considers two keys as different if they do not
|
||||
%%% match (=:=), this module considers two keys as different if and
|
||||
%%% only if they do not compare equal (==).
|
||||
%%%
|
||||
%%% The algorithms here are taken directly from Okasaki and Rbset
|
||||
%%% in ML/Scheme. The interface is compatible with the standard dict
|
||||
%%% interface.
|
||||
%%%
|
||||
%%% The following structures are used to build the the RB-dict:
|
||||
%%%
|
||||
%%% {r,Left,Key,Val,Right}
|
||||
%%% {b,Left,Key,Val,Right}
|
||||
%%% empty
|
||||
%%%
|
||||
%%% It is interesting to note that expanding out the first argument of
|
||||
%%% l/rbalance, the colour, in store etc. is actually slower than not
|
||||
%%% doing it. Measured.
|
||||
%%%
|
||||
%%% see ec_dictionary
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ec_rbdict).
|
||||
|
||||
-behaviour(ec_dictionary).
|
||||
|
||||
%% Standard interface.
|
||||
-export([add/3, from_list/1, get/2, get/3, has_key/2,
|
||||
has_value/2, new/0, remove/2, size/1, to_list/1,
|
||||
keys/1]).
|
||||
|
||||
-export_type([dictionary/2]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Types
|
||||
%%%===================================================================
|
||||
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||
%% however you should not rely on the internal representation here
|
||||
-type dictionary(K, V) :: empty | {color(),
|
||||
dictionary(K, V),
|
||||
ec_dictionary:key(K),
|
||||
ec_dictionary:value(V),
|
||||
dictionary(K, V)}.
|
||||
|
||||
-type color() :: r | b.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec new() -> dictionary(_K, _V).
|
||||
new() -> empty.
|
||||
|
||||
-spec has_key(ec_dictionary:key(K), dictionary(K, _V)) -> boolean().
|
||||
has_key(_, empty) ->
|
||||
false;
|
||||
has_key(K, {_, Left, K1, _, _}) when K < K1 ->
|
||||
has_key(K, Left);
|
||||
has_key(K, {_, _, K1, _, Right}) when K > K1 ->
|
||||
has_key(K, Right);
|
||||
has_key(_, {_, _, _, _, _}) ->
|
||||
true.
|
||||
|
||||
-spec get(ec_dictionary:key(K), dictionary(K, V)) -> ec_dictionary:value(V).
|
||||
get(_, empty) ->
|
||||
throw(not_found);
|
||||
get(K, {_, Left, K1, _, _}) when K < K1 ->
|
||||
get(K, Left);
|
||||
get(K, {_, _, K1, _, Right}) when K > K1 ->
|
||||
get(K, Right);
|
||||
get(_, {_, _, _, Val, _}) ->
|
||||
Val.
|
||||
|
||||
-spec get(ec_dictionary:key(K),
|
||||
ec_dictionary:value(V),
|
||||
dictionary(K, V)) -> ec_dictionary:value(V).
|
||||
get(_, Default, empty) ->
|
||||
Default;
|
||||
get(K, Default, {_, Left, K1, _, _}) when K < K1 ->
|
||||
get(K, Default, Left);
|
||||
get(K, Default, {_, _, K1, _, Right}) when K > K1 ->
|
||||
get(K, Default, Right);
|
||||
get(_, _, {_, _, _, Val, _}) ->
|
||||
Val.
|
||||
|
||||
-spec add(ec_dictionary:key(K), ec_dictionary:value(V),
|
||||
dictionary(K, V)) -> dictionary(K, V).
|
||||
add(Key, Value, Dict) ->
|
||||
{_, L, K1, V1, R} = add1(Key, Value, Dict),
|
||||
{b, L, K1, V1, R}.
|
||||
|
||||
-spec remove(ec_dictionary:key(K), dictionary(K, V)) -> dictionary(K, V).
|
||||
remove(Key, Dictionary) ->
|
||||
{Dict1, _} = erase_aux(Key, Dictionary), Dict1.
|
||||
|
||||
-spec has_value(ec_dictionary:value(V), dictionary(_K, V)) -> boolean().
|
||||
has_value(Value, Dict) ->
|
||||
fold(fun (_, NValue, _) when NValue == Value -> true;
|
||||
(_, _, Acc) -> Acc
|
||||
end,
|
||||
false, Dict).
|
||||
|
||||
-spec size(dictionary(_K, _V)) -> non_neg_integer().
|
||||
size(T) ->
|
||||
size1(T).
|
||||
|
||||
-spec to_list(dictionary(K, V)) ->
|
||||
[{ec_dictionary:key(K), ec_dictionary:value(V)}].
|
||||
to_list(T) ->
|
||||
to_list(T, []).
|
||||
|
||||
-spec from_list([{ec_dictionary:key(K), ec_dictionary:value(V)}]) ->
|
||||
dictionary(K, V).
|
||||
from_list(L) ->
|
||||
lists:foldl(fun ({K, V}, D) ->
|
||||
add(K, V, D)
|
||||
end, new(),
|
||||
L).
|
||||
|
||||
-spec keys(dictionary(K, _V)) -> [ec_dictionary:key(K)].
|
||||
keys(Dict) ->
|
||||
keys(Dict, []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Enternal functions
|
||||
%%%===================================================================
|
||||
-spec keys(dictionary(K, _V), [ec_dictionary:key(K)]) ->
|
||||
[ec_dictionary:key(K)].
|
||||
keys(empty, Tail) ->
|
||||
Tail;
|
||||
keys({_, L, K, _, R}, Tail) ->
|
||||
keys(L, [K | keys(R, Tail)]).
|
||||
|
||||
|
||||
-spec erase_aux(ec_dictionary:key(K), dictionary(K, V)) ->
|
||||
{dictionary(K, V), boolean()}.
|
||||
erase_aux(_, empty) ->
|
||||
{empty, false};
|
||||
erase_aux(K, {b, A, Xk, Xv, B}) ->
|
||||
if K < Xk ->
|
||||
{A1, Dec} = erase_aux(K, A),
|
||||
if Dec ->
|
||||
unbalright(b, A1, Xk, Xv, B);
|
||||
true ->
|
||||
{{b, A1, Xk, Xv, B}, false}
|
||||
end;
|
||||
K > Xk ->
|
||||
{B1, Dec} = erase_aux(K, B),
|
||||
if Dec ->
|
||||
unballeft(b, A, Xk, Xv, B1);
|
||||
true ->
|
||||
{{b, A, Xk, Xv, B1}, false}
|
||||
end;
|
||||
true ->
|
||||
case B of
|
||||
empty ->
|
||||
blackify(A);
|
||||
_ ->
|
||||
{B1, {Mk, Mv}, Dec} = erase_min(B),
|
||||
if Dec ->
|
||||
unballeft(b, A, Mk, Mv, B1);
|
||||
true ->
|
||||
{{b, A, Mk, Mv, B1}, false}
|
||||
end
|
||||
end
|
||||
end;
|
||||
erase_aux(K, {r, A, Xk, Xv, B}) ->
|
||||
if K < Xk ->
|
||||
{A1, Dec} = erase_aux(K, A),
|
||||
if Dec ->
|
||||
unbalright(r, A1, Xk, Xv, B);
|
||||
true ->
|
||||
{{r, A1, Xk, Xv, B}, false}
|
||||
end;
|
||||
K > Xk ->
|
||||
{B1, Dec} = erase_aux(K, B),
|
||||
if Dec ->
|
||||
unballeft(r, A, Xk, Xv, B1);
|
||||
true ->
|
||||
{{r, A, Xk, Xv, B1}, false}
|
||||
end;
|
||||
true ->
|
||||
case B of
|
||||
empty ->
|
||||
{A, false};
|
||||
_ ->
|
||||
{B1, {Mk, Mv}, Dec} = erase_min(B),
|
||||
if Dec ->
|
||||
unballeft(r, A, Mk, Mv, B1);
|
||||
true ->
|
||||
{{r, A, Mk, Mv, B1}, false}
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
-spec erase_min(dictionary(K, V)) ->
|
||||
{dictionary(K, V), {ec_dictionary:key(K), ec_dictionary:value(V)}, boolean()}.
|
||||
erase_min({b, empty, Xk, Xv, empty}) ->
|
||||
{empty, {Xk, Xv}, true};
|
||||
erase_min({b, empty, Xk, Xv, {r, A, Yk, Yv, B}}) ->
|
||||
{{b, A, Yk, Yv, B}, {Xk, Xv}, false};
|
||||
erase_min({b, empty, _, _, {b, _, _, _, _}}) ->
|
||||
exit(boom);
|
||||
erase_min({r, empty, Xk, Xv, A}) ->
|
||||
{A, {Xk, Xv}, false};
|
||||
erase_min({b, A, Xk, Xv, B}) ->
|
||||
{A1, Min, Dec} = erase_min(A),
|
||||
if Dec ->
|
||||
{T, Dec1} = unbalright(b, A1, Xk, Xv, B),
|
||||
{T, Min, Dec1};
|
||||
true -> {{b, A1, Xk, Xv, B}, Min, false}
|
||||
end;
|
||||
erase_min({r, A, Xk, Xv, B}) ->
|
||||
{A1, Min, Dec} = erase_min(A),
|
||||
if Dec ->
|
||||
{T, Dec1} = unbalright(r, A1, Xk, Xv, B),
|
||||
{T, Min, Dec1};
|
||||
true -> {{r, A1, Xk, Xv, B}, Min, false}
|
||||
end.
|
||||
|
||||
blackify({r, A, K, V, B}) -> {{b, A, K, V, B}, false};
|
||||
blackify(Node) -> {Node, true}.
|
||||
|
||||
unballeft(r, {b, A, Xk, Xv, B}, Yk, Yv, C) ->
|
||||
{lbalance(b, {r, A, Xk, Xv, B}, Yk, Yv, C), false};
|
||||
unballeft(b, {b, A, Xk, Xv, B}, Yk, Yv, C) ->
|
||||
{lbalance(b, {r, A, Xk, Xv, B}, Yk, Yv, C), true};
|
||||
unballeft(b, {r, A, Xk, Xv, {b, B, Yk, Yv, C}}, Zk, Zv,
|
||||
D) ->
|
||||
{{b, A, Xk, Xv,
|
||||
lbalance(b, {r, B, Yk, Yv, C}, Zk, Zv, D)},
|
||||
false}.
|
||||
|
||||
unbalright(r, A, Xk, Xv, {b, B, Yk, Yv, C}) ->
|
||||
{rbalance(b, A, Xk, Xv, {r, B, Yk, Yv, C}), false};
|
||||
unbalright(b, A, Xk, Xv, {b, B, Yk, Yv, C}) ->
|
||||
{rbalance(b, A, Xk, Xv, {r, B, Yk, Yv, C}), true};
|
||||
unbalright(b, A, Xk, Xv,
|
||||
{r, {b, B, Yk, Yv, C}, Zk, Zv, D}) ->
|
||||
{{b, rbalance(b, A, Xk, Xv, {r, B, Yk, Yv, C}), Zk, Zv,
|
||||
D},
|
||||
false}.
|
||||
|
||||
-spec fold(fun((ec_dictionary:key(K), ec_dictionary:value(V), any()) -> any()),
|
||||
any(), dictionary(K, V)) -> any().
|
||||
fold(_, Acc, empty) -> Acc;
|
||||
fold(F, Acc, {_, A, Xk, Xv, B}) ->
|
||||
fold(F, F(Xk, Xv, fold(F, Acc, B)), A).
|
||||
|
||||
add1(K, V, empty) -> {r, empty, K, V, empty};
|
||||
add1(K, V, {C, Left, K1, V1, Right}) when K < K1 ->
|
||||
lbalance(C, add1(K, V, Left), K1, V1, Right);
|
||||
add1(K, V, {C, Left, K1, V1, Right}) when K > K1 ->
|
||||
rbalance(C, Left, K1, V1, add1(K, V, Right));
|
||||
add1(K, V, {C, L, _, _, R}) -> {C, L, K, V, R}.
|
||||
|
||||
size1(empty) -> 0;
|
||||
size1({_, L, _, _, R}) -> size1(L) + size1(R) + 1.
|
||||
|
||||
to_list(empty, List) -> List;
|
||||
to_list({_, A, Xk, Xv, B}, List) ->
|
||||
to_list(A, [{Xk, Xv} | to_list(B, List)]).
|
||||
|
||||
%% Balance a tree afer (possibly) adding a node to the left/right.
|
||||
-spec lbalance(color(), dictionary(K, V),
|
||||
ec_dictionary:key(K), ec_dictionary:value(V),
|
||||
dictionary(K, V)) ->
|
||||
dictionary(K, V).
|
||||
lbalance(b, {r, {r, A, Xk, Xv, B}, Yk, Yv, C}, Zk, Zv,
|
||||
D) ->
|
||||
{r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
|
||||
lbalance(b, {r, A, Xk, Xv, {r, B, Yk, Yv, C}}, Zk, Zv,
|
||||
D) ->
|
||||
{r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
|
||||
lbalance(C, A, Xk, Xv, B) -> {C, A, Xk, Xv, B}.
|
||||
|
||||
-spec rbalance(color(), dictionary(K, V),
|
||||
ec_dictionary:key(K), ec_dictionary:value(V),
|
||||
dictionary(K, V)) ->
|
||||
dictionary(K, V).
|
||||
rbalance(b, A, Xk, Xv,
|
||||
{r, {r, B, Yk, Yv, C}, Zk, Zv, D}) ->
|
||||
{r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
|
||||
rbalance(b, A, Xk, Xv,
|
||||
{r, B, Yk, Yv, {r, C, Zk, Zv, D}}) ->
|
||||
{r, {b, A, Xk, Xv, B}, Yk, Yv, {b, C, Zk, Zv, D}};
|
||||
rbalance(C, A, Xk, Xv, B) -> {C, A, Xk, Xv, B}.
|
|
@ -0,0 +1,763 @@
|
|||
%%% vi:ts=4 sw=4 et
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @copyright (C) 2011, Erlware LLC
|
||||
%%% @doc
|
||||
%%% Helper functions for working with semver versioning strings.
|
||||
%%% See http://semver.org/ for the spec.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ec_semver).
|
||||
|
||||
-export([parse/1,
|
||||
format/1,
|
||||
eql/2,
|
||||
gt/2,
|
||||
gte/2,
|
||||
lt/2,
|
||||
lte/2,
|
||||
pes/2,
|
||||
between/3]).
|
||||
|
||||
%% For internal use by the ec_semver_parser peg
|
||||
-export([internal_parse_version/1]).
|
||||
|
||||
-export_type([semver/0,
|
||||
version_string/0,
|
||||
any_version/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Public Types
|
||||
%%%===================================================================
|
||||
|
||||
-type version_element() :: non_neg_integer() | binary().
|
||||
|
||||
-type major_minor_patch_minpatch() ::
|
||||
version_element()
|
||||
| {version_element(), version_element()}
|
||||
| {version_element(), version_element(), version_element()}
|
||||
| {version_element(), version_element(),
|
||||
version_element(), version_element()}.
|
||||
|
||||
-type alpha_part() :: integer() | binary() | string().
|
||||
-type alpha_info() :: {PreRelease::[alpha_part()],
|
||||
BuildVersion::[alpha_part()]}.
|
||||
|
||||
-type semver() :: {major_minor_patch_minpatch(), alpha_info()}.
|
||||
|
||||
-type version_string() :: string() | binary().
|
||||
|
||||
-type any_version() :: version_string() | semver().
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc parse a string or binary into a valid semver representation
|
||||
-spec parse(any_version()) -> semver().
|
||||
parse(Version) when erlang:is_list(Version) ->
|
||||
case ec_semver_parser:parse(Version) of
|
||||
{fail, _} ->
|
||||
{erlang:iolist_to_binary(Version), {[],[]}};
|
||||
Good ->
|
||||
Good
|
||||
end;
|
||||
parse(Version) when erlang:is_binary(Version) ->
|
||||
case ec_semver_parser:parse(Version) of
|
||||
{fail, _} ->
|
||||
{Version, {[],[]}};
|
||||
Good ->
|
||||
Good
|
||||
end;
|
||||
parse(Version) ->
|
||||
Version.
|
||||
|
||||
-spec format(semver()) -> iolist().
|
||||
format({Maj, {AlphaPart, BuildPart}})
|
||||
when erlang:is_integer(Maj);
|
||||
erlang:is_binary(Maj) ->
|
||||
[format_version_part(Maj),
|
||||
format_vsn_rest(<<"-">>, AlphaPart),
|
||||
format_vsn_rest(<<"+">>, BuildPart)];
|
||||
format({{Maj, Min}, {AlphaPart, BuildPart}}) ->
|
||||
[format_version_part(Maj), ".",
|
||||
format_version_part(Min),
|
||||
format_vsn_rest(<<"-">>, AlphaPart),
|
||||
format_vsn_rest(<<"+">>, BuildPart)];
|
||||
format({{Maj, Min, Patch}, {AlphaPart, BuildPart}}) ->
|
||||
[format_version_part(Maj), ".",
|
||||
format_version_part(Min), ".",
|
||||
format_version_part(Patch),
|
||||
format_vsn_rest(<<"-">>, AlphaPart),
|
||||
format_vsn_rest(<<"+">>, BuildPart)];
|
||||
format({{Maj, Min, Patch, MinPatch}, {AlphaPart, BuildPart}}) ->
|
||||
[format_version_part(Maj), ".",
|
||||
format_version_part(Min), ".",
|
||||
format_version_part(Patch), ".",
|
||||
format_version_part(MinPatch),
|
||||
format_vsn_rest(<<"-">>, AlphaPart),
|
||||
format_vsn_rest(<<"+">>, BuildPart)].
|
||||
|
||||
-spec format_version_part(integer() | binary()) -> iolist().
|
||||
format_version_part(Vsn)
|
||||
when erlang:is_integer(Vsn) ->
|
||||
erlang:integer_to_list(Vsn);
|
||||
format_version_part(Vsn)
|
||||
when erlang:is_binary(Vsn) ->
|
||||
Vsn.
|
||||
|
||||
|
||||
|
||||
%% @doc test for quality between semver versions
|
||||
-spec eql(any_version(), any_version()) -> boolean().
|
||||
eql(VsnA, VsnB) ->
|
||||
NVsnA = normalize(parse(VsnA)),
|
||||
NVsnB = normalize(parse(VsnB)),
|
||||
NVsnA =:= NVsnB.
|
||||
|
||||
%% @doc Test that VsnA is greater than VsnB
|
||||
-spec gt(any_version(), any_version()) -> boolean().
|
||||
gt(VsnA, VsnB) ->
|
||||
{MMPA, {AlphaA, PatchA}} = normalize(parse(VsnA)),
|
||||
{MMPB, {AlphaB, PatchB}} = normalize(parse(VsnB)),
|
||||
((MMPA > MMPB)
|
||||
orelse
|
||||
((MMPA =:= MMPB)
|
||||
andalso
|
||||
((AlphaA =:= [] andalso AlphaB =/= [])
|
||||
orelse
|
||||
((not (AlphaB =:= [] andalso AlphaA =/= []))
|
||||
andalso
|
||||
(AlphaA > AlphaB))))
|
||||
orelse
|
||||
((MMPA =:= MMPB)
|
||||
andalso
|
||||
(AlphaA =:= AlphaB)
|
||||
andalso
|
||||
((PatchB =:= [] andalso PatchA =/= [])
|
||||
orelse
|
||||
PatchA > PatchB))).
|
||||
|
||||
%% @doc Test that VsnA is greater than or equal to VsnB
|
||||
-spec gte(any_version(), any_version()) -> boolean().
|
||||
gte(VsnA, VsnB) ->
|
||||
NVsnA = normalize(parse(VsnA)),
|
||||
NVsnB = normalize(parse(VsnB)),
|
||||
gt(NVsnA, NVsnB) orelse eql(NVsnA, NVsnB).
|
||||
|
||||
%% @doc Test that VsnA is less than VsnB
|
||||
-spec lt(any_version(), any_version()) -> boolean().
|
||||
lt(VsnA, VsnB) ->
|
||||
{MMPA, {AlphaA, PatchA}} = normalize(parse(VsnA)),
|
||||
{MMPB, {AlphaB, PatchB}} = normalize(parse(VsnB)),
|
||||
((MMPA < MMPB)
|
||||
orelse
|
||||
((MMPA =:= MMPB)
|
||||
andalso
|
||||
((AlphaB =:= [] andalso AlphaA =/= [])
|
||||
orelse
|
||||
((not (AlphaA =:= [] andalso AlphaB =/= []))
|
||||
andalso
|
||||
(AlphaA < AlphaB))))
|
||||
orelse
|
||||
((MMPA =:= MMPB)
|
||||
andalso
|
||||
(AlphaA =:= AlphaB)
|
||||
andalso
|
||||
((PatchA =:= [] andalso PatchB =/= [])
|
||||
orelse
|
||||
PatchA < PatchB))).
|
||||
|
||||
%% @doc Test that VsnA is less than or equal to VsnB
|
||||
-spec lte(any_version(), any_version()) -> boolean().
|
||||
lte(VsnA, VsnB) ->
|
||||
NVsnA = normalize(parse(VsnA)),
|
||||
NVsnB = normalize(parse(VsnB)),
|
||||
lt(NVsnA, NVsnB) orelse eql(NVsnA, NVsnB).
|
||||
|
||||
%% @doc Test that VsnMatch is greater than or equal to Vsn1 and
|
||||
%% less than or equal to Vsn2
|
||||
-spec between(any_version(), any_version(), any_version()) -> boolean().
|
||||
between(Vsn1, Vsn2, VsnMatch) ->
|
||||
NVsnA = normalize(parse(Vsn1)),
|
||||
NVsnB = normalize(parse(Vsn2)),
|
||||
NVsnMatch = normalize(parse(VsnMatch)),
|
||||
gte(NVsnMatch, NVsnA) andalso
|
||||
lte(NVsnMatch, NVsnB).
|
||||
|
||||
%% @doc check that VsnA is Approximately greater than VsnB
|
||||
%%
|
||||
%% Specifying ">= 2.6.5" is an optimistic version constraint. All
|
||||
%% versions greater than the one specified, including major releases
|
||||
%% (e.g. 3.0.0) are allowed.
|
||||
%%
|
||||
%% Conversely, specifying "~> 2.6" is pessimistic about future major
|
||||
%% revisions and "~> 2.6.5" is pessimistic about future minor
|
||||
%% revisions.
|
||||
%%
|
||||
%% "~> 2.6" matches cookbooks >= 2.6.0 AND < 3.0.0
|
||||
%% "~> 2.6.5" matches cookbooks >= 2.6.5 AND < 2.7.0
|
||||
pes(VsnA, VsnB) ->
|
||||
internal_pes(parse(VsnA), parse(VsnB)).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Friend Functions
|
||||
%%%===================================================================
|
||||
%% @doc helper function for the peg grammer to parse the iolist into a semver
|
||||
-spec internal_parse_version(iolist()) -> semver().
|
||||
internal_parse_version([MMP, AlphaPart, BuildPart, _]) ->
|
||||
{parse_major_minor_patch_minpatch(MMP), {parse_alpha_part(AlphaPart),
|
||||
parse_alpha_part(BuildPart)}}.
|
||||
|
||||
%% @doc helper function for the peg grammer to parse the iolist into a major_minor_patch
|
||||
-spec parse_major_minor_patch_minpatch(iolist()) -> major_minor_patch_minpatch().
|
||||
parse_major_minor_patch_minpatch([MajVsn, [], [], []]) ->
|
||||
strip_maj_version(MajVsn);
|
||||
parse_major_minor_patch_minpatch([MajVsn, [<<".">>, MinVsn], [], []]) ->
|
||||
{strip_maj_version(MajVsn), MinVsn};
|
||||
parse_major_minor_patch_minpatch([MajVsn,
|
||||
[<<".">>, MinVsn],
|
||||
[<<".">>, PatchVsn], []]) ->
|
||||
{strip_maj_version(MajVsn), MinVsn, PatchVsn};
|
||||
parse_major_minor_patch_minpatch([MajVsn,
|
||||
[<<".">>, MinVsn],
|
||||
[<<".">>, PatchVsn],
|
||||
[<<".">>, MinPatch]]) ->
|
||||
{strip_maj_version(MajVsn), MinVsn, PatchVsn, MinPatch}.
|
||||
|
||||
%% @doc helper function for the peg grammer to parse the iolist into an alpha part
|
||||
-spec parse_alpha_part(iolist()) -> [alpha_part()].
|
||||
parse_alpha_part([]) ->
|
||||
[];
|
||||
parse_alpha_part([_, AV1, Rest]) ->
|
||||
[erlang:iolist_to_binary(AV1) |
|
||||
[format_alpha_part(Part) || Part <- Rest]].
|
||||
|
||||
%% @doc according to semver alpha parts that can be treated like
|
||||
%% numbers must be. We implement that here by taking the alpha part
|
||||
%% and trying to convert it to a number, if it succeeds we use
|
||||
%% it. Otherwise we do not.
|
||||
-spec format_alpha_part(iolist()) -> integer() | binary().
|
||||
format_alpha_part([<<".">>, AlphaPart]) ->
|
||||
Bin = erlang:iolist_to_binary(AlphaPart),
|
||||
try
|
||||
erlang:list_to_integer(erlang:binary_to_list(Bin))
|
||||
catch
|
||||
error:badarg ->
|
||||
Bin
|
||||
end.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal Functions
|
||||
%%%===================================================================
|
||||
-spec strip_maj_version(iolist()) -> version_element().
|
||||
strip_maj_version([<<"v">>, MajVsn]) ->
|
||||
MajVsn;
|
||||
strip_maj_version([[], MajVsn]) ->
|
||||
MajVsn;
|
||||
strip_maj_version(MajVsn) ->
|
||||
MajVsn.
|
||||
|
||||
-spec to_list(integer() | binary() | string()) -> string() | binary().
|
||||
to_list(Detail) when erlang:is_integer(Detail) ->
|
||||
erlang:integer_to_list(Detail);
|
||||
to_list(Detail) when erlang:is_list(Detail); erlang:is_binary(Detail) ->
|
||||
Detail.
|
||||
|
||||
-spec format_vsn_rest(binary() | string(), [integer() | binary()]) -> iolist().
|
||||
format_vsn_rest(_TypeMark, []) ->
|
||||
[];
|
||||
format_vsn_rest(TypeMark, [Head | Rest]) ->
|
||||
[TypeMark, Head |
|
||||
[[".", to_list(Detail)] || Detail <- Rest]].
|
||||
|
||||
%% @doc normalize the semver so they can be compared
|
||||
-spec normalize(semver()) -> semver().
|
||||
normalize({Vsn, Rest})
|
||||
when erlang:is_binary(Vsn);
|
||||
erlang:is_integer(Vsn) ->
|
||||
{{Vsn, 0, 0, 0}, Rest};
|
||||
normalize({{Maj, Min}, Rest}) ->
|
||||
{{Maj, Min, 0, 0}, Rest};
|
||||
normalize({{Maj, Min, Patch}, Rest}) ->
|
||||
{{Maj, Min, Patch, 0}, Rest};
|
||||
normalize(Other = {{_, _, _, _}, {_,_}}) ->
|
||||
Other.
|
||||
|
||||
%% @doc to do the pessimistic compare we need a parsed semver. This is
|
||||
%% the internal implementation of the of the pessimistic run. The
|
||||
%% external just ensures that versions are parsed.
|
||||
-spec internal_pes(semver(), semver()) -> boolean().
|
||||
internal_pes(VsnA, {{LM, LMI}, Alpha})
|
||||
when erlang:is_integer(LM),
|
||||
erlang:is_integer(LMI) ->
|
||||
gte(VsnA, {{LM, LMI, 0}, Alpha}) andalso
|
||||
lt(VsnA, {{LM + 1, 0, 0, 0}, {[], []}});
|
||||
internal_pes(VsnA, {{LM, LMI, LP}, Alpha})
|
||||
when erlang:is_integer(LM),
|
||||
erlang:is_integer(LMI),
|
||||
erlang:is_integer(LP) ->
|
||||
gte(VsnA, {{LM, LMI, LP}, Alpha})
|
||||
andalso
|
||||
lt(VsnA, {{LM, LMI + 1, 0, 0}, {[], []}});
|
||||
internal_pes(VsnA, {{LM, LMI, LP, LMP}, Alpha})
|
||||
when erlang:is_integer(LM),
|
||||
erlang:is_integer(LMI),
|
||||
erlang:is_integer(LP),
|
||||
erlang:is_integer(LMP) ->
|
||||
gte(VsnA, {{LM, LMI, LP, LMP}, Alpha})
|
||||
andalso
|
||||
lt(VsnA, {{LM, LMI, LP + 1, 0}, {[], []}});
|
||||
internal_pes(Vsn, LVsn) ->
|
||||
gte(Vsn, LVsn).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Test Functions
|
||||
%%%===================================================================
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
eql_test() ->
|
||||
?assertMatch(true, eql("1.0.0-alpha",
|
||||
"1.0.0-alpha")),
|
||||
?assertMatch(true, eql("v1.0.0-alpha",
|
||||
"1.0.0-alpha")),
|
||||
?assertMatch(true, eql("1",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, eql("v1",
|
||||
"v1.0.0")),
|
||||
?assertMatch(true, eql("1.0",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, eql("1.0.0",
|
||||
"1")),
|
||||
?assertMatch(true, eql("1.0.0.0",
|
||||
"1")),
|
||||
?assertMatch(true, eql("1.0+alpha.1",
|
||||
"1.0.0+alpha.1")),
|
||||
?assertMatch(true, eql("1.0-alpha.1+build.1",
|
||||
"1.0.0-alpha.1+build.1")),
|
||||
?assertMatch(true, eql("1.0-alpha.1+build.1",
|
||||
"1.0.0.0-alpha.1+build.1")),
|
||||
?assertMatch(true, eql("1.0-alpha.1+build.1",
|
||||
"v1.0.0.0-alpha.1+build.1")),
|
||||
?assertMatch(true, eql("1.0-pre-alpha.1",
|
||||
"1.0.0-pre-alpha.1")),
|
||||
?assertMatch(true, eql("aa", "aa")),
|
||||
?assertMatch(true, eql("AA.BB", "AA.BB")),
|
||||
?assertMatch(true, eql("BBB-super", "BBB-super")),
|
||||
?assertMatch(true, not eql("1.0.0",
|
||||
"1.0.1")),
|
||||
?assertMatch(true, not eql("1.0.0-alpha",
|
||||
"1.0.1+alpha")),
|
||||
?assertMatch(true, not eql("1.0.0+build.1",
|
||||
"1.0.1+build.2")),
|
||||
?assertMatch(true, not eql("1.0.0.0+build.1",
|
||||
"1.0.0.1+build.2")),
|
||||
?assertMatch(true, not eql("FFF", "BBB")),
|
||||
?assertMatch(true, not eql("1", "1BBBB")).
|
||||
|
||||
gt_test() ->
|
||||
?assertMatch(true, gt("1.0.0-alpha.1",
|
||||
"1.0.0-alpha")),
|
||||
?assertMatch(true, gt("1.0.0.1-alpha.1",
|
||||
"1.0.0.1-alpha")),
|
||||
?assertMatch(true, gt("1.0.0.4-alpha.1",
|
||||
"1.0.0.2-alpha")),
|
||||
?assertMatch(true, gt("1.0.0.0-alpha.1",
|
||||
"1.0.0-alpha")),
|
||||
?assertMatch(true, gt("1.0.0-beta.2",
|
||||
"1.0.0-alpha.1")),
|
||||
?assertMatch(true, gt("1.0.0-beta.11",
|
||||
"1.0.0-beta.2")),
|
||||
?assertMatch(true, gt("1.0.0-pre-alpha.14",
|
||||
"1.0.0-pre-alpha.3")),
|
||||
?assertMatch(true, gt("1.0.0-beta.11",
|
||||
"1.0.0.0-beta.2")),
|
||||
?assertMatch(true, gt("1.0.0-rc.1", "1.0.0-beta.11")),
|
||||
?assertMatch(true, gt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
|
||||
?assertMatch(true, gt("1.0.0", "1.0.0-rc.1+build.1")),
|
||||
?assertMatch(true, gt("1.0.0+0.3.7", "1.0.0")),
|
||||
?assertMatch(true, gt("1.3.7+build", "1.0.0+0.3.7")),
|
||||
?assertMatch(true, gt("1.3.7+build.2.b8f12d7",
|
||||
"1.3.7+build")),
|
||||
?assertMatch(true, gt("1.3.7+build.2.b8f12d7",
|
||||
"1.3.7.0+build")),
|
||||
?assertMatch(true, gt("1.3.7+build.11.e0f985a",
|
||||
"1.3.7+build.2.b8f12d7")),
|
||||
?assertMatch(true, gt("aa.cc",
|
||||
"aa.bb")),
|
||||
?assertMatch(true, not gt("1.0.0-alpha",
|
||||
"1.0.0-alpha.1")),
|
||||
?assertMatch(true, not gt("1.0.0-alpha",
|
||||
"1.0.0.0-alpha.1")),
|
||||
?assertMatch(true, not gt("1.0.0-alpha.1",
|
||||
"1.0.0-beta.2")),
|
||||
?assertMatch(true, not gt("1.0.0-beta.2",
|
||||
"1.0.0-beta.11")),
|
||||
?assertMatch(true, not gt("1.0.0-beta.11",
|
||||
"1.0.0-rc.1")),
|
||||
?assertMatch(true, not gt("1.0.0-pre-alpha.3",
|
||||
"1.0.0-pre-alpha.14")),
|
||||
?assertMatch(true, not gt("1.0.0-rc.1",
|
||||
"1.0.0-rc.1+build.1")),
|
||||
?assertMatch(true, not gt("1.0.0-rc.1+build.1",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, not gt("1.0.0",
|
||||
"1.0.0+0.3.7")),
|
||||
?assertMatch(true, not gt("1.0.0+0.3.7",
|
||||
"1.3.7+build")),
|
||||
?assertMatch(true, not gt("1.3.7+build",
|
||||
"1.3.7+build.2.b8f12d7")),
|
||||
?assertMatch(true, not gt("1.3.7+build.2.b8f12d7",
|
||||
"1.3.7+build.11.e0f985a")),
|
||||
?assertMatch(true, not gt("1.0.0-alpha",
|
||||
"1.0.0-alpha")),
|
||||
?assertMatch(true, not gt("1",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, not gt("aa.bb",
|
||||
"aa.bb")),
|
||||
?assertMatch(true, not gt("aa.cc",
|
||||
"aa.dd")),
|
||||
?assertMatch(true, not gt("1.0",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, not gt("1.0.0",
|
||||
"1")),
|
||||
?assertMatch(true, not gt("1.0+alpha.1",
|
||||
"1.0.0+alpha.1")),
|
||||
?assertMatch(true, not gt("1.0-alpha.1+build.1",
|
||||
"1.0.0-alpha.1+build.1")).
|
||||
|
||||
lt_test() ->
|
||||
?assertMatch(true, lt("1.0.0-alpha",
|
||||
"1.0.0-alpha.1")),
|
||||
?assertMatch(true, lt("1.0.0-alpha",
|
||||
"1.0.0.0-alpha.1")),
|
||||
?assertMatch(true, lt("1.0.0-alpha.1",
|
||||
"1.0.0-beta.2")),
|
||||
?assertMatch(true, lt("1.0.0-beta.2",
|
||||
"1.0.0-beta.11")),
|
||||
?assertMatch(true, lt("1.0.0-pre-alpha.3",
|
||||
"1.0.0-pre-alpha.14")),
|
||||
?assertMatch(true, lt("1.0.0-beta.11",
|
||||
"1.0.0-rc.1")),
|
||||
?assertMatch(true, lt("1.0.0.1-beta.11",
|
||||
"1.0.0.1-rc.1")),
|
||||
?assertMatch(true, lt("1.0.0-rc.1",
|
||||
"1.0.0-rc.1+build.1")),
|
||||
?assertMatch(true, lt("1.0.0-rc.1+build.1",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, lt("1.0.0",
|
||||
"1.0.0+0.3.7")),
|
||||
?assertMatch(true, lt("1.0.0+0.3.7",
|
||||
"1.3.7+build")),
|
||||
?assertMatch(true, lt("1.3.7+build",
|
||||
"1.3.7+build.2.b8f12d7")),
|
||||
?assertMatch(true, lt("1.3.7+build.2.b8f12d7",
|
||||
"1.3.7+build.11.e0f985a")),
|
||||
?assertMatch(true, not lt("1.0.0-alpha",
|
||||
"1.0.0-alpha")),
|
||||
?assertMatch(true, not lt("1",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, lt("1",
|
||||
"1.0.0.1")),
|
||||
?assertMatch(true, lt("AA.DD",
|
||||
"AA.EE")),
|
||||
?assertMatch(true, not lt("1.0",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, not lt("1.0.0.0",
|
||||
"1")),
|
||||
?assertMatch(true, not lt("1.0+alpha.1",
|
||||
"1.0.0+alpha.1")),
|
||||
?assertMatch(true, not lt("AA.DD", "AA.CC")),
|
||||
?assertMatch(true, not lt("1.0-alpha.1+build.1",
|
||||
"1.0.0-alpha.1+build.1")),
|
||||
?assertMatch(true, not lt("1.0.0-alpha.1",
|
||||
"1.0.0-alpha")),
|
||||
?assertMatch(true, not lt("1.0.0-beta.2",
|
||||
"1.0.0-alpha.1")),
|
||||
?assertMatch(true, not lt("1.0.0-beta.11",
|
||||
"1.0.0-beta.2")),
|
||||
?assertMatch(true, not lt("1.0.0-pre-alpha.14",
|
||||
"1.0.0-pre-alpha.3")),
|
||||
?assertMatch(true, not lt("1.0.0-rc.1", "1.0.0-beta.11")),
|
||||
?assertMatch(true, not lt("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
|
||||
?assertMatch(true, not lt("1.0.0", "1.0.0-rc.1+build.1")),
|
||||
?assertMatch(true, not lt("1.0.0+0.3.7", "1.0.0")),
|
||||
?assertMatch(true, not lt("1.3.7+build", "1.0.0+0.3.7")),
|
||||
?assertMatch(true, not lt("1.3.7+build.2.b8f12d7",
|
||||
"1.3.7+build")),
|
||||
?assertMatch(true, not lt("1.3.7+build.11.e0f985a",
|
||||
"1.3.7+build.2.b8f12d7")).
|
||||
|
||||
gte_test() ->
|
||||
?assertMatch(true, gte("1.0.0-alpha",
|
||||
"1.0.0-alpha")),
|
||||
|
||||
?assertMatch(true, gte("1",
|
||||
"1.0.0")),
|
||||
|
||||
?assertMatch(true, gte("1.0",
|
||||
"1.0.0")),
|
||||
|
||||
?assertMatch(true, gte("1.0.0",
|
||||
"1")),
|
||||
|
||||
?assertMatch(true, gte("1.0.0.0",
|
||||
"1")),
|
||||
|
||||
?assertMatch(true, gte("1.0+alpha.1",
|
||||
"1.0.0+alpha.1")),
|
||||
|
||||
?assertMatch(true, gte("1.0-alpha.1+build.1",
|
||||
"1.0.0-alpha.1+build.1")),
|
||||
|
||||
?assertMatch(true, gte("1.0.0-alpha.1+build.1",
|
||||
"1.0.0.0-alpha.1+build.1")),
|
||||
?assertMatch(true, gte("1.0.0-alpha.1",
|
||||
"1.0.0-alpha")),
|
||||
?assertMatch(true, gte("1.0.0-pre-alpha.2",
|
||||
"1.0.0-pre-alpha")),
|
||||
?assertMatch(true, gte("1.0.0-beta.2",
|
||||
"1.0.0-alpha.1")),
|
||||
?assertMatch(true, gte("1.0.0-beta.11",
|
||||
"1.0.0-beta.2")),
|
||||
?assertMatch(true, gte("aa.bb", "aa.bb")),
|
||||
?assertMatch(true, gte("dd", "aa")),
|
||||
?assertMatch(true, gte("1.0.0-rc.1", "1.0.0-beta.11")),
|
||||
?assertMatch(true, gte("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
|
||||
?assertMatch(true, gte("1.0.0", "1.0.0-rc.1+build.1")),
|
||||
?assertMatch(true, gte("1.0.0+0.3.7", "1.0.0")),
|
||||
?assertMatch(true, gte("1.3.7+build", "1.0.0+0.3.7")),
|
||||
?assertMatch(true, gte("1.3.7+build.2.b8f12d7",
|
||||
"1.3.7+build")),
|
||||
?assertMatch(true, gte("1.3.7+build.11.e0f985a",
|
||||
"1.3.7+build.2.b8f12d7")),
|
||||
?assertMatch(true, not gte("1.0.0-alpha",
|
||||
"1.0.0-alpha.1")),
|
||||
?assertMatch(true, not gte("1.0.0-pre-alpha",
|
||||
"1.0.0-pre-alpha.1")),
|
||||
?assertMatch(true, not gte("CC", "DD")),
|
||||
?assertMatch(true, not gte("1.0.0-alpha.1",
|
||||
"1.0.0-beta.2")),
|
||||
?assertMatch(true, not gte("1.0.0-beta.2",
|
||||
"1.0.0-beta.11")),
|
||||
?assertMatch(true, not gte("1.0.0-beta.11",
|
||||
"1.0.0-rc.1")),
|
||||
?assertMatch(true, not gte("1.0.0-rc.1",
|
||||
"1.0.0-rc.1+build.1")),
|
||||
?assertMatch(true, not gte("1.0.0-rc.1+build.1",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, not gte("1.0.0",
|
||||
"1.0.0+0.3.7")),
|
||||
?assertMatch(true, not gte("1.0.0+0.3.7",
|
||||
"1.3.7+build")),
|
||||
?assertMatch(true, not gte("1.0.0",
|
||||
"1.0.0+build.1")),
|
||||
?assertMatch(true, not gte("1.3.7+build",
|
||||
"1.3.7+build.2.b8f12d7")),
|
||||
?assertMatch(true, not gte("1.3.7+build.2.b8f12d7",
|
||||
"1.3.7+build.11.e0f985a")).
|
||||
lte_test() ->
|
||||
?assertMatch(true, lte("1.0.0-alpha",
|
||||
"1.0.0-alpha.1")),
|
||||
?assertMatch(true, lte("1.0.0-alpha.1",
|
||||
"1.0.0-beta.2")),
|
||||
?assertMatch(true, lte("1.0.0-beta.2",
|
||||
"1.0.0-beta.11")),
|
||||
?assertMatch(true, lte("1.0.0-pre-alpha.2",
|
||||
"1.0.0-pre-alpha.11")),
|
||||
?assertMatch(true, lte("1.0.0-beta.11",
|
||||
"1.0.0-rc.1")),
|
||||
?assertMatch(true, lte("1.0.0-rc.1",
|
||||
"1.0.0-rc.1+build.1")),
|
||||
?assertMatch(true, lte("1.0.0-rc.1+build.1",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, lte("1.0.0",
|
||||
"1.0.0+0.3.7")),
|
||||
?assertMatch(true, lte("1.0.0+0.3.7",
|
||||
"1.3.7+build")),
|
||||
?assertMatch(true, lte("1.3.7+build",
|
||||
"1.3.7+build.2.b8f12d7")),
|
||||
?assertMatch(true, lte("1.3.7+build.2.b8f12d7",
|
||||
"1.3.7+build.11.e0f985a")),
|
||||
?assertMatch(true, lte("1.0.0-alpha",
|
||||
"1.0.0-alpha")),
|
||||
?assertMatch(true, lte("1",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, lte("1.0",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, lte("1.0.0",
|
||||
"1")),
|
||||
?assertMatch(true, lte("1.0+alpha.1",
|
||||
"1.0.0+alpha.1")),
|
||||
?assertMatch(true, lte("1.0.0.0+alpha.1",
|
||||
"1.0.0+alpha.1")),
|
||||
?assertMatch(true, lte("1.0-alpha.1+build.1",
|
||||
"1.0.0-alpha.1+build.1")),
|
||||
?assertMatch(true, lte("aa","cc")),
|
||||
?assertMatch(true, lte("cc","cc")),
|
||||
?assertMatch(true, not lte("1.0.0-alpha.1",
|
||||
"1.0.0-alpha")),
|
||||
?assertMatch(true, not lte("1.0.0-pre-alpha.2",
|
||||
"1.0.0-pre-alpha")),
|
||||
?assertMatch(true, not lte("cc", "aa")),
|
||||
?assertMatch(true, not lte("1.0.0-beta.2",
|
||||
"1.0.0-alpha.1")),
|
||||
?assertMatch(true, not lte("1.0.0-beta.11",
|
||||
"1.0.0-beta.2")),
|
||||
?assertMatch(true, not lte("1.0.0-rc.1", "1.0.0-beta.11")),
|
||||
?assertMatch(true, not lte("1.0.0-rc.1+build.1", "1.0.0-rc.1")),
|
||||
?assertMatch(true, not lte("1.0.0", "1.0.0-rc.1+build.1")),
|
||||
?assertMatch(true, not lte("1.0.0+0.3.7", "1.0.0")),
|
||||
?assertMatch(true, not lte("1.3.7+build", "1.0.0+0.3.7")),
|
||||
?assertMatch(true, not lte("1.3.7+build.2.b8f12d7",
|
||||
"1.3.7+build")),
|
||||
?assertMatch(true, not lte("1.3.7+build.11.e0f985a",
|
||||
"1.3.7+build.2.b8f12d7")).
|
||||
|
||||
between_test() ->
|
||||
?assertMatch(true, between("1.0.0-alpha",
|
||||
"1.0.0-alpha.3",
|
||||
"1.0.0-alpha.2")),
|
||||
?assertMatch(true, between("1.0.0-alpha.1",
|
||||
"1.0.0-beta.2",
|
||||
"1.0.0-alpha.25")),
|
||||
?assertMatch(true, between("1.0.0-beta.2",
|
||||
"1.0.0-beta.11",
|
||||
"1.0.0-beta.7")),
|
||||
?assertMatch(true, between("1.0.0-pre-alpha.2",
|
||||
"1.0.0-pre-alpha.11",
|
||||
"1.0.0-pre-alpha.7")),
|
||||
?assertMatch(true, between("1.0.0-beta.11",
|
||||
"1.0.0-rc.3",
|
||||
"1.0.0-rc.1")),
|
||||
?assertMatch(true, between("1.0.0-rc.1",
|
||||
"1.0.0-rc.1+build.3",
|
||||
"1.0.0-rc.1+build.1")),
|
||||
|
||||
?assertMatch(true, between("1.0.0.0-rc.1",
|
||||
"1.0.0-rc.1+build.3",
|
||||
"1.0.0-rc.1+build.1")),
|
||||
?assertMatch(true, between("1.0.0-rc.1+build.1",
|
||||
"1.0.0",
|
||||
"1.0.0-rc.33")),
|
||||
?assertMatch(true, between("1.0.0",
|
||||
"1.0.0+0.3.7",
|
||||
"1.0.0+0.2")),
|
||||
?assertMatch(true, between("1.0.0+0.3.7",
|
||||
"1.3.7+build",
|
||||
"1.2")),
|
||||
?assertMatch(true, between("1.3.7+build",
|
||||
"1.3.7+build.2.b8f12d7",
|
||||
"1.3.7+build.1")),
|
||||
?assertMatch(true, between("1.3.7+build.2.b8f12d7",
|
||||
"1.3.7+build.11.e0f985a",
|
||||
"1.3.7+build.10.a36faa")),
|
||||
?assertMatch(true, between("1.0.0-alpha",
|
||||
"1.0.0-alpha",
|
||||
"1.0.0-alpha")),
|
||||
?assertMatch(true, between("1",
|
||||
"1.0.0",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, between("1.0",
|
||||
"1.0.0",
|
||||
"1.0.0")),
|
||||
|
||||
?assertMatch(true, between("1.0",
|
||||
"1.0.0.0",
|
||||
"1.0.0.0")),
|
||||
?assertMatch(true, between("1.0.0",
|
||||
"1",
|
||||
"1")),
|
||||
?assertMatch(true, between("1.0+alpha.1",
|
||||
"1.0.0+alpha.1",
|
||||
"1.0.0+alpha.1")),
|
||||
?assertMatch(true, between("1.0-alpha.1+build.1",
|
||||
"1.0.0-alpha.1+build.1",
|
||||
"1.0.0-alpha.1+build.1")),
|
||||
?assertMatch(true, between("aaa",
|
||||
"ddd",
|
||||
"cc")),
|
||||
?assertMatch(true, not between("1.0.0-alpha.1",
|
||||
"1.0.0-alpha.22",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, not between("1.0.0-pre-alpha.1",
|
||||
"1.0.0-pre-alpha.22",
|
||||
"1.0.0")),
|
||||
?assertMatch(true, not between("1.0.0",
|
||||
"1.0.0-alpha.1",
|
||||
"2.0")),
|
||||
?assertMatch(true, not between("1.0.0-beta.1",
|
||||
"1.0.0-beta.11",
|
||||
"1.0.0-alpha")),
|
||||
?assertMatch(true, not between("1.0.0-beta.11", "1.0.0-rc.1",
|
||||
"1.0.0-rc.22")),
|
||||
?assertMatch(true, not between("aaa", "ddd", "zzz")).
|
||||
|
||||
pes_test() ->
|
||||
?assertMatch(true, pes("1.0.0-rc.0", "1.0.0-rc.0")),
|
||||
?assertMatch(true, pes("1.0.0-rc.1", "1.0.0-rc.0")),
|
||||
?assertMatch(true, pes("1.0.0", "1.0.0-rc.0")),
|
||||
?assertMatch(false, pes("1.0.0-rc.0", "1.0.0-rc.1")),
|
||||
?assertMatch(true, pes("2.6.0", "2.6")),
|
||||
?assertMatch(true, pes("2.7", "2.6")),
|
||||
?assertMatch(true, pes("2.8", "2.6")),
|
||||
?assertMatch(true, pes("2.9", "2.6")),
|
||||
?assertMatch(true, pes("A.B", "A.A")),
|
||||
?assertMatch(true, not pes("3.0.0", "2.6")),
|
||||
?assertMatch(true, not pes("2.5", "2.6")),
|
||||
?assertMatch(true, pes("2.6.5", "2.6.5")),
|
||||
?assertMatch(true, pes("2.6.6", "2.6.5")),
|
||||
?assertMatch(true, pes("2.6.7", "2.6.5")),
|
||||
?assertMatch(true, pes("2.6.8", "2.6.5")),
|
||||
?assertMatch(true, pes("2.6.9", "2.6.5")),
|
||||
?assertMatch(true, pes("2.6.0.9", "2.6.0.5")),
|
||||
?assertMatch(true, not pes("2.7", "2.6.5")),
|
||||
?assertMatch(true, not pes("2.1.7", "2.1.6.5")),
|
||||
?assertMatch(true, not pes("A.A", "A.B")),
|
||||
?assertMatch(true, not pes("2.5", "2.6.5")).
|
||||
|
||||
parse_test() ->
|
||||
?assertEqual({1, {[],[]}}, parse(<<"1">>)),
|
||||
?assertEqual({{1,2,34},{[],[]}}, parse(<<"1.2.34">>)),
|
||||
?assertEqual({<<"a">>, {[],[]}}, parse(<<"a">>)),
|
||||
?assertEqual({{<<"a">>,<<"b">>}, {[],[]}}, parse(<<"a.b">>)),
|
||||
?assertEqual({1, {[],[]}}, parse(<<"1">>)),
|
||||
?assertEqual({{1,2}, {[],[]}}, parse(<<"1.2">>)),
|
||||
?assertEqual({{1,2,2}, {[],[]}}, parse(<<"1.2.2">>)),
|
||||
?assertEqual({{1,99,2}, {[],[]}}, parse(<<"1.99.2">>)),
|
||||
?assertEqual({{1,99,2}, {[<<"alpha">>],[]}}, parse(<<"1.99.2-alpha">>)),
|
||||
?assertEqual({{1,99,2}, {[<<"alpha">>,1], []}}, parse(<<"1.99.2-alpha.1">>)),
|
||||
?assertEqual({{1,99,2}, {[<<"pre-alpha">>,1], []}}, parse(<<"1.99.2-pre-alpha.1">>)),
|
||||
?assertEqual({{1,99,2}, {[], [<<"build">>, 1, <<"a36">>]}},
|
||||
parse(<<"1.99.2+build.1.a36">>)),
|
||||
?assertEqual({{1,99,2,44}, {[], [<<"build">>, 1, <<"a36">>]}},
|
||||
parse(<<"1.99.2.44+build.1.a36">>)),
|
||||
?assertEqual({{1,99,2}, {[<<"alpha">>, 1], [<<"build">>, 1, <<"a36">>]}},
|
||||
parse("1.99.2-alpha.1+build.1.a36")),
|
||||
?assertEqual({{1,99,2}, {[<<"pre-alpha">>, 1], [<<"build">>, 1, <<"a36">>]}},
|
||||
parse("1.99.2-pre-alpha.1+build.1.a36")).
|
||||
|
||||
version_format_test() ->
|
||||
?assertEqual(["1", [], []], format({1, {[],[]}})),
|
||||
?assertEqual(["1", ".", "2", ".", "34", [], []], format({{1,2,34},{[],[]}})),
|
||||
?assertEqual(<<"a">>, erlang:iolist_to_binary(format({<<"a">>, {[],[]}}))),
|
||||
?assertEqual(<<"a.b">>, erlang:iolist_to_binary(format({{<<"a">>,<<"b">>}, {[],[]}}))),
|
||||
?assertEqual(<<"1">>, erlang:iolist_to_binary(format({1, {[],[]}}))),
|
||||
?assertEqual(<<"1.2">>, erlang:iolist_to_binary(format({{1,2}, {[],[]}}))),
|
||||
?assertEqual(<<"1.2.2">>, erlang:iolist_to_binary(format({{1,2,2}, {[],[]}}))),
|
||||
?assertEqual(<<"1.99.2">>, erlang:iolist_to_binary(format({{1,99,2}, {[],[]}}))),
|
||||
?assertEqual(<<"1.99.2-alpha">>, erlang:iolist_to_binary(format({{1,99,2}, {[<<"alpha">>],[]}}))),
|
||||
?assertEqual(<<"1.99.2-alpha.1">>, erlang:iolist_to_binary(format({{1,99,2}, {[<<"alpha">>,1], []}}))),
|
||||
?assertEqual(<<"1.99.2-pre-alpha.1">>, erlang:iolist_to_binary(format({{1,99,2}, {[<<"pre-alpha">>,1], []}}))),
|
||||
?assertEqual(<<"1.99.2+build.1.a36">>,
|
||||
erlang:iolist_to_binary(format({{1,99,2}, {[], [<<"build">>, 1, <<"a36">>]}}))),
|
||||
?assertEqual(<<"1.99.2.44+build.1.a36">>,
|
||||
erlang:iolist_to_binary(format({{1,99,2,44}, {[], [<<"build">>, 1, <<"a36">>]}}))),
|
||||
?assertEqual(<<"1.99.2-alpha.1+build.1.a36">>,
|
||||
erlang:iolist_to_binary(format({{1,99,2}, {[<<"alpha">>, 1], [<<"build">>, 1, <<"a36">>]}}))),
|
||||
?assertEqual(<<"1.99.2-pre-alpha.1+build.1.a36">>,
|
||||
erlang:iolist_to_binary(format({{1,99,2}, {[<<"pre-alpha">>, 1], [<<"build">>, 1, <<"a36">>]}}))),
|
||||
?assertEqual(<<"1">>, erlang:iolist_to_binary(format({1, {[],[]}}))).
|
||||
|
||||
-endif.
|
|
@ -0,0 +1,302 @@
|
|||
-module(ec_semver_parser).
|
||||
-export([parse/1,file/1]).
|
||||
-define(p_anything,true).
|
||||
-define(p_charclass,true).
|
||||
-define(p_choose,true).
|
||||
-define(p_not,true).
|
||||
-define(p_one_or_more,true).
|
||||
-define(p_optional,true).
|
||||
-define(p_scan,true).
|
||||
-define(p_seq,true).
|
||||
-define(p_string,true).
|
||||
-define(p_zero_or_more,true).
|
||||
|
||||
|
||||
|
||||
-spec file(file:name()) -> any().
|
||||
file(Filename) -> case file:read_file(Filename) of {ok,Bin} -> parse(Bin); Err -> Err end.
|
||||
|
||||
-spec parse(binary() | list()) -> any().
|
||||
parse(List) when is_list(List) -> parse(unicode:characters_to_binary(List));
|
||||
parse(Input) when is_binary(Input) ->
|
||||
_ = setup_memo(),
|
||||
Result = case 'semver'(Input,{{line,1},{column,1}}) of
|
||||
{AST, <<>>, _Index} -> AST;
|
||||
Any -> Any
|
||||
end,
|
||||
release_memo(), Result.
|
||||
|
||||
-spec 'semver'(input(), index()) -> parse_result().
|
||||
'semver'(Input, Index) ->
|
||||
p(Input, Index, 'semver', fun(I,D) -> (p_seq([fun 'major_minor_patch_min_patch'/2, p_optional(p_seq([p_string(<<"-">>), fun 'alpha_part'/2, p_zero_or_more(p_seq([p_string(<<".">>), fun 'alpha_part'/2]))])), p_optional(p_seq([p_string(<<"+">>), fun 'alpha_part'/2, p_zero_or_more(p_seq([p_string(<<".">>), fun 'alpha_part'/2]))])), p_not(p_anything())]))(I,D) end, fun(Node, _Idx) -> ec_semver:internal_parse_version(Node) end).
|
||||
|
||||
-spec 'major_minor_patch_min_patch'(input(), index()) -> parse_result().
|
||||
'major_minor_patch_min_patch'(Input, Index) ->
|
||||
p(Input, Index, 'major_minor_patch_min_patch', fun(I,D) -> (p_seq([p_choose([p_seq([p_optional(p_string(<<"v">>)), fun 'numeric_part'/2]), fun 'alpha_part'/2]), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2])), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2])), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2]))]))(I,D) end, fun(Node, Idx) ->transform('major_minor_patch_min_patch', Node, Idx) end).
|
||||
|
||||
-spec 'version_part'(input(), index()) -> parse_result().
|
||||
'version_part'(Input, Index) ->
|
||||
p(Input, Index, 'version_part', fun(I,D) -> (p_choose([fun 'numeric_part'/2, fun 'alpha_part'/2]))(I,D) end, fun(Node, Idx) ->transform('version_part', Node, Idx) end).
|
||||
|
||||
-spec 'numeric_part'(input(), index()) -> parse_result().
|
||||
'numeric_part'(Input, Index) ->
|
||||
p(Input, Index, 'numeric_part', fun(I,D) -> (p_one_or_more(p_charclass(<<"[0-9]">>)))(I,D) end, fun(Node, _Idx) ->erlang:list_to_integer(erlang:binary_to_list(erlang:iolist_to_binary(Node))) end).
|
||||
|
||||
-spec 'alpha_part'(input(), index()) -> parse_result().
|
||||
'alpha_part'(Input, Index) ->
|
||||
p(Input, Index, 'alpha_part', fun(I,D) -> (p_one_or_more(p_charclass(<<"[A-Za-z0-9-]">>)))(I,D) end, fun(Node, _Idx) ->erlang:iolist_to_binary(Node) end).
|
||||
|
||||
|
||||
transform(_,Node,_Index) -> Node.
|
||||
-type index() :: {{line, pos_integer()}, {column, pos_integer()}}.
|
||||
-type input() :: binary().
|
||||
-type parse_failure() :: {fail, term()}.
|
||||
-type parse_success() :: {term(), input(), index()}.
|
||||
-type parse_result() :: parse_failure() | parse_success().
|
||||
-type parse_fun() :: fun((input(), index()) -> parse_result()).
|
||||
-type xform_fun() :: fun((input(), index()) -> term()).
|
||||
|
||||
-spec p(input(), index(), atom(), parse_fun(), xform_fun()) -> parse_result().
|
||||
p(Inp, StartIndex, Name, ParseFun, TransformFun) ->
|
||||
case get_memo(StartIndex, Name) of % See if the current reduction is memoized
|
||||
{ok, Memo} -> %Memo; % If it is, return the stored result
|
||||
Memo;
|
||||
_ -> % If not, attempt to parse
|
||||
Result = case ParseFun(Inp, StartIndex) of
|
||||
{fail,_} = Failure -> % If it fails, memoize the failure
|
||||
Failure;
|
||||
{Match, InpRem, NewIndex} -> % If it passes, transform and memoize the result.
|
||||
Transformed = TransformFun(Match, StartIndex),
|
||||
{Transformed, InpRem, NewIndex}
|
||||
end,
|
||||
memoize(StartIndex, Name, Result),
|
||||
Result
|
||||
end.
|
||||
|
||||
-spec setup_memo() -> ets:tid().
|
||||
setup_memo() ->
|
||||
put({parse_memo_table, ?MODULE}, ets:new(?MODULE, [set])).
|
||||
|
||||
-spec release_memo() -> true.
|
||||
release_memo() ->
|
||||
ets:delete(memo_table_name()).
|
||||
|
||||
-spec memoize(index(), atom(), parse_result()) -> true.
|
||||
memoize(Index, Name, Result) ->
|
||||
Memo = case ets:lookup(memo_table_name(), Index) of
|
||||
[] -> [];
|
||||
[{Index, Plist}] -> Plist
|
||||
end,
|
||||
ets:insert(memo_table_name(), {Index, [{Name, Result}|Memo]}).
|
||||
|
||||
-spec get_memo(index(), atom()) -> {ok, term()} | {error, not_found}.
|
||||
get_memo(Index, Name) ->
|
||||
case ets:lookup(memo_table_name(), Index) of
|
||||
[] -> {error, not_found};
|
||||
[{Index, Plist}] ->
|
||||
case proplists:lookup(Name, Plist) of
|
||||
{Name, Result} -> {ok, Result};
|
||||
_ -> {error, not_found}
|
||||
end
|
||||
end.
|
||||
|
||||
-spec memo_table_name() -> ets:tid().
|
||||
memo_table_name() ->
|
||||
get({parse_memo_table, ?MODULE}).
|
||||
|
||||
-ifdef(p_eof).
|
||||
-spec p_eof() -> parse_fun().
|
||||
p_eof() ->
|
||||
fun(<<>>, Index) -> {eof, [], Index};
|
||||
(_, Index) -> {fail, {expected, eof, Index}} end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_optional).
|
||||
-spec p_optional(parse_fun()) -> parse_fun().
|
||||
p_optional(P) ->
|
||||
fun(Input, Index) ->
|
||||
case P(Input, Index) of
|
||||
{fail,_} -> {[], Input, Index};
|
||||
{_, _, _} = Success -> Success
|
||||
end
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_not).
|
||||
-spec p_not(parse_fun()) -> parse_fun().
|
||||
p_not(P) ->
|
||||
fun(Input, Index)->
|
||||
case P(Input,Index) of
|
||||
{fail,_} ->
|
||||
{[], Input, Index};
|
||||
{Result, _, _} -> {fail, {expected, {no_match, Result},Index}}
|
||||
end
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_assert).
|
||||
-spec p_assert(parse_fun()) -> parse_fun().
|
||||
p_assert(P) ->
|
||||
fun(Input,Index) ->
|
||||
case P(Input,Index) of
|
||||
{fail,_} = Failure-> Failure;
|
||||
_ -> {[], Input, Index}
|
||||
end
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_seq).
|
||||
-spec p_seq([parse_fun()]) -> parse_fun().
|
||||
p_seq(P) ->
|
||||
fun(Input, Index) ->
|
||||
p_all(P, Input, Index, [])
|
||||
end.
|
||||
|
||||
-spec p_all([parse_fun()], input(), index(), [term()]) -> parse_result().
|
||||
p_all([], Inp, Index, Accum ) -> {lists:reverse( Accum ), Inp, Index};
|
||||
p_all([P|Parsers], Inp, Index, Accum) ->
|
||||
case P(Inp, Index) of
|
||||
{fail, _} = Failure -> Failure;
|
||||
{Result, InpRem, NewIndex} -> p_all(Parsers, InpRem, NewIndex, [Result|Accum])
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_choose).
|
||||
-spec p_choose([parse_fun()]) -> parse_fun().
|
||||
p_choose(Parsers) ->
|
||||
fun(Input, Index) ->
|
||||
p_attempt(Parsers, Input, Index, none)
|
||||
end.
|
||||
|
||||
-spec p_attempt([parse_fun()], input(), index(), none | parse_failure()) -> parse_result().
|
||||
p_attempt([], _Input, _Index, Failure) -> Failure;
|
||||
p_attempt([P|Parsers], Input, Index, FirstFailure)->
|
||||
case P(Input, Index) of
|
||||
{fail, _} = Failure ->
|
||||
case FirstFailure of
|
||||
none -> p_attempt(Parsers, Input, Index, Failure);
|
||||
_ -> p_attempt(Parsers, Input, Index, FirstFailure)
|
||||
end;
|
||||
Result -> Result
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_zero_or_more).
|
||||
-spec p_zero_or_more(parse_fun()) -> parse_fun().
|
||||
p_zero_or_more(P) ->
|
||||
fun(Input, Index) ->
|
||||
p_scan(P, Input, Index, [])
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_one_or_more).
|
||||
-spec p_one_or_more(parse_fun()) -> parse_fun().
|
||||
p_one_or_more(P) ->
|
||||
fun(Input, Index)->
|
||||
Result = p_scan(P, Input, Index, []),
|
||||
case Result of
|
||||
{[_|_], _, _} ->
|
||||
Result;
|
||||
_ ->
|
||||
{fail, {expected, Failure, _}} = P(Input,Index),
|
||||
{fail, {expected, {at_least_one, Failure}, Index}}
|
||||
end
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_label).
|
||||
-spec p_label(atom(), parse_fun()) -> parse_fun().
|
||||
p_label(Tag, P) ->
|
||||
fun(Input, Index) ->
|
||||
case P(Input, Index) of
|
||||
{fail,_} = Failure ->
|
||||
Failure;
|
||||
{Result, InpRem, NewIndex} ->
|
||||
{{Tag, Result}, InpRem, NewIndex}
|
||||
end
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_scan).
|
||||
-spec p_scan(parse_fun(), input(), index(), [term()]) -> {[term()], input(), index()}.
|
||||
p_scan(_, <<>>, Index, Accum) -> {lists:reverse(Accum), <<>>, Index};
|
||||
p_scan(P, Inp, Index, Accum) ->
|
||||
case P(Inp, Index) of
|
||||
{fail,_} -> {lists:reverse(Accum), Inp, Index};
|
||||
{Result, InpRem, NewIndex} -> p_scan(P, InpRem, NewIndex, [Result | Accum])
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_string).
|
||||
-spec p_string(binary()) -> parse_fun().
|
||||
p_string(S) ->
|
||||
Length = erlang:byte_size(S),
|
||||
fun(Input, Index) ->
|
||||
try
|
||||
<<S:Length/binary, Rest/binary>> = Input,
|
||||
{S, Rest, p_advance_index(S, Index)}
|
||||
catch
|
||||
error:{badmatch,_} -> {fail, {expected, {string, S}, Index}}
|
||||
end
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_anything).
|
||||
-spec p_anything() -> parse_fun().
|
||||
p_anything() ->
|
||||
fun(<<>>, Index) -> {fail, {expected, any_character, Index}};
|
||||
(Input, Index) when is_binary(Input) ->
|
||||
<<C/utf8, Rest/binary>> = Input,
|
||||
{<<C/utf8>>, Rest, p_advance_index(<<C/utf8>>, Index)}
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_charclass).
|
||||
-spec p_charclass(string() | binary()) -> parse_fun().
|
||||
p_charclass(Class) ->
|
||||
{ok, RE} = re:compile(Class, [unicode, dotall]),
|
||||
fun(Inp, Index) ->
|
||||
case re:run(Inp, RE, [anchored]) of
|
||||
{match, [{0, Length}|_]} ->
|
||||
{Head, Tail} = erlang:split_binary(Inp, Length),
|
||||
{Head, Tail, p_advance_index(Head, Index)};
|
||||
_ -> {fail, {expected, {character_class, binary_to_list(Class)}, Index}}
|
||||
end
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(p_regexp).
|
||||
-spec p_regexp(binary()) -> parse_fun().
|
||||
p_regexp(Regexp) ->
|
||||
{ok, RE} = re:compile(Regexp, [unicode, dotall, anchored]),
|
||||
fun(Inp, Index) ->
|
||||
case re:run(Inp, RE) of
|
||||
{match, [{0, Length}|_]} ->
|
||||
{Head, Tail} = erlang:split_binary(Inp, Length),
|
||||
{Head, Tail, p_advance_index(Head, Index)};
|
||||
_ -> {fail, {expected, {regexp, binary_to_list(Regexp)}, Index}}
|
||||
end
|
||||
end.
|
||||
-endif.
|
||||
|
||||
-ifdef(line).
|
||||
-spec line(index() | term()) -> pos_integer() | undefined.
|
||||
line({{line,L},_}) -> L;
|
||||
line(_) -> undefined.
|
||||
-endif.
|
||||
|
||||
-ifdef(column).
|
||||
-spec column(index() | term()) -> pos_integer() | undefined.
|
||||
column({_,{column,C}}) -> C;
|
||||
column(_) -> undefined.
|
||||
-endif.
|
||||
|
||||
-spec p_advance_index(input() | unicode:charlist() | pos_integer(), index()) -> index().
|
||||
p_advance_index(MatchedInput, Index) when is_list(MatchedInput) orelse is_binary(MatchedInput)-> % strings
|
||||
lists:foldl(fun p_advance_index/2, Index, unicode:characters_to_list(MatchedInput));
|
||||
p_advance_index(MatchedInput, Index) when is_integer(MatchedInput) -> % single characters
|
||||
{{line, Line}, {column, Col}} = Index,
|
||||
case MatchedInput of
|
||||
$\n -> {{line, Line+1}, {column, 1}};
|
||||
_ -> {{line, Line}, {column, Col+1}}
|
||||
end.
|
|
@ -0,0 +1,229 @@
|
|||
%% -*- mode: Erlang; fill-column: 79; comment-column: 70; -*-
|
||||
%% vi:ts=4 sw=4 et
|
||||
%%%---------------------------------------------------------------------------
|
||||
%%% Permission is hereby granted, free of charge, to any person
|
||||
%%% obtaining a copy of this software and associated documentation
|
||||
%%% files (the "Software"), to deal in the Software without
|
||||
%%% restriction, including without limitation the rights to use, copy,
|
||||
%%% modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
%%% of the Software, and to permit persons to whom the Software is
|
||||
%%% furnished to do so, subject to the following conditions:
|
||||
%%%
|
||||
%%% The above copyright notice and this permission notice shall be
|
||||
%%% included in all copies or substantial portions of the Software.
|
||||
%%%
|
||||
%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
%%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
%%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
%%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
%%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
%%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
%%% DEALINGS IN THE SOFTWARE.
|
||||
%%%---------------------------------------------------------------------------
|
||||
%%% @author Eric Merritt
|
||||
%%% @doc
|
||||
%%% Provides the ability to ask questions of the user and
|
||||
%%% get a response.
|
||||
%%% @end
|
||||
%%% @copyright Erlware 2006-2011
|
||||
%%%---------------------------------------------------------------------------
|
||||
-module(ec_talk).
|
||||
|
||||
%% API
|
||||
-export([ask/1,
|
||||
ask/2,
|
||||
ask_default/2,
|
||||
ask_default/3,
|
||||
ask/3,
|
||||
say/1,
|
||||
say/2]).
|
||||
|
||||
-export_type([prompt/0,
|
||||
type/0,
|
||||
supported/0]).
|
||||
|
||||
%%============================================================================
|
||||
%% Types
|
||||
%%============================================================================
|
||||
-type prompt() :: string().
|
||||
-type type() :: boolean | number | string.
|
||||
-type supported() :: boolean() | number() | string().
|
||||
|
||||
%%============================================================================
|
||||
%% API
|
||||
%%============================================================================
|
||||
|
||||
%% @doc Outputs the line to the screen
|
||||
-spec say(string()) -> ok.
|
||||
say(Say) ->
|
||||
io:format(lists:flatten([Say, "~n"])).
|
||||
|
||||
-spec say(string(), [term()] | term()) -> ok.
|
||||
say(Say, Args) when is_list(Args) ->
|
||||
io:format(lists:flatten([Say, "~n"]), Args);
|
||||
say(Say, Args) ->
|
||||
io:format(lists:flatten([Say, "~n"]), [Args]).
|
||||
|
||||
%% @doc Asks the user for a response to the specified prompt.
|
||||
-spec ask(prompt()) -> string().
|
||||
ask(Prompt) ->
|
||||
ask_convert(Prompt, fun get_string/1, string, none).
|
||||
|
||||
%% @doc Asks the user for a response to the specified prompt.
|
||||
-spec ask_default(prompt(), string()) -> string().
|
||||
ask_default(Prompt, Default) ->
|
||||
ask_convert(Prompt, fun get_string/1, string, Default).
|
||||
|
||||
%% @doc Asks the user to respond to the prompt. Trys to return the
|
||||
%% value in the format specified by 'Type'.
|
||||
-spec ask(prompt(), type()) -> supported().
|
||||
ask(Prompt, boolean) ->
|
||||
ask_convert(Prompt, fun get_boolean/1, boolean, none);
|
||||
ask(Prompt, number) ->
|
||||
ask_convert(Prompt, fun get_integer/1, number, none);
|
||||
ask(Prompt, string) ->
|
||||
ask_convert(Prompt, fun get_string/1, string, none).
|
||||
|
||||
%% @doc Asks the user to respond to the prompt. Trys to return the
|
||||
%% value in the format specified by 'Type'.
|
||||
-spec ask_default(prompt(), type(), supported()) -> supported().
|
||||
ask_default(Prompt, boolean, Default) ->
|
||||
ask_convert(Prompt, fun get_boolean/1, boolean, Default);
|
||||
ask_default(Prompt, number, Default) ->
|
||||
ask_convert(Prompt, fun get_integer/1, number, Default);
|
||||
ask_default(Prompt, string, Default) ->
|
||||
ask_convert(Prompt, fun get_string/1, string, Default).
|
||||
|
||||
%% @doc Asks the user to respond to the number prompt with a value
|
||||
%% between min and max.
|
||||
-spec ask(prompt(), number(), number()) -> number().
|
||||
ask(Prompt, Min, Max)
|
||||
when erlang:is_list(Prompt),
|
||||
erlang:is_number(Min),
|
||||
erlang:is_number(Max),
|
||||
Min =< Max ->
|
||||
Res = ask_convert(Prompt, fun get_integer/1, number, none),
|
||||
case (Res >= Min andalso Res =< Max) of
|
||||
true ->
|
||||
Res;
|
||||
false ->
|
||||
say("Your answer must be between ~w and ~w!", [Min, Max]),
|
||||
ask(Prompt, Min, Max)
|
||||
end.
|
||||
|
||||
%%============================================================================
|
||||
%% Internal functions
|
||||
%% ============================================================================
|
||||
%% @doc Actually does the work of asking, checking result and
|
||||
%% translating result into the requested format.
|
||||
-spec ask_convert(prompt(), fun((any()) -> any()), type(), supported() | none) -> supported().
|
||||
ask_convert(Prompt, TransFun, Type, Default) ->
|
||||
NewPrompt =
|
||||
erlang:binary_to_list(erlang:iolist_to_binary([Prompt,
|
||||
case Default of
|
||||
none ->
|
||||
[];
|
||||
Default ->
|
||||
[" (", io_lib:format("~p", [Default]) , ")"]
|
||||
end, "> "])),
|
||||
Data = trim(trim(io:get_line(NewPrompt)), both, [$\n]),
|
||||
Ret = TransFun(Data),
|
||||
case Ret of
|
||||
no_data ->
|
||||
case Default of
|
||||
none ->
|
||||
say("I didn't get that. This ~p kind of question.~n", [Type]),
|
||||
ask_convert(Prompt, TransFun, Type, Default);
|
||||
Default ->
|
||||
TransFun(Default)
|
||||
end;
|
||||
no_clue ->
|
||||
say("I didn't get that. This ~p kind of question.~n", [Type]),
|
||||
ask_convert(Prompt, TransFun, Type, Default);
|
||||
_ ->
|
||||
Ret
|
||||
end.
|
||||
|
||||
%% @doc Trys to translate the result into a boolean
|
||||
-spec get_boolean(string()) -> boolean().
|
||||
get_boolean([]) ->
|
||||
no_data;
|
||||
get_boolean([$T | _]) ->
|
||||
true;
|
||||
get_boolean([$t | _]) ->
|
||||
true;
|
||||
get_boolean("ok") ->
|
||||
true;
|
||||
get_boolean("OK") ->
|
||||
true;
|
||||
get_boolean([$Y | _]) ->
|
||||
true;
|
||||
get_boolean([$y | _]) ->
|
||||
true;
|
||||
get_boolean([$f | _]) ->
|
||||
false;
|
||||
get_boolean([$F | _]) ->
|
||||
false;
|
||||
get_boolean([$n | _]) ->
|
||||
false;
|
||||
get_boolean([$N | _]) ->
|
||||
false;
|
||||
get_boolean(_) ->
|
||||
no_clue.
|
||||
|
||||
%% @doc Trys to translate the result into an integer
|
||||
-spec get_integer(string()) -> integer().
|
||||
get_integer([]) ->
|
||||
no_data;
|
||||
get_integer(String) ->
|
||||
case (catch list_to_integer(String)) of
|
||||
{'Exit', _} ->
|
||||
no_clue;
|
||||
Integer ->
|
||||
Integer
|
||||
end.
|
||||
|
||||
%% @doc Solely returns a string give the string. This is so the same
|
||||
%% translate function can be used across the board
|
||||
-spec get_string(string()) -> string().
|
||||
get_string([]) ->
|
||||
no_data;
|
||||
get_string(String) ->
|
||||
case is_list(String) of
|
||||
true ->
|
||||
String;
|
||||
false ->
|
||||
no_clue
|
||||
end.
|
||||
|
||||
-ifdef(unicode_str).
|
||||
trim(Str) -> string:trim(Str).
|
||||
trim(Str, both, Chars) -> string:trim(Str, both, Chars).
|
||||
-else.
|
||||
trim(Str) -> string:strip(Str).
|
||||
trim(Str, Dir, [Chars|_]) -> string:strip(Str, Dir, Chars).
|
||||
-endif.
|
||||
|
||||
%%%====================================================================
|
||||
%%% tests
|
||||
%%%====================================================================
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
general_test_() ->
|
||||
[?_test(42 == get_integer("42")),
|
||||
?_test(500211 == get_integer("500211")),
|
||||
?_test(1234567890 == get_integer("1234567890")),
|
||||
?_test(12345678901234567890 == get_integer("12345678901234567890")),
|
||||
?_test(true == get_boolean("true")),
|
||||
?_test(false == get_boolean("false")),
|
||||
?_test(true == get_boolean("Ok")),
|
||||
?_test(true == get_boolean("ok")),
|
||||
?_test(true == get_boolean("Y")),
|
||||
?_test(true == get_boolean("y")),
|
||||
?_test(false == get_boolean("False")),
|
||||
?_test(false == get_boolean("No")),
|
||||
?_test(false == get_boolean("no"))].
|
||||
|
||||
-endif.
|
|
@ -0,0 +1,66 @@
|
|||
%%% vi:ts=4 sw=4 et
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author Eric Merritt <ericbmerritt@gmail.com>
|
||||
%%% @copyright 2014 Erlware, LLC.
|
||||
%%% @doc
|
||||
%%% Provides a signature to manage returning semver formatted versions
|
||||
%%% from various version control repositories.
|
||||
%%%
|
||||
%%% This interface is a member of the Erlware Commons Library.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ec_vsn).
|
||||
|
||||
%% API
|
||||
-export([new/1,
|
||||
vsn/1]).
|
||||
|
||||
-export_type([t/0]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Types
|
||||
%%%===================================================================
|
||||
|
||||
-record(t, {callback, data}).
|
||||
|
||||
%% This should be opaque, but that kills dialyzer so for now we export it
|
||||
%% however you should not rely on the internal representation here
|
||||
-type t() :: #t{}.
|
||||
|
||||
-ifdef(have_callback_support).
|
||||
|
||||
-callback new() -> any().
|
||||
-callback vsn(any()) -> {ok, string()} | {error, Reason::any()}.
|
||||
|
||||
-else.
|
||||
|
||||
%% In the case where R14 or lower is being used to compile the system
|
||||
%% we need to export a behaviour info
|
||||
-export([behaviour_info/1]).
|
||||
-spec behaviour_info(atom()) -> [{atom(), arity()}] | undefined.
|
||||
behaviour_info(callbacks) ->
|
||||
[{new, 0},
|
||||
{vsn, 1}];
|
||||
behaviour_info(_Other) ->
|
||||
undefined.
|
||||
-endif.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc create a new dictionary object from the specified module. The
|
||||
%% module should implement the dictionary behaviour.
|
||||
%%
|
||||
%% @param ModuleName The module name.
|
||||
-spec new(module()) -> t().
|
||||
new(ModuleName) when erlang:is_atom(ModuleName) ->
|
||||
#t{callback = ModuleName, data = ModuleName:new()}.
|
||||
|
||||
%% @doc Return the semver or an error depending on what is possible
|
||||
%% with this implementation in this directory.
|
||||
%%
|
||||
%% @param The dictionary object
|
||||
-spec vsn(t()) -> {ok, string()} | {error, Reason::any()}.
|
||||
vsn(#t{callback = Mod, data = Data}) ->
|
||||
Mod:vsn(Data).
|
|
@ -0,0 +1,11 @@
|
|||
{application,erlware_commons,
|
||||
[{description,"Additional standard library for Erlang"},
|
||||
{vsn,"1.5.0"},
|
||||
{modules,[]},
|
||||
{registered,[]},
|
||||
{applications,[kernel,stdlib,cf]},
|
||||
{maintainers,["Eric Merritt","Tristan Sloughter",
|
||||
"Jordan Wilberding","Martin Logan"]},
|
||||
{licenses,["Apache","MIT"]},
|
||||
{links,[{"Github",
|
||||
"https://github.com/erlware/erlware_commons"}]}]}.
|
|
@ -0,0 +1,67 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution.
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks.
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability.
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
|
@ -0,0 +1,54 @@
|
|||
# eunit_formatters
|
||||
|
||||
Because eunit's output sucks. Let's make it better.
|
||||
|
||||
Here's the progress formatter running with profiling and ANSI colors
|
||||
turned on:
|
||||
|
||||
![neotoma eunit](demo.gif)
|
||||
|
||||
## Setup
|
||||
|
||||
### Rebar 3
|
||||
|
||||
[rebar3](https://github.com/erlang/rebar3) already includes this
|
||||
library! There's no need for special configuration at all.
|
||||
|
||||
### erlang.mk
|
||||
|
||||
For erlang.mk, add the following before `include erlang.mk`:
|
||||
|
||||
``` Makefile
|
||||
TEST_DEPS = eunit_formatters
|
||||
|
||||
EUNIT_OPTS = no_tty, {report, {eunit_progress, [colored, profile]}}
|
||||
```
|
||||
|
||||
### Rebar 2 (legacy)
|
||||
Add `eunit_formatters` as a dep in your `rebar.config`. Now configure
|
||||
eunit to use one of the output formatters (currently only
|
||||
`eunit_progress`):
|
||||
|
||||
```erlang
|
||||
{eunit_opts, [
|
||||
no_tty, %% This turns off the default output, MUST HAVE
|
||||
{report, {eunit_progress, [colored, profile]}} %% Use `profile' to see test timing information
|
||||
%% Uses the progress formatter with ANSI-colored output
|
||||
]}.
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2014 Sean Cribbs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,3 @@
|
|||
%% Dogfooding
|
||||
{erl_opts, [{platform_define, "^[0-9]+", namespaced_dicts}]}.
|
||||
{eunit_opts, [no_tty, {report, {eunit_progress, [colored, profile]}}]}.
|
|
@ -0,0 +1 @@
|
|||
[].
|
|
@ -0,0 +1,117 @@
|
|||
%% Copyright 2014 Sean Cribbs
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
|
||||
%% @doc Binomial heap based on Okasaki 6.2.2
|
||||
-module(binomial_heap).
|
||||
-export([new/0, insert/2, insert/3, merge/2, delete/1, to_list/1, take/2, size/1]).
|
||||
-record(node,{
|
||||
rank = 0 :: non_neg_integer(),
|
||||
key :: term(),
|
||||
value :: term(),
|
||||
children = new() :: binomial_heap()
|
||||
}).
|
||||
|
||||
-export_type([binomial_heap/0, heap_node/0]).
|
||||
-type binomial_heap() :: [ heap_node() ].
|
||||
-type heap_node() :: #node{}.
|
||||
|
||||
-spec new() -> binomial_heap().
|
||||
new() ->
|
||||
[].
|
||||
|
||||
% Inserts a new pair into the heap (or creates a new heap)
|
||||
-spec insert(term(), term()) -> binomial_heap().
|
||||
insert(Key,Value) ->
|
||||
insert(Key,Value,[]).
|
||||
|
||||
-spec insert(term(), term(), binomial_heap()) -> binomial_heap().
|
||||
insert(Key,Value,Forest) ->
|
||||
insTree(#node{key=Key,value=Value},Forest).
|
||||
|
||||
% Merges two heaps
|
||||
-spec merge(binomial_heap(), binomial_heap()) -> binomial_heap().
|
||||
merge(TS1,[]) when is_list(TS1) -> TS1;
|
||||
merge([],TS2) when is_list(TS2) -> TS2;
|
||||
merge([#node{rank=R1}=T1|TS1]=F1,[#node{rank=R2}=T2|TS2]=F2) ->
|
||||
if
|
||||
R1 < R2 ->
|
||||
[T1 | merge(TS1,F2)];
|
||||
R2 < R1 ->
|
||||
[T2 | merge(F1, TS2)];
|
||||
true ->
|
||||
insTree(link(T1,T2),merge(TS1,TS2))
|
||||
end.
|
||||
|
||||
% Deletes the top entry from the heap and returns it
|
||||
-spec delete(binomial_heap()) -> {{term(), term()}, binomial_heap()}.
|
||||
delete(TS) ->
|
||||
{#node{key=Key,value=Value,children=TS1},TS2} = getMin(TS),
|
||||
{{Key,Value},merge(lists:reverse(TS1),TS2)}.
|
||||
|
||||
% Turns the heap into list in heap order
|
||||
-spec to_list(binomial_heap()) -> [{term(), term()}].
|
||||
to_list([]) -> [];
|
||||
to_list(List) when is_list(List) ->
|
||||
to_list([],List).
|
||||
to_list(Acc, []) ->
|
||||
lists:reverse(Acc);
|
||||
to_list(Acc,Forest) ->
|
||||
{Next, Trees} = delete(Forest),
|
||||
to_list([Next|Acc], Trees).
|
||||
|
||||
% Take N elements from the top of the heap
|
||||
-spec take(non_neg_integer(), binomial_heap()) -> [{term(), term()}].
|
||||
take(N,Trees) when is_integer(N), is_list(Trees) ->
|
||||
take(N,Trees,[]).
|
||||
take(0,_Trees,Acc) ->
|
||||
lists:reverse(Acc);
|
||||
take(_N,[],Acc)->
|
||||
lists:reverse(Acc);
|
||||
take(N,Trees,Acc) ->
|
||||
{Top,T2} = delete(Trees),
|
||||
take(N-1,T2,[Top|Acc]).
|
||||
|
||||
% Get an estimate of the size based on the binomial property
|
||||
-spec size(binomial_heap()) -> non_neg_integer().
|
||||
size(Forest) ->
|
||||
erlang:trunc(lists:sum([math:pow(2,R) || #node{rank=R} <- Forest])).
|
||||
|
||||
%% Private API
|
||||
-spec link(heap_node(), heap_node()) -> heap_node().
|
||||
link(#node{rank=R,key=X1,children=C1}=T1,#node{key=X2,children=C2}=T2) ->
|
||||
case X1 < X2 of
|
||||
true ->
|
||||
T1#node{rank=R+1,children=[T2|C1]};
|
||||
_ ->
|
||||
T2#node{rank=R+1,children=[T1|C2]}
|
||||
end.
|
||||
|
||||
insTree(Tree, []) ->
|
||||
[Tree];
|
||||
insTree(#node{rank=R1}=T1, [#node{rank=R2}=T2|Rest] = TS) ->
|
||||
case R1 < R2 of
|
||||
true ->
|
||||
[T1|TS];
|
||||
_ ->
|
||||
insTree(link(T1,T2),Rest)
|
||||
end.
|
||||
|
||||
getMin([T]) ->
|
||||
{T,[]};
|
||||
getMin([#node{key=K} = T|TS]) ->
|
||||
{#node{key=K1} = T1,TS1} = getMin(TS),
|
||||
case K < K1 of
|
||||
true -> {T,TS};
|
||||
_ -> {T1,[T|TS1]}
|
||||
end.
|
|
@ -0,0 +1,9 @@
|
|||
{application,eunit_formatters,
|
||||
[{description,"Better output for eunit suites"},
|
||||
{vsn,"0.5.0"},
|
||||
{applications,[kernel,stdlib,eunit]},
|
||||
{env,[]},
|
||||
{maintainers,["Sean Cribbs","Tristan Sloughter"]},
|
||||
{licenses,["Apache2"]},
|
||||
{links,[{"Github",
|
||||
"https://github.com/seancribbs/eunit_formatters"}]}]}.
|
|
@ -0,0 +1,467 @@
|
|||
%% Copyright 2014 Sean Cribbs
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
|
||||
|
||||
%% @doc A listener/reporter for eunit that prints '.' for each
|
||||
%% success, 'F' for each failure, and 'E' for each error. It can also
|
||||
%% optionally summarize the failures at the end.
|
||||
-module(eunit_progress).
|
||||
-behaviour(eunit_listener).
|
||||
-define(NOTEST, true).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(RED, "\e[0;31m").
|
||||
-define(GREEN, "\e[0;32m").
|
||||
-define(YELLOW, "\e[0;33m").
|
||||
-define(WHITE, "\e[0;37m").
|
||||
-define(CYAN, "\e[0;36m").
|
||||
-define(RESET, "\e[0m").
|
||||
|
||||
%% eunit_listener callbacks
|
||||
-export([
|
||||
init/1,
|
||||
handle_begin/3,
|
||||
handle_end/3,
|
||||
handle_cancel/3,
|
||||
terminate/2
|
||||
]).
|
||||
|
||||
-export([
|
||||
start/0,
|
||||
start/1
|
||||
]).
|
||||
|
||||
-ifdef(namespaced_dicts).
|
||||
-type euf_dict() :: dict:dict().
|
||||
-else.
|
||||
-type euf_dict() :: dict().
|
||||
-endif.
|
||||
|
||||
-record(state, {
|
||||
status = dict:new() :: euf_dict(),
|
||||
failures = [] :: [[pos_integer()]],
|
||||
skips = [] :: [[pos_integer()]],
|
||||
timings = binomial_heap:new() :: binomial_heap:binomial_heap(),
|
||||
colored = true :: boolean(),
|
||||
profile = false :: boolean()
|
||||
}).
|
||||
|
||||
%% Startup
|
||||
start() ->
|
||||
start([]).
|
||||
|
||||
start(Options) ->
|
||||
eunit_listener:start(?MODULE, Options).
|
||||
|
||||
%%------------------------------------------
|
||||
%% eunit_listener callbacks
|
||||
%%------------------------------------------
|
||||
init(Options) ->
|
||||
#state{colored=proplists:get_bool(colored, Options),
|
||||
profile=proplists:get_bool(profile, Options)}.
|
||||
|
||||
handle_begin(group, Data, St) ->
|
||||
GID = proplists:get_value(id, Data),
|
||||
Dict = St#state.status,
|
||||
St#state{status=dict:store(GID, orddict:from_list([{type, group}|Data]), Dict)};
|
||||
handle_begin(test, Data, St) ->
|
||||
TID = proplists:get_value(id, Data),
|
||||
Dict = St#state.status,
|
||||
St#state{status=dict:store(TID, orddict:from_list([{type, test}|Data]), Dict)}.
|
||||
|
||||
handle_end(group, Data, St) ->
|
||||
St#state{status=merge_on_end(Data, St#state.status)};
|
||||
handle_end(test, Data, St) ->
|
||||
NewStatus = merge_on_end(Data, St#state.status),
|
||||
St1 = print_progress(Data, St),
|
||||
St2 = record_timing(Data, St1),
|
||||
St2#state{status=NewStatus}.
|
||||
|
||||
handle_cancel(_, Data, #state{status=Status, skips=Skips}=St) ->
|
||||
Status1 = merge_on_end(Data, Status),
|
||||
ID = proplists:get_value(id, Data),
|
||||
St#state{status=Status1, skips=[ID|Skips]}.
|
||||
|
||||
terminate({ok, Data}, St) ->
|
||||
print_failures(St),
|
||||
print_pending(St),
|
||||
print_profile(St),
|
||||
print_timing(St),
|
||||
print_results(Data, St);
|
||||
terminate({error, Reason}, St) ->
|
||||
io:nl(), io:nl(),
|
||||
print_colored(io_lib:format("Eunit failed: ~25p~n", [Reason]), ?RED, St),
|
||||
sync_end(error).
|
||||
|
||||
sync_end(Result) ->
|
||||
receive
|
||||
{stop, Reference, ReplyTo} ->
|
||||
ReplyTo ! {result, Reference, Result},
|
||||
ok
|
||||
end.
|
||||
|
||||
%%------------------------------------------
|
||||
%% Print and collect information during run
|
||||
%%------------------------------------------
|
||||
print_progress(Data, St) ->
|
||||
TID = proplists:get_value(id, Data),
|
||||
case proplists:get_value(status, Data) of
|
||||
ok ->
|
||||
print_progress_success(St),
|
||||
St;
|
||||
{skipped, _Reason} ->
|
||||
print_progress_skipped(St),
|
||||
St#state{skips=[TID|St#state.skips]};
|
||||
{error, Exception} ->
|
||||
print_progress_failed(Exception, St),
|
||||
St#state{failures=[TID|St#state.failures]}
|
||||
end.
|
||||
|
||||
record_timing(Data, State=#state{timings=T, profile=true}) ->
|
||||
TID = proplists:get_value(id, Data),
|
||||
case lists:keyfind(time, 1, Data) of
|
||||
{time, Int} ->
|
||||
%% It's a min-heap, so we insert negative numbers instead
|
||||
%% of the actuals and normalize when we report on them.
|
||||
T1 = binomial_heap:insert(-Int, TID, T),
|
||||
State#state{timings=T1};
|
||||
false ->
|
||||
State
|
||||
end;
|
||||
record_timing(_Data, State) ->
|
||||
State.
|
||||
|
||||
print_progress_success(St) ->
|
||||
print_colored(".", ?GREEN, St).
|
||||
|
||||
print_progress_skipped(St) ->
|
||||
print_colored("*", ?YELLOW, St).
|
||||
|
||||
print_progress_failed(_Exc, St) ->
|
||||
print_colored("F", ?RED, St).
|
||||
|
||||
merge_on_end(Data, Dict) ->
|
||||
ID = proplists:get_value(id, Data),
|
||||
dict:update(ID,
|
||||
fun(Old) ->
|
||||
orddict:merge(fun merge_data/3, Old, orddict:from_list(Data))
|
||||
end, Dict).
|
||||
|
||||
merge_data(_K, undefined, X) -> X;
|
||||
merge_data(_K, X, undefined) -> X;
|
||||
merge_data(_K, _, X) -> X.
|
||||
|
||||
%%------------------------------------------
|
||||
%% Print information at end of run
|
||||
%%------------------------------------------
|
||||
print_failures(#state{failures=[]}) ->
|
||||
ok;
|
||||
print_failures(#state{failures=Fails}=State) ->
|
||||
io:nl(),
|
||||
io:fwrite("Failures:~n~n",[]),
|
||||
lists:foldr(print_failure_fun(State), 1, Fails),
|
||||
ok.
|
||||
|
||||
print_failure_fun(#state{status=Status}=State) ->
|
||||
fun(Key, Count) ->
|
||||
TestData = dict:fetch(Key, Status),
|
||||
TestId = format_test_identifier(TestData),
|
||||
io:fwrite(" ~p) ~ts~n", [Count, TestId]),
|
||||
print_failure_reason(proplists:get_value(status, TestData),
|
||||
proplists:get_value(output, TestData),
|
||||
State),
|
||||
io:nl(),
|
||||
Count + 1
|
||||
end.
|
||||
|
||||
print_failure_reason({skipped, Reason}, _Output, State) ->
|
||||
print_colored(io_lib:format(" ~ts~n", [format_pending_reason(Reason)]),
|
||||
?RED, State);
|
||||
print_failure_reason({error, {_Class, Term, Stack}}, Output, State) when
|
||||
is_tuple(Term), tuple_size(Term) == 2, is_list(element(2, Term)) ->
|
||||
print_assertion_failure(Term, Stack, Output, State),
|
||||
print_failure_output(5, Output, State);
|
||||
print_failure_reason({error, Reason}, Output, State) ->
|
||||
print_colored(indent(5, "Failure/Error: ~p~n", [Reason]), ?RED, State),
|
||||
print_failure_output(5, Output, State).
|
||||
|
||||
print_failure_output(_, <<>>, _) -> ok;
|
||||
print_failure_output(_, undefined, _) -> ok;
|
||||
print_failure_output(Indent, Output, State) ->
|
||||
print_colored(indent(Indent, "Output: ~ts", [Output]), ?CYAN, State).
|
||||
|
||||
print_assertion_failure({Type, Props}, Stack, Output, State) ->
|
||||
FailureDesc = format_assertion_failure(Type, Props, 5),
|
||||
{M,F,A,Loc} = lists:last(Stack),
|
||||
LocationText = io_lib:format(" %% ~ts:~p:in `~ts`", [proplists:get_value(file, Loc),
|
||||
proplists:get_value(line, Loc),
|
||||
format_function_name(M,F,A)]),
|
||||
print_colored(FailureDesc, ?RED, State),
|
||||
io:nl(),
|
||||
print_colored(LocationText, ?CYAN, State),
|
||||
io:nl(),
|
||||
print_failure_output(5, Output, State),
|
||||
io:nl().
|
||||
|
||||
print_pending(#state{skips=[]}) ->
|
||||
ok;
|
||||
print_pending(#state{status=Status, skips=Skips}=State) ->
|
||||
io:nl(),
|
||||
io:fwrite("Pending:~n", []),
|
||||
lists:foreach(fun(ID) ->
|
||||
Info = dict:fetch(ID, Status),
|
||||
case proplists:get_value(reason, Info) of
|
||||
undefined ->
|
||||
ok;
|
||||
Reason ->
|
||||
print_pending_reason(Reason, Info, State)
|
||||
end
|
||||
end, lists:reverse(Skips)),
|
||||
io:nl().
|
||||
|
||||
print_pending_reason(Reason0, Data, State) ->
|
||||
Text = case proplists:get_value(type, Data) of
|
||||
group ->
|
||||
io_lib:format(" ~ts~n", [proplists:get_value(desc, Data)]);
|
||||
test ->
|
||||
io_lib:format(" ~ts~n", [format_test_identifier(Data)])
|
||||
end,
|
||||
Reason = io_lib:format(" %% ~ts~n", [format_pending_reason(Reason0)]),
|
||||
print_colored(Text, ?YELLOW, State),
|
||||
print_colored(Reason, ?CYAN, State).
|
||||
|
||||
print_profile(#state{timings=T, status=Status, profile=true}=State) ->
|
||||
TopN = binomial_heap:take(10, T),
|
||||
TopNTime = abs(lists:sum([ Time || {Time, _} <- TopN ])),
|
||||
TLG = dict:fetch([], Status),
|
||||
TotalTime = proplists:get_value(time, TLG),
|
||||
if TotalTime =/= undefined andalso TotalTime > 0 andalso TopN =/= [] ->
|
||||
TopNPct = (TopNTime / TotalTime) * 100,
|
||||
io:nl(), io:nl(),
|
||||
io:fwrite("Top ~p slowest tests (~ts, ~.1f% of total time):", [length(TopN), format_time(TopNTime), TopNPct]),
|
||||
lists:foreach(print_timing_fun(State), TopN),
|
||||
io:nl();
|
||||
true -> ok
|
||||
end;
|
||||
print_profile(#state{profile=false}) ->
|
||||
ok.
|
||||
|
||||
print_timing(#state{status=Status}) ->
|
||||
TLG = dict:fetch([], Status),
|
||||
Time = proplists:get_value(time, TLG),
|
||||
io:nl(),
|
||||
io:fwrite("Finished in ~ts~n", [format_time(Time)]),
|
||||
ok.
|
||||
|
||||
print_results(Data, State) ->
|
||||
Pass = proplists:get_value(pass, Data, 0),
|
||||
Fail = proplists:get_value(fail, Data, 0),
|
||||
Skip = proplists:get_value(skip, Data, 0),
|
||||
Cancel = proplists:get_value(cancel, Data, 0),
|
||||
Total = Pass + Fail + Skip + Cancel,
|
||||
{Color, Result} = if Fail > 0 -> {?RED, error};
|
||||
Skip > 0; Cancel > 0 -> {?YELLOW, error};
|
||||
Pass =:= 0 -> {?YELLOW, ok};
|
||||
true -> {?GREEN, ok}
|
||||
end,
|
||||
print_results(Color, Total, Fail, Skip, Cancel, State),
|
||||
sync_end(Result).
|
||||
|
||||
print_results(Color, 0, _, _, _, State) ->
|
||||
print_colored(Color, "0 tests\n", State);
|
||||
print_results(Color, Total, Fail, Skip, Cancel, State) ->
|
||||
SkipText = format_optional_result(Skip, "skipped"),
|
||||
CancelText = format_optional_result(Cancel, "cancelled"),
|
||||
Text = io_lib:format("~p tests, ~p failures~ts~ts~n", [Total, Fail, SkipText, CancelText]),
|
||||
print_colored(Text, Color, State).
|
||||
|
||||
print_timing_fun(#state{status=Status}=State) ->
|
||||
fun({Time, Key}) ->
|
||||
TestData = dict:fetch(Key, Status),
|
||||
TestId = format_test_identifier(TestData),
|
||||
io:nl(),
|
||||
io:fwrite(" ~ts~n", [TestId]),
|
||||
print_colored([" "|format_time(abs(Time))], ?CYAN, State)
|
||||
end.
|
||||
|
||||
%%------------------------------------------
|
||||
%% Print to the console with the given color
|
||||
%% if enabled.
|
||||
%%------------------------------------------
|
||||
print_colored(Text, Color, #state{colored=true}) ->
|
||||
io:fwrite("~s~ts~s", [Color, Text, ?RESET]);
|
||||
print_colored(Text, _Color, #state{colored=false}) ->
|
||||
io:fwrite("~ts", [Text]).
|
||||
|
||||
%%------------------------------------------
|
||||
%% Generic data formatters
|
||||
%%------------------------------------------
|
||||
format_function_name(M, F, A) ->
|
||||
io_lib:format("~ts:~ts/~p", [M, F, A]).
|
||||
|
||||
format_optional_result(0, _) ->
|
||||
[];
|
||||
format_optional_result(Count, Text) ->
|
||||
io_lib:format(", ~p ~ts", [Count, Text]).
|
||||
|
||||
format_test_identifier(Data) ->
|
||||
{Mod, Fun, Arity} = proplists:get_value(source, Data),
|
||||
Line = case proplists:get_value(line, Data) of
|
||||
0 -> "";
|
||||
L -> io_lib:format(":~p", [L])
|
||||
end,
|
||||
Desc = case proplists:get_value(desc, Data) of
|
||||
undefined -> "";
|
||||
DescText -> io_lib:format(": ~ts", [DescText])
|
||||
end,
|
||||
io_lib:format("~ts~ts~ts", [format_function_name(Mod, Fun, Arity), Line, Desc]).
|
||||
|
||||
format_time(undefined) ->
|
||||
"? seconds";
|
||||
format_time(Time) ->
|
||||
io_lib:format("~.3f seconds", [Time / 1000]).
|
||||
|
||||
format_pending_reason({module_not_found, M}) ->
|
||||
io_lib:format("Module '~ts' missing", [M]);
|
||||
format_pending_reason({no_such_function, {M,F,A}}) ->
|
||||
io_lib:format("Function ~ts undefined", [format_function_name(M,F,A)]);
|
||||
format_pending_reason({exit, Reason}) ->
|
||||
io_lib:format("Related process exited with reason: ~p", [Reason]);
|
||||
format_pending_reason(Reason) ->
|
||||
io_lib:format("Unknown error: ~p", [Reason]).
|
||||
|
||||
%% @doc Formats all the known eunit assertions, you're on your own if
|
||||
%% you make an assertion yourself.
|
||||
format_assertion_failure(Type, Props, I) when Type =:= assertion_failed
|
||||
; Type =:= assert ->
|
||||
Keys = proplists:get_keys(Props),
|
||||
HasEUnitProps = ([expression, value] -- Keys) =:= [],
|
||||
HasHamcrestProps = ([expected, actual, matcher] -- Keys) =:= [],
|
||||
if
|
||||
HasEUnitProps ->
|
||||
[indent(I, "Failure/Error: ?assert(~ts)~n", [proplists:get_value(expression, Props)]),
|
||||
indent(I, " expected: true~n", []),
|
||||
case proplists:get_value(value, Props) of
|
||||
false ->
|
||||
indent(I, " got: false", []);
|
||||
{not_a_boolean, V} ->
|
||||
indent(I, " got: ~p", [V])
|
||||
end];
|
||||
HasHamcrestProps ->
|
||||
[indent(I, "Failure/Error: ?assertThat(~p)~n", [proplists:get_value(matcher, Props)]),
|
||||
indent(I, " expected: ~p~n", [proplists:get_value(expected, Props)]),
|
||||
indent(I, " got: ~p", [proplists:get_value(actual, Props)])];
|
||||
true ->
|
||||
[indent(I, "Failure/Error: unknown assert: ~p", [Props])]
|
||||
end;
|
||||
|
||||
format_assertion_failure(Type, Props, I) when Type =:= assertMatch_failed
|
||||
; Type =:= assertMatch ->
|
||||
Expr = proplists:get_value(expression, Props),
|
||||
Pattern = proplists:get_value(pattern, Props),
|
||||
Value = proplists:get_value(value, Props),
|
||||
[indent(I, "Failure/Error: ?assertMatch(~ts, ~ts)~n", [Pattern, Expr]),
|
||||
indent(I, " expected: = ~ts~n", [Pattern]),
|
||||
indent(I, " got: ~p", [Value])];
|
||||
|
||||
format_assertion_failure(Type, Props, I) when Type =:= assertNotMatch_failed
|
||||
; Type =:= assertNotMatch ->
|
||||
Expr = proplists:get_value(expression, Props),
|
||||
Pattern = proplists:get_value(pattern, Props),
|
||||
Value = proplists:get_value(value, Props),
|
||||
[indent(I, "Failure/Error: ?assertNotMatch(~ts, ~ts)~n", [Pattern, Expr]),
|
||||
indent(I, " expected not: = ~ts~n", [Pattern]),
|
||||
indent(I, " got: ~p", [Value])];
|
||||
|
||||
format_assertion_failure(Type, Props, I) when Type =:= assertEqual_failed
|
||||
; Type =:= assertEqual ->
|
||||
Expr = proplists:get_value(expression, Props),
|
||||
Expected = proplists:get_value(expected, Props),
|
||||
Value = proplists:get_value(value, Props),
|
||||
[indent(I, "Failure/Error: ?assertEqual(~w, ~ts)~n", [Expected,
|
||||
Expr]),
|
||||
indent(I, " expected: ~p~n", [Expected]),
|
||||
indent(I, " got: ~p", [Value])];
|
||||
|
||||
format_assertion_failure(Type, Props, I) when Type =:= assertNotEqual_failed
|
||||
; Type =:= assertNotEqual ->
|
||||
Expr = proplists:get_value(expression, Props),
|
||||
Value = proplists:get_value(value, Props),
|
||||
[indent(I, "Failure/Error: ?assertNotEqual(~p, ~ts)~n",
|
||||
[Value, Expr]),
|
||||
indent(I, " expected not: == ~p~n", [Value]),
|
||||
indent(I, " got: ~p", [Value])];
|
||||
|
||||
format_assertion_failure(Type, Props, I) when Type =:= assertException_failed
|
||||
; Type =:= assertException ->
|
||||
Expr = proplists:get_value(expression, Props),
|
||||
Pattern = proplists:get_value(pattern, Props),
|
||||
{Class, Term} = extract_exception_pattern(Pattern), % I hate that we have to do this, why not just give DATA
|
||||
[indent(I, "Failure/Error: ?assertException(~ts, ~ts, ~ts)~n", [Class, Term, Expr]),
|
||||
case proplists:is_defined(unexpected_success, Props) of
|
||||
true ->
|
||||
[indent(I, " expected: exception ~ts but nothing was raised~n", [Pattern]),
|
||||
indent(I, " got: value ~p", [proplists:get_value(unexpected_success, Props)])];
|
||||
false ->
|
||||
Ex = proplists:get_value(unexpected_exception, Props),
|
||||
[indent(I, " expected: exception ~ts~n", [Pattern]),
|
||||
indent(I, " got: exception ~p", [Ex])]
|
||||
end];
|
||||
|
||||
format_assertion_failure(Type, Props, I) when Type =:= assertNotException_failed
|
||||
; Type =:= assertNotException ->
|
||||
Expr = proplists:get_value(expression, Props),
|
||||
Pattern = proplists:get_value(pattern, Props),
|
||||
{Class, Term} = extract_exception_pattern(Pattern), % I hate that we have to do this, why not just give DAT
|
||||
Ex = proplists:get_value(unexpected_exception, Props),
|
||||
[indent(I, "Failure/Error: ?assertNotException(~ts, ~ts, ~ts)~n", [Class, Term, Expr]),
|
||||
indent(I, " expected not: exception ~ts~n", [Pattern]),
|
||||
indent(I, " got: exception ~p", [Ex])];
|
||||
|
||||
format_assertion_failure(Type, Props, I) when Type =:= command_failed
|
||||
; Type =:= command ->
|
||||
Cmd = proplists:get_value(command, Props),
|
||||
Expected = proplists:get_value(expected_status, Props),
|
||||
Status = proplists:get_value(status, Props),
|
||||
[indent(I, "Failure/Error: ?cmdStatus(~p, ~p)~n", [Expected, Cmd]),
|
||||
indent(I, " expected: status ~p~n", [Expected]),
|
||||
indent(I, " got: status ~p", [Status])];
|
||||
|
||||
format_assertion_failure(Type, Props, I) when Type =:= assertCmd_failed
|
||||
; Type =:= assertCmd ->
|
||||
Cmd = proplists:get_value(command, Props),
|
||||
Expected = proplists:get_value(expected_status, Props),
|
||||
Status = proplists:get_value(status, Props),
|
||||
[indent(I, "Failure/Error: ?assertCmdStatus(~p, ~p)~n", [Expected, Cmd]),
|
||||
indent(I, " expected: status ~p~n", [Expected]),
|
||||
indent(I, " got: status ~p", [Status])];
|
||||
|
||||
format_assertion_failure(Type, Props, I) when Type =:= assertCmdOutput_failed
|
||||
; Type =:= assertCmdOutput ->
|
||||
Cmd = proplists:get_value(command, Props),
|
||||
Expected = proplists:get_value(expected_output, Props),
|
||||
Output = proplists:get_value(output, Props),
|
||||
[indent(I, "Failure/Error: ?assertCmdOutput(~p, ~p)~n", [Expected, Cmd]),
|
||||
indent(I, " expected: ~p~n", [Expected]),
|
||||
indent(I, " got: ~p", [Output])];
|
||||
|
||||
format_assertion_failure(Type, Props, I) ->
|
||||
indent(I, "~p", [{Type, Props}]).
|
||||
|
||||
indent(I, Fmt, Args) ->
|
||||
io_lib:format("~" ++ integer_to_list(I) ++ "s" ++ Fmt, [" "|Args]).
|
||||
|
||||
extract_exception_pattern(Str) ->
|
||||
["{", Class, Term|_] = re:split(Str, "[, ]{1,2}", [unicode,{return,list}]),
|
||||
{Class, Term}.
|
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2009 Juan Jose Comellas
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
- Neither the name of Juan Jose Comellas nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,487 @@
|
|||
Getopt for Erlang
|
||||
=================
|
||||
|
||||
Command-line parsing module that uses a syntax similar to that of GNU *getopt*.
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
You should only need a somewhat recent version of Erlang/OTP. The module has
|
||||
been tested with all versions of Erlang starting with R13B and ending with 20.
|
||||
|
||||
You also need a recent version of [rebar3](http://www.rebar3.org/) in
|
||||
the system path.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
To compile the module you simply run `rebar3 compile`.
|
||||
|
||||
To run the unit tests run `rebar3 eunit`.
|
||||
|
||||
To build the (very) limited documentation run `rebar edoc`.
|
||||
|
||||
To use getopt in your project you can just add it as a dependency in your
|
||||
`rebar.config` file in the following way:
|
||||
```sh
|
||||
{deps,
|
||||
[
|
||||
{getopt, "1.0.1"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
The `getopt` module provides four functions:
|
||||
|
||||
```erlang
|
||||
parse([{Name, Short, Long, ArgSpec, Help}], Args :: string() | [string()]) ->
|
||||
{ok, {Options, NonOptionArgs}} | {error, {Reason, Data}}
|
||||
|
||||
tokenize(CmdLine :: string()) -> [string()]
|
||||
|
||||
usage([{Name, Short, Long, ArgSpec, Help}], ProgramName :: string()) -> ok
|
||||
|
||||
usage([{Name, Short, Long, ArgSpec, Help}], ProgramName :: string(),
|
||||
CmdLineTail :: string()) -> ok
|
||||
|
||||
usage([{Name, Short, Long, ArgSpec, Help}], ProgramName :: string(),
|
||||
CmdLineTail :: string(), OptionsTail :: [{string(), string}]) -> ok
|
||||
```
|
||||
|
||||
The `parse/2` function receives a list of tuples with the command line option
|
||||
specifications. The type specification for the tuple is:
|
||||
|
||||
```erlang
|
||||
-type arg_type() :: 'atom' | 'binary' | 'boolean' | 'float' | 'integer' | 'string'.
|
||||
|
||||
-type arg_value() :: atom() | binary() | boolean() | float() | integer() | string().
|
||||
|
||||
-type arg_spec() :: arg_type() | {arg_type(), arg_value()} | undefined.
|
||||
|
||||
-type option_spec() :: {
|
||||
Name :: atom(),
|
||||
Short :: char() | undefined,
|
||||
Long :: string() | undefined,
|
||||
ArgSpec :: arg_spec(),
|
||||
Help :: string() | undefined
|
||||
}.
|
||||
```
|
||||
|
||||
The elements of the tuple are:
|
||||
|
||||
- `Name`: name of the option.
|
||||
- `Short`: character for the short option (e.g. $i for -i).
|
||||
- `Long`: string for the long option (e.g. "info" for --info).
|
||||
- `ArgSpec`: data type and optional default value the argument will be converted to.
|
||||
- `Help`: help message that is shown for the option when `usage/2` is called.
|
||||
|
||||
e.g.
|
||||
|
||||
```erlang
|
||||
{port, $p, "port", {integer, 5432}, "Database server port"}
|
||||
```
|
||||
|
||||
The second parameter receives the list of arguments as passed to the `main/1`
|
||||
function in escripts or the unparsed command line as a string.
|
||||
|
||||
If the function is successful parsing the command line arguments it will return
|
||||
a tuple containing the parsed options and the non-option arguments. The options
|
||||
will be represented by a list of key-value pairs with the `Name` of the
|
||||
option as *key* and the argument from the command line as *value*. If the option
|
||||
doesn't have an argument, only the atom corresponding to its `Name` will be
|
||||
added to the list of options. For the example given above we could get something
|
||||
like `{port, 5432}`. The non-option arguments are just a list of strings with
|
||||
all the arguments that did not have corresponding options.
|
||||
|
||||
e.g. Given the following option specifications:
|
||||
|
||||
```erlang
|
||||
OptSpecList =
|
||||
[
|
||||
{host, $h, "host", {string, "localhost"}, "Database server host"},
|
||||
{port, $p, "port", integer, "Database server port"},
|
||||
{dbname, undefined, "dbname", {string, "users"}, "Database name"},
|
||||
{xml, $x, undefined, undefined, "Output data in XML"},
|
||||
{verbose, $v, "verbose", integer, "Verbosity level"},
|
||||
{file, undefined, undefined, string, "Output file"}
|
||||
].
|
||||
```
|
||||
|
||||
And this command line:
|
||||
|
||||
```erlang
|
||||
Args = "-h myhost --port=1000 -x myfile.txt -vvv dummy1 dummy2"
|
||||
```
|
||||
|
||||
Which could also be passed in the format the `main/1` function receives the arguments in escripts:
|
||||
|
||||
```erlang
|
||||
Args = ["-h", "myhost", "--port=1000", "-x", "file.txt", "-vvv", "dummy1", "dummy2"].
|
||||
```
|
||||
|
||||
The call to `getopt:parse/2`:
|
||||
|
||||
```erlang
|
||||
getopt:parse(OptSpecList, Args).
|
||||
```
|
||||
|
||||
Will return:
|
||||
|
||||
```erlang
|
||||
{ok,{[{host,"myhost"},
|
||||
{port,1000},
|
||||
xml,
|
||||
{file,"file.txt"},
|
||||
{dbname,"users"},
|
||||
{verbose,3}],
|
||||
["dummy1","dummy2"]}}
|
||||
```
|
||||
|
||||
The `tokenize/1` function will separate a command line string into
|
||||
tokens, taking into account whether an argument is single or double
|
||||
quoted, a character is escaped or if there are environment variables to
|
||||
be expanded. e.g.:
|
||||
|
||||
```erlang
|
||||
getopt:tokenize(" --name John\\ Smith --path \"John's Files\" -u ${USER}").
|
||||
```
|
||||
|
||||
Will return something like:
|
||||
|
||||
```erlang
|
||||
["--name","John Smith","--path","John's Files","-u","jsmith"]
|
||||
```
|
||||
|
||||
The other functions exported by the `getopt` module (`usage/2`, `usage/3`
|
||||
and `usage/4`) are used to show the command line syntax for the program.
|
||||
For example, given the above-mentioned option specifications, the call to
|
||||
`getopt:usage/2`:
|
||||
|
||||
```erlang
|
||||
getopt:usage(OptSpecList, "ex1").
|
||||
```
|
||||
|
||||
Will show (on *standard_error*):
|
||||
|
||||
Usage: ex1 [-h <host>] [-p <port>] [--dbname <dbname>] [-x] [-v] <file>
|
||||
|
||||
-h, --host Database server host
|
||||
-p, --port Database server port
|
||||
--dbname Database name
|
||||
-x Output data in XML
|
||||
-v Verbosity level
|
||||
<file> Output file
|
||||
|
||||
This call to `getopt:usage/3` will add a string after the usage command line:
|
||||
|
||||
```erlang
|
||||
getopt:usage(OptSpecList, "ex1", "[var=value ...] [command ...]").
|
||||
```
|
||||
|
||||
Will show (on *standard_error*):
|
||||
|
||||
Usage: ex1 [-h <host>] [-p <port>] [--dbname <dbname>] [-x] [-v <verbose>] <file> [var=value ...] [command ...]
|
||||
|
||||
-h, --host Database server host
|
||||
-p, --port Database server port
|
||||
--dbname Database name
|
||||
-x Output data in XML
|
||||
-v, --verbose Verbosity level
|
||||
<file> Output file
|
||||
|
||||
Whereas this call to `getopt:usage/3` will also add some lines to the options
|
||||
help text:
|
||||
|
||||
```erlang
|
||||
getopt:usage(OptSpecList, "ex1", "[var=value ...] [command ...]",
|
||||
[{"var=value", "Variables that will affect the execution (e.g. debug=1)"},
|
||||
{"command", "Commands that will be executed (e.g. count)"}]).
|
||||
```
|
||||
|
||||
Will show (on *standard_error*):
|
||||
|
||||
Usage: ex1 [-h <host>] [-p <port>] [--dbname <dbname>] [-x] [-v <verbose>] <file> [var=value ...] [command ...]
|
||||
|
||||
-h, --host Database server host
|
||||
-p, --port Database server port
|
||||
--dbname Database name
|
||||
-x Output data in XML
|
||||
-v, --verbose Verbosity level
|
||||
<file> Output file
|
||||
var=value Variables that will affect the execution (e.g. debug=1)
|
||||
command Commands that will be executed (e.g. count)
|
||||
|
||||
|
||||
Command-line Syntax
|
||||
-------------------
|
||||
|
||||
The syntax supported by the `getopt` module is very similar to that followed
|
||||
by GNU programs, which is described [here](http://www.gnu.org/s/libc/manual/html_node/Argument-Syntax.html).
|
||||
|
||||
Options can have both short (single character) and long (string) option names.
|
||||
|
||||
A short option can have the following syntax:
|
||||
|
||||
-a Single option 'a', no argument or implicit boolean argument
|
||||
-a foo Single option 'a', argument "foo"
|
||||
-afoo Single option 'a', argument "foo"
|
||||
-abc Multiple options: 'a'; 'b'; 'c'
|
||||
-bcafoo Multiple options: 'b'; 'c'; 'a' with argument "foo"
|
||||
-aaa Multiple repetitions of option 'a'
|
||||
|
||||
A long option can have the following syntax:
|
||||
|
||||
--foo Single option 'foo', no argument
|
||||
--foo=bar Single option 'foo', argument "bar"
|
||||
--foo bar Single option 'foo', argument "bar"
|
||||
|
||||
|
||||
Argument Types
|
||||
--------------
|
||||
|
||||
The arguments allowed for options are: *atom*; *binary*; *boolean*; *float*; *integer*; *string*.
|
||||
The `getopt` module checks every argument to see if it can be converted to its
|
||||
correct type.
|
||||
|
||||
In the case of boolean arguments, the following values (in lower or
|
||||
upper case) are considered `true`: *true*; *t*; *yes*; *y*; *on*; *enabled*; *1*.
|
||||
These ones are considered `false`: *false*; *f*; *no*; *n*; *off*; *disabled*; *0*.
|
||||
|
||||
Numeric arguments can only be negative when passed as part of an assignment expression.
|
||||
|
||||
e.g. `--increment=-100` is a valid expression; whereas `--increment -100` is invalid
|
||||
|
||||
|
||||
Implicit Arguments
|
||||
------------------
|
||||
|
||||
The arguments for options with the *boolean* and *integer* data types can sometimes
|
||||
be omitted. In those cases the value assigned to the option is *true* for *boolean*
|
||||
arguments and *1* for integer arguments.
|
||||
|
||||
|
||||
Multiple Repetitions
|
||||
--------------------
|
||||
|
||||
An option can be repeated several times, in which case there will be multiple
|
||||
appearances of the option in the resulting list. The only exceptions are short
|
||||
options with integer arguments. In that particular case, each appearance of
|
||||
the short option within a single command line argument will increment the
|
||||
number that will be returned for that specific option.
|
||||
|
||||
e.g. Given an option specification list with the following format:
|
||||
|
||||
```erlang
|
||||
OptSpecList =
|
||||
[
|
||||
{define, $D, "define", string, "Define a variable"},
|
||||
{verbose, $v, "verbose", integer, "Verbosity level"}
|
||||
].
|
||||
```
|
||||
|
||||
The following invocation:
|
||||
|
||||
```erlang
|
||||
getopt:parse(OptSpecList, "-DFOO -DVAR1=VAL1 -DBAR --verbose --verbose=3 -v -vvvv dummy").
|
||||
```
|
||||
|
||||
would return:
|
||||
|
||||
```erlang
|
||||
{ok,{[{define,"FOO"}, {define,"VAR1=VAL1"}, {define,"BAR"},
|
||||
{verbose,1}, {verbose,3}, {verbose,1}, {verbose,4}],
|
||||
["dummy"]}}
|
||||
```
|
||||
|
||||
|
||||
Positional Options
|
||||
------------------
|
||||
|
||||
We can also have options with neither short nor long option names. In this case,
|
||||
the options will be taken according to their position in the option specification
|
||||
list passed to `getopt:/parse2`.
|
||||
|
||||
For example, with the following option specifications:
|
||||
|
||||
```erlang
|
||||
OptSpecList =
|
||||
[
|
||||
{xml, $x, "xml", undefined, "Output data as XML"},
|
||||
{dbname, undefined, undefined, string, "Database name"},
|
||||
{output_file, undefined, undefined, string, "File where the data will be saved to"}
|
||||
].
|
||||
```
|
||||
|
||||
This call to `getopt:parse/2`:
|
||||
|
||||
```erlang
|
||||
getopt:parse(OptSpecList, "-x mydb file.out dummy dummy").
|
||||
```
|
||||
|
||||
Will return:
|
||||
|
||||
```erlang
|
||||
{ok,{[xml,{dbname,"mydb"},{output_file,"file.out"}],
|
||||
["dummy","dummy"]}}
|
||||
```
|
||||
|
||||
|
||||
Option Terminators
|
||||
------------------
|
||||
|
||||
The string `--` is considered an option terminator. This means that all the
|
||||
command-line arguments after it are considered non-option arguments and will be
|
||||
returned without being evaluated even if they follow the *getopt* syntax.
|
||||
|
||||
e.g. This invocation using the first option specification list in the document:
|
||||
|
||||
```erlang
|
||||
getopt:parse(OptSpecList, "-h myhost -p 1000 -- --dbname mydb dummy").
|
||||
```
|
||||
|
||||
will return:
|
||||
|
||||
```erlang
|
||||
{ok,{[{host,"myhost"}, {port,1000},{dbname,"users"}],
|
||||
["--dbname","mydb","dummy"]}}
|
||||
```
|
||||
|
||||
Notice that the *dbname* option was assigned the value `users` instead of `mydb`.
|
||||
This happens because the option terminator prevented *getopt* from evaluating it
|
||||
and the default value was assigned to it.
|
||||
|
||||
|
||||
Non-option Arguments
|
||||
--------------------
|
||||
|
||||
The single `-` character is always considered as a non-option argument.
|
||||
|
||||
e.g. This invocation using the specification list from the previous example:
|
||||
|
||||
```erlang
|
||||
getopt:parse(OptSpecList, "-h myhost -p 1000 - --dbname mydb dummy").
|
||||
```
|
||||
|
||||
will return:
|
||||
|
||||
```erlang
|
||||
{ok,{[{host,"myhost"}, {port,1000}, {dbname,"mydb"}],
|
||||
["-","dummy"]}}
|
||||
```
|
||||
|
||||
|
||||
Arguments with embedded whitespace
|
||||
----------------------------------
|
||||
|
||||
Arguments that have embedded whitespace have to be quoted with either
|
||||
single or double quotes to be considered as a single
|
||||
argument.
|
||||
|
||||
|
||||
e.g. Given an option specification list with the following format:
|
||||
|
||||
```erlang
|
||||
OptSpecList =
|
||||
[
|
||||
{define, $D, "define", string, "Define a variable"},
|
||||
{user, $u, "user", string, "User name"}
|
||||
].
|
||||
```
|
||||
|
||||
The following invocation:
|
||||
|
||||
```erlang
|
||||
getopt:parse(OptSpecList,
|
||||
"-D'FOO=VAR 123' --define \"VAR WITH SPACES\" -u\"my user name\"").
|
||||
```
|
||||
|
||||
would return:
|
||||
|
||||
```erlang
|
||||
{ok,{[{define,"FOO=VAR 123"},
|
||||
{define,"VAR WITH SPACES"},
|
||||
{user,"my user name"}],
|
||||
[]}}
|
||||
```
|
||||
|
||||
When parsing a command line with unclosed quotes the last argument
|
||||
will be a single string starting at the position where the last quote
|
||||
was entered.
|
||||
|
||||
e.g. The following invocation:
|
||||
|
||||
```erlang
|
||||
getopt:parse(OptSpecList, "--user ' my user ' \"argument with unclosed quotes").
|
||||
```
|
||||
|
||||
would return:
|
||||
|
||||
```erlang
|
||||
{ok,{[{user," my user "}],
|
||||
["argument with unclosed quotes"]}}
|
||||
```
|
||||
|
||||
|
||||
Environment variable expansion
|
||||
------------------------------
|
||||
|
||||
`getopt:parse/2` will expand environment variables when used with a command
|
||||
line that is passed as a single string. The formats that are supported
|
||||
for environment variable expansion are:
|
||||
|
||||
- $VAR (simple Unix/bash format)
|
||||
- ${VAR} (full Unix/bash format)
|
||||
- %VAR% (Windows format)
|
||||
|
||||
If a variable is not present in the environment it will not be
|
||||
expanded. Variables can be expanded within double-quoted and free
|
||||
arguments. *getopt* will not expand environment variables within
|
||||
single-quoted arguments.
|
||||
|
||||
e.g. Given the following option specification list:
|
||||
|
||||
```erlang
|
||||
OptSpecList =
|
||||
[
|
||||
{path, $p, "path", string, "File path"}
|
||||
].
|
||||
```
|
||||
|
||||
The following invocation:
|
||||
|
||||
```erlang
|
||||
getopt:parse(OptSpecList, "--path ${PATH} $NONEXISTENT_DUMMY_VAR").
|
||||
```
|
||||
|
||||
would return (depending on the value of your PATH variable) something like:
|
||||
|
||||
```erlang
|
||||
{ok,{[{path, "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}],
|
||||
["$NONEXISTENT_DUMMY_VAR"]}}
|
||||
```
|
||||
|
||||
Currently, *getopt* does not perform wildcard expansion of file paths.
|
||||
|
||||
|
||||
Escaping arguments
|
||||
==================
|
||||
|
||||
Any character can be escaped by prepending the \ (backslash) character
|
||||
to it.
|
||||
|
||||
e.g.
|
||||
|
||||
```erlang
|
||||
getopt:parse(OptSpecList, "--path /john\\'s\\ files dummy").
|
||||
```
|
||||
|
||||
Will return:
|
||||
|
||||
```erlang
|
||||
{ok,{[{path,"/john's files"}],["dummy"]}}
|
||||
```
|
|
@ -0,0 +1,30 @@
|
|||
{erl_opts, [warn_unused_vars,
|
||||
warn_export_all,
|
||||
warn_shadow_vars,
|
||||
warn_unused_import,
|
||||
warn_unused_function,
|
||||
warn_bif_clash,
|
||||
warn_unused_record,
|
||||
warn_deprecated_function,
|
||||
warn_obsolete_guard,
|
||||
strict_validation,
|
||||
warn_export_vars,
|
||||
warn_exported_vars,
|
||||
warn_missing_spec,
|
||||
warn_untyped_record, debug_info,
|
||||
{platform_define, "^2", unicode_str}
|
||||
]}.
|
||||
|
||||
{dialyzer,
|
||||
[
|
||||
{warnings, [no_return, no_undefined_callbacks, no_unused]},
|
||||
{get_warnings, true},
|
||||
{plt_apps, top_level_deps},
|
||||
{plt_location, local},
|
||||
{base_plt_apps, [kernel, stdlib, sasl, inets, crypto, public_key, ssl,
|
||||
runtime_tools, erts, compiler, tools, syntax_tools, hipe,
|
||||
mnesia]},
|
||||
{base_plt_location, global}
|
||||
]}.
|
||||
|
||||
{xref_checks, [undefined_function_calls]}.
|
|
@ -0,0 +1 @@
|
|||
[].
|
|
@ -0,0 +1,9 @@
|
|||
{application,getopt,
|
||||
[{description,"Command-line options parser for Erlang"},
|
||||
{vsn,"1.0.1"},
|
||||
{modules,[]},
|
||||
{registered,[]},
|
||||
{maintainers,["Juan Jose Comellas"]},
|
||||
{licenses,["BSD"]},
|
||||
{links,[{"GitHub","https://github.com/jcomellas/getopt"}]},
|
||||
{applications,[kernel,stdlib]}]}.
|
|
@ -0,0 +1,947 @@
|
|||
%%%-------------------------------------------------------------------
|
||||
%%% @author Juan Jose Comellas <juanjo@comellas.org>
|
||||
%%% @copyright (C) 2009-2017 Juan Jose Comellas
|
||||
%%% @doc Parses command line options with a format similar to that of GNU getopt.
|
||||
%%% @end
|
||||
%%%
|
||||
%%% This source file is subject to the New BSD License. You should have received
|
||||
%%% a copy of the New BSD license with this software. If not, it can be
|
||||
%%% retrieved from: http://www.opensource.org/licenses/bsd-license.php
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(getopt).
|
||||
-author('juanjo@comellas.org').
|
||||
|
||||
-export([parse/2, check/2, parse_and_check/2, format_error/2,
|
||||
usage/2, usage/3, usage/4, usage/6, tokenize/1]).
|
||||
-export([usage_cmd_line/2]).
|
||||
|
||||
-define(LINE_LENGTH, 75).
|
||||
-define(MIN_USAGE_COMMAND_LINE_OPTION_LENGTH, 25).
|
||||
|
||||
%% Position of each field in the option specification tuple.
|
||||
-define(OPT_NAME, 1).
|
||||
-define(OPT_SHORT, 2).
|
||||
-define(OPT_LONG, 3).
|
||||
-define(OPT_ARG, 4).
|
||||
-define(OPT_HELP, 5).
|
||||
|
||||
-define(IS_OPT_SPEC(Opt), (tuple_size(Opt) =:= ?OPT_HELP)).
|
||||
-define(IS_WHITESPACE(Char), ((Char) =:= $\s orelse (Char) =:= $\t orelse
|
||||
(Char) =:= $\n orelse (Char) =:= $\r)).
|
||||
|
||||
%% Atom indicating the data type that an argument can be converted to.
|
||||
-type arg_type() :: 'atom' | 'binary' | 'boolean' | 'float' | 'integer' | 'string'.
|
||||
%% Data type that an argument can be converted to.
|
||||
-type arg_value() :: atom() | binary() | boolean() | float() | integer() | string().
|
||||
%% Argument specification.
|
||||
-type arg_spec() :: arg_type() | {arg_type(), arg_value()} | undefined.
|
||||
%% Option type and optional default argument.
|
||||
-type simple_option() :: atom().
|
||||
-type compound_option() :: {atom(), arg_value()}.
|
||||
-type option() :: simple_option() | compound_option().
|
||||
%% Command line option specification.
|
||||
-type option_spec() :: {
|
||||
Name :: atom(),
|
||||
Short :: char() | undefined,
|
||||
Long :: string() | undefined,
|
||||
ArgSpec :: arg_spec(),
|
||||
Help :: string() | undefined
|
||||
}.
|
||||
%% Output streams
|
||||
-type output_stream() :: 'standard_io' | 'standard_error'.
|
||||
|
||||
%% For internal use
|
||||
-type usage_line() :: {OptionText :: string(), HelpText :: string()}.
|
||||
-type usage_line_with_length() :: {OptionLength :: non_neg_integer(), OptionText :: string(), HelpText :: string()}.
|
||||
|
||||
|
||||
-export_type([arg_type/0, arg_value/0, arg_spec/0, simple_option/0, compound_option/0, option/0, option_spec/0]).
|
||||
|
||||
|
||||
%% @doc Parse the command line options and arguments returning a list of tuples
|
||||
%% and/or atoms using the Erlang convention for sending options to a
|
||||
%% function. Additionally perform check if all required options (the ones
|
||||
%% without default values) are present. The function is a combination of
|
||||
%% two calls: parse/2 and check/2.
|
||||
-spec parse_and_check([option_spec()], string() | [string()]) ->
|
||||
{ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: term()}}.
|
||||
parse_and_check(OptSpecList, CmdLine) when is_list(OptSpecList), is_list(CmdLine) ->
|
||||
case parse(OptSpecList, CmdLine) of
|
||||
{ok, {Opts, _}} = Result ->
|
||||
case check(OptSpecList, Opts) of
|
||||
ok -> Result;
|
||||
Error -> Error
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% @doc Check the parsed command line arguments returning ok if all required
|
||||
%% options (i.e. that don't have defaults) are present, and returning
|
||||
%% error otherwise.
|
||||
-spec check([option_spec()], [option()]) ->
|
||||
ok | {error, {Reason :: atom(), Option :: atom()}}.
|
||||
check(OptSpecList, ParsedOpts) when is_list(OptSpecList), is_list(ParsedOpts) ->
|
||||
try
|
||||
RequiredOpts = [Name || {Name, _, _, Arg, _} <- OptSpecList,
|
||||
not is_tuple(Arg) andalso Arg =/= undefined],
|
||||
lists:foreach(fun (Option) ->
|
||||
case proplists:is_defined(Option, ParsedOpts) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
throw({error, {missing_required_option, Option}})
|
||||
end
|
||||
end, RequiredOpts)
|
||||
catch
|
||||
_:Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
|
||||
%% @doc Parse the command line options and arguments returning a list of tuples
|
||||
%% and/or atoms using the Erlang convention for sending options to a
|
||||
%% function.
|
||||
-spec parse([option_spec()], string() | [string()]) ->
|
||||
{ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: term()}}.
|
||||
parse(OptSpecList, CmdLine) when is_list(CmdLine) ->
|
||||
try
|
||||
Args = if
|
||||
is_integer(hd(CmdLine)) -> tokenize(CmdLine);
|
||||
true -> CmdLine
|
||||
end,
|
||||
parse(OptSpecList, [], [], 0, Args)
|
||||
catch
|
||||
throw: {error, {_Reason, _Data}} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
|
||||
-spec parse([option_spec()], [option()], [string()], integer(), [string()]) ->
|
||||
{ok, {[option()], [string()]}}.
|
||||
%% Process the option terminator.
|
||||
parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, ["--" | Tail]) ->
|
||||
%% Any argument present after the terminator is not considered an option.
|
||||
{ok, {lists:reverse(append_default_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc, Tail)}};
|
||||
%% Process long options.
|
||||
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["--" ++ OptArg = OptStr | Tail]) ->
|
||||
parse_long_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg);
|
||||
%% Process short options.
|
||||
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["-" ++ ([_Char | _] = OptArg) = OptStr | Tail]) ->
|
||||
parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg);
|
||||
%% Process non-option arguments.
|
||||
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail]) ->
|
||||
case find_non_option_arg(OptSpecList, ArgPos) of
|
||||
{value, OptSpec} when ?IS_OPT_SPEC(OptSpec) ->
|
||||
parse(OptSpecList, add_option_with_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos + 1, Tail);
|
||||
false ->
|
||||
parse(OptSpecList, OptAcc, [Arg | ArgAcc], ArgPos, Tail)
|
||||
end;
|
||||
parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, []) ->
|
||||
%% Once we have completed gathering the options we add the ones that were
|
||||
%% not present but had default arguments in the specification.
|
||||
{ok, {lists:reverse(append_default_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc)}}.
|
||||
|
||||
|
||||
%% @doc Format the error code returned by prior call to parse/2 or check/2.
|
||||
-spec format_error([option_spec()], {error, {Reason :: atom(), Data :: term()}} |
|
||||
{Reason :: term(), Data :: term()}) -> string().
|
||||
format_error(OptSpecList, {error, Reason}) ->
|
||||
format_error(OptSpecList, Reason);
|
||||
format_error(OptSpecList, {missing_required_option, Name}) ->
|
||||
OptStr = case lists:keyfind(Name, 1, OptSpecList) of
|
||||
{Name, undefined, undefined, _Type, _Help} -> ["<", to_string(Name), ">"];
|
||||
{_Name, undefined, Long, _Type, _Help} -> ["--", Long];
|
||||
{_Name, Short, undefined, _Type, _Help} -> ["-", Short];
|
||||
{_Name, Short, Long, _Type, _Help} -> ["-", Short, " (", Long, ")"]
|
||||
end,
|
||||
lists:flatten(["missing required option: ", OptStr]);
|
||||
format_error(_OptSpecList, {invalid_option, OptStr}) ->
|
||||
lists:flatten(["invalid option: ", to_string(OptStr)]);
|
||||
format_error(_OptSpecList, {invalid_option_arg, {Name, Arg}}) ->
|
||||
lists:flatten(["option \'", to_string(Name) ++ "\' has invalid argument: ", to_string(Arg)]);
|
||||
format_error(_OptSpecList, {invalid_option_arg, OptStr}) ->
|
||||
lists:flatten(["invalid option argument: ", to_string(OptStr)]);
|
||||
format_error(_OptSpecList, {Reason, Data}) ->
|
||||
lists:flatten([to_string(Reason), " ", to_string(Data)]).
|
||||
|
||||
|
||||
%% @doc Parse a long option, add it to the option accumulator and continue
|
||||
%% parsing the rest of the arguments recursively.
|
||||
%% A long option can have the following syntax:
|
||||
%% --foo Single option 'foo', no argument
|
||||
%% --foo=bar Single option 'foo', argument "bar"
|
||||
%% --foo bar Single option 'foo', argument "bar"
|
||||
-spec parse_long_option([option_spec()], [option()], [string()], integer(), [string()], string(), string()) ->
|
||||
{ok, {[option()], [string()]}}.
|
||||
parse_long_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) ->
|
||||
case split_assigned_arg(OptArg) of
|
||||
{Long, Arg} ->
|
||||
%% Get option that has its argument within the same string
|
||||
%% separated by an equal ('=') character (e.g. "--port=1000").
|
||||
parse_long_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg);
|
||||
|
||||
Long ->
|
||||
case lists:keyfind(Long, ?OPT_LONG, OptSpecList) of
|
||||
{Name, _Short, Long, undefined, _Help} ->
|
||||
parse(OptSpecList, [Name | OptAcc], ArgAcc, ArgPos, Args);
|
||||
|
||||
{_Name, _Short, Long, _ArgSpec, _Help} = OptSpec ->
|
||||
%% The option argument string is empty, but the option requires
|
||||
%% an argument, so we look into the next string in the list.
|
||||
%% e.g ["--port", "1000"]
|
||||
parse_long_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec);
|
||||
false ->
|
||||
throw({error, {invalid_option, OptStr}})
|
||||
end
|
||||
end.
|
||||
|
||||
|
||||
%% @doc Parse an option where the argument is 'assigned' in the same string using
|
||||
%% the '=' character, add it to the option accumulator and continue parsing the
|
||||
%% rest of the arguments recursively. This syntax is only valid for long options.
|
||||
-spec parse_long_option_assigned_arg([option_spec()], [option()], [string()], integer(),
|
||||
[string()], string(), string(), string()) ->
|
||||
{ok, {[option()], [string()]}}.
|
||||
parse_long_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg) ->
|
||||
case lists:keyfind(Long, ?OPT_LONG, OptSpecList) of
|
||||
{_Name, _Short, Long, ArgSpec, _Help} = OptSpec ->
|
||||
case ArgSpec of
|
||||
undefined ->
|
||||
throw({error, {invalid_option_arg, OptStr}});
|
||||
_ ->
|
||||
parse(OptSpecList, add_option_with_assigned_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos, Args)
|
||||
end;
|
||||
false ->
|
||||
throw({error, {invalid_option, OptStr}})
|
||||
end.
|
||||
|
||||
|
||||
%% @doc Split an option string that may contain an option with its argument
|
||||
%% separated by an equal ('=') character (e.g. "port=1000").
|
||||
-spec split_assigned_arg(string()) -> {Name :: string(), Arg :: string()} | string().
|
||||
split_assigned_arg(OptStr) ->
|
||||
split_assigned_arg(OptStr, OptStr, []).
|
||||
|
||||
split_assigned_arg(_OptStr, "=" ++ Tail, Acc) ->
|
||||
{lists:reverse(Acc), Tail};
|
||||
split_assigned_arg(OptStr, [Char | Tail], Acc) ->
|
||||
split_assigned_arg(OptStr, Tail, [Char | Acc]);
|
||||
split_assigned_arg(OptStr, [], _Acc) ->
|
||||
OptStr.
|
||||
|
||||
|
||||
%% @doc Retrieve the argument for an option from the next string in the list of
|
||||
%% command-line parameters or set the value of the argument from the argument
|
||||
%% specification (for boolean and integer arguments), if possible.
|
||||
parse_long_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec) ->
|
||||
ArgSpecType = arg_spec_type(ArgSpec),
|
||||
case Args =:= [] orelse is_implicit_arg(ArgSpecType, hd(Args)) of
|
||||
true ->
|
||||
parse(OptSpecList, add_option_with_implicit_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args);
|
||||
false ->
|
||||
[Arg | Tail] = Args,
|
||||
try
|
||||
parse(OptSpecList, [{Name, to_type(ArgSpecType, Arg)} | OptAcc], ArgAcc, ArgPos, Tail)
|
||||
catch
|
||||
error:_ ->
|
||||
throw({error, {invalid_option_arg, {Name, Arg}}})
|
||||
end
|
||||
end.
|
||||
|
||||
|
||||
%% @doc Parse a short option, add it to the option accumulator and continue
|
||||
%% parsing the rest of the arguments recursively.
|
||||
%% A short option can have the following syntax:
|
||||
%% -a Single option 'a', no argument or implicit boolean argument
|
||||
%% -a foo Single option 'a', argument "foo"
|
||||
%% -afoo Single option 'a', argument "foo"
|
||||
%% -abc Multiple options: 'a'; 'b'; 'c'
|
||||
%% -bcafoo Multiple options: 'b'; 'c'; 'a' with argument "foo"
|
||||
%% -aaa Multiple repetitions of option 'a' (only valid for options with integer arguments)
|
||||
-spec parse_short_option([option_spec()], [option()], [string()], integer(), [string()], string(), string()) ->
|
||||
{ok, {[option()], [string()]}}.
|
||||
parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) ->
|
||||
parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, first, OptArg).
|
||||
|
||||
parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptPos, [Short | Arg]) ->
|
||||
case lists:keyfind(Short, ?OPT_SHORT, OptSpecList) of
|
||||
{Name, Short, _Long, undefined, _Help} ->
|
||||
parse_short_option(OptSpecList, [Name | OptAcc], ArgAcc, ArgPos, Args, OptStr, first, Arg);
|
||||
|
||||
{_Name, Short, _Long, ArgSpec, _Help} = OptSpec ->
|
||||
%% The option has a specification, so it requires an argument.
|
||||
case Arg of
|
||||
[] ->
|
||||
%% The option argument string is empty, but the option requires
|
||||
%% an argument, so we look into the next string in the list.
|
||||
parse_short_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec, OptPos);
|
||||
|
||||
_ ->
|
||||
case is_valid_arg(ArgSpec, Arg) of
|
||||
true ->
|
||||
parse(OptSpecList, add_option_with_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos, Args);
|
||||
_ ->
|
||||
NewOptAcc = case OptPos of
|
||||
first -> add_option_with_implicit_arg(OptSpec, OptAcc);
|
||||
_ -> add_option_with_implicit_incrementable_arg(OptSpec, OptAcc)
|
||||
end,
|
||||
parse_short_option(OptSpecList, NewOptAcc, ArgAcc, ArgPos, Args, OptStr, next, Arg)
|
||||
end
|
||||
end;
|
||||
|
||||
false ->
|
||||
throw({error, {invalid_option, OptStr}})
|
||||
end;
|
||||
parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, _OptStr, _OptPos, []) ->
|
||||
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, Args).
|
||||
|
||||
|
||||
%% @doc Retrieve the argument for an option from the next string in the list of
|
||||
%% command-line parameters or set the value of the argument from the argument
|
||||
%% specification (for boolean and integer arguments), if possible.
|
||||
parse_short_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec, OptPos) ->
|
||||
case Args =:= [] orelse is_implicit_arg(ArgSpec, hd(Args)) of
|
||||
true when OptPos =:= first ->
|
||||
parse(OptSpecList, add_option_with_implicit_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args);
|
||||
true ->
|
||||
parse(OptSpecList, add_option_with_implicit_incrementable_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args);
|
||||
false ->
|
||||
[Arg | Tail] = Args,
|
||||
try
|
||||
parse(OptSpecList, [{Name, to_type(ArgSpec, Arg)} | OptAcc], ArgAcc, ArgPos, Tail)
|
||||
catch
|
||||
error:_ ->
|
||||
throw({error, {invalid_option_arg, {Name, Arg}}})
|
||||
end
|
||||
end.
|
||||
|
||||
|
||||
%% @doc Find the option for the discrete argument in position specified in the
|
||||
%% Pos argument.
|
||||
-spec find_non_option_arg([option_spec()], integer()) -> {value, option_spec()} | false.
|
||||
find_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} = OptSpec | _Tail], 0) ->
|
||||
{value, OptSpec};
|
||||
find_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} | Tail], Pos) ->
|
||||
find_non_option_arg(Tail, Pos - 1);
|
||||
find_non_option_arg([_Head | Tail], Pos) ->
|
||||
find_non_option_arg(Tail, Pos);
|
||||
find_non_option_arg([], _Pos) ->
|
||||
false.
|
||||
|
||||
|
||||
%% @doc Append options that were not present in the command line arguments with
|
||||
%% their default arguments.
|
||||
-spec append_default_options([option_spec()], [option()]) -> [option()].
|
||||
append_default_options([{Name, _Short, _Long, {_Type, DefaultArg}, _Help} | Tail], OptAcc) ->
|
||||
append_default_options(Tail,
|
||||
case lists:keymember(Name, 1, OptAcc) of
|
||||
false ->
|
||||
[{Name, DefaultArg} | OptAcc];
|
||||
_ ->
|
||||
OptAcc
|
||||
end);
|
||||
%% For options with no default argument.
|
||||
append_default_options([_Head | Tail], OptAcc) ->
|
||||
append_default_options(Tail, OptAcc);
|
||||
append_default_options([], OptAcc) ->
|
||||
OptAcc.
|
||||
|
||||
|
||||
%% @doc Add an option with argument converting it to the data type indicated by the
|
||||
%% argument specification.
|
||||
-spec add_option_with_arg(option_spec(), string(), [option()]) -> [option()].
|
||||
add_option_with_arg({Name, _Short, _Long, ArgSpec, _Help} = OptSpec, Arg, OptAcc) ->
|
||||
case is_valid_arg(ArgSpec, Arg) of
|
||||
true ->
|
||||
try
|
||||
[{Name, to_type(ArgSpec, Arg)} | OptAcc]
|
||||
catch
|
||||
error:_ ->
|
||||
throw({error, {invalid_option_arg, {Name, Arg}}})
|
||||
end;
|
||||
false ->
|
||||
add_option_with_implicit_arg(OptSpec, OptAcc)
|
||||
end.
|
||||
|
||||
|
||||
%% @doc Add an option with argument that was part of an assignment expression
|
||||
%% (e.g. "--verbose=3") converting it to the data type indicated by the
|
||||
%% argument specification.
|
||||
-spec add_option_with_assigned_arg(option_spec(), string(), [option()]) -> [option()].
|
||||
add_option_with_assigned_arg({Name, _Short, _Long, ArgSpec, _Help}, Arg, OptAcc) ->
|
||||
try
|
||||
[{Name, to_type(ArgSpec, Arg)} | OptAcc]
|
||||
catch
|
||||
error:_ ->
|
||||
throw({error, {invalid_option_arg, {Name, Arg}}})
|
||||
end.
|
||||
|
||||
|
||||
%% @doc Add an option that required an argument but did not have one. Some data
|
||||
%% types (boolean, integer) allow implicit or assumed arguments.
|
||||
-spec add_option_with_implicit_arg(option_spec(), [option()]) -> [option()].
|
||||
add_option_with_implicit_arg({Name, _Short, _Long, ArgSpec, _Help}, OptAcc) ->
|
||||
case arg_spec_type(ArgSpec) of
|
||||
boolean ->
|
||||
%% Special case for boolean arguments: if there is no argument we
|
||||
%% set the value to 'true'.
|
||||
[{Name, true} | OptAcc];
|
||||
integer ->
|
||||
%% Special case for integer arguments: if the option had not been set
|
||||
%% before we set the value to 1. This is needed to support options like
|
||||
%% "-v" to return something like {verbose, 1}.
|
||||
[{Name, 1} | OptAcc];
|
||||
_ ->
|
||||
throw({error, {missing_option_arg, Name}})
|
||||
end.
|
||||
|
||||
|
||||
%% @doc Add an option with an implicit or assumed argument.
|
||||
-spec add_option_with_implicit_incrementable_arg(option_spec() | arg_spec(), [option()]) -> [option()].
|
||||
add_option_with_implicit_incrementable_arg({Name, _Short, _Long, ArgSpec, _Help}, OptAcc) ->
|
||||
case arg_spec_type(ArgSpec) of
|
||||
boolean ->
|
||||
%% Special case for boolean arguments: if there is no argument we
|
||||
%% set the value to 'true'.
|
||||
[{Name, true} | OptAcc];
|
||||
integer ->
|
||||
%% Special case for integer arguments: if the option had not been set
|
||||
%% before we set the value to 1; if not we increment the previous value
|
||||
%% the option had. This is needed to support options like "-vvv" to
|
||||
%% return something like {verbose, 3}.
|
||||
case OptAcc of
|
||||
[{Name, Count} | Tail] ->
|
||||
[{Name, Count + 1} | Tail];
|
||||
_ ->
|
||||
[{Name, 1} | OptAcc]
|
||||
end;
|
||||
_ ->
|
||||
throw({error, {missing_option_arg, Name}})
|
||||
end.
|
||||
|
||||
|
||||
%% @doc Retrieve the data type form an argument specification.
|
||||
-spec arg_spec_type(arg_spec()) -> arg_type() | undefined.
|
||||
arg_spec_type({Type, _DefaultArg}) ->
|
||||
Type;
|
||||
arg_spec_type(Type) when is_atom(Type) ->
|
||||
Type.
|
||||
|
||||
|
||||
%% @doc Convert an argument string to its corresponding data type.
|
||||
-spec to_type(arg_spec() | arg_type(), string()) -> arg_value().
|
||||
to_type({Type, _DefaultArg}, Arg) ->
|
||||
to_type(Type, Arg);
|
||||
to_type(binary, Arg) ->
|
||||
list_to_binary(Arg);
|
||||
to_type(atom, Arg) ->
|
||||
list_to_atom(Arg);
|
||||
to_type(integer, Arg) ->
|
||||
list_to_integer(Arg);
|
||||
to_type(float, Arg) ->
|
||||
list_to_float(Arg);
|
||||
to_type(boolean, Arg) ->
|
||||
LowerArg = lowercase(Arg),
|
||||
case is_arg_true(LowerArg) of
|
||||
true ->
|
||||
true;
|
||||
_ ->
|
||||
case is_arg_false(LowerArg) of
|
||||
true ->
|
||||
false;
|
||||
false ->
|
||||
erlang:error(badarg)
|
||||
end
|
||||
end;
|
||||
to_type(_Type, Arg) ->
|
||||
Arg.
|
||||
|
||||
|
||||
-spec is_arg_true(string()) -> boolean().
|
||||
is_arg_true(Arg) ->
|
||||
(Arg =:= "true") orelse (Arg =:= "t") orelse
|
||||
(Arg =:= "yes") orelse (Arg =:= "y") orelse
|
||||
(Arg =:= "on") orelse (Arg =:= "enabled") orelse
|
||||
(Arg =:= "1").
|
||||
|
||||
|
||||
-spec is_arg_false(string()) -> boolean().
|
||||
is_arg_false(Arg) ->
|
||||
(Arg =:= "false") orelse (Arg =:= "f") orelse
|
||||
(Arg =:= "no") orelse (Arg =:= "n") orelse
|
||||
(Arg =:= "off") orelse (Arg =:= "disabled") orelse
|
||||
(Arg =:= "0").
|
||||
|
||||
|
||||
-spec is_valid_arg(arg_spec(), nonempty_string()) -> boolean().
|
||||
is_valid_arg({Type, _DefaultArg}, Arg) ->
|
||||
is_valid_arg(Type, Arg);
|
||||
is_valid_arg(boolean, Arg) ->
|
||||
is_boolean_arg(Arg);
|
||||
is_valid_arg(integer, Arg) ->
|
||||
is_non_neg_integer_arg(Arg);
|
||||
is_valid_arg(float, Arg) ->
|
||||
is_non_neg_float_arg(Arg);
|
||||
is_valid_arg(_Type, _Arg) ->
|
||||
true.
|
||||
|
||||
|
||||
-spec is_implicit_arg(arg_spec(), nonempty_string()) -> boolean().
|
||||
is_implicit_arg({Type, _DefaultArg}, Arg) ->
|
||||
is_implicit_arg(Type, Arg);
|
||||
is_implicit_arg(boolean, Arg) ->
|
||||
not is_boolean_arg(Arg);
|
||||
is_implicit_arg(integer, Arg) ->
|
||||
not is_integer_arg(Arg);
|
||||
is_implicit_arg(_Type, _Arg) ->
|
||||
false.
|
||||
|
||||
|
||||
-spec is_boolean_arg(string()) -> boolean().
|
||||
is_boolean_arg(Arg) ->
|
||||
LowerArg = lowercase(Arg),
|
||||
is_arg_true(LowerArg) orelse is_arg_false(LowerArg).
|
||||
|
||||
|
||||
-spec is_integer_arg(string()) -> boolean().
|
||||
is_integer_arg("-" ++ Tail) ->
|
||||
is_non_neg_integer_arg(Tail);
|
||||
is_integer_arg(Arg) ->
|
||||
is_non_neg_integer_arg(Arg).
|
||||
|
||||
|
||||
-spec is_non_neg_integer_arg(string()) -> boolean().
|
||||
is_non_neg_integer_arg([Head | Tail]) when Head >= $0, Head =< $9 ->
|
||||
is_non_neg_integer_arg(Tail);
|
||||
is_non_neg_integer_arg([_Head | _Tail]) ->
|
||||
false;
|
||||
is_non_neg_integer_arg([]) ->
|
||||
true.
|
||||
|
||||
|
||||
-spec is_non_neg_float_arg(string()) -> boolean().
|
||||
is_non_neg_float_arg([Head | Tail]) when (Head >= $0 andalso Head =< $9) orelse Head =:= $. ->
|
||||
is_non_neg_float_arg(Tail);
|
||||
is_non_neg_float_arg([_Head | _Tail]) ->
|
||||
false;
|
||||
is_non_neg_float_arg([]) ->
|
||||
true.
|
||||
|
||||
|
||||
%% @doc Show a message on standard_error indicating the command line options and
|
||||
%% arguments that are supported by the program.
|
||||
-spec usage([option_spec()], string()) -> ok.
|
||||
usage(OptSpecList, ProgramName) ->
|
||||
usage(OptSpecList, ProgramName, standard_error).
|
||||
|
||||
|
||||
%% @doc Show a message on standard_error or standard_io indicating the command line options and
|
||||
%% arguments that are supported by the program.
|
||||
-spec usage([option_spec()], string(), output_stream() | string()) -> ok.
|
||||
usage(OptSpecList, ProgramName, OutputStream) when is_atom(OutputStream) ->
|
||||
io:format(OutputStream, "~ts~n~n~ts~n",
|
||||
[unicode:characters_to_list(usage_cmd_line(ProgramName, OptSpecList)), unicode:characters_to_list(usage_options(OptSpecList))]);
|
||||
%% @doc Show a message on standard_error indicating the command line options and
|
||||
%% arguments that are supported by the program. The CmdLineTail argument
|
||||
%% is a string that is added to the end of the usage command line.
|
||||
usage(OptSpecList, ProgramName, CmdLineTail) ->
|
||||
usage(OptSpecList, ProgramName, CmdLineTail, standard_error).
|
||||
|
||||
|
||||
%% @doc Show a message on standard_error or standard_io indicating the command line options and
|
||||
%% arguments that are supported by the program. The CmdLineTail argument
|
||||
%% is a string that is added to the end of the usage command line.
|
||||
-spec usage([option_spec()], ProgramName :: string(), CmdLineTail :: string(), output_stream() | [{string(), string()}]) -> ok.
|
||||
usage(OptSpecList, ProgramName, CmdLineTail, OutputStream) when is_atom(OutputStream) ->
|
||||
io:format(OutputStream, "~ts~n~n~ts~n",
|
||||
[unicode:characters_to_list(usage_cmd_line(ProgramName, OptSpecList, CmdLineTail)), unicode:characters_to_list(usage_options(OptSpecList))]);
|
||||
%% @doc Show a message on standard_error indicating the command line options and
|
||||
%% arguments that are supported by the program. The CmdLineTail and OptionsTail
|
||||
%% arguments are a string that is added to the end of the usage command line
|
||||
%% and a list of tuples that are added to the end of the options' help lines.
|
||||
usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail) ->
|
||||
usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail, standard_error).
|
||||
|
||||
|
||||
%% @doc Show a message on standard_error or standard_io indicating the command line options and
|
||||
%% arguments that are supported by the program. The CmdLineTail and OptionsTail
|
||||
%% arguments are a string that is added to the end of the usage command line
|
||||
%% and a list of tuples that are added to the end of the options' help lines.
|
||||
-spec usage([option_spec()], ProgramName :: string(), CmdLineTail :: string(),
|
||||
[{OptionName :: string(), Help :: string()}], output_stream()) -> ok.
|
||||
usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail, OutputStream) ->
|
||||
io:format(OutputStream, "~ts~n~n~ts~n",
|
||||
[unicode:characters_to_list(usage_cmd_line(ProgramName, OptSpecList, CmdLineTail)), unicode:characters_to_list(usage_options(OptSpecList, OptionsTail))]).
|
||||
|
||||
%% @doc Show a message on standard_error or standard_io indicating the
|
||||
%% command line options and arguments that are supported by the
|
||||
%% program. The Description allows for structured command line usage
|
||||
%% that works in addition to the standard options, and appears between
|
||||
%% the usage_cmd_line and usage_options sections. The CmdLineTail and
|
||||
%% OptionsTail arguments are a string that is added to the end of the
|
||||
%% usage command line and a list of tuples that are added to the end of
|
||||
%% the options' help lines.
|
||||
-spec usage([option_spec()], ProgramName :: string(), CmdLineTail :: string(),
|
||||
Description :: string(),
|
||||
[{OptionName :: string(), Help :: string()}],
|
||||
output_stream()) -> ok.
|
||||
usage(OptSpecList, ProgramName, CmdLineTail, Description, OptionsTail, OutputStream) ->
|
||||
io:format(OutputStream, "~ts~n~n~ts~n~n~ts~n",
|
||||
[unicode:characters_to_list(usage_cmd_line(ProgramName, OptSpecList, CmdLineTail)), Description, unicode:characters_to_list(usage_options(OptSpecList, OptionsTail))]).
|
||||
|
||||
|
||||
-spec usage_cmd_line(ProgramName :: string(), [option_spec()]) -> iolist().
|
||||
usage_cmd_line(ProgramName, OptSpecList) ->
|
||||
usage_cmd_line(ProgramName, OptSpecList, "").
|
||||
|
||||
-spec usage_cmd_line(ProgramName :: string(), [option_spec()], CmdLineTail :: string()) -> iolist().
|
||||
usage_cmd_line(ProgramName, OptSpecList, CmdLineTail) ->
|
||||
Prefix = "Usage: " ++ ProgramName,
|
||||
PrefixLength = length(Prefix),
|
||||
LineLength = line_length(),
|
||||
%% Only align the command line options after the program name when there is
|
||||
%% enough room to do so (i.e. at least 25 characters). If not, show the
|
||||
%% command line options below the program name with a 2-character indentation.
|
||||
if
|
||||
(LineLength - PrefixLength) > ?MIN_USAGE_COMMAND_LINE_OPTION_LENGTH ->
|
||||
Indentation = lists:duplicate(PrefixLength, $\s),
|
||||
[FirstOptLine | OptLines] = usage_cmd_line_options(LineLength - PrefixLength, OptSpecList, CmdLineTail),
|
||||
IndentedOptLines = [[Indentation | OptLine] || OptLine <- OptLines],
|
||||
[Prefix, FirstOptLine | IndentedOptLines];
|
||||
true ->
|
||||
IndentedOptLines = [[" " | OptLine] || OptLine <- usage_cmd_line_options(LineLength, OptSpecList, CmdLineTail)],
|
||||
[Prefix, $\n, IndentedOptLines]
|
||||
end.
|
||||
|
||||
|
||||
%% @doc Return a list of the lines corresponding to the usage command line
|
||||
%% already wrapped according to the maximum MaxLineLength.
|
||||
-spec usage_cmd_line_options(MaxLineLength :: non_neg_integer(), [option_spec()], CmdLineTail :: string()) -> iolist().
|
||||
usage_cmd_line_options(MaxLineLength, OptSpecList, CmdLineTail) ->
|
||||
usage_cmd_line_options(MaxLineLength, OptSpecList ++ lexemes(CmdLineTail, " "), [], 0, []).
|
||||
|
||||
usage_cmd_line_options(MaxLineLength, [OptSpec | Tail], LineAcc, LineAccLength, Acc) ->
|
||||
Option = [$\s | lists:flatten(usage_cmd_line_option(OptSpec))],
|
||||
OptionLength = length(Option),
|
||||
%% We accumulate the options in LineAcc until its length is over the
|
||||
%% maximum allowed line length. When that happens, we append the line in
|
||||
%% LineAcc to the list with all the lines in the command line (Acc).
|
||||
NewLineAccLength = LineAccLength + OptionLength,
|
||||
if
|
||||
NewLineAccLength < MaxLineLength ->
|
||||
usage_cmd_line_options(MaxLineLength, Tail, [Option | LineAcc], NewLineAccLength, Acc);
|
||||
true ->
|
||||
usage_cmd_line_options(MaxLineLength, Tail, [Option], OptionLength + 1,
|
||||
[lists:reverse([$\n | LineAcc]) | Acc])
|
||||
end;
|
||||
usage_cmd_line_options(MaxLineLength, [], [_ | _] = LineAcc, _LineAccLength, Acc) ->
|
||||
%% If there was a non-empty line in LineAcc when there are no more options
|
||||
%% to process, we add it to the list of lines to return.
|
||||
usage_cmd_line_options(MaxLineLength, [], [], 0, [lists:reverse(LineAcc) | Acc]);
|
||||
usage_cmd_line_options(_MaxLineLength, [], [], _LineAccLength, Acc) ->
|
||||
lists:reverse(Acc).
|
||||
|
||||
|
||||
-spec usage_cmd_line_option(option_spec()) -> string().
|
||||
usage_cmd_line_option({_Name, Short, _Long, undefined, _Help}) when Short =/= undefined ->
|
||||
%% For options with short form and no argument.
|
||||
[$[, $-, Short, $]];
|
||||
usage_cmd_line_option({_Name, _Short, Long, undefined, _Help}) when Long =/= undefined ->
|
||||
%% For options with only long form and no argument.
|
||||
[$[, $-, $-, Long, $]];
|
||||
usage_cmd_line_option({_Name, _Short, _Long, undefined, _Help}) ->
|
||||
[];
|
||||
usage_cmd_line_option({Name, Short, Long, ArgSpec, _Help}) when is_atom(ArgSpec) ->
|
||||
%% For options with no default argument.
|
||||
if
|
||||
%% For options with short form and argument.
|
||||
Short =/= undefined -> [$[, $-, Short, $\s, $<, atom_to_list(Name), $>, $]];
|
||||
%% For options with only long form and argument.
|
||||
Long =/= undefined -> [$[, $-, $-, Long, $\s, $<, atom_to_list(Name), $>, $]];
|
||||
%% For options with neither short nor long form and argument.
|
||||
true -> [$[, $<, atom_to_list(Name), $>, $]]
|
||||
end;
|
||||
usage_cmd_line_option({Name, Short, Long, ArgSpec, _Help}) when is_tuple(ArgSpec) ->
|
||||
%% For options with default argument.
|
||||
if
|
||||
%% For options with short form and default argument.
|
||||
Short =/= undefined -> [$[, $-, Short, $\s, $[, $<, atom_to_list(Name), $>, $], $]];
|
||||
%% For options with only long form and default argument.
|
||||
Long =/= undefined -> [$[, $-, $-, Long, $\s, $[, $<, atom_to_list(Name), $>, $], $]];
|
||||
%% For options with neither short nor long form and default argument.
|
||||
true -> [$[, $<, atom_to_list(Name), $>, $]]
|
||||
end;
|
||||
usage_cmd_line_option(Option) when is_list(Option) ->
|
||||
%% For custom options that are added to the command line.
|
||||
Option.
|
||||
|
||||
|
||||
%% @doc Return a list of help messages to print for each of the options and arguments.
|
||||
-spec usage_options([option_spec()]) -> [string()].
|
||||
usage_options(OptSpecList) ->
|
||||
usage_options(OptSpecList, []).
|
||||
|
||||
|
||||
%% @doc Return a list of usage lines to print for each of the options and arguments.
|
||||
-spec usage_options([option_spec()], [{OptionName :: string(), Help :: string()}]) -> [string()].
|
||||
usage_options(OptSpecList, CustomHelp) ->
|
||||
%% Add the usage lines corresponding to the option specifications.
|
||||
{MaxOptionLength0, UsageLines0} = add_option_spec_help_lines(OptSpecList, 0, []),
|
||||
%% Add the custom usage lines.
|
||||
{MaxOptionLength, UsageLines} = add_custom_help_lines(CustomHelp, MaxOptionLength0, UsageLines0),
|
||||
MaxLineLength = line_length(),
|
||||
lists:reverse([format_usage_line(MaxOptionLength + 1, MaxLineLength, UsageLine) || UsageLine <- UsageLines]).
|
||||
|
||||
|
||||
-spec add_option_spec_help_lines([option_spec()], PrevMaxOptionLength :: non_neg_integer(), [usage_line_with_length()]) ->
|
||||
{MaxOptionLength :: non_neg_integer(), [usage_line_with_length()]}.
|
||||
add_option_spec_help_lines([OptSpec | Tail], PrevMaxOptionLength, Acc) ->
|
||||
OptionText = usage_option_text(OptSpec),
|
||||
HelpText = usage_help_text(OptSpec),
|
||||
{MaxOptionLength, ColsWithLength} = get_max_option_length({OptionText, HelpText}, PrevMaxOptionLength),
|
||||
add_option_spec_help_lines(Tail, MaxOptionLength, [ColsWithLength | Acc]);
|
||||
add_option_spec_help_lines([], MaxOptionLength, Acc) ->
|
||||
{MaxOptionLength, Acc}.
|
||||
|
||||
|
||||
-spec add_custom_help_lines([usage_line()], PrevMaxOptionLength :: non_neg_integer(), [usage_line_with_length()]) ->
|
||||
{MaxOptionLength :: non_neg_integer(), [usage_line_with_length()]}.
|
||||
add_custom_help_lines([CustomCols | Tail], PrevMaxOptionLength, Acc) ->
|
||||
{MaxOptionLength, ColsWithLength} = get_max_option_length(CustomCols, PrevMaxOptionLength),
|
||||
add_custom_help_lines(Tail, MaxOptionLength, [ColsWithLength | Acc]);
|
||||
add_custom_help_lines([], MaxOptionLength, Acc) ->
|
||||
{MaxOptionLength, Acc}.
|
||||
|
||||
|
||||
-spec usage_option_text(option_spec()) -> string().
|
||||
usage_option_text({Name, undefined, undefined, _ArgSpec, _Help}) ->
|
||||
%% Neither short nor long form (non-option argument).
|
||||
"<" ++ atom_to_list(Name) ++ ">";
|
||||
usage_option_text({_Name, Short, undefined, _ArgSpec, _Help}) ->
|
||||
%% Only short form.
|
||||
[$-, Short];
|
||||
usage_option_text({_Name, undefined, Long, _ArgSpec, _Help}) ->
|
||||
%% Only long form.
|
||||
[$-, $- | Long];
|
||||
usage_option_text({_Name, Short, Long, _ArgSpec, _Help}) ->
|
||||
%% Both short and long form.
|
||||
[$-, Short, $,, $\s, $-, $- | Long].
|
||||
|
||||
|
||||
-spec usage_help_text(option_spec()) -> string().
|
||||
usage_help_text({_Name, _Short, _Long, {_ArgType, ArgValue}, [_ | _] = Help}) ->
|
||||
Help ++ " [default: " ++ default_arg_value_to_string(ArgValue) ++ "]";
|
||||
usage_help_text({_Name, _Short, _Long, _ArgSpec, Help}) ->
|
||||
Help.
|
||||
|
||||
|
||||
%% @doc Calculate the maximum width of the column that shows the option's short
|
||||
%% and long form.
|
||||
-spec get_max_option_length(usage_line(), PrevMaxOptionLength :: non_neg_integer()) ->
|
||||
{MaxOptionLength :: non_neg_integer(), usage_line_with_length()}.
|
||||
get_max_option_length({OptionText, HelpText}, PrevMaxOptionLength) ->
|
||||
OptionLength = length(OptionText),
|
||||
{erlang:max(OptionLength, PrevMaxOptionLength), {OptionLength, OptionText, HelpText}}.
|
||||
|
||||
|
||||
%% @doc Format the usage line that is shown for the options' usage. Each usage
|
||||
%% line has 2 columns. The first column shows the options in their short
|
||||
%% and long form. The second column shows the wrapped (if necessary) help
|
||||
%% text lines associated with each option. e.g.:
|
||||
%%
|
||||
%% -h, --host Database server host name or IP address; this is the
|
||||
%% hostname of the server where the database is running
|
||||
%% [default: localhost]
|
||||
%% -p, --port Database server port [default: 1000]
|
||||
%%
|
||||
-spec format_usage_line(MaxOptionLength :: non_neg_integer(), MaxLineLength :: non_neg_integer(),
|
||||
usage_line_with_length()) -> iolist().
|
||||
format_usage_line(MaxOptionLength, MaxLineLength, {OptionLength, OptionText, [_ | _] = HelpText})
|
||||
when MaxOptionLength < (MaxLineLength div 2) ->
|
||||
%% If the width of the column where the options are shown is smaller than
|
||||
%% half the width of a console line then we show the help text line aligned
|
||||
%% next to its corresponding option, with a separation of at least 2
|
||||
%% characters.
|
||||
[Head | Tail] = wrap_text_line(MaxLineLength - MaxOptionLength - 3, HelpText),
|
||||
FirstLineIndentation = lists:duplicate(MaxOptionLength - OptionLength + 1, $\s),
|
||||
Indentation = [$\n | lists:duplicate(MaxOptionLength + 3, $\s)],
|
||||
[" ", OptionText, FirstLineIndentation, Head,
|
||||
[[Indentation, Line] || Line <- Tail], $\n];
|
||||
format_usage_line(_MaxOptionLength, MaxLineLength, {_OptionLength, OptionText, [_ | _] = HelpText}) ->
|
||||
%% If the width of the first column is bigger than the width of a console
|
||||
%% line, we show the help text on the next line with an indentation of 6
|
||||
%% characters.
|
||||
HelpLines = wrap_text_line(MaxLineLength - 6, HelpText),
|
||||
[" ", OptionText, [["\n ", Line] || Line <- HelpLines], $\n];
|
||||
format_usage_line(_MaxOptionLength, _MaxLineLength, {_OptionLength, OptionText, _HelpText}) ->
|
||||
[" ", OptionText, $\n].
|
||||
|
||||
|
||||
%% @doc Wrap a text line converting it into several text lines so that the
|
||||
%% length of each one of them is never over Length characters.
|
||||
-spec wrap_text_line(Length :: non_neg_integer(), Text :: string()) -> [string()].
|
||||
wrap_text_line(Length, Text) ->
|
||||
wrap_text_line(Length, Text, [], 0, []).
|
||||
|
||||
wrap_text_line(Length, [Char | Tail], Acc, Count, CurrentLineAcc) when Count < Length ->
|
||||
wrap_text_line(Length, Tail, Acc, Count + 1, [Char | CurrentLineAcc]);
|
||||
wrap_text_line(Length, [_ | _] = Help, Acc, Count, CurrentLineAcc) ->
|
||||
%% Look for the first whitespace character in the current (reversed) line
|
||||
%% buffer to get a wrapped line. If there is no whitespace just cut the
|
||||
%% line at the position corresponding to the maximum length.
|
||||
{NextLineAcc, WrappedLine} = case cspan(CurrentLineAcc, " \t") of
|
||||
WhitespacePos when WhitespacePos < Count ->
|
||||
lists:split(WhitespacePos, CurrentLineAcc);
|
||||
_ ->
|
||||
{[], CurrentLineAcc}
|
||||
end,
|
||||
wrap_text_line(Length, Help, [lists:reverse(WrappedLine) | Acc], length(NextLineAcc), NextLineAcc);
|
||||
wrap_text_line(_Length, [], Acc, _Count, [_ | _] = CurrentLineAcc) ->
|
||||
%% If there was a non-empty line when we reached the buffer, add it to the accumulator
|
||||
lists:reverse([lists:reverse(CurrentLineAcc) | Acc]);
|
||||
wrap_text_line(_Length, [], Acc, _Count, _CurrentLineAcc) ->
|
||||
lists:reverse(Acc).
|
||||
|
||||
|
||||
default_arg_value_to_string(Value) when is_atom(Value) ->
|
||||
atom_to_list(Value);
|
||||
default_arg_value_to_string(Value) when is_binary(Value) ->
|
||||
binary_to_list(Value);
|
||||
default_arg_value_to_string(Value) when is_integer(Value) ->
|
||||
integer_to_list(Value);
|
||||
default_arg_value_to_string(Value) when is_float(Value) ->
|
||||
lists:flatten(io_lib:format("~w", [Value]));
|
||||
default_arg_value_to_string(Value) ->
|
||||
Value.
|
||||
|
||||
|
||||
%% @doc Tokenize a command line string with support for single and double
|
||||
%% quoted arguments (needed for arguments that have embedded whitespace).
|
||||
%% The function also supports the expansion of environment variables in
|
||||
%% both the Unix (${VAR}; $VAR) and Windows (%VAR%) formats. It does NOT
|
||||
%% support wildcard expansion of paths.
|
||||
-spec tokenize(CmdLine :: string()) -> [nonempty_string()].
|
||||
tokenize(CmdLine) ->
|
||||
tokenize(CmdLine, [], []).
|
||||
|
||||
-spec tokenize(CmdLine :: string(), Acc :: [string()], ArgAcc :: string()) -> [string()].
|
||||
tokenize([Sep | Tail], Acc, ArgAcc) when ?IS_WHITESPACE(Sep) ->
|
||||
NewAcc = case ArgAcc of
|
||||
[_ | _] ->
|
||||
%% Found separator: add to the list of arguments.
|
||||
[lists:reverse(ArgAcc) | Acc];
|
||||
[] ->
|
||||
%% Found separator with no accumulated argument; discard it.
|
||||
Acc
|
||||
end,
|
||||
tokenize(Tail, NewAcc, []);
|
||||
tokenize([QuotationMark | Tail], Acc, ArgAcc) when QuotationMark =:= $"; QuotationMark =:= $' ->
|
||||
%% Quoted argument (might contain spaces, tabs, etc.)
|
||||
tokenize_quoted_arg(QuotationMark, Tail, Acc, ArgAcc);
|
||||
tokenize([Char | _Tail] = CmdLine, Acc, ArgAcc) when Char =:= $$; Char =:= $% ->
|
||||
%% Unix and Windows environment variable expansion: ${VAR}; $VAR; %VAR%
|
||||
{NewCmdLine, Var} = expand_env_var(CmdLine),
|
||||
tokenize(NewCmdLine, Acc, lists:reverse(Var, ArgAcc));
|
||||
tokenize([$\\, Char | Tail], Acc, ArgAcc) ->
|
||||
%% Escaped char.
|
||||
tokenize(Tail, Acc, [Char | ArgAcc]);
|
||||
tokenize([Char | Tail], Acc, ArgAcc) ->
|
||||
tokenize(Tail, Acc, [Char | ArgAcc]);
|
||||
tokenize([], Acc, []) ->
|
||||
lists:reverse(Acc);
|
||||
tokenize([], Acc, ArgAcc) ->
|
||||
lists:reverse([lists:reverse(ArgAcc) | Acc]).
|
||||
|
||||
-spec tokenize_quoted_arg(QuotationMark :: char(), CmdLine :: string(), Acc :: [string()], ArgAcc :: string()) -> [string()].
|
||||
tokenize_quoted_arg(QuotationMark, [QuotationMark | Tail], Acc, ArgAcc) ->
|
||||
%% End of quoted argument
|
||||
tokenize(Tail, Acc, ArgAcc);
|
||||
tokenize_quoted_arg(QuotationMark, [$\\, Char | Tail], Acc, ArgAcc) ->
|
||||
%% Escaped char.
|
||||
tokenize_quoted_arg(QuotationMark, Tail, Acc, [Char | ArgAcc]);
|
||||
tokenize_quoted_arg($" = QuotationMark, [Char | _Tail] = CmdLine, Acc, ArgAcc) when Char =:= $$; Char =:= $% ->
|
||||
%% Unix and Windows environment variable expansion (only for double-quoted arguments): ${VAR}; $VAR; %VAR%
|
||||
{NewCmdLine, Var} = expand_env_var(CmdLine),
|
||||
tokenize_quoted_arg(QuotationMark, NewCmdLine, Acc, lists:reverse(Var, ArgAcc));
|
||||
tokenize_quoted_arg(QuotationMark, [Char | Tail], Acc, ArgAcc) ->
|
||||
tokenize_quoted_arg(QuotationMark, Tail, Acc, [Char | ArgAcc]);
|
||||
tokenize_quoted_arg(_QuotationMark, CmdLine, Acc, ArgAcc) ->
|
||||
tokenize(CmdLine, Acc, ArgAcc).
|
||||
|
||||
|
||||
-spec expand_env_var(CmdLine :: nonempty_string()) -> {string(), string()}.
|
||||
expand_env_var(CmdLine) ->
|
||||
case CmdLine of
|
||||
"${" ++ Tail ->
|
||||
expand_env_var("${", $}, Tail, []);
|
||||
"$" ++ Tail ->
|
||||
expand_env_var("$", Tail, []);
|
||||
"%" ++ Tail ->
|
||||
expand_env_var("%", $%, Tail, [])
|
||||
end.
|
||||
|
||||
-spec expand_env_var(Prefix :: string(), EndMark :: char(), CmdLine :: string(), Acc :: string()) -> {string(), string()}.
|
||||
expand_env_var(Prefix, EndMark, [Char | Tail], Acc)
|
||||
when (Char >= $A andalso Char =< $Z) orelse (Char >= $a andalso Char =< $z) orelse
|
||||
(Char >= $0 andalso Char =< $9) orelse (Char =:= $_) ->
|
||||
expand_env_var(Prefix, EndMark, Tail, [Char | Acc]);
|
||||
expand_env_var(Prefix, EndMark, [EndMark | Tail], Acc) ->
|
||||
{Tail, get_env_var(Prefix, [EndMark], Acc)};
|
||||
expand_env_var(Prefix, _EndMark, CmdLine, Acc) ->
|
||||
{CmdLine, Prefix ++ lists:reverse(Acc)}.
|
||||
|
||||
|
||||
-spec expand_env_var(Prefix :: string(), CmdLine :: string(), Acc :: string()) -> {string(), string()}.
|
||||
expand_env_var(Prefix, [Char | Tail], Acc)
|
||||
when (Char >= $A andalso Char =< $Z) orelse (Char >= $a andalso Char =< $z) orelse
|
||||
(Char >= $0 andalso Char =< $9) orelse (Char =:= $_) ->
|
||||
expand_env_var(Prefix, Tail, [Char | Acc]);
|
||||
expand_env_var(Prefix, CmdLine, Acc) ->
|
||||
{CmdLine, get_env_var(Prefix, "", Acc)}.
|
||||
|
||||
|
||||
-spec get_env_var(Prefix :: string(), Suffix :: string(), Acc :: string()) -> string().
|
||||
get_env_var(Prefix, Suffix, [_ | _] = Acc) ->
|
||||
Name = lists:reverse(Acc),
|
||||
%% Only expand valid/existing variables.
|
||||
case os:getenv(Name) of
|
||||
false -> Prefix ++ Name ++ Suffix;
|
||||
Value -> Value
|
||||
end;
|
||||
get_env_var(Prefix, Suffix, []) ->
|
||||
Prefix ++ Suffix.
|
||||
|
||||
|
||||
-spec line_length() -> 0..?LINE_LENGTH.
|
||||
line_length() ->
|
||||
case io:columns() of
|
||||
{ok, Columns} when Columns < ?LINE_LENGTH ->
|
||||
Columns - 1;
|
||||
_ ->
|
||||
?LINE_LENGTH
|
||||
end.
|
||||
|
||||
|
||||
-spec to_string(term()) -> string().
|
||||
to_string(List) when is_list(List) ->
|
||||
case io_lib:printable_list(List) of
|
||||
true -> List;
|
||||
false -> io_lib:format("~p", [List])
|
||||
end;
|
||||
to_string(Atom) when is_atom(Atom) ->
|
||||
atom_to_list(Atom);
|
||||
to_string(Value) ->
|
||||
io_lib:format("~p", [Value]).
|
||||
|
||||
%% OTP-20/21 conversion to unicode string module
|
||||
-ifdef(unicode_str).
|
||||
lowercase(Str) -> string:lowercase(Str).
|
||||
lexemes(Str, Separators) -> string:lexemes(Str, Separators).
|
||||
cspan(Str, Chars) -> length(element(1,string:take(Str, Chars, true))).
|
||||
-else.
|
||||
lowercase(Str) -> string:to_lower(Str).
|
||||
lexemes(Str, Separators) -> string:tokens(Str, Separators).
|
||||
cspan(Str, Chars) -> string:cspan(Str, Chars).
|
||||
-endif.
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,9 @@
|
|||
providers
|
||||
=====
|
||||
|
||||
An Erlang providers library.
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
$ rebar compile
|
|
@ -0,0 +1,7 @@
|
|||
%% This is the default form of error messages for the systems using
|
||||
%% providers. It is expected that everything that returns an error use
|
||||
%% this and that they all expose a format_error/2 message that returns
|
||||
%% an iolist and any changes to state it needs to make on error.
|
||||
|
||||
-define(PRV_ERROR(Reason),
|
||||
{error, {?MODULE, Reason}}).
|
|
@ -0,0 +1,5 @@
|
|||
{erl_opts, [{platform_define, "R14", no_callback_support}
|
||||
,debug_info]}.
|
||||
{deps, [{getopt, "1.0.1"}]}.
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{"1.1.0",
|
||||
[{<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},0}]}.
|
||||
[
|
||||
{pkg_hash,[
|
||||
{<<"getopt">>, <<"C73A9FA687B217F2FF79F68A3B637711BB1936E712B521D8CE466B29CBF7808A">>}]}
|
||||
].
|
|
@ -0,0 +1,25 @@
|
|||
-module(provider).
|
||||
|
||||
-export([]).
|
||||
|
||||
-ifdef(no_callback_support).
|
||||
|
||||
%% In the case where R14 or lower is being used to compile the system
|
||||
%% we need to export a behaviour info
|
||||
-export([behaviour_info/1]).
|
||||
|
||||
-spec behaviour_info(atom()) -> [{atom(), arity()}] | undefined.
|
||||
behaviour_info(callbacks) ->
|
||||
[{init, 1},
|
||||
{do, 1},
|
||||
{format_error, 1}];
|
||||
behaviour_info(_) ->
|
||||
undefined.
|
||||
|
||||
-else.
|
||||
|
||||
-callback init(any()) -> {ok, any()}.
|
||||
-callback do(any()) -> {ok, any()} | {error, string()} | {error, {module(), any()}}.
|
||||
-callback format_error(any()) -> iolist().
|
||||
|
||||
-endif.
|
|
@ -0,0 +1,7 @@
|
|||
{application,providers,
|
||||
[{description,"Providers provider."},
|
||||
{vsn,"1.8.1"},
|
||||
{registered,[]},
|
||||
{applications,[kernel,stdlib,getopt]},
|
||||
{licenses,["Apache 2.0"]},
|
||||
{links,[{"Github","https://github.com/tsloughter/providers"}]}]}.
|
|
@ -0,0 +1,327 @@
|
|||
-module(providers).
|
||||
|
||||
%% API
|
||||
-export([create/1,
|
||||
new/2,
|
||||
do/2,
|
||||
profiles/1,
|
||||
namespace/1,
|
||||
module/1,
|
||||
impl/1,
|
||||
opts/1,
|
||||
desc/1,
|
||||
process_deps/2,
|
||||
get_provider/2,
|
||||
get_provider/3,
|
||||
get_provider_by_module/2,
|
||||
get_providers_by_namespace/2,
|
||||
get_target_providers/2,
|
||||
get_target_providers/3,
|
||||
hooks/1,
|
||||
hooks/2,
|
||||
help/1,
|
||||
help/2,
|
||||
help/3,
|
||||
format_error/1,
|
||||
format_error/2,
|
||||
format/1]).
|
||||
|
||||
-export_type([t/0]).
|
||||
|
||||
-include("providers.hrl").
|
||||
|
||||
%%%===================================================================
|
||||
%%% Types
|
||||
%%%===================================================================
|
||||
|
||||
-record(provider, { name :: atom(), % The 'user friendly' name of the task
|
||||
module :: module(), % The module implementation of the task
|
||||
hooks :: {list(), list()},
|
||||
bare :: boolean(), % Indicates whether task can be run by user
|
||||
deps :: [atom()], % The list of dependencies
|
||||
desc :: string(), % The description for the task
|
||||
short_desc :: string(), % A one line short description of the task
|
||||
example :: string() | undefined, % An example of the task usage
|
||||
opts :: list(), % The list of options that the task requires/understands
|
||||
profiles :: [atom()], % Profile to use for provider
|
||||
namespace=default :: atom() % namespace the provider is registered in
|
||||
}).
|
||||
|
||||
-type t() :: #provider{}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc create a new provider object from the specified module. The
|
||||
%% module should implement the provider behaviour.
|
||||
%%
|
||||
%% @param ModuleName The module name.
|
||||
%% @param State0 The current state of the system
|
||||
-spec new(module(), any()) -> {ok, any()} | {error, string()}.
|
||||
new(ModuleName, State) when is_atom(ModuleName) ->
|
||||
case code:which(ModuleName) of
|
||||
non_existing ->
|
||||
{error, io_lib:format("Module ~p does not exist.", [ModuleName])};
|
||||
_ ->
|
||||
ModuleName:init(State)
|
||||
end.
|
||||
|
||||
-spec create(list()) -> t().
|
||||
create(Attrs) ->
|
||||
#provider{ name = proplists:get_value(name, Attrs, undefined)
|
||||
, module = proplists:get_value(module, Attrs, undefined)
|
||||
, hooks = proplists:get_value(hooks, Attrs, {[], []})
|
||||
, bare = proplists:get_value(bare, Attrs, true)
|
||||
, deps = proplists:get_value(deps, Attrs, [])
|
||||
, desc = proplists:get_value(desc, Attrs, "")
|
||||
, short_desc = proplists:get_value(short_desc, Attrs, "")
|
||||
, example = proplists:get_value(example, Attrs, "")
|
||||
, opts = proplists:get_value(opts, Attrs, [])
|
||||
, profiles = proplists:get_value(profiles, Attrs, [default])
|
||||
, namespace = proplists:get_value(namespace, Attrs, default) }.
|
||||
|
||||
%% @doc Run provider and hooks.
|
||||
%%
|
||||
%% @param Provider the provider object
|
||||
%% @param State the current state of the system
|
||||
-spec do(t(), any()) -> {ok, any()} | {error, string()} | {error, {module(), any()}}.
|
||||
do(Provider, State) ->
|
||||
(Provider#provider.module):do(State).
|
||||
|
||||
-spec profiles(t()) -> [atom()].
|
||||
profiles(Provider) ->
|
||||
Provider#provider.profiles.
|
||||
|
||||
-spec namespace(t()) -> atom().
|
||||
namespace(Provider) ->
|
||||
Provider#provider.namespace.
|
||||
|
||||
%%% @doc get the name of the module that implements the provider
|
||||
-spec module(t()) -> module().
|
||||
module(Provider) ->
|
||||
Provider#provider.module.
|
||||
|
||||
-spec impl(t()) -> atom().
|
||||
impl(Provider) ->
|
||||
Provider#provider.name.
|
||||
|
||||
-spec opts(t()) -> list().
|
||||
opts(Provider) ->
|
||||
Provider#provider.opts.
|
||||
|
||||
-spec desc(t()) -> string().
|
||||
desc(Provider) ->
|
||||
Provider#provider.desc.
|
||||
|
||||
-spec hooks(t()) -> {[t()], [t()]}.
|
||||
hooks(Provider) ->
|
||||
Provider#provider.hooks.
|
||||
|
||||
-spec hooks(t(), {[t()], [t()]}) -> t().
|
||||
hooks(Provider, Hooks) ->
|
||||
Provider#provider{hooks=Hooks}.
|
||||
|
||||
help(Providers) when is_list(Providers) ->
|
||||
Dict = lists:foldl(
|
||||
fun(P, Dict) when P#provider.bare =:= true ->
|
||||
dict:append(P#provider.namespace,
|
||||
{ec_cnv:to_list(P#provider.name),
|
||||
P#provider.short_desc},
|
||||
Dict)
|
||||
; (_, Dict) -> Dict
|
||||
end,
|
||||
dict:new(),
|
||||
Providers),
|
||||
Namespaces = [default |
|
||||
lists:usort([NS || #provider{namespace=NS} <- Providers,
|
||||
NS =/= default])],
|
||||
namespace_help(Dict, Namespaces);
|
||||
help(#provider{opts=Opts
|
||||
,desc=Desc
|
||||
,namespace=Namespace
|
||||
,name=Name}) ->
|
||||
case Desc of
|
||||
Desc when length(Desc) > 0 ->
|
||||
io:format(Desc++"~n");
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
|
||||
StrNS = case Namespace of
|
||||
default -> "";
|
||||
_ -> atom_to_list(Namespace) ++ " "
|
||||
end,
|
||||
|
||||
case Opts of
|
||||
[] ->
|
||||
io:format("Usage: rebar3 ~s~p~n", [StrNS, Name]);
|
||||
_ ->
|
||||
getopt:usage(Opts, "rebar3 " ++ StrNS ++ atom_to_list(Name), "", [])
|
||||
end.
|
||||
|
||||
help(Name, Providers) when is_list(Name) ->
|
||||
help(list_to_atom(Name), Providers, default);
|
||||
help(Name, Providers) when is_atom(Name) ->
|
||||
help(Name, Providers, default).
|
||||
|
||||
help(Name, Providers, Namespace) when is_list(Name) ->
|
||||
help(list_to_atom(Name), Providers, Namespace);
|
||||
help(Name, Providers, Namespace) when is_atom(Name) ->
|
||||
Provider = providers:get_provider(Name, Providers, Namespace),
|
||||
help(Provider).
|
||||
|
||||
format_error({provider_not_found, Namespace, ProviderName}) ->
|
||||
io_lib:format("Unable to resolve provider ~s in namespace ~s", [ProviderName, Namespace]).
|
||||
|
||||
%% @doc format an error produced from a provider.
|
||||
-spec format_error(t(), Reason::term()) -> iolist().
|
||||
format_error(#provider{module=Mod}, Error) ->
|
||||
Mod:format_error(Error).
|
||||
|
||||
%% @doc print the provider module name
|
||||
%%
|
||||
%% @param T - The provider
|
||||
%% @return An iolist describing the provider
|
||||
-spec format(t()) -> iolist().
|
||||
format(#provider{name=Name}) ->
|
||||
atom_to_list(Name).
|
||||
|
||||
-spec get_target_providers({atom(),atom()} | atom(), list()) -> [{atom(), atom()}].
|
||||
get_target_providers({Namespace, Target}, Providers) ->
|
||||
get_target_providers(Target, Providers, Namespace);
|
||||
get_target_providers(Target, Providers) ->
|
||||
get_target_providers(Target, Providers, default).
|
||||
|
||||
get_target_providers(Target, Providers, Namespace) ->
|
||||
TargetProviders = lists:filter(fun(#provider{name=T, namespace=NS})
|
||||
when T =:= Target, NS =:= Namespace ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end, Providers),
|
||||
expand_hooks(process_deps(TargetProviders, Providers), [], Providers).
|
||||
|
||||
expand_hooks([], TargetProviders, _Providers) ->
|
||||
TargetProviders;
|
||||
expand_hooks([Provider | Tail], TargetProviders, Providers) ->
|
||||
{PreHooks, PostHooks} = hooks(get_provider(Provider, Providers)),
|
||||
expand_hooks(Tail, TargetProviders++PreHooks++[Provider | PostHooks], Providers).
|
||||
|
||||
-spec get_provider(atom() | {atom(), atom()}, [t()]) -> t() | not_found.
|
||||
get_provider({Namespace, ProviderName}, Providers) ->
|
||||
get_provider(ProviderName, Providers, Namespace);
|
||||
get_provider(ProviderName, Providers) ->
|
||||
get_provider(ProviderName, Providers, default).
|
||||
|
||||
-spec get_provider(atom(), [t()], atom()) -> t() | not_found.
|
||||
get_provider(ProviderName,
|
||||
[Provider = #provider{name = ProviderName, namespace=Namespace} | _],
|
||||
Namespace) ->
|
||||
Provider;
|
||||
get_provider(ProviderName, [_ | Rest], Namespace) ->
|
||||
get_provider(ProviderName, Rest, Namespace);
|
||||
get_provider(_, _, _) ->
|
||||
not_found.
|
||||
|
||||
-spec get_provider_by_module(atom(), [t()]) -> t() | not_found.
|
||||
get_provider_by_module(ProviderModule, [Provider = #provider{module = ProviderModule} | _]) ->
|
||||
Provider;
|
||||
get_provider_by_module(ProviderModule, [_ | Rest]) ->
|
||||
get_provider_by_module(ProviderModule, Rest);
|
||||
get_provider_by_module(_ProviderModule, _) ->
|
||||
not_found.
|
||||
|
||||
-spec get_providers_by_namespace(atom(), [t()]) -> [t()].
|
||||
get_providers_by_namespace(Namespace, [Provider = #provider{namespace = Namespace} | Rest]) ->
|
||||
[Provider | get_providers_by_namespace(Namespace, Rest)];
|
||||
get_providers_by_namespace(Namespace, [_ | Rest]) ->
|
||||
get_providers_by_namespace(Namespace, Rest);
|
||||
get_providers_by_namespace(_Namespace, []) ->
|
||||
[].
|
||||
|
||||
process_deps([], _Providers) ->
|
||||
[];
|
||||
process_deps(TargetProviders, Providers) ->
|
||||
DepChain = lists:flatmap(fun(Provider) ->
|
||||
{DC, _, _} = process_deps(Provider, Providers, []),
|
||||
DC
|
||||
end, TargetProviders),
|
||||
Providers1 = lists:flatten([{{none, none},
|
||||
{P#provider.namespace, P#provider.name}} || P <- TargetProviders]
|
||||
++ DepChain),
|
||||
case reorder_providers(Providers1) of
|
||||
{error, _}=Error ->
|
||||
Error;
|
||||
[{none, none} | Rest] ->
|
||||
Rest
|
||||
end.
|
||||
|
||||
process_deps(Provider, Providers, Seen) ->
|
||||
case lists:member(Provider, Seen) of
|
||||
true ->
|
||||
{[], Providers, Seen};
|
||||
false ->
|
||||
Deps = Provider#provider.deps,
|
||||
Namespace = Provider#provider.namespace,
|
||||
DepList = lists:map(fun({NS, Dep}) ->
|
||||
{{NS, Dep}, {Namespace, Provider#provider.name}};
|
||||
(Dep) ->
|
||||
{{Namespace, Dep}, {Namespace, Provider#provider.name}}
|
||||
end, Deps),
|
||||
{NewDeps, _, NewSeen} =
|
||||
lists:foldl(fun({NS, Arg}, Acc) ->
|
||||
process_dep({NS, Arg}, Acc);
|
||||
(Arg, Acc) ->
|
||||
process_dep({Namespace, Arg}, Acc)
|
||||
end,
|
||||
{[], Providers, Seen}, Deps),
|
||||
{[DepList | NewDeps], Providers, NewSeen}
|
||||
end.
|
||||
|
||||
process_dep({Namespace, ProviderName}, {Deps, Providers, Seen}) ->
|
||||
case get_provider(ProviderName, Providers, Namespace) of
|
||||
not_found ->
|
||||
throw(?PRV_ERROR({provider_not_found, Namespace, ProviderName}));
|
||||
Provider ->
|
||||
{NewDeps, _, NewSeen} = process_deps(Provider#provider{namespace=Namespace}, Providers, [ProviderName | Seen]),
|
||||
{[Deps | NewDeps], Providers, NewSeen}
|
||||
end.
|
||||
|
||||
%% @doc Reorder the providers according to thier dependency set.
|
||||
reorder_providers(OProviderList) ->
|
||||
case providers_topo:sort(OProviderList) of
|
||||
{ok, ProviderList} ->
|
||||
ProviderList;
|
||||
{error, _} ->
|
||||
{error, "There was a cycle in the provider list. Unable to complete build!"}
|
||||
end.
|
||||
|
||||
%% @doc Extract help values from a list on a per-namespace order
|
||||
namespace_help(_, []) -> ok;
|
||||
namespace_help(Dict, [NS|Namespaces]) ->
|
||||
Providers = case dict:find(NS, Dict) of
|
||||
{ok, Found} -> Found;
|
||||
error -> []
|
||||
end,
|
||||
Help = [case NS of
|
||||
default -> {Name, Desc};
|
||||
_ -> {" "++Name, Desc}
|
||||
end || {Name, Desc} <- lists:sort(Providers)],
|
||||
if Help =:= [] ->
|
||||
no_public_providers;
|
||||
NS =/= default ->
|
||||
io:format("~n~p <task>:~n", [NS]),
|
||||
display_help(Help);
|
||||
NS =:= default ->
|
||||
display_help(Help)
|
||||
end,
|
||||
namespace_help(Dict, Namespaces).
|
||||
|
||||
display_help(Help) ->
|
||||
Longest = lists:max([length(X) || {X, _} <- Help]),
|
||||
lists:foreach(fun({Name, ShortDesc}) ->
|
||||
Length = length(Name),
|
||||
Spacing = lists:duplicate(Longest - Length + 8, " "),
|
||||
io:format("~s~s~s~n", [Name, Spacing, ShortDesc])
|
||||
end, Help).
|
|
@ -0,0 +1,109 @@
|
|||
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
|
||||
%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
|
||||
%%%
|
||||
%%% This file is provided to you under the Apache License,
|
||||
%%% Version 2.0 (the "License"); you may not use this file
|
||||
%%% except in compliance with the License. You may obtain
|
||||
%%% a copy of the License at
|
||||
%%%
|
||||
%%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%%
|
||||
%%% Unless required by applicable law or agreed to in writing,
|
||||
%%% software distributed under the License is distributed on an
|
||||
%%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
%%% KIND, either express or implied. See the License for the
|
||||
%%% specific language governing permissions and limitations
|
||||
%%% under the License.
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author Joe Armstrong
|
||||
%%% @author Eric Merritt
|
||||
%%% @doc
|
||||
%%% This is a pretty simple topological sort for erlang. It was
|
||||
%%% originally written for ermake by Joe Armstrong back in '98. It
|
||||
%%% has been pretty heavily modified by Eric Merritt since '06 and
|
||||
%%% modified again for relx/rebar3 by Tristan Sloughter.
|
||||
%%%
|
||||
%%% A partial order on the set S is a set of pairs {Xi,Xj} such that
|
||||
%%% some relation between Xi and Xj is obeyed.
|
||||
%%%
|
||||
%%% A topological sort of a partial order is a sequence of elements
|
||||
%%% [X1, X2, X3 ...] such that if whenever {Xi, Xj} is in the partial
|
||||
%%% order i < j
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(providers_topo).
|
||||
|
||||
-export([sort/1]).
|
||||
|
||||
%%====================================================================
|
||||
%% Types
|
||||
%%====================================================================
|
||||
-type pair() :: {{atom(), atom()}, {atom(), atom()}}.
|
||||
-type name() :: {atom(), atom()}.
|
||||
-type element() :: name() | pair().
|
||||
|
||||
%%====================================================================
|
||||
%% API
|
||||
%%====================================================================
|
||||
|
||||
%% @doc Do a topological sort on the list of pairs.
|
||||
-spec sort([pair()]) -> {ok, [{atom(), atom()}]} | {error, any()}.
|
||||
sort(Pairs) ->
|
||||
iterate(Pairs, [], all(Pairs)).
|
||||
|
||||
%%====================================================================
|
||||
%% Internal Functions
|
||||
%%====================================================================
|
||||
|
||||
%% @doc Iterate over the system. @private
|
||||
-spec iterate([pair()], [name()], [name()]) ->
|
||||
{ok, [name()]} | {error, string()}.
|
||||
iterate([], L, All) ->
|
||||
{ok, remove_duplicates(L ++ subtract(All, L))};
|
||||
iterate(Pairs, L, All) ->
|
||||
case subtract(lhs(Pairs), rhs(Pairs)) of
|
||||
[] ->
|
||||
{error, "Cycle found in providers dependencies."};
|
||||
Lhs ->
|
||||
iterate(remove_pairs(Lhs, Pairs), L ++ Lhs, All)
|
||||
end.
|
||||
|
||||
-spec all([pair()]) -> [{atom(), atom()}].
|
||||
all(L) ->
|
||||
lhs(L) ++ rhs(L).
|
||||
|
||||
-spec lhs([pair()]) -> [{atom(), atom()}].
|
||||
lhs(L) ->
|
||||
[X || {X, _} <- L].
|
||||
|
||||
-spec rhs([pair()]) -> [{atom(), atom()}].
|
||||
rhs(L) ->
|
||||
[Y || {_, Y} <- L].
|
||||
|
||||
%% @doc all the elements in L1 which are not in L2
|
||||
%% @private
|
||||
-spec subtract([element()], [element()]) -> [element()].
|
||||
subtract(L1, L2) ->
|
||||
[X || X <- L1, not lists:member(X, L2)].
|
||||
|
||||
%% @doc remove dups from the list. @private
|
||||
-spec remove_duplicates([element()]) -> [element()].
|
||||
remove_duplicates([H|T]) ->
|
||||
case lists:member(H, T) of
|
||||
true ->
|
||||
remove_duplicates(T);
|
||||
false ->
|
||||
[H|remove_duplicates(T)]
|
||||
end;
|
||||
remove_duplicates([]) ->
|
||||
[].
|
||||
|
||||
%% @doc
|
||||
%% removes all pairs from L2 where the first element
|
||||
%% of each pair is a member of L1
|
||||
%%
|
||||
%% L2' L1 = [X] L2 = [{X,Y}].
|
||||
%% @private
|
||||
-spec remove_pairs([{atom(), atom()}], [pair()]) -> [pair()].
|
||||
remove_pairs(L1, L2) ->
|
||||
[All || All={X, _Y} <- L2, not lists:member(X, L1)].
|
|
@ -0,0 +1,194 @@
|
|||
Apache License
|
||||
==============
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
------------------------------------------------------------
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use,
|
||||
reproduction, and distribution as defined by Sections 1 through 9
|
||||
of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making
|
||||
modifications, including but not limited to software source code,
|
||||
documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but not
|
||||
limited to compiled object code, generated documentation, and
|
||||
conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work (an
|
||||
example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other
|
||||
modifications represent, as a whole, an original work of
|
||||
authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link
|
||||
(or bind by name) to the interfaces of, the Work and Derivative
|
||||
Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the
|
||||
original version of the Work and any modifications or additions to
|
||||
that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright
|
||||
owner or by an individual or Legal Entity authorized to submit on
|
||||
behalf of the copyright owner. For the purposes of this definition,
|
||||
"submitted" means any form of electronic, verbal, or written
|
||||
communication sent to the Licensor or its representatives,
|
||||
including but not limited to communication on electronic mailing
|
||||
lists, source code control systems, and issue tracking systems that
|
||||
are managed by, or on behalf of, the Licensor for the purpose of
|
||||
discussing and improving the Work, but excluding communication that
|
||||
is conspicuously marked or otherwise designated in writing by the
|
||||
copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal
|
||||
Entity on behalf of whom a Contribution has been received by
|
||||
Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the
|
||||
Work, where such license applies only to those patent claims
|
||||
licensable by such Contributor that are necessarily infringed by
|
||||
their Contribution(s) alone or by combination of their
|
||||
Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging
|
||||
that the Work or a Contribution incorporated within the Work
|
||||
constitutes direct or contributory patent infringement, then any
|
||||
patent licenses granted to You under this License for that Work
|
||||
shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work
|
||||
or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You meet
|
||||
the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative
|
||||
Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that
|
||||
You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work, excluding
|
||||
those notices that do not pertain to any part of the Derivative
|
||||
Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one of
|
||||
the following places: within a NOTICE text file distributed as
|
||||
part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and do
|
||||
not modify the License. You may add Your own attribution notices
|
||||
within Derivative Works that You distribute, alongside or as an
|
||||
addendum to the NOTICE text from the Work, provided that such
|
||||
additional attribution notices cannot be construed as modifying
|
||||
the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing
|
||||
the origin of the Work and reproducing the content of the NOTICE
|
||||
file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed
|
||||
to in writing, Licensor provides the Work (and each Contributor
|
||||
provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES
|
||||
OR CONDITIONS OF ANY KIND, either express or implied, including,
|
||||
without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR
|
||||
PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this
|
||||
License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer, and
|
||||
charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
|
@ -0,0 +1,44 @@
|
|||
![](https://github.com/erlware/relx/workflows/Common%20Test/badge.svg)
|
||||
![Cirrus CI - Task and Script Build Status](https://img.shields.io/cirrus/github/erlware/relx?label=OSX%20Tests)
|
||||
|
||||
Relx
|
||||
=======
|
||||
|
||||
Relx is a library that assembles Erlang/OTP releases. Given a release
|
||||
specification and a list of directories in which to search for OTP
|
||||
applications it will generate a release output.
|
||||
|
||||
It is generally used through the Erlang/OTP build tool
|
||||
[rebar3](https://www.rebar3.org/) which provides a cli interface.
|
||||
|
||||
Documentation
|
||||
-----------
|
||||
|
||||
`relx` is a library used by [rebar3](https://www.rebar3.org/). Documentation on
|
||||
using `rebar3` for building releases with `relx` can be found on
|
||||
[rebar3.org](https://rebar3.org/docs/deployment/releases/).
|
||||
|
||||
Also see [Adopting Erlang's Releases
|
||||
chapter](https://adoptingerlang.org/docs/production/releases/).
|
||||
|
||||
|
||||
Building and Testing
|
||||
--------
|
||||
|
||||
Common Test suites can be run with `rebar3`:
|
||||
|
||||
``` shell
|
||||
$ rebar3 compile
|
||||
$ rebar3 ct
|
||||
```
|
||||
|
||||
Tests for the start scripts that are generated by `relx` are tested with
|
||||
[shelltestrunner](https://github.com/simonmichael/shelltestrunner/).
|
||||
|
||||
The script `shelltests/run_tests.sh` will clone `rebar3` master and build it
|
||||
with the current `relx` as a checkout dependency and then run the tests using
|
||||
that `rebar3` escript:
|
||||
|
||||
``` shell
|
||||
$ shelltests/run_tests.sh
|
||||
```
|
|
@ -0,0 +1,95 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT=$(readlink "$0" || true)
|
||||
if [ -z "$SCRIPT" ]; then
|
||||
SCRIPT=$0
|
||||
fi;
|
||||
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd -P)"
|
||||
RELEASE_ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
export REL_NAME="{{ rel_name }}"
|
||||
REL_VSN="{{ rel_vsn }}"
|
||||
|
||||
# export these to match mix release environment variables
|
||||
export RELEASE_NAME="{{ rel_name }}"
|
||||
export RELEASE_VSN="{{ rel_vsn }}"
|
||||
export RELEASE_PROG="${SCRIPT}"
|
||||
|
||||
ERTS_VSN="{{ erts_vsn }}"
|
||||
REL_DIR="$RELEASE_ROOT_DIR/releases/$REL_VSN"
|
||||
ERL_OPTS="{{ erl_opts }}"
|
||||
export ESCRIPT_NAME="${ESCRIPT_NAME-$SCRIPT}"
|
||||
|
||||
find_erts_dir() {
|
||||
__erts_dir="$RELEASE_ROOT_DIR/erts-$ERTS_VSN"
|
||||
if [ -d "$__erts_dir" ]; then
|
||||
ERTS_DIR="$__erts_dir";
|
||||
else
|
||||
__erl="$(command -v erl)"
|
||||
code="io:format(\"~s\", [code:root_dir()]), halt()."
|
||||
__erl_root="$("$__erl" -boot no_dot_erlang -noshell -eval "$code")"
|
||||
ERTS_DIR="$__erl_root/erts-$ERTS_VSN"
|
||||
fi
|
||||
}
|
||||
|
||||
find_sys_config() {
|
||||
__possible_sys="$REL_DIR/sys.config"
|
||||
if [ -f "$__possible_sys" ]; then
|
||||
SYS_CONFIG="$__possible_sys"
|
||||
else
|
||||
if [ -L "$__possible_sys".orig ]; then
|
||||
mv "$__possible_sys".orig "$__possible_sys"
|
||||
SYS_CONFIG="$__possible_sys"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
find_vm_args() {
|
||||
__possible_vm_args="$REL_DIR/vm.args"
|
||||
if [ -f "$__possible_vm_args" ]; then
|
||||
VM_ARGS="$__possible_vm_args"
|
||||
else
|
||||
if [ -L "$__possible_vm_args".orig ]; then
|
||||
mv "$__possible_vm_args".orig "$__possible_vm_args"
|
||||
VM_ARGS="$__possible_vm_args"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
find_erts_dir
|
||||
find_sys_config
|
||||
find_vm_args
|
||||
export ROOTDIR="$RELEASE_ROOT_DIR"
|
||||
export BINDIR="$ERTS_DIR/bin"
|
||||
export EMU="beam"
|
||||
export PROGNAME="erl"
|
||||
export LD_LIBRARY_PATH="$ERTS_DIR/lib:$LD_LIBRARY_PATH"
|
||||
SYSTEM_LIB_DIR="$(dirname "$ERTS_DIR")/lib"
|
||||
[ -f "$REL_DIR/$REL_NAME.boot" ] && BOOTFILE="$REL_NAME" || BOOTFILE=start
|
||||
cd "$ROOTDIR"
|
||||
|
||||
{{! Define alternative field separator using ASCII US (unit separator). US is
|
||||
not expected to appear in any argument. }}
|
||||
IFS_NORM="$IFS"
|
||||
IFS_ARGS="$(printf '\x1f')"
|
||||
|
||||
# Save extra arguments
|
||||
{{! Join extra arguments into a US-delimited string. }}
|
||||
IFS="$IFS_ARGS"
|
||||
ARGS="$*"
|
||||
IFS="$IFS_NORM"
|
||||
|
||||
# Build arguments for erlexec
|
||||
set --
|
||||
[ "$ERL_OPTS" ] && set -- "$@" "$ERL_OPTS"
|
||||
[ "$SYS_CONFIG" ] && set -- "$@" -config "$SYS_CONFIG"
|
||||
[ "$VM_ARGS" ] && set -- "$@" -args_file "$VM_ARGS"
|
||||
set -- "$@" -boot_var SYSTEM_LIB_DIR "$SYSTEM_LIB_DIR" -boot "$REL_DIR/$BOOTFILE"
|
||||
{{! Split string with extra arguments back into an argument list. }}
|
||||
IFS="$IFS_ARGS"
|
||||
# shellcheck disable=SC2086
|
||||
set -- "$@" $ARGS
|
||||
IFS="$IFS_NORM"
|
||||
|
||||
exec "$BINDIR/erlexec" "$@"
|
|
@ -0,0 +1,108 @@
|
|||
@echo off
|
||||
:: This is a simple start batch file that runs the release in an Erlang shell
|
||||
|
||||
:: Set variables that describe the release
|
||||
set rel_name={{ rel_name }}
|
||||
set rel_vsn={{ rel_vsn }}
|
||||
set erts_vsn={{ erts_vsn }}
|
||||
set erl_opts={{ erl_opts }}
|
||||
|
||||
:: export these to match mix release environment variables
|
||||
set RELEASE_NAME={{ rel_name }}
|
||||
set RELEASE_VSN={{ rel_vsn }}
|
||||
set RELEASE_PROG=%~nx0
|
||||
|
||||
:: Set the root release directory based on the location of this batch file
|
||||
set script_dir=%~dp0
|
||||
for %%A in ("%script_dir%\..") do (
|
||||
set "release_root_dir=%%~fA"
|
||||
)
|
||||
set "rel_dir=%release_root_dir%\releases\%rel_vsn%"
|
||||
|
||||
call :find_erts_dir
|
||||
call :find_sys_config
|
||||
call :set_boot_script_var
|
||||
|
||||
set "rootdir=%release_root_dir%"
|
||||
set "bindir=%erts_dir%\bin"
|
||||
set progname=erl
|
||||
set erl=%bindir%\erl
|
||||
|
||||
cd %rootdir%
|
||||
|
||||
:: Write the erl.ini file
|
||||
set erl_ini=%erts_dir%\bin\erl.ini
|
||||
set converted_bindir=%bindir:\=\\%
|
||||
set converted_rootdir=%rootdir:\=\\%
|
||||
echo [erlang] > "%erl_ini%"
|
||||
echo Bindir=%converted_bindir% >> "%erl_ini%"
|
||||
echo Progname=%progname% >> "%erl_ini%"
|
||||
echo Rootdir=%converted_rootdir% >> "%erl_ini%"
|
||||
|
||||
:: Start the release in an `erl` shell
|
||||
set boot=-boot "%boot_script%" -boot_var RELEASE_DIR "%release_root_dir%"
|
||||
"%erl%" %erl_opts% %sys_config% %boot% %*
|
||||
|
||||
goto :eof
|
||||
|
||||
:: Find the ERTS dir
|
||||
:find_erts_dir
|
||||
set "erts_dir=%release_root_dir%\erts-%erts_vsn%"
|
||||
if exist %erts_dir% (
|
||||
goto :set_erts_dir_from_default
|
||||
) else (
|
||||
goto :set_erts_dir_from_erl
|
||||
)
|
||||
goto :eof
|
||||
|
||||
:: Set the ERTS dir from the passed in erts_vsn
|
||||
:set_erts_dir_from_default
|
||||
set erts_dir=%erts_dir%
|
||||
set root_dir=%release_root_dir%
|
||||
goto :eof
|
||||
|
||||
:: Set the ERTS dir from erl
|
||||
:set_erts_dir_from_erl
|
||||
for /f "delims=" %%i in ('where erl') do (
|
||||
set erl=%%i
|
||||
)
|
||||
for /f "delims=" %%i in ('call "%erl%" -boot no_dot_erlang -boot_var RELEASE_DIR "%release_root_dir%" -noshell -eval "io:format(\"~s\", [filename:nativename(code:root_dir())])." -s init stop') do (
|
||||
set erl_root=%%i
|
||||
)
|
||||
set "erts_dir=%erl_root%\erts-%erts_vsn%"
|
||||
set rootdir=%erl_root%
|
||||
goto :eof
|
||||
|
||||
:: Find the sys.config file
|
||||
:find_sys_config
|
||||
set "possible_sys=%rel_dir%\sys.config"
|
||||
if exist "%possible_sys%" (
|
||||
set sys_config=-config "%possible_sys%"
|
||||
) else (
|
||||
if exist "%possible_sys%.orig" (
|
||||
ren "%possible_sys%.orig" sys.config
|
||||
set sys_config=-config "%possible_sys%"
|
||||
)
|
||||
)
|
||||
|
||||
:: Find the vm.args file
|
||||
:find_vm_args
|
||||
set "possible_vm_args=%rel_dir%\vm.args"
|
||||
if exist "%possible_vm_args%" (
|
||||
set vm_args="%possible_vm_args%"
|
||||
) else (
|
||||
if exist "%possible_vm_args%.orig" (
|
||||
ren "%possible_vm_args%.orig" vm.args
|
||||
set vm_args="%possible_vm_args%"
|
||||
)
|
||||
)
|
||||
goto :eof
|
||||
|
||||
:: set boot_script variable
|
||||
:set_boot_script_var
|
||||
if exist "%rel_dir%\%rel_name%.boot" (
|
||||
set "boot_script=%rel_dir%\%rel_name%"
|
||||
) else (
|
||||
set "boot_script=%rel_dir%\start"
|
||||
)
|
||||
goto :eof
|
|
@ -0,0 +1,48 @@
|
|||
#! /usr/bin/pwsh
|
||||
# This is a simple start script file that runs the release in an Erlang shell
|
||||
|
||||
# Terminate on error
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Set variables that describe the release
|
||||
$rel_name = '{{ rel_name }}'
|
||||
$rel_vsn = '{{ rel_vsn }}'
|
||||
$erts_vsn = '{{ erts_vsn }}'
|
||||
$erl_opts = '{{ erl_opts }}'
|
||||
|
||||
# export these to match mix release environment variables
|
||||
$RELEASE_NAME = '{{ rel_name }}'
|
||||
$RELEASE_VERSION = '{{ rel_vsn }}'
|
||||
$RELEASE_PROG = $MyInvocation.MyCommand.Name
|
||||
|
||||
# Ensure we have PSScriptRoot
|
||||
if (!(Test-Path variable:global:PSScriptRoot)) {
|
||||
# Support for powershell 2.0
|
||||
$PSScriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
|
||||
}
|
||||
|
||||
# Import psutil helper functions
|
||||
. $PSScriptRoot\psutil.ps1
|
||||
|
||||
# Set the root release directory based on the location of this script
|
||||
$rootdir = Split-Path -Parent -Path $PSScriptRoot
|
||||
$rel_dir = "$rootdir\releases\$rel_vsn"
|
||||
|
||||
$erts_root, $erts_dir = Find-ERTS -RelRoot $rootdir -Vsn $erts_vsn
|
||||
$sys_config = Find-FormatConfig -RelDir $rel_dir -File 'sys.config'
|
||||
$vm_args = Find-FormatConfig -RelDir $rel_dir -File 'vm.args'
|
||||
$boot_script = Find-BootScript -RelDir $rel_dir -RelName $rel_name
|
||||
$werl = "$erts_dir\bin\werl.exe"
|
||||
|
||||
# Set the ERL_LIBS environment variable
|
||||
$env:ERL_LIBS = "$rootdir\lib"
|
||||
|
||||
# Start the release in an `erl` shell
|
||||
$params = @($erl_opts, '-boot', $boot_script)
|
||||
if (![string]::IsNullOrEmpty($sys_config)) { $params += '-config', $sys_config }
|
||||
if (![string]::IsNullOrEmpty($vm_args)) { $params += '-args_file', $vm_args }
|
||||
$params += $args
|
||||
& $werl $params
|
||||
|
||||
# Cleanup
|
||||
$env:ERL_LIBS = ''
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/sh
|
||||
|
||||
# loop until the VM starts responding to pings
|
||||
while ! erl_rpc erlang is_alive > /dev/null
|
||||
do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# get the beam pid and write it to the file passed as
|
||||
# argument
|
||||
PID="$(relx_get_pid)"
|
||||
echo "$PID" > "$1"
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
erl_eval "application:which_applications()."
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/sh
|
||||
|
||||
# loop until the VM starts responding to pings
|
||||
while ! erl_rpc erlang is_alive > /dev/null
|
||||
do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# loop until the name provided as argument gets
|
||||
# registered
|
||||
while true
|
||||
do
|
||||
if [ "$(erl_eval "whereis($1).")" != "undefined" ]
|
||||
then
|
||||
break
|
||||
fi
|
||||
done
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
# loop until the VM starts responding to pings
|
||||
while ! erl_rpc erlang is_alive > /dev/null
|
||||
do
|
||||
sleep 1
|
||||
done
|
|
@ -0,0 +1,4 @@
|
|||
[erlang]
|
||||
Bindir={{ bin_dir }}
|
||||
Progname=erl
|
||||
Rootdir={{ output_dir }}
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR=`dirname $0`
|
||||
ROOTDIR=`cd $SCRIPT_DIR/../../ && pwd`
|
||||
BINDIR=$ROOTDIR/erts-{{ erts_vsn }}/bin
|
||||
EMU=beam
|
||||
PROGNAME=`echo $0 | sed 's/.*\\///'`
|
||||
export EMU
|
||||
export ROOTDIR
|
||||
export BINDIR
|
||||
export PROGNAME
|
||||
exec "$BINDIR/erlexec" ${1+"$@"}
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue