Modify module to make it GNU getopt compliant.

Add support for short options with embedded arguments (e.g. -j2).
Add support for implicit boolean options (e.g. -q -> {quiet, true}).
Add support for option terminator ("--").
This commit is contained in:
Juan Jose Comellas 2010-01-06 16:39:48 -03:00
parent 5ce0e76f87
commit b2e8bcd291
5 changed files with 340 additions and 85 deletions

View File

@ -1,7 +1,7 @@
Getopt for Erlang
=================
Command-line parsing module that uses a syntax similar to that of GNU Getopt.
Command-line parsing module that uses a syntax similar to that of GNU *getopt*.
Requirements
@ -26,13 +26,19 @@ To build the (very) limited documentation run ``make docs``.
Usage
-----
The *getopt* module provides two functions:
The *getopt* module provides four functions:
parse([{Name, Short, Long, ArgSpec, Help}], Args :: string() | [string()]) ->
{ok, {Options, NonOptionArgs}} | {error, {Reason, Data}}
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:
@ -50,7 +56,7 @@ specifications. The type specification for the tuple is:
Help :: string() | undefined
}.
The fields of the record are:
The elements of the tuple are:
- ``Name``: name of the option.
- ``Short``: character for the short option (e.g. $i for -i).
@ -58,11 +64,13 @@ The fields of the record are:
- ``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.
The second parameter holds the list of arguments as passed to the ``main/1``
function in escripts. e.g.
e.g.
{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
@ -74,53 +82,168 @@ all the arguments that did not have corresponding options.
e.g. For a program named ``ex.escript`` with the following option specifications:
OptSpec =
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"},
{file, undefined, undefined, string, "Output file"}
{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", boolean, "List all the actions executed"},
{file, undefined, undefined, string, "Output file"}
].
And this command line:
Args = "-h myhost --port=1000 -x myfile.txt dummy1 dummy2"
Args = "-h myhost --port=1000 -x myfile.txt -v dummy1 dummy2"
Which could also be passed in the format the ``main/1`` function receives the arguments in escripts:
Args = ["-h", "myhost", "--port=1000", "-x", "myfile.txt", "dummy1", "dummy2"].
Args = ["-h", "myhost", "--port=1000", "-x", "file.txt", "-v", "dummy1", "dummy2"].
The call to ``getopt:parse/2``:
getopt:parse(OptSpec, Args).
getopt:parse(OptSpecList, Args).
Will return:
{ok,{[{host,"myhost"},
{port,1000},
xml,
{file,"myfile.txt"},
{dbname,"users"}],
{file,"file.txt"},
{dbname,"users"},
{verbose,true}],
["dummy1","dummy2"]}}
Also, the call to ``getopt:usage/2``:
getopt:usage(OptSpec, "ex1").
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``:
getopt:usage(OptSpecList, "ex1").
Will show (on *stdout*):
Usage: ex1 [-h <host>] [-p <port>] [--dbname <dbname>] [-x] <file>
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 List all the actions executed
<file> Output file
This call to ``getopt:usage/3`` will add a string after the usage command line:
Known limitations
-----------------
getopt:usage(OptSpecList, "ex1", "[var=value ...] [command ...]").
- The syntax for non-option arguments that start with '-' (e.g. -a -- -b)
is not supported yet.
Will show (on *stdout*):
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 List all the actions executed
<file> Output file
Whereas this call to ``getopt:usage/3`` will also add some lines to the options
help text:
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 *stdout*):
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 List all the actions executed
<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"
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"
We can also have options with neither short nor long option name. 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:
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"}
].
And these arguments:
Args = "-x mydb file.out dummy1 dummy1".
The call to ``getopt:parse/2``:
getopt:parse(OptSpecList, Args).
Will return:
{ok,{[xml,{dbname,"mydb"},{output_file,"file.out"}],
["dummy1","dummy1"]}}
Finally, the string ``--`` is considered an option terminator (i.e. all
arguments after it are considered non-option arguments) and the single ``-``
character is considered as non-option argument too.
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
And these ones are considered ``false``:
- false
- f
- no
- n
- off
- disabled
- 0

View File

@ -16,7 +16,7 @@
-author('juanjo@comellas.org').
main([]) ->
getopt:usage(option_spec_list(), escript:script_name());
usage();
main(Args) ->
OptSpecList = option_spec_list(),
io:format("For command line: ~p~n"
@ -26,16 +26,25 @@ main(Args) ->
io:format("Options:~n ~p~n~nNon-option arguments:~n ~p~n", [Options, NonOptArgs]);
{error, {Reason, Data}} ->
io:format("Error: ~s ~p~n~n", [Reason, Data]),
getopt:usage(OptSpecList, "ex1.escript")
usage(OptSpecList)
end.
usage() ->
usage(option_spec_list()).
usage(OptSpecList) ->
getopt:usage(OptSpecList, escript:script_name(), "[var1=val1 ...] [command1 ...]",
[{"var=value", "Variables that will affect the compilation (e.g. debug=1)"},
{"command", "Commands that will be executed by rebar (e.g. compile)"}]).
option_spec_list() ->
CpuCount = erlang:system_info(logical_processors),
[
%% {Name, ShortOpt, LongOpt, ArgSpec, HelpMsg}
{help, $h, "help", undefined, "Show the program options"},
{jobs, $j, "jobs", {integer, 1}, "Number of concurrent jobs"},
{jobs, $j, "jobs", {integer, CpuCount}, "Number of concurrent jobs"},
{verbose, $v, "verbose", {boolean, false}, "Be verbose about what gets done"},
{quiet, $q, "quiet", {boolean, false}, "Be quiet about what gets done"},
{force, $f, "force", {boolean, false}, "Force"}
].

View File

@ -11,10 +11,10 @@
-module(rebar_test).
-author('juanjo@comellas.org').
-export([test/0, test/1]).
-export([test/0, test/1, usage/0]).
test() ->
test("-f verbose=1 --quiet=on dummy1 dummy2").
test("-f verbose=1 --quiet=on -j2 dummy1 dummy2").
test(CmdLine) ->
@ -27,16 +27,24 @@ test(CmdLine) ->
io:format("Options:~n ~p~n~nNon-option arguments:~n ~p~n", [Options, NonOptArgs]);
{error, {Reason, Data}} ->
io:format("Error: ~s ~p~n~n", [Reason, Data]),
getopt:usage(OptSpecList, "ex1")
usage(OptSpecList)
end.
usage() ->
usage(option_spec_list()).
usage(OptSpecList) ->
getopt:usage(OptSpecList, "rebar_test", "[var1=val1 ...] [command1 ...]",
[{"var=value", "Variables that will affect the compilation (e.g. debug=1)"},
{"command", "Commands that will be executed by rebar (e.g. compile)"}]).
option_spec_list() ->
CpuCount = erlang:system_info(logical_processors),
[
%% {Name, ShortOpt, LongOpt, ArgSpec, HelpMsg}
{help, $h, "help", undefined, "Show the program options"},
{jobs, $j, "jobs", {integer, 1}, "Number of concurrent jobs"},
{jobs, $j, "jobs", {integer, CpuCount}, "Number of concurrent jobs"},
{verbose, $v, "verbose", {boolean, false}, "Be verbose about what gets done"},
{quiet, $q, "quiet", {boolean, false}, "Be quiet about what gets done"},
{force, $f, "force", {boolean, false}, "Force"}
].

View File

@ -11,13 +11,14 @@
-module(getopt).
-author('juanjo@comellas.org').
-export([parse/2, usage/2]).
-export([parse/2, usage/2, usage/3, usage/4]).
-define(TAB_LENGTH, 8).
%% Indentation of the help messages in number of tabs.
-define(INDENTATION, 3).
%% Position of each field in the option specification tuple.
-define(OPT_NAME, 1).
-define(OPT_SHORT, 2).
-define(OPT_LONG, 3).
@ -80,7 +81,7 @@ parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, ["--" | Tail]) ->
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [[$-, $- | OptArg] = OptStr | Tail]) ->
parse_option_long(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg);
%% Process short options.
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [[$- | OptArg] = OptStr | Tail]) ->
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [[$- | [_Char | _] = OptArg] = OptStr | Tail]) ->
parse_option_short(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg);
%% Process non-option arguments.
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail]) ->
@ -97,13 +98,14 @@ parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, []) ->
{ok, {lists:reverse(append_default_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc)}}.
%% A long option can have the following formats:
%% --foo Single option 'foo', no argument
%% --foo=bar Single option 'foo', argument "bar"
%% --foo bar Single option 'foo', argument "bar"
-spec parse_option_long([option_spec()], [option()], [string()], integer(), [string()], string(), string()) ->
{ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data:: any()}}.
{ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data:: any()}}.
%% @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"
parse_option_long(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) ->
case split_assigned_arg(OptArg) of
{Long, Arg} ->
@ -126,6 +128,12 @@ parse_option_long(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) ->
end.
-spec parse_option_assigned_arg([option_spec()], [option()], [string()], integer(),
[string()], string(), string(), string()) ->
{ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data:: any()}}.
%% @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.
parse_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg) ->
case lists:keysearch(Long, ?OPT_LONG, OptSpecList) of
{value, {_Name, _Short, Long, ArgSpec, _Help} = OptSpec} ->
@ -141,7 +149,7 @@ parse_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Lon
-spec split_assigned_arg(string()) -> {Name :: string(), Arg :: string()} | string().
%% @doc Split an option string that may contain and option with its argument
%% @doc Split an option string that may contain an option with its argument
%% separated by an equal ('=') character (e.g. "port=1000").
split_assigned_arg(OptStr) ->
split_assigned_arg(OptStr, OptStr, []).
@ -154,14 +162,14 @@ split_assigned_arg(OptStr, [], _Acc) ->
OptStr.
%% A short option can have the following formats:
%% -a Single option 'a', no 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"
%% @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"
-spec parse_option_short([option_spec()], [option()], [string()], integer(), [string()], string(), string()) ->
{ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data:: any()}}.
parse_option_short(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, [Short | Arg]) ->
@ -228,7 +236,8 @@ find_non_option_arg([], _Pos) ->
-spec append_default_options([option_spec()], [option()]) -> [option()].
%% @doc Appends the default values of the options that are not present.
%% @doc Append options that were not present in the command line arguments with
%% their default arguments.
append_default_options([{Name, _Short, _Long, {_Type, DefaultArg}, _Help} | Tail], OptAcc) ->
append_default_options(Tail,
case lists:keymember(Name, 1, OptAcc) of
@ -253,8 +262,6 @@ convert_option_no_arg({Name, _Short, _Long, ArgSpec, _Help}) ->
{Name, true};
boolean ->
{Name, true};
{_Type, DefaultValue} ->
{Name, DefaultValue};
_ ->
throw({error, {missing_option_arg, Name}})
end.
@ -273,6 +280,7 @@ convert_option_arg({Name, _Short, _Long, ArgSpec, _Help}, Arg) ->
-spec arg_spec_type(arg_spec()) -> arg_type() | undefined.
%% @doc Retrieve the data type form an argument specification.
arg_spec_type({Type, _DefaultArg}) ->
Type;
arg_spec_type(Type) when is_atom(Type) ->
@ -280,6 +288,7 @@ arg_spec_type(Type) when is_atom(Type) ->
-spec to_type(atom(), string()) -> arg_value().
%% @doc Convert an argument string to its corresponding data type.
to_type(binary, Arg) ->
list_to_binary(Arg);
to_type(atom, Arg) ->
@ -289,19 +298,36 @@ to_type(integer, Arg) ->
to_type(float, Arg) ->
list_to_float(Arg);
to_type(boolean, Arg) ->
is_boolean_arg(Arg);
LowerArg = string:to_lower(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_valid_option([option_spec()], Opt :: char() | string(), FieldPos :: integer()) -> boolean().
% is_valid_option(OptSpecList, Opt, FieldPos) ->
% case lists:keysearch(Opt, FieldPos, OptSpecList) of
% {value, {_Name, _Short, _Long, _ArgSpec, _Help}} ->
% true;
% _ ->
% false
% end.
-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() | arg_type(), string()) -> boolean().
@ -320,10 +346,7 @@ is_valid_arg(_Type, _Arg) ->
-spec is_boolean_arg(string()) -> boolean().
is_boolean_arg(Arg) ->
LowerArg = string:to_lower(Arg),
(LowerArg =:= "true") orelse (LowerArg =:= "t") orelse
(LowerArg =:= "yes") orelse (LowerArg =:= "y") orelse
(LowerArg =:= "on") orelse (LowerArg =:= "enabled") orelse
(LowerArg =:= "1").
is_arg_true(LowerArg) orelse is_arg_false(LowerArg).
-spec is_integer_arg(string()) -> boolean().
@ -346,7 +369,7 @@ is_float_arg([]) ->
-spec usage([option_spec()], string()) -> ok.
%%--------------------------------------------------------------------
%% @spec usage(OptSpecList :: option_spec_list(), ProgramName :: string()) -> ok.
%% @spec usage(OptSpecList :: [option_spec()], ProgramName :: string()) -> ok.
%% @doc Show a message on stdout indicating the command line options and
%% arguments that are supported by the program.
%%--------------------------------------------------------------------
@ -355,6 +378,37 @@ usage(OptSpecList, ProgramName) ->
[ProgramName, usage_cmd_line(OptSpecList), usage_options(OptSpecList)]).
-spec usage([option_spec()], string(), string()) -> ok.
%%--------------------------------------------------------------------
%% @spec usage(OptSpecList :: [option_spec()], ProgramName :: string(), CmdLineTail :: string()) -> ok.
%% @doc Show a message on stdout 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) ->
io:format("Usage: ~s~s ~s~n~n~s~n",
[ProgramName, usage_cmd_line(OptSpecList), CmdLineTail, usage_options(OptSpecList)]).
-spec usage([option_spec()], string(), string(), [{string(), string()}]) -> ok.
%%--------------------------------------------------------------------
%% @spec usage(OptSpecList :: [option_spec()], ProgramName :: string(),
%% CmdLineTail :: string(), OptionsTail :: [{string(), string()}]) -> ok.
%% @doc Show a message on stdout 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) ->
UsageOptions = lists:foldl(
fun ({Prefix, Help}, Acc) ->
add_option_help(Prefix, Help, Acc)
end, usage_options_reverse(OptSpecList, []), OptionsTail),
io:format("Usage: ~s~s ~s~n~n~s~n",
[ProgramName, usage_cmd_line(OptSpecList), CmdLineTail,
lists:flatten(lists:reverse(UsageOptions))]).
-spec usage_cmd_line([option_spec()]) -> string().
%% @doc Return a string with the syntax for the command line options and
%% arguments.
@ -397,9 +451,9 @@ usage_cmd_line([], Acc) ->
%% @doc Return a string with the help message for each of the options and
%% arguments.
usage_options(OptSpecList) ->
usage_options(OptSpecList, []).
lists:flatten(lists:reverse(usage_options_reverse(OptSpecList, []))).
usage_options([{Name, Short, Long, _ArgSpec, _Help} = OptSpec | Tail], Acc) ->
usage_options_reverse([{Name, Short, Long, _ArgSpec, Help} | Tail], Acc) ->
Prefix =
case Long of
undefined ->
@ -421,14 +475,15 @@ usage_options([{Name, Short, Long, _ArgSpec, _Help} = OptSpec | Tail], Acc) ->
[$-, Short, $,, $\s, $-, $-, Long]
end
end,
usage_options(Tail, add_option_help(OptSpec, Prefix, Acc));
usage_options([], Acc) ->
lists:flatten(lists:reverse(Acc)).
usage_options_reverse(Tail, add_option_help(Prefix, Help, Acc));
usage_options_reverse([], Acc) ->
Acc.
-spec add_option_help(option_spec(), Prefix :: string(), Acc :: string()) -> string().
-spec add_option_help(Prefix :: string(), Help :: string(), Acc :: string()) -> string().
%% @doc Add the help message corresponding to an option specification to a list
%% with the correct indentation.
add_option_help({_Name, _Short, _Long, _ArgSpec, Help}, Prefix, Acc) when is_list(Help), Help =/= [] ->
add_option_help(Prefix, Help, Acc) when is_list(Help), Help =/= [] ->
FlatPrefix = lists:flatten(Prefix),
case ((?INDENTATION * ?TAB_LENGTH) - 2 - length(FlatPrefix)) of
TabSize when TabSize > 0 ->
@ -444,8 +499,9 @@ add_option_help(_Opt, _Prefix, Acc) ->
Acc.
-spec ceiling(float()) -> integer().
%% @doc Return the smallest integral valur not less than the argument.
%% @doc Return the smallest integral value not less than the argument.
ceiling(X) ->
T = erlang:trunc(X),
case (X - T) of

View File

@ -33,29 +33,40 @@ parse_1_test_() ->
Short3 = {short3, $c, undefined, undefined, "Third option with only short form and no argument"},
ShortArg = {short_arg, $d, undefined, string, "Option with only short form and argument"},
ShortDefArg = {short_def_arg, $e, undefined, {string, "default-short"}, "Option with only short form and default argument"},
ShortInt = {short_int, $f, undefined, integer, "Option with only short form and integer argument"},
ShortBool = {short_bool, $f, undefined, boolean, "Option with only short form and boolean argument"},
ShortInt = {short_int, $g, undefined, integer, "Option with only short form and integer argument"},
ShortFloat = {short_float, $h, undefined, float, "Option with only short form and float argument"},
Long = {long, undefined, "long", undefined, "Option with only long form and no argument"},
LongArg = {long_arg, undefined, "long-arg", string, "Option with only long form and argument"},
LongDefArg = {long_def_arg, undefined, "long-def-arg", {string, "default-long"}, "Option with only long form and default argument"},
LongBool = {long_bool, undefined, "long-bool", boolean, "Option with only long form and boolean argument"},
LongInt = {long_int, undefined, "long-int", integer, "Option with only long form and integer argument"},
ShortLong = {short_long, $g, "short-long", undefined, "Option with short form, long form and no argument"},
ShortLongArg = {short_long_arg, $h, "short-long-arg", string, "Option with short form, long form and argument"},
ShortLongDefArg = {short_long_def_arg, $i, "short-long-def-arg", {string, "default-short-long"}, "Option with short form, long form and default argument"},
ShortLongInt = {short_long_int, $j, "short-long-int", integer, "Option with short form, long form and integer argument"},
LongFloat = {long_float, undefined, "long-float", float, "Option with only long form and float argument"},
ShortLong = {short_long, $i, "short-long", undefined, "Option with short form, long form and no argument"},
ShortLongArg = {short_long_arg, $j, "short-long-arg", string, "Option with short form, long form and argument"},
ShortLongDefArg = {short_long_def_arg, $k, "short-long-def-arg", {string, "default-short-long"}, "Option with short form, long form and default argument"},
ShortLongBool = {short_long_bool, $l, "short-long-bool", boolean, "Option with short form, long form and boolean argument"},
ShortLongInt = {short_long_int, $m, "short-long-int", integer, "Option with short form, long form and integer argument"},
ShortLongFloat = {short_long_float, $n, "short-long-float", float, "Option with short form, long form and float argument"},
NonOptArg = {non_opt_arg, undefined, undefined, undefined, "Non-option argument"},
NonOptBool = {non_opt_bool, undefined, undefined, boolean, "Non-option boolean argument"},
NonOptInt = {non_opt_int, undefined, undefined, integer, "Non-option integer argument"},
NonOptFloat = {non_opt_float, undefined, undefined, float, "Non-option float argument"},
CombinedOptSpecs =
[
Short,
ShortArg,
ShortBool,
ShortInt,
Short2,
Short3,
Long,
LongArg,
LongBool,
LongInt,
ShortLong,
ShortLongArg,
ShortLongBool,
ShortLongInt,
NonOptArg,
NonOptInt,
@ -67,13 +78,16 @@ parse_1_test_() ->
[
[$-, ?SHORT(Short)],
[$-, ?SHORT(ShortArg)], "value1",
[$-, ?SHORT(ShortBool)],
[$-, ?SHORT(ShortInt)], "100",
[$-, ?SHORT(Short2), ?SHORT(Short3)],
"--long",
"--long-arg", "value2",
"--long-bool", "true",
"--long-int", "101",
[$-, ?SHORT(ShortLong)],
"--short-long-arg", "value3",
"--short-long-bool", "false",
"--short-long-int", "103",
"value4",
"104",
@ -84,14 +98,17 @@ parse_1_test_() ->
[
?NAME(Short),
{?NAME(ShortArg), "value1"},
{?NAME(ShortBool), true},
{?NAME(ShortInt), 100},
?NAME(Short2),
?NAME(Short3),
?NAME(Long),
{?NAME(LongArg), "value2"},
{?NAME(LongBool), true},
{?NAME(LongInt), 101},
?NAME(ShortLong),
{?NAME(ShortLongArg), "value3"},
{?NAME(ShortLongBool), false},
{?NAME(ShortLongInt), 103},
{?NAME(NonOptArg), "value4"},
{?NAME(NonOptInt), 104},
@ -112,11 +129,30 @@ parse_1_test_() ->
?_assertMatch({ok, {[], ["arg1", "arg2"]}}, parse([], ["arg1", "arg2"]))},
{"Unused options and arguments",
?_assertMatch({ok, {[], ["arg1", "arg2"]}}, parse([Short], ["arg1", "arg2"]))},
%% Option terminator
{"Option terminator before arguments",
?_assertEqual({ok, {[], [[$-, ?SHORT(Short)], "arg1", "arg2"]}}, parse([Short], ["--", [$-, ?SHORT(Short)], "arg1", "arg2"]))},
{"Option terminator between arguments",
?_assertEqual({ok, {[], ["arg1", [$-, ?SHORT(Short)], "arg2"]}}, parse([Short], ["arg1", "--", [$-, ?SHORT(Short)], "arg2"]))},
{"Option terminator after options",
?_assertMatch({ok, {[short], ["arg1", "arg2"]}}, parse([Short], [[$-, ?SHORT(Short)], "--", "arg1", "arg2"]))},
{"Option terminator at the end",
?_assertMatch({ok, {[short], ["arg1", "arg2"]}}, parse([Short], [[$-, ?SHORT(Short)], "arg1", "arg2", "--"]))},
%% Options with only the short form
{?HELP(Short), ?_assertEqual({ok, {[short], []}}, parse([Short], [[$-, ?SHORT(Short)]]))},
{?HELP(ShortArg), ?_assertEqual({ok, {[{short_arg, "value"}], []}}, parse([ShortArg], [[$-, ?SHORT(ShortArg)], "value"]))},
{?HELP(Short), ?_assertMatch({ok, {[short], []}}, parse([Short], [[$-, ?SHORT(Short)]]))},
{?HELP(ShortArg), ?_assertMatch({ok, {[{short_arg, "value"}], []}}, parse([ShortArg], [[$-, ?SHORT(ShortArg)], "value"]))},
{?HELP(ShortDefArg), ?_assertMatch({ok, {[{short_def_arg, "default-short"}], []}}, parse([ShortDefArg], []))},
{?HELP(ShortInt), ?_assertEqual({ok, {[{short_int, 100}], []}}, parse([ShortInt], [[$-, ?SHORT(ShortInt), $1, $0, $0]]))},
{?HELP(ShortBool), ?_assertMatch({ok, {[{short_bool, true}], []}}, parse([ShortBool], [[$-, ?SHORT(ShortBool)]]))},
{?HELP(ShortBool), ?_assertMatch({ok, {[{short_bool, true}], []}}, parse([ShortBool], [[$-, ?SHORT(ShortBool), $t]]))},
{?HELP(ShortBool), ?_assertMatch({ok, {[{short_bool, true}], []}}, parse([ShortBool], [[$-, ?SHORT(ShortBool), $1]]))},
{?HELP(ShortBool), ?_assertMatch({ok, {[{short_bool, true}], []}}, parse([ShortBool], [[$-, ?SHORT(ShortBool)], "true"]))},
{?HELP(ShortBool), ?_assertMatch({ok, {[{short_bool, false}], []}}, parse([ShortBool], [[$-, ?SHORT(ShortBool), $f]]))},
{?HELP(ShortBool), ?_assertMatch({ok, {[{short_bool, false}], []}}, parse([ShortBool], [[$-, ?SHORT(ShortBool), $0]]))},
{?HELP(ShortBool), ?_assertMatch({ok, {[{short_bool, false}], []}}, parse([ShortBool], [[$-, ?SHORT(ShortBool)], "false"]))},
{?HELP(ShortInt), ?_assertMatch({ok, {[{short_int, 100}], []}}, parse([ShortInt], [[$-, ?SHORT(ShortInt), $1, $0, $0]]))},
{?HELP(ShortInt), ?_assertMatch({ok, {[{short_int, 100}], []}}, parse([ShortInt], [[$-, ?SHORT(ShortInt)], "100"]))},
{?HELP(ShortFloat), ?_assertMatch({ok, {[{short_float, 1.0}], []}}, parse([ShortFloat], [[$-, ?SHORT(ShortFloat), $1, $., $0]]))},
{?HELP(ShortFloat), ?_assertMatch({ok, {[{short_float, 1.0}], []}}, parse([ShortFloat], [[$-, ?SHORT(ShortFloat)], "1.0"]))},
{"Unsorted multiple short form options and arguments in a single string",
?_assertMatch({ok, {[short, short2, short3], ["arg1", "arg2"]}}, parse([Short, Short2, Short3], "arg1 -abc arg2"))},
{"Short form option and arguments",
@ -129,25 +165,48 @@ parse_1_test_() ->
{?HELP(LongArg), ?_assertMatch({ok, {[{long_arg, "value"}], []}}, parse([LongArg], ["--long-arg=value"]))},
{?HELP(LongArg), ?_assertMatch({ok, {[{long_arg, "value=1"}], []}}, parse([LongArg], ["--long-arg=value=1"]))},
{?HELP(LongDefArg), ?_assertMatch({ok, {[{long_def_arg, "default-long"}], []}}, parse([LongDefArg], []))},
{?HELP(LongBool), ?_assertMatch({ok, {[{long_bool, true}], []}}, parse([LongBool], ["--long-bool"]))},
{?HELP(LongBool), ?_assertMatch({ok, {[{long_bool, true}], []}}, parse([LongBool], ["--long-bool=t"]))},
{?HELP(LongBool), ?_assertMatch({ok, {[{long_bool, true}], []}}, parse([LongBool], ["--long-bool=1"]))},
{?HELP(LongBool), ?_assertMatch({ok, {[{long_bool, true}], []}}, parse([LongBool], ["--long-bool", "true"]))},
{?HELP(LongBool), ?_assertMatch({ok, {[{long_bool, false}], []}}, parse([LongBool], ["--long-bool=f"]))},
{?HELP(LongBool), ?_assertMatch({ok, {[{long_bool, false}], []}}, parse([LongBool], ["--long-bool=0"]))},
{?HELP(LongBool), ?_assertMatch({ok, {[{long_bool, false}], []}}, parse([LongBool], ["--long-bool", "false"]))},
{?HELP(LongInt), ?_assertMatch({ok, {[{long_int, 100}], []}}, parse([LongInt], ["--long-int", "100"]))},
{?HELP(LongInt), ?_assertMatch({ok, {[{long_int, 100}], []}}, parse([LongInt], ["--long-int=100"]))},
{?HELP(LongFloat), ?_assertMatch({ok, {[{long_float, 1.0}], []}}, parse([LongFloat], ["--long-float", "1.0"]))},
{?HELP(LongFloat), ?_assertMatch({ok, {[{long_float, 1.0}], []}}, parse([LongFloat], ["--long-float=1.0"]))},
{"Long form option and arguments",
?_assertMatch({ok, {[long], ["arg1", "arg2"]}}, parse([Long], ["--long", "arg1", "arg2"]))},
{"Long form option and arguments (unsorted)",
?_assertMatch({ok, {[long], ["arg1", "arg2"]}}, parse([Long], ["arg1", "--long", "arg2"]))},
%% Options with both the short and long form
{?HELP(ShortLong), ?_assertEqual({ok, {[short_long], []}}, parse([ShortLong], [[$-, ?SHORT(ShortLong)]]))},
{?HELP(ShortLong), ?_assertMatch({ok, {[short_long], []}}, parse([ShortLong], [[$-, ?SHORT(ShortLong)]]))},
{?HELP(ShortLong), ?_assertMatch({ok, {[short_long], []}}, parse([ShortLong], ["--short-long"]))},
{?HELP(ShortLongArg), ?_assertEqual({ok, {[{short_long_arg, "value"}], []}}, parse([ShortLongArg], [[$-, ?SHORT(ShortLongArg)], "value"]))},
{?HELP(ShortLongArg), ?_assertMatch({ok, {[{short_long_arg, "value"}], []}}, parse([ShortLongArg], [[$-, ?SHORT(ShortLongArg)], "value"]))},
{?HELP(ShortLongArg), ?_assertMatch({ok, {[{short_long_arg, "value"}], []}}, parse([ShortLongArg], ["--short-long-arg", "value"]))},
{?HELP(ShortLongArg), ?_assertMatch({ok, {[{short_long_arg, "value"}], []}}, parse([ShortLongArg], ["--short-long-arg=value"]))},
{?HELP(ShortLongDefArg), ?_assertMatch({ok, {[{short_long_def_arg, "default-short-long"}], []}}, parse([ShortLongDefArg], []))},
{?HELP(ShortLongInt), ?_assertEqual({ok, {[{short_long_int, 1234}], []}}, parse([ShortLongInt], [[$-, ?SHORT(ShortLongInt)], "1234"]))},
{?HELP(ShortLongBool), ?_assertMatch({ok, {[{short_long_bool, true}], []}}, parse([ShortLongBool], [[$-, ?SHORT(ShortLongBool)]]))},
{?HELP(ShortLongBool), ?_assertMatch({ok, {[{short_long_bool, true}], []}}, parse([ShortLongBool], [[$-, ?SHORT(ShortLongBool), $1]]))},
{?HELP(ShortLongBool), ?_assertMatch({ok, {[{short_long_bool, true}], []}}, parse([ShortLongBool], [[$-, ?SHORT(ShortLongBool)], "yes"]))},
{?HELP(ShortLongBool), ?_assertMatch({ok, {[{short_long_bool, true}], []}}, parse([ShortLongBool], ["--short-long-bool", "on"]))},
{?HELP(ShortLongBool), ?_assertMatch({ok, {[{short_long_bool, true}], []}}, parse([ShortLongBool], ["--short-long-bool=enabled"]))},
{?HELP(ShortLongBool), ?_assertMatch({ok, {[{short_long_bool, false}], []}}, parse([ShortLongBool], [[$-, ?SHORT(ShortLongBool), $0]]))},
{?HELP(ShortLongBool), ?_assertMatch({ok, {[{short_long_bool, false}], []}}, parse([ShortLongBool], [[$-, ?SHORT(ShortLongBool)], "no"]))},
{?HELP(ShortLongBool), ?_assertMatch({ok, {[{short_long_bool, false}], []}}, parse([ShortLongBool], ["--short-long-bool", "off"]))},
{?HELP(ShortLongBool), ?_assertMatch({ok, {[{short_long_bool, false}], []}}, parse([ShortLongBool], ["--short-long-bool=disabled"]))},
{?HELP(ShortLongInt), ?_assertMatch({ok, {[{short_long_int, 1234}], []}}, parse([ShortLongInt], [[$-, ?SHORT(ShortLongInt)], "1234"]))},
{?HELP(ShortLongInt), ?_assertMatch({ok, {[{short_long_int, 1234}], []}}, parse([ShortLongInt], ["--short-long-int", "1234"]))},
{?HELP(ShortLongInt), ?_assertMatch({ok, {[{short_long_int, 1234}], []}}, parse([ShortLongInt], ["--short-long-int=1234"]))},
{?HELP(ShortLongFloat), ?_assertMatch({ok, {[{short_long_float, 1.0}], []}}, parse([ShortLongFloat], [[$-, ?SHORT(ShortLongFloat)], "1.0"]))},
{?HELP(ShortLongFloat), ?_assertMatch({ok, {[{short_long_float, 1.0}], []}}, parse([ShortLongFloat], ["--short-long-float", "1.0"]))},
{?HELP(ShortLongFloat), ?_assertMatch({ok, {[{short_long_float, 1.0}], []}}, parse([ShortLongFloat], ["--short-long-float=1.0"]))},
%% Non-option arguments
{?HELP(NonOptArg), ?_assertMatch({ok, {[{non_opt_arg, "value"}], []}}, parse([NonOptArg], ["value"]))},
{?HELP(NonOptBool), ?_assertMatch({ok, {[{non_opt_bool, false}], []}}, parse([NonOptBool], ["n"]))},
{?HELP(NonOptInt), ?_assertMatch({ok, {[{non_opt_int, 1234}], []}}, parse([NonOptInt], ["1234"]))},
{?HELP(NonOptFloat), ?_assertMatch({ok, {[{non_opt_float, 1.0}], []}}, parse([NonOptFloat], ["1.0"]))},
{"Declared and undeclared non-option arguments",
?_assertMatch({ok, {[{non_opt_arg, "arg1"}], ["arg2", "arg3"]}}, parse([NonOptArg], ["arg1", "arg2", "arg3"]))},
%% Combined