Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add check/3, parse_and_check/3, and complete test cases #25

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,89 @@ Will return:
``` erlang
{ok,{[{path,"/john's files"}],["dummy"]}}
```

Checking required options
=========================

There are four functions available for checking required options:
``` erlang
getopt:check/2, getopt:check/3,
getopt:parse_and_check/2, getopt:parse_and_check/3
```
If there are any options in the specification whose option types are not
tuples and not ``undefined``, these functions will return ``{error, Reason}``
so that the caller can output the program usage by calling ``getopt:usage/2``

These functions accept an optional ``CheckOpt`` argument that controls their
behavior:

``` erlang
CheckOpts = [help | {skip, [Name::atom()]}]

help - instructs getopt:check/3 to return 'help'
when "-h" command line option is given and
option specification contains definition of 'help'.
This is needed to short-circuit the checker of
required options when help is requested.

{skip, [Name]} - tells the checker to skip checking listed options
```
Example:

``` erlang
Spec =
[{help,$h,"help",undefined,"Help string"}
,{port,$p,"port",integer, "Server port"}],

{error,{missing_required_option,port}} =
getopt:parse_and_check(Spec, "-h").

help = getopt:parse_and_check(Spec, "-h", [help]).
```

Converting parsed list of arguments to a record
===============================================

Occasionally it is more convenient to represent the parsed arguments in the
form of a record rather than a list. Use ``getopt:to_record/3,4`` for this
purpose.

```erlang
-record(args, {host, port, verbose}).

OptionSpecs = [{host, $h, undefined, string, []},
{port, $p, undefined, integer, []},
{verbose, $v, undefined, integer, []}],

{ok, ParsedArgs, _Other} = getopt:parse(OptionSpecs, "-v -h localhost -p 8000"),

% Now ParsedArgs is a list:
% [{verbose, 1}, {host, "localhost"}, {port, 8000}], []}

Args = getopt:to_record(ParsedArgs, record_info(fields, args), #args{}),

% Test that Args record was filled with all parsed arguments from the OptionSpecs:

Args = #args{host = "localhost", port = 8000, verbose = 1}.
```

It is also possible to pass a custom validation function to ``getopt:to_record/4``.
That function allows to translate the values assigned to the record's fields, as
well as to ignore some options. The arguments to the function are:
``(OptionName::atom(), OldFieldValue, ArgumentValue) -> {ok, FieldValue} | ignore``.

```erlang
-record(args2, {host, port}).

Fun = fun(verbose, _Old, _) -> ignore;
(port, _Old, N) when N < 1024 -> throw({invalid_port, N});
(port, _Old, N) -> {ok, N+1};
(_Opt, _Old, Value) -> {ok, Value}
end,

Args2 = getopt:to_record(ParsedArgs, record_info(fields, args2), #args2{}, Fun),

% Test that Args2 record was filled with all parsed arguments from the OptionSpecs:

Args2 = #args2{host = "localhost", port = 8001}.
```
113 changes: 103 additions & 10 deletions src/getopt.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
-module(getopt).
-author('juanjo@comellas.org').

-export([parse/2, check/2, parse_and_check/2, format_error/2,
-export([parse/2, check/2, check/3,
parse_and_check/2, parse_and_check/3,
to_record/3, to_record/4,
format_error/2,
usage/2, usage/3, usage/4, tokenize/1]).
-export([usage_cmd_line/2]).

Expand Down Expand Up @@ -39,6 +42,10 @@
-type simple_option() :: atom().
-type compound_option() :: {atom(), arg_value()}.
-type option() :: simple_option() | compound_option().
%% Option types for configuration option checking.
-type check_option() :: help | {skip, [Name::atom()]}.
-type check_options() :: [ check_option() ].

%% Command line option specification.
-type option_spec() :: {
Name :: atom(),
Expand Down Expand Up @@ -66,14 +73,28 @@
-spec parse_and_check([option_spec()], string() | [string()]) ->
{ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: term()}}.
parse_and_check(OptSpecList, CmdLine) when is_list(OptSpecList), is_list(CmdLine) ->
parse_and_check(OptSpecList, CmdLine, []).

%% @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. Additionally perform check if all required options (the ones
%% without default values) are present. The function is a combination of
%% two calls: parse/2 and check/2. The `CheckOpts' argument allows to specify
%% a list of options to exclude from the required check (via `{skip, [Name::atom()]}')
%% and to specify `help', which will ignore required options check if
%% `CmdLine' contains help switch and `help' option is present in the `OptSpecList'.
-spec parse_and_check([option_spec()], string() | [string()], check_options()) ->
{ok, {[option()], [string()]}} | help | {error, {Reason :: atom(), Data :: term()}}.
parse_and_check(OptSpecList, CmdLine, CheckOpts)
when is_list(OptSpecList), is_list(CmdLine), is_list(CheckOpts) ->
case parse(OptSpecList, CmdLine) of
{ok, {Opts, _}} = Result ->
case check(OptSpecList, Opts) of
case check(OptSpecList, Opts, CheckOpts) of
ok -> Result;
Error -> Error
Other -> Other
end;
Error ->
Error
Other ->
Other
end.

%% @doc Check the parsed command line arguments returning ok if all required
Expand All @@ -82,9 +103,32 @@ parse_and_check(OptSpecList, CmdLine) when is_list(OptSpecList), is_list(CmdLine
-spec check([option_spec()], [option()]) ->
ok | {error, {Reason :: atom(), Option :: atom()}}.
check(OptSpecList, ParsedOpts) when is_list(OptSpecList), is_list(ParsedOpts) ->
check(OptSpecList, ParsedOpts, []).

%% @doc Check the parsed command line arguments returning ok if all required
%% options (i.e. that don't have defaults) are present, and returning
%% error otherwise. The `CheckOpts' argument allows to specify
%% a list of options to exclude from the required check (via `{skip, [Name::atom()]}')
%% and to specify `help', which will ignore required options check if
%% `CmdLine' contains help switch and `help' option is present in the `OptSpecList'.

-spec check([option_spec()], [option()], check_options()) ->
ok | help | {error, {Reason :: atom(), Option :: atom()}}.
check(OptSpecList, ParsedOpts, CheckOpts)
when is_list(OptSpecList), is_list(ParsedOpts), is_list(CheckOpts) ->
try
CheckHelp = proplists:get_value(help, CheckOpts, false),
HasHelp = lists:keymember(help, 1, OptSpecList)
andalso proplists:get_value(help, ParsedOpts, false),
SkipOpts = proplists:get_value(skip, CheckOpts, []),

{CheckHelp, HasHelp} =:= {true, true}
andalso throw(help),

% Ignore checking of options present in the {skip, Skip} list
RequiredOpts = [Name || {Name, _, _, Arg, _} <- OptSpecList,
not is_tuple(Arg) andalso Arg =/= undefined],
not is_tuple(Arg), Arg =/= undefined,
not lists:member(Name, SkipOpts)],
lists:foreach(fun (Option) ->
case proplists:is_defined(Option, ParsedOpts) of
true ->
Expand All @@ -94,6 +138,8 @@ check(OptSpecList, ParsedOpts) when is_list(OptSpecList), is_list(ParsedOpts) ->
end
end, RequiredOpts)
catch
throw:help ->
help;
_:Error ->
Error
end.
Expand Down Expand Up @@ -142,6 +188,45 @@ parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, []) ->
%% not present but had default arguments in the specification.
{ok, {lists:reverse(append_default_options(OptSpecList, OptAcc)), lists:reverse(ArgAcc)}}.

%% @doc Convert options from a list into a record.
%% The `FieldNames' list is the result of a call to `record_info(fields, Record)', and
%% `Record' is the initial value of the record to be populated with option values.
-spec to_record([option()], [atom()], tuple()) -> tuple().
to_record(Options, FieldNames, Record) ->
to_record(Options, FieldNames, Record, fun(_,_,V) -> {ok, V} end).

%% @doc Convert options from a list into a record.
%% This function is equivalent to `opts_to_record/3' except for taking another
%% `Validate' argument, which is a function
%% `(Field, OldFieldValue, OptionValue) -> {ok, NewFieldValue} | ignore'
%% used for validating the `Field' before it's assigned to the corresponding field
%% in the `Record'. Options with `undefined' values are skipped.
-spec to_record([option()], [atom()], tuple(),
fun((atom(), term(), term()) -> {ok, term()} | ignore)) -> tuple().
to_record(Options, RecordFieldNames, Record, Validate) when is_function(Validate, 3) ->
lists:foldl(fun
({_Opt,undefined}, Rec) ->
Rec;
(Opt, Rec) when is_atom(Opt) ->
set_val(Opt, true, Rec, RecordFieldNames, Validate);
({Opt, Value}, Rec) ->
set_val(Opt, Value, Rec, RecordFieldNames, Validate)
end, Record, Options).

set_val(Opt, Value, Rec, RecordFieldNames, Validate) ->
I = pos(RecordFieldNames, Opt, 2),
case Validate(Opt, old_val(I, Rec), Value) of
{ok, V} when I > 1 -> setelement(I, Rec, V);
{ok, _} -> throw({field_not_found, Opt, RecordFieldNames});
ignore -> Rec
end.

old_val(0,_Rec) -> undefined;
old_val(N, Rec) -> element(N, Rec).

pos([], _, _) -> 0;
pos([H|_], H, N) -> N;
pos([_|T], H, N) -> pos(T, H, N+1).

%% @doc Format the error code returned by prior call to parse/2 or check/2.
-spec format_error([option_spec()], {error, {Reason :: atom(), Data :: term()}} |
Expand All @@ -151,12 +236,20 @@ format_error(OptSpecList, {error, Reason}) ->
format_error(OptSpecList, {missing_required_option, Name}) ->
{_Name, Short, Long, _Type, _Help} = lists:keyfind(Name, 1, OptSpecList),
lists:flatten(["missing required option: -", [Short], " (", to_string(Long), ")"]);
format_error(_OptSpecList, {invalid_option, OptStr}) ->
lists:flatten(["invalid option: ", to_string(OptStr)]);
format_error(_OptSpecList, {invalid_option_arg, {Name, Arg}}) ->
lists:flatten(["option \'", to_string(Name) ++ "\' has invalid argument: ", to_string(Arg)]);
format_error(OptSpecList, {missing_option_arg, Name}) ->
Opt = lists:keyfind(Name, 1, OptSpecList),
lists:flatten(["missing required option argument: -", [element(2,Opt)], " (",
to_string(Name), ")"]);
format_error(OptSpecList, {invalid_option_arg, {Name, Arg}}) ->
L = case lists:keyfind(Name, 1, OptSpecList) of
{_, Short, undefined, _, _} -> [$-, Short, $ , to_string(Arg)];
{_, _, Long, _, _} -> ["--", Long, $=, to_string(Arg)]
end,
lists:flatten(["option \'", to_string(Name) ++ "\' has invalid argument: ", L]);
format_error(_OptSpecList, {invalid_option_arg, OptStr}) ->
lists:flatten(["invalid option argument: ", to_string(OptStr)]);
format_error(_OptSpecList, {invalid_option, OptStr}) ->
lists:flatten(["invalid option: ", to_string(OptStr)]);
format_error(_OptSpecList, {Reason, Data}) ->
lists:flatten([to_string(Reason), " ", to_string(Data)]).

Expand Down
Loading