Add support for repetitions of options with integer arguments

This commit adds support for a type of command-line options that are
commonly used by many GNU tools. Now with this modification if you
have an option named 'verbose' with an integer argument and you enter
"-vvv" as in the command line you'll get {verbose, 3} as a result.
This commit is contained in:
Juan Jose Comellas 2012-01-17 00:33:40 -03:00
parent 2379dc96b2
commit 9283bc0697
5 changed files with 122 additions and 84 deletions

View File

@ -88,17 +88,17 @@ e.g. For a program named ``ex.escript`` with the following option specifications
{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"},
{verbose, $v, "verbose", integer, "Verbosity level"},
{file, undefined, undefined, string, "Output file"}
].
And this command line:
Args = "-h myhost --port=1000 -x myfile.txt -v dummy1 dummy2"
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:
Args = ["-h", "myhost", "--port=1000", "-x", "file.txt", "-v", "dummy1", "dummy2"].
Args = ["-h", "myhost", "--port=1000", "-x", "file.txt", "-vvv", "dummy1", "dummy2"].
The call to ``getopt:parse/2``:
@ -111,7 +111,7 @@ Will return:
xml,
{file,"file.txt"},
{dbname,"users"},
{verbose,true}],
{verbose,3}],
["dummy1","dummy2"]}}
@ -122,7 +122,7 @@ For example, given the above-mentioned option specifications, the call to
getopt:usage(OptSpecList, "ex1").
Will show (on *stdout*):
Will show (on *stderr*):
Usage: ex1 [-h <host>] [-p <port>] [--dbname <dbname>] [-x] [-v] <file>
@ -130,14 +130,14 @@ Will show (on *stdout*):
-p, --port Database server port
--dbname Database name
-x Output data in XML
-v List all the actions executed
-v Verbosity level
<file> Output file
This call to ``getopt:usage/3`` will add a string after the usage command line:
getopt:usage(OptSpecList, "ex1", "[var=value ...] [command ...]").
Will show (on *stdout*):
Will show (on *stderr*):
Usage: ex1 [-h <host>] [-p <port>] [--dbname <dbname>] [-x] [-v <verbose>] <file> [var=value ...] [command ...]
@ -145,7 +145,7 @@ Will show (on *stdout*):
-p, --port Database server port
--dbname Database name
-x Output data in XML
-v, --verbose List all the actions executed
-v, --verbose Verbosity level
<file> Output file
Whereas this call to ``getopt:usage/3`` will also add some lines to the options
@ -163,7 +163,7 @@ Will show (on *stdout*):
-p, --port Database server port
--dbname Database name
-x Output data in XML
-v, --verbose List all the actions executed
-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)
@ -184,6 +184,7 @@ A short option can have the following syntax:
-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' (when 'a' has integer arguments)
A long option can have the following syntax:
@ -225,25 +226,9 @@ character is considered as non-option argument too.
Argument Types
--------------
The arguments allowed for options are: atom; binary; boolean; float; integer; string.
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``:
upper case) are considered ``true``: *true*; *t*; *yes*; *y*; *on*; *enabled*; *1*.
- true
- t
- yes
- y
- on
- enabled
- 1
And these ones are considered ``false``:
- false
- f
- no
- n
- off
- disabled
- 0
And these ones are considered ``false``: *false*; *f*; *no*; *n*; *off*; *disabled*; *0*.

View File

@ -42,5 +42,6 @@ option_spec_list() ->
{port, $p, "port", {integer, 1000}, "Database server port"},
{output_file, $o, "output-file", string, "File where the data will be saved to"},
{xml, $x, "xml", undefined, "Output data as XML"},
{verbose, $v, "verbose", integer, "Verbosity level"},
{dbname, undefined, undefined, string, "Database name"}
].

View File

@ -46,5 +46,6 @@ option_spec_list() ->
{port, $p, "port", {integer, 1000}, "Database server port"},
{output_file, $o, "output-file", string, "File where the data will be saved to"},
{xml, $x, "xml", undefined, "Output data as XML"},
{verbose, $v, "verbose", integer, "Verbosity level"},
{dbname, undefined, undefined, string, "Database name"}
].

View File

@ -93,7 +93,7 @@ parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["-" ++ ([_Char | _] = OptArg) = OptS
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail]) ->
case find_non_option_arg(OptSpecList, ArgPos) of
{value, OptSpec} when ?IS_OPT_SPEC(OptSpec) ->
parse(OptSpecList, [convert_option_arg(OptSpec, Arg) | OptAcc],
parse(OptSpecList, add_option_arg(OptSpec, Arg, OptAcc),
ArgAcc, ArgPos + 1, Tail);
false ->
parse(OptSpecList, OptAcc, [Arg | ArgAcc], ArgPos, Tail)
@ -147,7 +147,7 @@ parse_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Lon
undefined ->
throw({error, {invalid_option_arg, OptStr}});
_ ->
parse(OptSpecList, [convert_option_arg(OptSpec, Arg) | OptAcc], ArgAcc, ArgPos, Args)
parse(OptSpecList, add_option_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos, Args)
end;
false ->
throw({error, {invalid_option, OptStr}})
@ -176,6 +176,7 @@ split_assigned_arg(OptStr, [], _Acc) ->
%% -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_option_short([option_spec()], [option()], [string()], integer(), [string()], string(), string()) ->
{ok, {[option()], [string()]}}.
parse_option_short(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, [Short | Arg]) ->
@ -186,16 +187,22 @@ parse_option_short(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, [Short | A
{_Name, Short, _Long, ArgSpec, _Help} = OptSpec ->
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.
%% The option argument string is empty, but the option requires
%% an argument, so we look into the next string in the list.
parse_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec);
_ ->
case is_valid_arg(ArgSpec, Arg) of
true ->
parse(OptSpecList, [convert_option_arg(OptSpec, Arg) | OptAcc], ArgAcc, ArgPos, Args);
parse(OptSpecList, add_option_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos, Args);
_ ->
parse_option_short(OptSpecList, [convert_option_no_arg(OptSpec) | OptAcc], ArgAcc, ArgPos, Args, OptStr, Arg)
%% There are 2 valid cases in which we may not receive the expected argument:
%% 1) When the expected argument is a boolean: in this case the presence
%% of the option makes the argument true.
%% 2) When the expected argument is an integer: in this case the presence
%% of the option sets the value to 1 and any additional appearances of
%% the option increment it by 1 (e.g. "-vvv" would return {verbose, 3}).
parse_option_short(OptSpecList, add_option_no_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args, OptStr, Arg)
end
end;
@ -207,25 +214,34 @@ parse_option_short(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, _OptStr, []) ->
%% @doc Retrieve the argument for an option from the next string in the list of
%% command-line parameters.
%% command-line parameters or set the value of the argument from the argument
%% specification (for boolean and integer arguments), if possible.
parse_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail] = Args, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec) ->
% Special case for booleans: when the next string is an option we assume
% the value is 'true'.
case (arg_spec_type(ArgSpec) =:= boolean) andalso not is_boolean_arg(Arg) of
ArgSpecType = arg_spec_type(ArgSpec),
case (ArgSpecType =:= boolean) andalso not is_boolean_arg(Arg) of
true ->
parse(OptSpecList, [{Name, true} | OptAcc], ArgAcc, ArgPos, Args);
_ ->
parse(OptSpecList, [convert_option_arg(OptSpec, Arg) | OptAcc], ArgAcc, ArgPos, Tail)
%% Special case for booleans: when the next string is not a boolean
%% argument we assume the value is 'true'.
parse(OptSpecList, add_arg(OptSpec, true, OptAcc), ArgAcc, ArgPos, Args);
false ->
case (ArgSpecType =:= integer) andalso not is_integer_arg(Arg) of
true ->
%% 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}.
parse(OptSpecList, add_implicit_integer_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args);
false ->
try
parse(OptSpecList, add_arg(OptSpec, to_type(ArgSpecType, Arg), OptAcc), ArgAcc, ArgPos, Tail)
catch
error:_ ->
throw({error, {invalid_option_arg, {Name, Arg}}})
end
end
end;
parse_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, [] = Args, {Name, _Short, _Long, ArgSpec, _Help}) ->
% Special case for booleans: when the next string is missing we assume the
% value is 'true'.
case arg_spec_type(ArgSpec) of
boolean ->
parse(OptSpecList, [{Name, true} | OptAcc], ArgAcc, ArgPos, Args);
_ ->
throw({error, {missing_option_arg, Name}})
end.
parse_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, [] = Args, OptSpec) ->
parse(OptSpecList, add_option_no_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args).
%% @doc Find the option for the discrete argument in position specified in the
@ -259,32 +275,72 @@ append_default_options([], OptAcc) ->
OptAcc.
-spec convert_option_no_arg(option_spec()) -> compound_option().
convert_option_no_arg({Name, _Short, _Long, ArgSpec, _Help}) ->
case ArgSpec of
% Special case for booleans: if there is no argument we assume
% the value is 'true'.
{boolean, _DefaultValue} ->
{Name, true};
%% @doc Add an option with no argument.
-spec add_option_no_arg(option_spec(), [option()]) -> [option()].
add_option_no_arg({Name, _Short, _Long, ArgSpec, _Help} = OptSpec, OptAcc) ->
case arg_spec_type(ArgSpec) of
boolean ->
{Name, true};
_ ->
%% Special case for boolean arguments: if there is no argument we
%% set the value to 'true'.
add_arg(OptSpec, 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}.
add_implicit_integer_arg(OptSpec, OptAcc);
true ->
throw({error, {missing_option_arg, Name}})
end.
%% @doc Convert the argument passed in the command line to the data type
%% indicated by the argument specification.
-spec convert_option_arg(option_spec(), string()) -> compound_option().
convert_option_arg({Name, _Short, _Long, ArgSpec, _Help}, Arg) ->
try
{Name, to_type(arg_spec_type(ArgSpec), Arg)}
catch
error:_ ->
throw({error, {invalid_option_arg, {Name, Arg}}})
%% @doc Add an option with argument converting it to the data type indicated by the
%% argument specification.
-spec add_option_arg(option_spec(), string(), [option()]) -> [option()].
add_option_arg({Name, _Short, _Long, ArgSpec, _Help} = OptSpec, Arg, OptAcc) ->
ArgSpecType = arg_spec_type(ArgSpec),
case (ArgSpecType =:= boolean) andalso not is_boolean_arg(Arg) of
true ->
%% Special case for booleans: when the next string is not a boolean
%% argument we assume the value is 'true'.
add_arg(OptSpec, true, OptAcc);
false ->
case (ArgSpecType =:= integer) andalso not is_integer_arg(Arg) of
true ->
%% 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}.
add_implicit_integer_arg(OptSpec, OptAcc);
false ->
try
add_arg(OptSpec, to_type(ArgSpecType, Arg), OptAcc)
catch
error:_ ->
throw({error, {invalid_option_arg, {Name, Arg}}})
end
end
end.
%% Add an option with an integer argument.
-spec add_implicit_integer_arg(option_spec(), [option()]) -> [option()].
add_implicit_integer_arg({Name, _Short, _Long, _ArgSpec, _Help}, OptAcc) ->
case lists:keyfind(Name, 1, OptAcc) of
{Name, Count} ->
lists:keyreplace(Name, 1, OptAcc, {Name, Count + 1});
false ->
[{Name, 1} | OptAcc]
end.
%% @doc Add an option with an argument and convert it to the data type corresponding
%% to the argument specification.
-spec add_arg(option_spec(), string(), [option()]) -> [option()].
add_arg({Name, _Short, _Long, _ArgSpec, _Help}, Arg, OptAcc) ->
[{Name, Arg} | lists:keydelete(Name, 1, OptAcc)].
%% @doc Retrieve the data type form an argument specification.
-spec arg_spec_type(arg_spec()) -> arg_type() | undefined.
arg_spec_type({Type, _DefaultArg}) ->
@ -382,16 +438,13 @@ usage(OptSpecList, ProgramName) ->
%% @doc Show a message on stderr or stdout indicating the command line options and
%% arguments that are supported by the program.
-spec usage([option_spec()], string(), output_stream()) -> ok.
-spec usage([option_spec()], string(), output_stream() | string()) -> ok.
usage(OptSpecList, ProgramName, OutputStream) when is_atom(OutputStream) ->
io:format(OutputStream, "Usage: ~s~s~n~n~s~n",
[ProgramName, usage_cmd_line(OptSpecList), usage_options(OptSpecList)]);
%% @doc Show a message on stderr 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()], string(), string()) -> ok.
usage(OptSpecList, ProgramName, CmdLineTail) ->
usage(OptSpecList, ProgramName, CmdLineTail, standard_error).
@ -399,17 +452,14 @@ usage(OptSpecList, ProgramName, CmdLineTail) ->
%% @doc Show a message on stderr or 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.
-spec usage([option_spec()], string(), string(), output_stream()) -> ok.
usage(OptSpecList, ProgramName, CmdLineTail) when is_atom(OutputStream) ->
-spec usage([option_spec()], string(), string(), output_stream() | [{string(), string()}]) -> ok.
usage(OptSpecList, ProgramName, CmdLineTail, OutputStream) when is_atom(OutputStream) ->
io:format(OutputStream, "Usage: ~s~s ~s~n~n~s~n",
[ProgramName, usage_cmd_line(OptSpecList), CmdLineTail, usage_options(OptSpecList)]);
%% @doc Show a message on stderr 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()], string(), string(), [{string(), string()}]) -> ok.
usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail) ->
usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail, standard_error).

View File

@ -13,7 +13,7 @@
-include_lib("eunit/include/eunit.hrl").
-import(getopt, [parse/2, usage/2]).
-import(getopt, [parse/2]).
-define(NAME(Opt), element(1, Opt)).
-define(SHORT(Opt), element(2, Opt)).
@ -151,6 +151,7 @@ parse_1_test_() ->
{?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(ShortInt), ?_assertMatch({ok, {[{short_int, 3}], []}}, parse([ShortInt], [[$-, ?SHORT(ShortInt), ?SHORT(ShortInt), ?SHORT(ShortInt)]]))},
{?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",
@ -212,6 +213,6 @@ parse_1_test_() ->
%% Combined
{"Combined short, long and non-option arguments",
?_assertEqual({ok, {CombinedOpts, CombinedRest}}, parse(CombinedOptSpecs, CombinedArgs))},
{"Option with only short form and invalid integer argument",
?_assertEqual({error, {invalid_option_arg, {?NAME(ShortInt), "value"}}}, parse([ShortInt], [[$-, ?SHORT(ShortInt)], "value"]))}
{"Option with only short form and non-integer argument",
?_assertEqual({ok, {[{short_int, 1}], ["value"]}}, parse([ShortInt], [[$-, ?SHORT(ShortInt)], "value"]))}
].