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:
parent
2379dc96b2
commit
9283bc0697
@ -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*.
|
||||
|
@ -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"}
|
||||
].
|
||||
|
@ -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"}
|
||||
].
|
||||
|
148
src/getopt.erl
148
src/getopt.erl
@ -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).
|
||||
|
||||
|
@ -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"]))}
|
||||
].
|
||||
|
Loading…
x
Reference in New Issue
Block a user