New upstream version 3.17.0

This commit is contained in:
Philipp Huebner 2021-12-27 20:09:21 +01:00
commit 36bc1e1299
419 changed files with 79421 additions and 0 deletions

13
.editorconfig Normal file
View File

@ -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

322
CONTRIBUTING.md Normal file
View File

@ -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.

43
Dockerfile Normal file
View File

@ -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"]

178
LICENSE Normal file
View File

@ -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

170
README.md Normal file
View File

@ -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).

148
THANKS Normal file
View File

@ -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

View File

@ -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.

View File

@ -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 &amp; 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>{&quot;nested&quot;:&quot;value&quot;}</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)

View File

@ -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"}}}
]}
]}
]}.

View File

@ -0,0 +1 @@
[].

View File

@ -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,[]}]}.

View File

@ -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($<) -> <<"&lt;">>;
escape_char($>) -> <<"&gt;">>;
escape_char($&) -> <<"&amp;">>;
escape_char($") -> <<"&quot;">>;
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.

View File

@ -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.

View File

@ -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

View File

@ -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'}
]}.

View File

@ -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"]}]}.

View File

@ -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.

View File

@ -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}]}.

View File

@ -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))
].

View File

@ -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.

View File

@ -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]`.

View File

@ -0,0 +1,8 @@
{erl_opts, [debug_info]}.
{deps, []}.
{profiles, [
{shell, [
{deps, [sync]}
]}
]}.

View File

@ -0,0 +1 @@
[].

View File

@ -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"}]}]}.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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"}]}
]}
]}.

View File

@ -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.

View File

@ -0,0 +1,8 @@
{"1.2.0",
[{<<"cf">>,{pkg,<<"cf">>,<<"0.2.1">>},0}]}.
[
{pkg_hash,[
{<<"cf">>, <<"69D0B1349FD4D7D4DC55B7F407D29D7A840BF9A1EF5AF529F1EBE0CE153FC2AB">>}]},
{pkg_hash_ext,[
{<<"cf">>, <<"BAEE9AA7EC2DFA3CB4486B67211177CAA293F876780F0B313B45718EDEF6A0A5">>}]}
].

View File

@ -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"}]}]}.

View File

@ -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.

View File

@ -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.

View File

@ -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}.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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])).

View File

@ -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.

View File

@ -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).

View File

@ -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)` ;

View File

@ -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}}}]}]}
]}.

View File

@ -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.

View File

@ -0,0 +1,8 @@
{"1.2.0",
[{<<"cf">>,{pkg,<<"cf">>,<<"0.3.1">>},0}]}.
[
{pkg_hash,[
{<<"cf">>, <<"5CB902239476E141EA70A740340233782D363A31EEA8AD37049561542E6CD641">>}]},
{pkg_hash_ext,[
{<<"cf">>, <<"315E8D447D3A4B02BCDBFA397AD03BBB988A6E0AA6F44D3ADD0F4E3C3BF97672">>}]}
].

View File

@ -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).

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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).

View File

@ -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).

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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).

View File

@ -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), []}.

View File

@ -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}.

View File

@ -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 &lt; 3.0.0
%% "~> 2.6.5" matches cookbooks >= 2.6.5 AND &lt; 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.

View File

@ -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.

View File

@ -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.

View File

@ -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).

View File

@ -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"}]}]}.

View File

@ -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

View File

@ -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.

View File

@ -0,0 +1,3 @@
%% Dogfooding
{erl_opts, [{platform_define, "^[0-9]+", namespaced_dicts}]}.
{eunit_opts, [no_tty, {report, {eunit_progress, [colored, profile]}}]}.

View File

@ -0,0 +1 @@
[].

View File

@ -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.

View File

@ -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"}]}]}.

View File

@ -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}.

View File

@ -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.

View File

@ -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"]}}
```

View File

@ -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]}.

View File

@ -0,0 +1 @@
[].

View File

@ -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]}]}.

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1,9 @@
providers
=====
An Erlang providers library.
Build
-----
$ rebar compile

View File

@ -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}}).

View File

@ -0,0 +1,5 @@
{erl_opts, [{platform_define, "R14", no_callback_support}
,debug_info]}.
{deps, [{getopt, "1.0.1"}]}.

View File

@ -0,0 +1,6 @@
{"1.1.0",
[{<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},0}]}.
[
{pkg_hash,[
{<<"getopt">>, <<"C73A9FA687B217F2FF79F68A3B637711BB1936E712B521D8CE466B29CBF7808A">>}]}
].

View File

@ -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.

View File

@ -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"}]}]}.

View File

@ -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).

View File

@ -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 &lt; 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)].

View File

@ -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.

View File

@ -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
```

View File

@ -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" "$@"

View File

@ -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

View File

@ -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 = ''

View File

@ -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"

View File

@ -0,0 +1,3 @@
#!/bin/sh
erl_eval "application:which_applications()."

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
[erlang]
Bindir={{ bin_dir }}
Progname=erl
Rootdir={{ output_dir }}

View File

@ -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