From 34e72bacb94db63ea8eb8704d4a4ab58886fb32f Mon Sep 17 00:00:00 2001 From: Juan Jose Comellas Date: Tue, 13 Oct 2009 17:33:20 -0300 Subject: [PATCH] Added support for multiple short options in a single command-line argument: e.g. parse(SpecList, ["-abc"]). == parse(SpecList, ["-a", "-b", "-c"]). --- include/getopt.hrl | 4 +- src/getopt.erl | 188 ++++++++++++++++++++------------------- src/test/getopt_test.erl | 106 +++++++++++++++++++--- 3 files changed, 194 insertions(+), 104 deletions(-) diff --git a/include/getopt.hrl b/include/getopt.hrl index ebde3ce..c2054be 100644 --- a/include/getopt.hrl +++ b/include/getopt.hrl @@ -4,9 +4,9 @@ %% @type getopt_arg() = atom() | binary() | bool() | float() | integer() | string(). %% Data type that an argument can be converted to. -type getopt_arg() :: atom() | binary() | boolean() | float() | integer() | string(). -%% @type getopt_arg_spec() = getopt_arg_type() | {getopt_arg_type(), getopt_arg()} | help | undefined. +%% @type getopt_arg_spec() = getopt_arg_type() | {getopt_arg_type(), getopt_arg()} | undefined. %% Argument specification. --type getopt_arg_spec() :: getopt_arg_type() | {getopt_arg_type(), getopt_arg()} | help | undefined. +-type getopt_arg_spec() :: getopt_arg_type() | {getopt_arg_type(), getopt_arg()} | undefined. %% @doc Record that defines the option specifications. -record(option, { diff --git a/src/getopt.erl b/src/getopt.erl index feed7f9..48c2159 100644 --- a/src/getopt.erl +++ b/src/getopt.erl @@ -10,107 +10,109 @@ %% @headerfile "getopt.hrl" -define(TAB_LENGTH, 8). --define(HELP_INDENTATION, 4 * ?TAB_LENGTH). +-define(HELP_INDENTATION, 3 * ?TAB_LENGTH). -%% @type option() = atom() | {atom(), getopt_arg()}. Option type and optional default argument. --type option() :: atom() | {atom(), getopt_arg()}. -%% @type option_spec() = [#option{}]. Command line options specification. --type option_spec() :: [#option{}]. -%% @type option_list() = [option()]. List of option types. --type option_list() :: [option()]. -%% @type arg_list() = [getopt_arg()]. List of arguments returned to the calling function. --type arg_list() :: [getopt_arg()]. +%% @type option() = atom() | {atom(), getopt_arg()}. Option type and optional default argument. +-type option() :: atom() | {atom(), getopt_arg()}. +%% @type option_spec() = #option{}. Command line option specification. +-type option_spec() :: #option{}. -export([parse/2, usage/2]). --spec parse(option_spec(), [string()]) -> option_list(). +-spec parse([option_spec()], [string()]) -> {ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: any()}}. %%-------------------------------------------------------------------- -%% @spec parse(OptSpec::option_spec(), Args::[string()]) -> option_list(). +%% @spec parse(OptSpecList::[option_spec()], Args::[string()]) -> [option()]. %% @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. %%-------------------------------------------------------------------- -parse(OptSpec, Args) -> - catch parse(OptSpec, [], [], Args). +parse(OptSpecList, Args) -> + try + parse(OptSpecList, [], [], 0, Args) + catch + throw: {error, {_Reason, _Data}} = Error -> + Error + end. --spec parse(option_spec(), option_list(), arg_list(), [string()]) -> option_list(). -%% Process short and long options -parse(OptSpec, OptAcc, ArgAcc, [("-" ++ OptTail) = Opt | Tail]) -> - {SearchField, OptName} = case OptTail of - "-" ++ Str -> - {#option.long, Str}; - [Char] -> - {#option.short, Char}; - _ -> - throw({error, {invalid_option, Opt}}) - end, +-spec parse([option_spec()], [option()], [string()], integer(), [string()]) -> {ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data:: any()}}. +%% Process long options. +parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [[$-, $- | LongName] = OptStr | Tail]) -> + {Option, Tail1} = get_option(OptSpecList, OptStr, LongName, #option.long, Tail), + parse(OptSpecList, [Option | OptAcc], ArgAcc, ArgPos, Tail1); +%% Process short options. +parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [[$-, ShortName] = OptStr | Tail]) -> + {Option, Tail1} = get_option(OptSpecList, OptStr, ShortName, #option.short, Tail), + parse(OptSpecList, [Option | OptAcc], ArgAcc, ArgPos, Tail1); +%% Process multiple short options with no argument. +parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [[$- | ShortNameList] = OptStr | Tail]) -> + NewOptAcc = lists:foldl(fun (ShortName, OptAcc1) -> + [get_option_no_arg(OptSpecList, OptStr, ShortName, #option.short) | OptAcc1] + end, OptAcc, ShortNameList), + parse(OptSpecList, NewOptAcc, ArgAcc, ArgPos, Tail); +%% Process non-option arguments. +parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail]) -> + case find_non_option_arg(OptSpecList, ArgPos) of + {value, #option{} = OptSpec} -> + parse(OptSpecList, [convert_option_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_args(OptSpecList, OptAcc)), lists:reverse(ArgAcc)}}. - case lists:keysearch(OptName, SearchField, OptSpec) of - {value, #option{name = Name, arg = ArgSpec}} -> - if - ArgSpec =/= undefined -> + +-spec get_option([option_spec()], string(), string() | char(), integer(), [string()]) -> + {option(), [string()]}. +%% @doc Retrieve the specification corresponding to an option matching a string +%% received on the command line. +get_option(OptSpecList, OptStr, OptName, FieldPos, Tail) -> + case lists:keysearch(OptName, FieldPos, OptSpecList) of + {value, #option{name = Name, arg = ArgSpec} = OptSpec} -> + case ArgSpec of + undefined -> + {Name, Tail}; + _ -> case Tail of [Arg | Tail1] -> - parse(OptSpec, [convert_option_arg(Name, ArgSpec, Arg) | OptAcc], ArgAcc, Tail1); - [] -> + {convert_option_arg(OptSpec, Arg), Tail1}; + [] -> throw({error, {missing_option_arg, Name}}) - end; - true -> - parse(OptSpec, [Name | OptAcc], ArgAcc, Tail) + end end; false -> - {error, {invalid_option, Opt}} - end; -%% Process the discrete arguments -parse(OptSpec, OptAcc, ArgAcc, [Arg | Tail]) -> - parse(OptSpec, OptAcc, [Arg | ArgAcc], Tail); -parse(OptSpec, OptAcc, ArgAcc, []) -> - %% Once we have completed gathering the options that have short and long - %% option strings we merge the remaining discrete arguments that were - %% specified. - {MergedOpts, MergedArgs} = merge_discrete_args(OptSpec, OptAcc, ArgAcc), - %% Finally, we set the options that were not present to their default - %% arguments. - {ok, {lists:reverse(append_default_args(OptSpec, MergedOpts)), MergedArgs}}. + throw({error, {invalid_option, OptStr}}) + end. - - --spec merge_discrete_args(option_spec(), option_list(), arg_list()) -> {option_list(), arg_list()}. -%% @doc Merge the discrete arguments that were declared in the option -%% specification to the list of options. -merge_discrete_args(OptSpec, Options, Args) -> - merge_discrete_args(OptSpec, Args, 0, Options, []). - -merge_discrete_args(OptSpec, [Head | Tail] = Args, Count, OptAcc, ArgAcc) -> - case find_discrete_arg(OptSpec, 0) of - {value, #option{name = Name, arg = ArgSpec}} -> - merge_discrete_args(OptSpec, Tail, Count + 1, [convert_option_arg(Name, ArgSpec, Head) | OptAcc], ArgAcc); +-spec get_option_no_arg([option_spec()], string(), string() | char(), integer()) -> option(). +%% @doc Retrieve the specification corresponding to an option that has no +%% argument and matches a string received on the command line. +get_option_no_arg(OptSpecList, OptStr, OptName, FieldPos) -> + case lists:keysearch(OptName, FieldPos, OptSpecList) of + {value, #option{name = Name, arg = undefined}} -> + Name; + {value, #option{name = Name}} -> + throw({error, {missing_option_arg, Name}}); false -> - %% If we could not find a discrete option specification that means - %% that all the remaining elements in the list are discrete - %% arguments. - %% merge_discrete_args(OptSpec, Tail, Count, OptAcc, [Head | ArgAcc]) - {OptAcc, lists:foldl(fun (Arg, Acc) -> [Arg | Acc] end, ArgAcc, Args)} - end; -merge_discrete_args(_OptSpec, [], _Count, OptAcc, ArgAcc) -> - {OptAcc, ArgAcc}. + throw({error, {invalid_option, OptStr}}) + end. - --spec find_discrete_arg(option_spec(), integer()) -> {value, #option{}} | false. +-spec find_non_option_arg([option_spec()], integer()) -> {value, option_spec()} | false. %% @doc Find the option for the discrete argument in position specified in the %% Pos argument. -find_discrete_arg([#option{short = undefined, long = undefined} = Opt | _Tail], 0) -> +find_non_option_arg([#option{short = undefined, long = undefined} = Opt | _Tail], 0) -> {value, Opt}; -find_discrete_arg([#option{short = undefined, long = undefined} | Tail], Pos) -> - find_discrete_arg(Tail, Pos - 1); -find_discrete_arg([_Head | Tail], Pos) -> - find_discrete_arg(Tail, Pos); -find_discrete_arg([], _Pos) -> +find_non_option_arg([#option{short = undefined, long = undefined} | 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. --spec append_default_args(option_spec(), option_list()) -> option_list(). +-spec append_default_args([option_spec()], [option()]) -> [option()]. %% @doc Appends the default values of the options that are not present. append_default_args([#option{name = Name, arg = {_Type, DefaultArg}} | Tail], OptAcc) -> append_default_args(Tail, @@ -127,10 +129,10 @@ append_default_args([], OptAcc) -> OptAcc. --spec convert_option_arg(atom(), getopt_arg_spec(), Arg :: string()) -> option_list(). +-spec convert_option_arg(option_spec(), string()) -> [option()]. %% @doc Convert the argument passed in the command line to the data type %% indicated byt the argument specification. -convert_option_arg(Name, ArgSpec, Arg) -> +convert_option_arg(#option{name = Name, arg = ArgSpec}, Arg) -> try Converted = case ArgSpec of {Type, _DefaultArg} -> @@ -144,7 +146,7 @@ convert_option_arg(Name, ArgSpec, Arg) -> throw({error, {invalid_option_arg, {Name, Arg}}}) end. - +-spec to_type(atom(), string()) -> getopt_arg(). to_type(binary, Arg) -> list_to_binary(Arg); to_type(atom, Arg) -> @@ -162,21 +164,21 @@ to_type(_Type, Arg) -> Arg. --spec usage(option_spec(), string()) -> ok. +-spec usage([option_spec()], string()) -> ok. %%-------------------------------------------------------------------- -%% @spec usage(OptSpec :: option_spec(), ProgramName :: string()) -> ok. +%% @spec usage(OptSpecList :: option_spec_list(), ProgramName :: string()) -> ok. %% @doc Show a message on stdout indicating the command line options and %% arguments that are supported by the program. %%-------------------------------------------------------------------- -usage(OptSpec, ProgramName) -> - io:format("Usage: ~s~s~n~n~s~n", [ProgramName, usage_cmd_line(OptSpec), usage_options(OptSpec)]). +usage(OptSpecList, ProgramName) -> + io:format("Usage: ~s~s~n~n~s~n", [ProgramName, usage_cmd_line(OptSpecList), usage_options(OptSpecList)]). --spec usage_cmd_line(option_spec()) -> string(). +-spec usage_cmd_line([option_spec()]) -> string(). %% @doc Return a string with the syntax for the command line options and %% arguments. -usage_cmd_line(OptSpec) -> - usage_cmd_line(OptSpec, []). +usage_cmd_line(OptSpecList) -> + usage_cmd_line(OptSpecList, []). %% For options with short form and no argument. usage_cmd_line([#option{short = Short, arg = undefined} | Tail], Acc) when Short =/= undefined -> @@ -197,13 +199,13 @@ usage_cmd_line([], Acc) -> lists:flatten(lists:reverse(Acc)). --spec usage_options(option_spec()) -> string(). +-spec usage_options([option_spec()]) -> string(). %% @doc Return a string with the help message for each of the options and %% arguments. -usage_options(OptSpec) -> - usage_options(OptSpec, []). +usage_options(OptSpecList) -> + usage_options(OptSpecList, []). -%% Neither short nor long form (discrete argument). +%% Neither short nor long form (non-option argument). usage_options([#option{name = Name, short = undefined, long = undefined} = Opt | Tail], Acc) -> usage_options(Tail, add_option_help(Opt, [$<, atom_to_list(Name), $>], Acc)); %% Only short form. @@ -219,7 +221,7 @@ usage_options([], Acc) -> lists:flatten(lists:reverse(Acc)). --spec add_option_help(#option{}, Prefix :: list(), Acc :: list()) -> list(). +-spec add_option_help(option_spec(), Prefix :: string(), Acc :: string()) -> string(). %% @doc Add the help message corresponding to an option specification to a list %% with the correct indentation. add_option_help(#option{help = Help}, Prefix, Acc) when is_list(Help), Help =/= [] -> @@ -229,8 +231,10 @@ add_option_help(#option{help = Help}, Prefix, Acc) when is_list(Help), Help =/= Tab = lists:duplicate(ceiling(TabSize / ?TAB_LENGTH), $\t), [[$\s, $\s, FlatPrefix, Tab, Help, $\n] | Acc]; _ -> - %% The indentation for the option description is 4 tabs (i.e. 32 characters) - [[$\t, $\t, $\t, $\t, Help, $\n], [$\s, $\s, FlatPrefix, $\n] | Acc] + %% The indentation for the option description is 3 tabs (i.e. 24 characters) + %% IMPORTANT: Change the number of tabs below if you change the + %% value of the INDENTATION macro. + [[$\t, $\t, $\t, Help, $\n], [$\s, $\s, FlatPrefix, $\n] | Acc] end; add_option_help(_Opt, _Prefix, Acc) -> Acc. diff --git a/src/test/getopt_test.erl b/src/test/getopt_test.erl index 019c86b..b478ced 100644 --- a/src/test/getopt_test.erl +++ b/src/test/getopt_test.erl @@ -24,21 +24,31 @@ parse_1_test_() -> short = $a, help = "Option with only short form and no argument" }, + Short2 = + #option{name = short2, + short = $b, + help = "Second option with only short form and no argument" + }, + Short3 = + #option{name = short3, + short = $c, + help = "Third ption with only short form and no argument" + }, ShortArg = #option{name = short_arg, - short = $b, + short = $d, arg = string, help = "Option with only short form and argument" }, ShortDefArg = #option{name = short_def_arg, - short = $c, - arg = {string, "default"}, + short = $e, + arg = {string, "default-short"}, help = "Option with only short form and default argument" }, ShortInt = #option{name = short_int, - short = $e, + short = $f, arg = integer, help = "Option with only short form and integer argument" }, @@ -56,7 +66,7 @@ parse_1_test_() -> LongDefArg = #option{name = long_def_arg, long = "long-def-arg", - arg = {string, "default"}, + arg = {string, "default-long"}, help = "Option with only long form and default argument" }, LongInt = @@ -82,7 +92,7 @@ parse_1_test_() -> #option{name = short_long_def_arg, short = $i, long = "short-long-def-arg", - arg = {string, "default"}, + arg = {string, "default-short-long"}, help = "Option with short form, long form and default argument" }, ShortLongInt = @@ -92,7 +102,72 @@ parse_1_test_() -> arg = integer, help = "Option with short form, long form and integer argument" }, - + NonOptArg = + #option{name = non_opt_arg, + help = "Non-option argument" + }, + NonOptInt = + #option{name = non_opt_int, + arg = integer, + help = "Non-option integer argument" + }, + CombinedOptSpecs = + [ + Short, + ShortArg, + ShortInt, + Short2, + Short3, + Long, + LongArg, + LongInt, + ShortLong, + ShortLongArg, + ShortLongInt, + NonOptArg, + NonOptInt, + ShortDefArg, + LongDefArg, + ShortLongDefArg + ], + CombinedArgs = + [ + [$-, Short#option.short], + [$-, ShortArg#option.short], "value1", + [$-, ShortInt#option.short], "100", + [$-, Short2#option.short, Short3#option.short], + "--long", + "--long-arg", "value2", + "--long-int", "101", + [$-, ShortLong#option.short], + "--short-long-arg", "value3", + "--short-long-int", "103", + "value4", + "104", + "dummy1", + "dummy2" + ], + CombinedOpts = + [ + Short#option.name, + {ShortArg#option.name, "value1"}, + {ShortInt#option.name, 100}, + Short2#option.name, + Short3#option.name, + Long#option.name, + {LongArg#option.name, "value2"}, + {LongInt#option.name, 101}, + ShortLong#option.name, + {ShortLongArg#option.name, "value3"}, + {ShortLongInt#option.name, 103}, + {NonOptArg#option.name, "value4"}, + {NonOptInt#option.name, 104}, + {ShortDefArg#option.name, "default-short"}, + {LongDefArg#option.name, "default-long"}, + {ShortLongDefArg#option.name, "default-short-long"} + ], + CombinedRest = ["dummy1", "dummy2"], + [ {"No options and no arguments", ?_assertMatch({ok, {[], []}}, parse([], []))}, @@ -107,8 +182,11 @@ parse_1_test_() -> %% Options with only the short form {Short#option.help, ?_assertMatch({ok, {[short], []}}, parse([Short], [[$-, Short#option.short]]))}, {ShortArg#option.help, ?_assertMatch({ok, {[{short_arg, "value"}], []}}, parse([ShortArg], [[$-, ShortArg#option.short], "value"]))}, - {ShortDefArg#option.help, ?_assertMatch({ok, {[{short_def_arg, "default"}], []}}, parse([ShortDefArg], []))}, + {ShortDefArg#option.help, ?_assertMatch({ok, {[{short_def_arg, "default-short"}], []}}, parse([ShortDefArg], []))}, {ShortInt#option.help, ?_assertMatch({ok, {[{short_int, 100}], []}}, parse([ShortInt], [[$-, ShortInt#option.short], "100"]))}, + {"Unsorted multiple short form options and arguments", + ?_assertMatch({ok, {[short, short2, short3], ["arg1", "arg2"]}}, + parse([Short, Short2, Short3], ["arg1", [$-, Short#option.short, Short2#option.short, Short3#option.short], "arg2"]))}, {"Short form option and arguments", ?_assertMatch({ok, {[short], ["arg1", "arg2"]}}, parse([Short], [[$-, Short#option.short], "arg1", "arg2"]))}, {"Short form option and arguments (unsorted)", @@ -116,7 +194,7 @@ parse_1_test_() -> %% Options with only the long form {Long#option.help, ?_assertMatch({ok, {[long], []}}, parse([Long], ["--long"]))}, {LongArg#option.help, ?_assertMatch({ok, {[{long_arg, "value"}], []}}, parse([LongArg], ["--long-arg", "value"]))}, - {LongDefArg#option.help, ?_assertMatch({ok, {[{long_def_arg, "default"}], []}}, parse([LongDefArg], []))}, + {LongDefArg#option.help, ?_assertMatch({ok, {[{long_def_arg, "default-long"}], []}}, parse([LongDefArg], []))}, {LongInt#option.help, ?_assertMatch({ok, {[{long_int, 100}], []}}, parse([LongInt], ["--long-int", "100"]))}, {"Long form option and arguments", ?_assertMatch({ok, {[long], ["arg1", "arg2"]}}, parse([Long], ["--long", "arg1", "arg2"]))}, @@ -127,9 +205,17 @@ parse_1_test_() -> {ShortLong#option.help, ?_assertMatch({ok, {[short_long], []}}, parse([ShortLong], ["--short-long"]))}, {ShortLongArg#option.help, ?_assertMatch({ok, {[{short_long_arg, "value"}], []}}, parse([ShortLongArg], [[$-, ShortLongArg#option.short], "value"]))}, {ShortLongArg#option.help, ?_assertMatch({ok, {[{short_long_arg, "value"}], []}}, parse([ShortLongArg], ["--short-long-arg", "value"]))}, - {ShortLongDefArg#option.help, ?_assertMatch({ok, {[{short_long_def_arg, "default"}], []}}, parse([ShortLongDefArg], []))}, + {ShortLongDefArg#option.help, ?_assertMatch({ok, {[{short_long_def_arg, "default-short-long"}], []}}, parse([ShortLongDefArg], []))}, {ShortLongInt#option.help, ?_assertMatch({ok, {[{short_long_int, 1234}], []}}, parse([ShortLongInt], [[$-, ShortLongInt#option.short], "1234"]))}, {ShortLongInt#option.help, ?_assertMatch({ok, {[{short_long_int, 1234}], []}}, parse([ShortLongInt], ["--short-long-int", "1234"]))}, + %% Non-option arguments + {NonOptArg#option.help, ?_assertMatch({ok, {[{non_opt_arg, "value"}], []}}, parse([NonOptArg], ["value"]))}, + {NonOptInt#option.help, ?_assertMatch({ok, {[{non_opt_int, 1234}], []}}, parse([NonOptInt], ["1234"]))}, + {"Declared and undeclared non-option arguments", + ?_assertMatch({ok, {[{non_opt_arg, "arg1"}], ["arg2", "arg3"]}}, parse([NonOptArg], ["arg1", "arg2", "arg3"]))}, + %% 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, {ShortInt#option.name, "value"}}}, parse([ShortInt], [[$-, ShortInt#option.short], "value"]))} ].