Skip to content

Commit

Permalink
parse/3 accepting options list, command option
Browse files Browse the repository at this point in the history
`{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.
  • Loading branch information
sskorokhodov committed Jan 18, 2021
1 parent 1c963ce commit b376ec5
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 39 deletions.
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
85 changes: 48 additions & 37 deletions src/getopt.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
-module(getopt).
-author('[email protected]').

-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]).

Expand Down Expand Up @@ -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)}}.
Expand Down Expand Up @@ -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
Expand All @@ -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}})
Expand All @@ -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}}})
Expand All @@ -266,57 +277,57 @@ 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.
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.
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}}})
Expand Down
43 changes: 42 additions & 1 deletion test/getopt_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down Expand Up @@ -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}]))}].

0 comments on commit b376ec5

Please sign in to comment.