From b376ec506e6f88cb5631d57caf91730ed85a6af4 Mon Sep 17 00:00:00 2001 From: Simon Skorokhodov Date: Sat, 17 Mar 2018 20:08:22 +0100 Subject: [PATCH] `parse/3` accepting options list, `command` option `{command, true}` option directs to stop parsing at the first unrecognized token. It is useful when the program has subcommands with their own sets of options. The rest of the arguments is returned untouched as non-option arguments and can be parsed again as a different set of options. --- README.md | 31 +++++++++++++++- src/getopt.erl | 85 +++++++++++++++++++++++++------------------- test/getopt_test.erl | 43 +++++++++++++++++++++- 3 files changed, 120 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 5bdfa74..9273e27 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,15 @@ To use getopt in your project you can just add it as a dependency in your Usage ----- -The `getopt` module provides four functions: +The `getopt` module provides following functions: ```erlang parse([{Name, Short, Long, ArgSpec, Help}], Args :: string() | [string()]) -> {ok, {Options, NonOptionArgs}} | {error, {Reason, Data}} +parse([{Name, Short, Long, ArgSpec, Help}], Args :: string() | [string()], Opts :: [atom() | {atom(), term()}]) -> + {ok, {Options, NonOptionArgs}} | {error, {Reason, Data}} + tokenize(CmdLine :: string()) -> [string()] usage([{Name, Short, Long, ArgSpec, Help}], ProgramName :: string()) -> ok @@ -142,6 +145,32 @@ Will return: ["dummy1","dummy2"]}} ``` +The `parse/3` function takes parsing options as the third argument. + +The options are: + +`{command, true | false}` - if `true` directs to stop parsing at the first +unrecognized token. It is useful when the program has subcommands with their +own sets of options. The rest of the arguments is returned untouched as +non-option arguments and can be parsed again as a different set of options. + +E.g. the call: + +```erlang +Args = ["--long", "l", "-s", "s", "-v", "command1", "-v", "--long", "l", "command2", "-v"] +getopt:parse(OptSpecList, Args, [{command, true}]). +``` + +Will return: + +```erlang +{ok,{[{long,"l"}, + {short,"s"}, + {verbose,1}], + ["command1","-v","--long","l","command2","-v"]}} +``` + + The `tokenize/1` function will separate a command line string into tokens, taking into account whether an argument is single or double quoted, a character is escaped or if there are environment variables to diff --git a/src/getopt.erl b/src/getopt.erl index 8e9ea1b..7060f8f 100644 --- a/src/getopt.erl +++ b/src/getopt.erl @@ -11,7 +11,7 @@ -module(getopt). -author('juanjo@comellas.org'). --export([parse/2, check/2, parse_and_check/2, format_error/2, +-export([parse/2, parse/3, check/2, parse_and_check/2, format_error/2, usage/2, usage/3, usage/4, usage/6, tokenize/1]). -export([usage_cmd_line/2, usage_options/1]). @@ -105,39 +105,50 @@ check(OptSpecList, ParsedOpts) when is_list(OptSpecList), is_list(ParsedOpts) -> -spec parse([option_spec()], string() | [string()]) -> {ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: term()}}. parse(OptSpecList, CmdLine) when is_list(CmdLine) -> + parse(OptSpecList, CmdLine, []). + + +-spec parse([option_spec()], string() | [string()], ParseOpts :: list()) -> + {ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: term()}}. +parse(OptSpecList, CmdLine, ParseOpts) when is_list(CmdLine) -> try Args = if is_integer(hd(CmdLine)) -> tokenize(CmdLine); true -> CmdLine end, - parse(OptSpecList, [], [], 0, Args) + parse(OptSpecList, [], [], 0, Args, ParseOpts) catch throw: {error, {_Reason, _Data}} = Error -> Error end. --spec parse([option_spec()], [option()], [string()], integer(), [string()]) -> +-spec parse([option_spec()], [option()], [string()], integer(), [string()], list()) -> {ok, {[option()], [string()]}}. %% Process the option terminator. -parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, ["--" | Tail]) -> +parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, ["--" | Tail], _ParseOpts) -> %% Any argument present after the terminator is not considered an option. {ok, {lists:reverse(append_default_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc, Tail)}}; %% Process long options. -parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["--" ++ OptArg = OptStr | Tail]) -> - parse_long_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg); +parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["--" ++ OptArg = OptStr | Tail], ParseOpts) -> + parse_long_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg, ParseOpts); %% Process short options. -parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["-" ++ ([_Char | _] = OptArg) = OptStr | Tail]) -> - parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg); +parse(OptSpecList, OptAcc, ArgAcc, ArgPos, ["-" ++ ([_Char | _] = OptArg) = OptStr | Tail], ParseOpts) -> + parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Tail, OptStr, OptArg, ParseOpts); %% Process non-option arguments. -parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail]) -> +parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail] = Args, ParseOpts) -> case find_non_option_arg(OptSpecList, ArgPos) of {value, OptSpec} when ?IS_OPT_SPEC(OptSpec) -> - parse(OptSpecList, add_option_with_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos + 1, Tail); + parse(OptSpecList, add_option_with_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos + 1, Tail, ParseOpts); false -> - parse(OptSpecList, OptAcc, [Arg | ArgAcc], ArgPos, Tail) + case proplists:get_bool(command, ParseOpts) of + true -> + {ok, {lists:reverse(append_default_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc) ++ Args}}; + false -> + parse(OptSpecList, OptAcc, [Arg | ArgAcc], ArgPos, Tail, ParseOpts) + end end; -parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, []) -> +parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, [], _ParseOpts) -> %% 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_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc)}}. @@ -179,25 +190,25 @@ opt_to_list({_Name, Short, Long, _Type, _Help}) -> %% --foo Single option 'foo', no argument %% --foo=bar Single option 'foo', argument "bar" %% --foo bar Single option 'foo', argument "bar" --spec parse_long_option([option_spec()], [option()], [string()], integer(), [string()], string(), string()) -> +-spec parse_long_option([option_spec()], [option()], [string()], integer(), [string()], string(), string(), list()) -> {ok, {[option()], [string()]}}. -parse_long_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) -> +parse_long_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg, ParseOpts) -> case split_assigned_arg(OptArg) of {Long, Arg} -> %% Get option that has its argument within the same string %% separated by an equal ('=') character (e.g. "--port=1000"). - parse_long_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg); + parse_long_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg, ParseOpts); Long -> case lists:keyfind(Long, ?OPT_LONG, OptSpecList) of {Name, _Short, Long, undefined, _Help} -> - parse(OptSpecList, [Name | OptAcc], ArgAcc, ArgPos, Args); + parse(OptSpecList, [Name | OptAcc], ArgAcc, ArgPos, Args, ParseOpts); {_Name, _Short, Long, _ArgSpec, _Help} = OptSpec -> %% The option argument string is empty, but the option requires %% an argument, so we look into the next string in the list. %% e.g ["--port", "1000"] - parse_long_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec); + parse_long_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec, ParseOpts); false -> throw({error, {invalid_option, OptStr}}) end @@ -208,16 +219,16 @@ parse_long_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) -> %% 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. -spec parse_long_option_assigned_arg([option_spec()], [option()], [string()], integer(), - [string()], string(), string(), string()) -> + [string()], string(), string(), string(), list()) -> {ok, {[option()], [string()]}}. -parse_long_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg) -> +parse_long_option_assigned_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, Long, Arg, ParseOpts) -> case lists:keyfind(Long, ?OPT_LONG, OptSpecList) of {_Name, _Short, Long, ArgSpec, _Help} = OptSpec -> case ArgSpec of undefined -> throw({error, {invalid_option_arg, OptStr}}); _ -> - parse(OptSpecList, add_option_with_assigned_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos, Args) + parse(OptSpecList, add_option_with_assigned_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos, Args, ParseOpts) end; false -> throw({error, {invalid_option, OptStr}}) @@ -241,15 +252,15 @@ split_assigned_arg(OptStr, [], _Acc) -> %% @doc Retrieve the argument for an option from the next string in the list of %% command-line parameters or set the value of the argument from the argument %% specification (for boolean and integer arguments), if possible. -parse_long_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec) -> +parse_long_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec, ParseOpts) -> ArgSpecType = arg_spec_type(ArgSpec), case Args =:= [] orelse is_implicit_arg(ArgSpecType, hd(Args)) of true -> - parse(OptSpecList, add_option_with_implicit_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args); + parse(OptSpecList, add_option_with_implicit_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args, ParseOpts); false -> [Arg | Tail] = Args, try - parse(OptSpecList, [{Name, to_type(ArgSpecType, Arg)} | OptAcc], ArgAcc, ArgPos, Tail) + parse(OptSpecList, [{Name, to_type(ArgSpecType, Arg)} | OptAcc], ArgAcc, ArgPos, Tail, ParseOpts) catch error:_ -> throw({error, {invalid_option_arg, {Name, Arg}}}) @@ -266,15 +277,15 @@ parse_long_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _Sh %% -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_short_option([option_spec()], [option()], [string()], integer(), [string()], string(), string()) -> +-spec parse_short_option([option_spec()], [option()], [string()], integer(), [string()], string(), string(), list()) -> {ok, {[option()], [string()]}}. -parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) -> - parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, first, OptArg). +parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg, ParseOpts) -> + parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, first, OptArg, ParseOpts). -parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptPos, [Short | Arg]) -> +parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptPos, [Short | Arg], ParseOpts) -> case lists:keyfind(Short, ?OPT_SHORT, OptSpecList) of {Name, Short, _Long, undefined, _Help} -> - parse_short_option(OptSpecList, [Name | OptAcc], ArgAcc, ArgPos, Args, OptStr, first, Arg); + parse_short_option(OptSpecList, [Name | OptAcc], ArgAcc, ArgPos, Args, OptStr, first, Arg, ParseOpts); {_Name, Short, _Long, ArgSpec, _Help} = OptSpec -> %% The option has a specification, so it requires an argument. @@ -282,41 +293,41 @@ parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptPos, [S [] -> %% The option argument string is empty, but the option requires %% an argument, so we look into the next string in the list. - parse_short_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec, OptPos); + parse_short_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptSpec, OptPos, ParseOpts); _ -> case is_valid_arg(ArgSpec, Arg) of true -> - parse(OptSpecList, add_option_with_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos, Args); + parse(OptSpecList, add_option_with_arg(OptSpec, Arg, OptAcc), ArgAcc, ArgPos, Args, ParseOpts); _ -> NewOptAcc = case OptPos of first -> add_option_with_implicit_arg(OptSpec, OptAcc); _ -> add_option_with_implicit_incrementable_arg(OptSpec, OptAcc) end, - parse_short_option(OptSpecList, NewOptAcc, ArgAcc, ArgPos, Args, OptStr, next, Arg) + parse_short_option(OptSpecList, NewOptAcc, ArgAcc, ArgPos, Args, OptStr, next, Arg, ParseOpts) end end; false -> throw({error, {invalid_option, OptStr}}) end; -parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, _OptStr, _OptPos, []) -> - parse(OptSpecList, OptAcc, ArgAcc, ArgPos, Args). +parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, _OptStr, _OptPos, [], ParseOpts) -> + parse(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, ParseOpts). %% @doc Retrieve the argument for an option from the next string in the list of %% command-line parameters or set the value of the argument from the argument %% specification (for boolean and integer arguments), if possible. -parse_short_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec, OptPos) -> +parse_short_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec, OptPos, ParseOpts) -> case Args =:= [] orelse is_implicit_arg(ArgSpec, hd(Args)) of true when OptPos =:= first -> - parse(OptSpecList, add_option_with_implicit_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args); + parse(OptSpecList, add_option_with_implicit_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args, ParseOpts); true -> - parse(OptSpecList, add_option_with_implicit_incrementable_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args); + parse(OptSpecList, add_option_with_implicit_incrementable_arg(OptSpec, OptAcc), ArgAcc, ArgPos, Args, ParseOpts); false -> [Arg | Tail] = Args, try - parse(OptSpecList, [{Name, to_type(ArgSpec, Arg)} | OptAcc], ArgAcc, ArgPos, Tail) + parse(OptSpecList, [{Name, to_type(ArgSpec, Arg)} | OptAcc], ArgAcc, ArgPos, Tail, ParseOpts) catch error:_ -> throw({error, {invalid_option_arg, {Name, Arg}}}) diff --git a/test/getopt_test.erl b/test/getopt_test.erl index 4c7108f..d305f64 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, check/2, parse_and_check/2, format_error/2, tokenize/1]). +-import(getopt, [parse/2, parse/3, check/2, parse_and_check/2, format_error/2, tokenize/1]). -define(NAME(Opt), element(1, Opt)). -define(SHORT(Opt), element(2, Opt)). @@ -365,3 +365,44 @@ utf8_binary_test_() -> ?_assertEqual({ok, {[{utf8, Utf8}], []}}, parse(OptSpecsWithDefault, []))}, {"Default utf8_binary argument usage", ?_assert(is_list(string:find(getopt:usage_options(OptSpecsWithDefault), Unicode)))}]. + +parse_command_test_() -> + OptSpecList = [{arg, $a, "arg", string, "Required arg"}], + OptSpecList1 = [{arg, $a, "arg", string, "Required arg"}, + {positional, undefined, undefined, string, "Positional arg"}], + [{"Parse command: no opts, no command", + ?_assertEqual({ok, {[], []}}, + parse(OptSpecList, [], [{command, true}]))}, + {"Parse command: no opts, command, no command opts", + ?_assertEqual({ok, {[], ["command"]}}, + parse(OptSpecList, ["command"], [{command, true}]))}, + {"Parse command: opts, no command", + ?_assertEqual({ok, {[{arg, "a"}], []}}, + parse(OptSpecList, ["-a" "a"], [{command, true}]))}, + {"Parse command: opts, command, no command opts", + ?_assertEqual({ok, {[{arg, "a"}], ["command"]}}, + parse(OptSpecList, ["-a", "a", "command"], [{command, true}]))}, + {"Parse command: no opts, command, command opts", + ?_assertEqual({ok, {[], ["command", "--arg"]}}, + parse(OptSpecList, ["command", "--arg"], [{command, true}]))}, + {"Parse command: opts, command, command opts", + ?_assertEqual({ok, {[{arg, "a"}], ["command", "--arg", "-a2", "a2"]}}, + parse(OptSpecList, + ["--arg", "a", "command", "--arg", "-a2", "a2"], + [{command, true}]))}, + {"Parse command: opts, positional arg, command, command opts", + ?_assertEqual({ok, {[{arg, "a"}, {positional, "p"}], + ["command", "--arg", "-a2", "a2"]}}, + parse(OptSpecList1, + ["--arg", "a", "p", "command", "--arg", "-a2", "a2"], + [{command, true}]))}, + {"Parse command 'false': opts, command, command opts", + ?_assertEqual({ok, {[{arg, "a"}, {arg, "a2"}], ["command"]}}, + parse(OptSpecList, + ["--arg", "a", "command", "--arg", "a2"], + [{command, false}]))}, + {"Parse command 'false': opts, command, command opts", + ?_assertEqual({error, {invalid_option, "-b"}}, + parse(OptSpecList, + ["--arg", "a", "command", "--arg", "a2", "-b", "b"], + [{command, false}]))}].