diff --git a/README.markdown b/README.markdown index 9d497d8..d999988 100644 --- a/README.markdown +++ b/README.markdown @@ -69,7 +69,7 @@ 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. +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 @@ -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 ] [-p ] [--dbname ] [-x] [-v] @@ -130,22 +130,22 @@ 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 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 ] [-p ] [--dbname ] [-x] [-v ] [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 + -v, --verbose Verbosity level Output file Whereas this call to ``getopt:usage/3`` will also add some lines to the options @@ -158,12 +158,12 @@ help text: Will show (on *stdout*): Usage: ex1 [-h ] [-p ] [--dbname ] [-x] [-v ] [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 + -v, --verbose Verbosity level 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*. diff --git a/examples/ex1.erl b/examples/ex1.erl index 2ac5906..283f869 100644 --- a/examples/ex1.erl +++ b/examples/ex1.erl @@ -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"} ]. diff --git a/examples/ex1.escript b/examples/ex1.escript index caede32..2cfe66f 100755 --- a/examples/ex1.escript +++ b/examples/ex1.escript @@ -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"} ]. diff --git a/src/getopt.erl b/src/getopt.erl index 11c1e29..0124943 100644 --- a/src/getopt.erl +++ b/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). diff --git a/test/getopt_test.erl b/test/getopt_test.erl index ffb698a..b9fa514 100644 --- a/test/getopt_test.erl +++ b/test/getopt_test.erl @@ -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)). @@ -117,7 +117,7 @@ parse_1_test_() -> {?NAME(ShortLongDefArg), "default-short-long"} ], CombinedRest = ["dummy1", "dummy2"], - + [ {"No options and no arguments", ?_assertMatch({ok, {[], []}}, parse([], []))}, @@ -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"]))} ].