Dropped record syntax for the option specifications to simplify use from escripts.

This commit is contained in:
Juan Jose Comellas 2009-11-16 19:20:32 -03:00
parent 109d9f5f2a
commit 4b03285d9e
8 changed files with 270 additions and 329 deletions

9
ChangeLog Normal file
View File

@ -0,0 +1,9 @@
2009-11-16 Juan Jose Comellas <juanjo@comellas.org>
* *: Released v0.1
* src/getopt.erl:
* src/test/getopt_test.erl:
* src/examples/ex1.erl:
Dropped record syntax for the option specifications to simplify use from escripts.

View File

@ -1,4 +1,4 @@
Copyright (c) 2009, Novamens SA Copyright (c) 2009 Juan Jose Comellas
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,
@ -11,8 +11,8 @@ are permitted provided that the following conditions are met:
this list of conditions and the following disclaimer in the documentation this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. and/or other materials provided with the distribution.
- Neither the name of Novamens SA nor the names of its contributors may be - Neither the name of Juan Jose Comellas nor the names of its contributors may
used to endorse or promote products derived from this software without be used to endorse or promote products derived from this software without
specific prior written permission. specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND

View File

@ -28,81 +28,59 @@ Usage
The *getopt* module provides two functions: The *getopt* module provides two functions:
parse([#option{}], Args :: string() | [string()]) -> parse([{Name, Short, Long, ArgSpec, Help}], Args :: string() | [string()]) ->
{ok, {Options, NonOptionArgs}} | {error, {Reason, Data}} {ok, {Options, NonOptionArgs}} | {error, {Reason, Data}}
usage([#option{}], ProgramName :: string()) -> ok usage([{Name, Short, Long, ArgSpec, Help}], ProgramName :: string()) -> ok
The ``parse/2`` function receives a list of ``#option{}`` records (defined in The ``parse/2`` function receives a list of tuples with the command line option
``getopt.hrl``) with the command line option specifications. The ``#option{}`` specifications. The type specification for the tuple is:
record has the following elements:
-record(option, { -type arg_type() :: 'atom' | 'binary' | 'boolean' | 'float' | 'integer' | 'string'.
name :: atom(),
short :: char() | undefined, -type arg_value() :: atom() | binary() | boolean() | float() | integer() | string().
long :: string() | undefined,
arg :: getopt_arg_type() | {getopt_arg_type(), getopt_arg()} | undefined. -type arg_spec() :: arg_type() | {arg_type(), arg_value()} | undefined.
help :: string() | undefined
}). -type option_spec() :: {
Name :: atom(),
Short :: char() | undefined,
Long :: string() | undefined,
ArgSpec :: arg_spec(),
Help :: string() | undefined
}.
The fields of the record are: The fields of the record are:
- ``name``: name of the option. - ``Name``: name of the option.
- ``short``: character for the short option (e.g. $i for -i). - ``Short``: character for the short option (e.g. $i for -i).
- ``long``: string for the long option (e.g. "info" for --info). - ``Long``: string for the long option (e.g. "info" for --info).
- ``arg``: data type the argument will be converted to with an optional default value. It can either be an atom() (one of: 'atom', 'binary', 'boolean', 'float', 'integer', 'string') or a tuple with an atom() and the default value for that argument. - ``ArgSpec``: data type and optional default value the argument will be converted to.
- ``help``: help message that is shown for the option when usage/2 is called. - ``Help``: help message that is shown for the option when usage/2 is called.
The second parameter holds the list of arguments as passed to the ``main/1`` The second parameter holds the list of arguments as passed to the ``main/1``
function in escripts. function in escripts. e.g.
e.g. {port, $p, "port", {integer, 5432}, "Database server port"}
#option{name = port,
short = $p,
long = "port",
arg = {integer, 5432},
help = "Database server port"
}
If the function is successful parsing the command line arguments it will return 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 a tuple containing the parsed options and the non-option arguments. The options
will be represented by a list of key-value pairs with the ``name`` of the will be represented by a list of key-value pairs with the ``Name`` of the
option as *key* and the argument from the command line as *value*. If the option option as *key* and the argument from the command line as *value*. If the option
doesn't have an argument, only the atom corresponding to its ``name`` will be doesn't have an argument, only the atom corresponding to its ``Name`` will be
added to the list of options. For the example given above we could get something added to the list of options. For the example given above we could get something
like ``{port, 5432}``. The non-option arguments are just a list of strings with like ``{port, 5432}``. The non-option arguments are just a list of strings with
all the arguments that did not have corresponding options. all the arguments that did not have corresponding options.
e.g. For a program named ``ex1.escript`` with the following option specifications: e.g. For a program named ``ex.escript`` with the following option specifications:
OptSpec = OptSpec =
[ [
#option{name = host, {host, $h, "host", {string, "localhost"}, "Database server host"},
short = $h, {port, $p, "port", integer, "Database server port"},
long = "host", {dbname, undefined, "dbname", {string, "users"}, "Database name"},
arg = {string, "localhost"}, {xml, $x, undefined, undefined, "Output data in XML"},
help = "Database server host" {file, undefined, undefined, string, "Output file"}
},
#option{name = port,
short = $p,
long = "port",
arg = integer,
help = "Database server port"
},
#option{name = dbname,
long = "dbname",
arg = {string, "users"},
help = "Database name"
},
#option{name = xml,
short = $x,
help = "Output data in XML"
},
#option{name = file,
arg = string,
help = "Output file"
}
]. ].
And this command line: And this command line:
@ -132,7 +110,7 @@ Also, the call to ``getopt:usage/2``:
Will show (on *stdout*): Will show (on *stdout*):
Usage: ex1 [-h <host>] [-p <port>] [--dbname <dbname>] [-x] <file> Usage: ex.escript [-h <host>] [-p <port>] [--dbname <dbname>] [-x] <file>
-h, --host Database server host -h, --host Database server host
-p, --port Database server port -p, --port Database server port
@ -146,13 +124,3 @@ Known limitations
- The syntax for non-option arguments that start with '-' (e.g. -a -- -b) - The syntax for non-option arguments that start with '-' (e.g. -a -- -b)
is not supported yet. is not supported yet.
Known problems
--------------
Currently, with Erlang R13B (and older versions), escripts cannot use the
``-include()`` preprocessor directive so you're forced to install the *getopt*
package in Erlang's library path for your escripts to be able to include the
``getopt.hrl`` header file that defines the ``#option{}`` record used by the
functions in the *getopt* module.

51
examples/ex1.escript Executable file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env escript
%% -*- erlang -*-
%%! -sname ex1 -pz ebin
%%%-------------------------------------------------------------------
%%% @author Juan Jose Comellas <juanjo@comellas.org>
%%% @copyright (C) 2009 Juan Jose Comellas
%%% @doc Example file for the getopt module.
%%% @end
%%%
%%% This source file is subject to the New BSD License. You should have received
%%% a copy of the New BSD license with this software. If not, it can be
%%% retrieved from: http://www.opensource.org/licenses/bsd-license.php
%%%-------------------------------------------------------------------
-module(ex1).
-author('juanjo@comellas.org').
main([]) ->
getopt:usage(option_spec_list(), "ex1.escript");
main(Args) ->
OptSpecList = option_spec_list(),
io:format("For command line: ~p~n"
"getopt:parse/2 returns:~n~n", [Args]),
case getopt:parse(OptSpecList, Args) of
{ok, {Options, NonOptArgs}} ->
io:format("Options:~n ~p~n~nNon-option arguments:~n ~p~n", [Options, NonOptArgs]);
{error, {Reason, Data}} ->
io:format("Error: ~s ~p~n~n", [Reason, Data]),
getopt:usage(OptSpecList, "ex1.escript")
end.
option_spec_list() ->
CurrentUser = case os:getenv("USER") of
false ->
"user";
User ->
User
end,
[
%% {Name, ShortOpt, LongOpt, ArgSpec, HelpMsg}
{help, $?, "help", undefined, "Show the program options"},
{username, $U, "username", string, "Username to connect to the database"},
{password, $P, "password", {string, CurrentUser}, "Password to connect to the database"},
{host, $h, "host", {string, "localhost"}, "Database server host name or IP address"},
{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"},
{dbname, undefined, undefined, string, "Database name"}
].

View File

@ -1,23 +0,0 @@
%% @type getopt_arg_type() = 'atom' | 'binary' | 'bool' | 'float' | 'integer' | 'string'.
%% Atom indicating the data type that an argument can be converted to.
-type getopt_arg_type() :: 'atom' | 'binary' | 'boolean' | 'float' | 'integer' | 'string'.
%% @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()} | undefined.
%% Argument specification.
-type getopt_arg_spec() :: getopt_arg_type() | {getopt_arg_type(), getopt_arg()} | undefined.
%% @doc Record that defines the option specifications.
-record(option, {
%% @doc Name of the option
name :: atom(),
%% @doc Character for the short option (e.g. $i for -i)
short :: char() | undefined,
%% @doc String for the long option (e.g. "info" for --info)
long :: string() | undefined,
%% @doc Data type the argument will be converted to with an optional default value
arg :: getopt_arg_spec(),
%% @doc Help message that is shown for the option when usage/2 is called.
help :: string() | undefined
}).

View File

@ -1,6 +1,6 @@
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
%%% @author Juan Jose Comellas <jcomellas@novamens.com> %%% @author Juan Jose Comellas <juanjo@comellas.org>
%%% @copyright (C) 2009, Novamens SA (http://www.novamens.com) %%% @copyright (C) 2009 Juan Jose Comellas
%%% @doc Example file for the getopt module. %%% @doc Example file for the getopt module.
%%% @end %%% @end
%%% %%%
@ -9,9 +9,7 @@
%%% retrieved from: http://www.opensource.org/licenses/bsd-license.php %%% retrieved from: http://www.opensource.org/licenses/bsd-license.php
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-module(ex1). -module(ex1).
-author('Juan Jose Comellas <jcomellas@novamens.com>'). -author('juanjo@comellas.org').
-include("include/getopt.hrl").
-export([test/0, test/1]). -export([test/0, test/1]).
@ -26,7 +24,7 @@ test(CmdLine) ->
"getopt:parse/2 returns:~n~n", [CmdLine]), "getopt:parse/2 returns:~n~n", [CmdLine]),
case getopt:parse(OptSpecList, CmdLine) of case getopt:parse(OptSpecList, CmdLine) of
{ok, {Options, NonOptArgs}} -> {ok, {Options, NonOptArgs}} ->
io:format("Options:~n ~p~nNon-option arguments:~n ~p~n", [Options, NonOptArgs]); io:format("Options:~n ~p~n~nNon-option arguments:~n ~p~n", [Options, NonOptArgs]);
{error, {Reason, Data}} -> {error, {Reason, Data}} ->
io:format("Error: ~s ~p~n~n", [Reason, Data]), io:format("Error: ~s ~p~n~n", [Reason, Data]),
getopt:usage(OptSpecList, "ex1") getopt:usage(OptSpecList, "ex1")
@ -36,48 +34,13 @@ test(CmdLine) ->
option_spec() -> option_spec() ->
CurrentUser = os:getenv("USER"), CurrentUser = os:getenv("USER"),
[ [
#option{name = help, %% {Name, ShortOpt, LongOpt, ArgSpec, HelpMsg}
short = $?, {help, $?, "help", undefined, "Show the program options"},
long = "help", {username, $U, "username", string, "Username to connect to the database"},
help = "Show the program options" {password, $P, "password", {string, CurrentUser}, "Password to connect to the database"},
}, {host, $h, "host", {string, "localhost"}, "Database server host name or IP address"},
#option{name = username, {port, $p, "port", {integer, 1000}, "Database server port"},
short = $U, {output_file, $o, "output-file", string, "File where the data will be saved to"},
long = "username", {xml, $x, "xml", undefined, "Output data as XML"},
arg = string, {dbname, undefined, undefined, string, "Database name"}
help = "Username to connect to the database"
},
#option{name = password,
short = $P,
long = "password",
arg = {string, CurrentUser},
help = "Password to connect to the database"
},
#option{name = host,
short = $h,
long = "host",
arg = {string, "localhost"},
help = "Database server host name or IP address"
},
#option{name = port,
short = $p,
long = "port",
arg = {integer, 1000},
help = "Database server port"
},
#option{name = output_file,
short = $o,
long = "output-file",
arg = string,
help = "File where the data will be saved to"
},
#option{name = xml,
short = $x,
long = "xml",
help = "Output data as XML"
},
#option{name = dbname,
arg = string,
help = "Database name"
}
]. ].

View File

@ -1,6 +1,6 @@
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
%%% @author Juan Jose Comellas <jcomellas@novamens.com> %%% @author Juan Jose Comellas <juanjo@comellas.org>
%%% @copyright (C) 2009, Novamens SA (http://www.novamens.com) %%% @copyright (C) 2009 Juan Jose Comellas
%%% @doc Parses command line options with a format similar to that of GNU getopt. %%% @doc Parses command line options with a format similar to that of GNU getopt.
%%% @end %%% @end
%%% %%%
@ -9,19 +9,40 @@
%%% retrieved from: http://www.opensource.org/licenses/bsd-license.php %%% retrieved from: http://www.opensource.org/licenses/bsd-license.php
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-module(getopt). -module(getopt).
-author('Juan Jose Comellas <jcomellas@novamens.com>'). -author('juanjo@comellas.org').
-include("getopt.hrl").
%% @headerfile "getopt.hrl"
-define(TAB_LENGTH, 8). -define(TAB_LENGTH, 8).
%% Indentation of the help messages in number of tabs. %% Indentation of the help messages in number of tabs.
-define(INDENTATION, 3). -define(INDENTATION, 3).
%% @type option() = atom() | {atom(), getopt_arg()}. Option type and optional default argument. -define(OPT_NAME, 1).
-type option() :: atom() | {atom(), getopt_arg()}. -define(OPT_SHORT, 2).
-define(OPT_LONG, 3).
-define(OPT_ARG, 4).
-define(OPT_HELP, 5).
-define(IS_OPT_SPEC(Opt), (is_tuple(Opt) andalso (size(Opt) =:= ?OPT_HELP))).
%% @type arg_type() = 'atom' | 'binary' | 'bool' | 'float' | 'integer' | 'string'.
%% Atom indicating the data type that an argument can be converted to.
-type arg_type() :: 'atom' | 'binary' | 'boolean' | 'float' | 'integer' | 'string'.
%% @type arg_value() = atom() | binary() | bool() | float() | integer() | string().
%% Data type that an argument can be converted to.
-type arg_value() :: atom() | binary() | boolean() | float() | integer() | string().
%% @type arg_spec() = arg_type() | {arg_type(), arg_value()} | undefined.
%% Argument specification.
-type arg_spec() :: arg_type() | {arg_type(), arg_value()} | undefined.
%% @type option() = atom() | {atom(), arg_value()}. Option type and optional default argument.
-type option() :: atom() | {atom(), arg_value()}.
%% @type option_spec() = #option{}. Command line option specification. %% @type option_spec() = #option{}. Command line option specification.
-type option_spec() :: #option{}. -type option_spec() :: {
Name :: atom(),
Short :: char() | undefined,
Long :: string() | undefined,
ArgSpec :: arg_spec(),
Help :: string() | undefined
}.
-export([parse/2, usage/2]). -export([parse/2, usage/2]).
@ -51,24 +72,24 @@ parse(OptSpecList, CmdLine) ->
{ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data:: any()}}. {ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data:: any()}}.
%% Process long options. %% Process long options.
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [[$-, $- | LongName] = OptStr | Tail]) -> parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [[$-, $- | LongName] = OptStr | Tail]) ->
{Option, Tail1} = get_option(OptSpecList, OptStr, LongName, #option.long, Tail), {Option, Tail1} = get_option(OptSpecList, OptStr, LongName, ?OPT_LONG, Tail),
parse(OptSpecList, [Option | OptAcc], ArgAcc, ArgPos, Tail1); parse(OptSpecList, [Option | OptAcc], ArgAcc, ArgPos, Tail1);
%% Process short options. %% Process short options.
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [[$-, ShortName] = OptStr | Tail]) -> parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [[$-, ShortName] = OptStr | Tail]) ->
{Option, Tail1} = get_option(OptSpecList, OptStr, ShortName, #option.short, Tail), {Option, Tail1} = get_option(OptSpecList, OptStr, ShortName, ?OPT_SHORT, Tail),
parse(OptSpecList, [Option | OptAcc], ArgAcc, ArgPos, Tail1); parse(OptSpecList, [Option | OptAcc], ArgAcc, ArgPos, Tail1);
%% Process multiple short options with no argument. %% Process multiple short options with no argument.
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [[$- | ShortNameList] = OptStr | Tail]) -> parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [[$- | ShortNameList] = OptStr | Tail]) ->
NewOptAcc = NewOptAcc =
lists:foldl( lists:foldl(
fun (ShortName, OptAcc1) -> fun (ShortName, OptAcc1) ->
[get_option_no_arg(OptSpecList, OptStr, ShortName, #option.short) | OptAcc1] [get_option_no_arg(OptSpecList, OptStr, ShortName, ?OPT_SHORT) | OptAcc1]
end, OptAcc, ShortNameList), end, OptAcc, ShortNameList),
parse(OptSpecList, NewOptAcc, ArgAcc, ArgPos, Tail); parse(OptSpecList, NewOptAcc, ArgAcc, ArgPos, Tail);
%% Process non-option arguments. %% Process non-option arguments.
parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail]) -> parse(OptSpecList, OptAcc, ArgAcc, ArgPos, [Arg | Tail]) ->
case find_non_option_arg(OptSpecList, ArgPos) of case find_non_option_arg(OptSpecList, ArgPos) of
{value, #option{} = OptSpec} -> {value, OptSpec} when ?IS_OPT_SPEC(OptSpec) ->
parse(OptSpecList, [convert_option_arg(OptSpec, Arg) | OptAcc], ArgAcc, ArgPos + 1, Tail); parse(OptSpecList, [convert_option_arg(OptSpec, Arg) | OptAcc], ArgAcc, ArgPos + 1, Tail);
false -> false ->
parse(OptSpecList, OptAcc, [Arg | ArgAcc], ArgPos, Tail) parse(OptSpecList, OptAcc, [Arg | ArgAcc], ArgPos, Tail)
@ -85,7 +106,7 @@ parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, []) ->
%% received on the command line. %% received on the command line.
get_option(OptSpecList, OptStr, OptName, FieldPos, Tail) -> get_option(OptSpecList, OptStr, OptName, FieldPos, Tail) ->
case lists:keysearch(OptName, FieldPos, OptSpecList) of case lists:keysearch(OptName, FieldPos, OptSpecList) of
{value, #option{name = Name, arg = ArgSpec} = OptSpec} -> {value, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec} ->
case ArgSpec of case ArgSpec of
undefined -> undefined ->
{Name, Tail}; {Name, Tail};
@ -106,9 +127,9 @@ get_option(OptSpecList, OptStr, OptName, FieldPos, Tail) ->
%% argument and matches a string received on the command line. %% argument and matches a string received on the command line.
get_option_no_arg(OptSpecList, OptStr, OptName, FieldPos) -> get_option_no_arg(OptSpecList, OptStr, OptName, FieldPos) ->
case lists:keysearch(OptName, FieldPos, OptSpecList) of case lists:keysearch(OptName, FieldPos, OptSpecList) of
{value, #option{name = Name, arg = undefined}} -> {value, {Name, _Short, _Long, undefined, _Help}} ->
Name; Name;
{value, #option{name = Name}} -> {value, {Name, _Short, _Long, _ArgSpec, _Help}} ->
throw({error, {missing_option_arg, Name}}); throw({error, {missing_option_arg, Name}});
false -> false ->
throw({error, {invalid_option, OptStr}}) throw({error, {invalid_option, OptStr}})
@ -117,9 +138,9 @@ get_option_no_arg(OptSpecList, OptStr, OptName, FieldPos) ->
-spec find_non_option_arg([option_spec()], integer()) -> {value, option_spec()} | 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 %% @doc Find the option for the discrete argument in position specified in the
%% Pos argument. %% Pos argument.
find_non_option_arg([#option{short = undefined, long = undefined} = Opt | _Tail], 0) -> find_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} = OptSpec | _Tail], 0) ->
{value, Opt}; {value, OptSpec};
find_non_option_arg([#option{short = undefined, long = undefined} | Tail], Pos) -> find_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} | Tail], Pos) ->
find_non_option_arg(Tail, Pos - 1); find_non_option_arg(Tail, Pos - 1);
find_non_option_arg([_Head | Tail], Pos) -> find_non_option_arg([_Head | Tail], Pos) ->
find_non_option_arg(Tail, Pos); find_non_option_arg(Tail, Pos);
@ -129,7 +150,7 @@ find_non_option_arg([], _Pos) ->
-spec append_default_args([option_spec()], [option()]) -> [option()]. -spec append_default_args([option_spec()], [option()]) -> [option()].
%% @doc Appends the default values of the options that are not present. %% @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([{Name, _Short, _Long, {_Type, DefaultArg}, _Help} | Tail], OptAcc) ->
append_default_args(Tail, append_default_args(Tail,
case lists:keymember(Name, 1, OptAcc) of case lists:keymember(Name, 1, OptAcc) of
false -> false ->
@ -147,7 +168,7 @@ append_default_args([], OptAcc) ->
-spec convert_option_arg(option_spec(), string()) -> [option()]. -spec convert_option_arg(option_spec(), string()) -> [option()].
%% @doc Convert the argument passed in the command line to the data type %% @doc Convert the argument passed in the command line to the data type
%% indicated byt the argument specification. %% indicated byt the argument specification.
convert_option_arg(#option{name = Name, arg = ArgSpec}, Arg) -> convert_option_arg({Name, _Short, _Long, ArgSpec, _Help}, Arg) ->
try try
Converted = case ArgSpec of Converted = case ArgSpec of
{Type, _DefaultArg} -> {Type, _DefaultArg} ->
@ -161,7 +182,7 @@ convert_option_arg(#option{name = Name, arg = ArgSpec}, Arg) ->
throw({error, {invalid_option_arg, {Name, Arg}}}) throw({error, {invalid_option_arg, {Name, Arg}}})
end. end.
-spec to_type(atom(), string()) -> getopt_arg(). -spec to_type(atom(), string()) -> arg_value().
to_type(binary, Arg) -> to_type(binary, Arg) ->
list_to_binary(Arg); list_to_binary(Arg);
to_type(atom, Arg) -> to_type(atom, Arg) ->
@ -196,21 +217,34 @@ usage(OptSpecList, ProgramName) ->
usage_cmd_line(OptSpecList) -> usage_cmd_line(OptSpecList) ->
usage_cmd_line(OptSpecList, []). usage_cmd_line(OptSpecList, []).
%% For options with short form and no argument. usage_cmd_line([{Name, Short, Long, ArgSpec, _Help} | Tail], Acc) ->
usage_cmd_line([#option{short = Short, arg = undefined} | Tail], Acc) when Short =/= undefined -> CmdLine =
usage_cmd_line(Tail, [[$\s, $[, $-, Short, $]] | Acc]); case ArgSpec of
%% For options with only long form and no argument. undefined ->
usage_cmd_line([#option{long = Long, arg = undefined} | Tail], Acc) when Long =/= undefined -> if
usage_cmd_line(Tail, [[$\s, $[, $-, $-, Long, $]] | Acc]); %% For options with short form and no argument.
%% For options with short form and argument. Short =/= undefined ->
usage_cmd_line([#option{name = Name, short = Short} | Tail], Acc) when Short =/= undefined -> [$\s, $[, $-, Short, $]];
usage_cmd_line(Tail, [[$\s, $[, $-, Short, $\s, $<, atom_to_list(Name), $>, $]] | Acc]); %% For options with only long form and no argument.
%% For options with only long form and argument. Long =/= undefined ->
usage_cmd_line([#option{name = Name, long = Long} | Tail], Acc) when Long =/= undefined -> [$\s, $[, $-, $-, Long, $]];
usage_cmd_line(Tail, [[$\s, $[, $-, $-, Long, $\s, $<, atom_to_list(Name), $>, $]] | Acc]); true ->
%% For options with neither short nor long form and argument. []
usage_cmd_line([#option{name = Name, arg = ArgSpec} | Tail], Acc) when ArgSpec =/= undefined -> end;
usage_cmd_line(Tail, [[$\s, $<, atom_to_list(Name), $>] | Acc]); _ ->
if
%% For options with short form and argument.
Short =/= undefined ->
[$\s, $[, $-, Short, $\s, $<, atom_to_list(Name), $>, $]];
%% For options with only long form and argument.
Long =/= undefined ->
[$\s, $[, $-, $-, Long, $\s, $<, atom_to_list(Name), $>, $]];
%% For options with neither short nor long form and argument.
true ->
[$\s, $<, atom_to_list(Name), $>]
end
end,
usage_cmd_line(Tail, [CmdLine | Acc]);
usage_cmd_line([], Acc) -> usage_cmd_line([], Acc) ->
lists:flatten(lists:reverse(Acc)). lists:flatten(lists:reverse(Acc)).
@ -221,26 +255,36 @@ usage_cmd_line([], Acc) ->
usage_options(OptSpecList) -> usage_options(OptSpecList) ->
usage_options(OptSpecList, []). usage_options(OptSpecList, []).
%% Neither short nor long form (non-option argument). usage_options([{Name, Short, Long, _ArgSpec, _Help} = OptSpec | Tail], Acc) ->
usage_options([#option{name = Name, short = undefined, long = undefined} = Opt | Tail], Acc) -> Prefix =
usage_options(Tail, add_option_help(Opt, [$<, atom_to_list(Name), $>], Acc)); case Long of
%% Only short form. undefined ->
usage_options([#option{short = Short, long = undefined} = Opt | Tail], Acc) -> case Short of
usage_options(Tail, add_option_help(Opt, [$-, Short], Acc)); %% Neither short nor long form (non-option argument).
%% Only long form. undefined ->
usage_options([#option{short = undefined, long = Long} = Opt | Tail], Acc) -> [$<, atom_to_list(Name), $>];
usage_options(Tail, add_option_help(Opt, [$-, $-, Long], Acc)); %% Only short form.
%% Both short and long form. _ ->
usage_options([#option{short = Short, long = Long} = Opt | Tail], Acc) -> [$-, Short]
usage_options(Tail, add_option_help(Opt, [$-, Short, $,, $\s, $-, $-, Long], Acc)); end;
_ ->
case Short of
%% Only long form.
undefined ->
[$-, $-, Long];
%% Both short and long form.
_ ->
[$-, Short, $,, $\s, $-, $-, Long]
end
end,
usage_options(Tail, add_option_help(OptSpec, Prefix, Acc));
usage_options([], Acc) -> usage_options([], Acc) ->
lists:flatten(lists:reverse(Acc)). lists:flatten(lists:reverse(Acc)).
-spec add_option_help(option_spec(), Prefix :: string(), Acc :: string()) -> string(). -spec add_option_help(option_spec(), Prefix :: string(), Acc :: string()) -> string().
%% @doc Add the help message corresponding to an option specification to a list %% @doc Add the help message corresponding to an option specification to a list
%% with the correct indentation. %% with the correct indentation.
add_option_help(#option{help = Help}, Prefix, Acc) when is_list(Help), Help =/= [] -> add_option_help({_Name, _Short, _Long, _ArgSpec, Help}, Prefix, Acc) when is_list(Help), Help =/= [] ->
FlatPrefix = lists:flatten(Prefix), FlatPrefix = lists:flatten(Prefix),
case ((?INDENTATION * ?TAB_LENGTH) - 2 - length(FlatPrefix)) of case ((?INDENTATION * ?TAB_LENGTH) - 2 - length(FlatPrefix)) of
TabSize when TabSize > 0 -> TabSize when TabSize > 0 ->

View File

@ -1,6 +1,6 @@
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
%%% @author Juan Jose Comellas <jcomellas@novamens.com> %%% @author Juan Jose Comellas <juanjo@comellas.org>
%%% @copyright (C) 2009, Novamens SA (http://www.novamens.com) %%% @copyright (C) 2009 Juan Jose Comellas
%%% @doc Parses command line options with a format similar to that of GNU getopt. %%% @doc Parses command line options with a format similar to that of GNU getopt.
%%% %%%
%%% This source file is subject to the New BSD License. You should have received %%% This source file is subject to the New BSD License. You should have received
@ -9,13 +9,18 @@
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-module(getopt_test). -module(getopt_test).
-author('Juan Jose Comellas <jcomellas@novamens.com>'). -author('juanjo@comellas.org').
-include("getopt.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-import(getopt, [parse/2, usage/2]). -import(getopt, [parse/2, usage/2]).
-define(NAME(Opt), element(1, Opt)).
-define(SHORT(Opt), element(2, Opt)).
-define(LONG(Opt), element(3, Opt)).
-define(ARG_SPEC(Opt), element(4, Opt)).
-define(HELP(Opt), element(5, Opt)).
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
%%% UNIT TESTS %%% UNIT TESTS
@ -23,98 +28,22 @@
%%% Test for the getopt/1 function %%% Test for the getopt/1 function
parse_1_test_() -> parse_1_test_() ->
Short = Short = {short, $a, undefined, undefined, "Option with only short form and no argument"},
#option{name = short, Short2 = {short2, $b, undefined, undefined, "Second option with only short form and no argument"},
short = $a, Short3 = {short3, $c, undefined, undefined, "Third option with only short form and no argument"},
help = "Option with only short form and no argument" ShortArg = {short_arg, $d, undefined, string, "Option with only short form and argument"},
}, ShortDefArg = {short_def_arg, $e, undefined, {string, "default-short"}, "Option with only short form and default argument"},
Short2 = ShortInt = {short_int, $f, undefined, integer, "Option with only short form and integer argument"},
#option{name = short2, Long = {long, undefined, "long", undefined, "Option with only long form and no argument"},
short = $b, LongArg = {long_arg, undefined, "long-arg", string, "Option with only long form and argument"},
help = "Second option with only short form and no argument" LongDefArg = {long_def_arg, undefined, "long-def-arg", {string, "default-long"}, "Option with only long form and default argument"},
}, LongInt = {long_int, undefined, "long-int", integer, "Option with only long form and integer argument"},
Short3 = ShortLong = {short_long, $g, "short-long", undefined, "Option with short form, long form and no argument"},
#option{name = short3, ShortLongArg = {short_long_arg, $h, "short-long-arg", string, "Option with short form, long form and argument"},
short = $c, ShortLongDefArg = {short_long_def_arg, $i, "short-long-def-arg", {string, "default-short-long"}, "Option with short form, long form and default argument"},
help = "Third ption with only short form and no argument" ShortLongInt = {short_long_int, $j, "short-long-int", integer, "Option with short form, long form and integer argument"},
}, NonOptArg = {non_opt_arg, undefined, undefined, undefined, "Non-option argument"},
ShortArg = NonOptInt = {non_opt_int, undefined, undefined, integer, "Non-option integer argument"},
#option{name = short_arg,
short = $d,
arg = string,
help = "Option with only short form and argument"
},
ShortDefArg =
#option{name = short_def_arg,
short = $e,
arg = {string, "default-short"},
help = "Option with only short form and default argument"
},
ShortInt =
#option{name = short_int,
short = $f,
arg = integer,
help = "Option with only short form and integer argument"
},
Long =
#option{name = long,
long = "long",
help = "Option with only long form and no argument"
},
LongArg =
#option{name = long_arg,
long = "long-arg",
arg = string,
help = "Option with only long form and argument"
},
LongDefArg =
#option{name = long_def_arg,
long = "long-def-arg",
arg = {string, "default-long"},
help = "Option with only long form and default argument"
},
LongInt =
#option{name = long_int,
long = "long-int",
arg = integer,
help = "Option with only long form and integer argument"
},
ShortLong =
#option{name = short_long,
short = $g,
long = "short-long",
help = "Option with short form, long form and no argument"
},
ShortLongArg =
#option{name = short_long_arg,
short = $h,
long = "short-long-arg",
arg = string,
help = "Option with short form, long form and argument"
},
ShortLongDefArg =
#option{name = short_long_def_arg,
short = $i,
long = "short-long-def-arg",
arg = {string, "default-short-long"},
help = "Option with short form, long form and default argument"
},
ShortLongInt =
#option{name = short_long_int,
short = $j,
long = "short-long-int",
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 = CombinedOptSpecs =
[ [
Short, Short,
@ -136,14 +65,14 @@ parse_1_test_() ->
], ],
CombinedArgs = CombinedArgs =
[ [
[$-, Short#option.short], [$-, ?SHORT(Short)],
[$-, ShortArg#option.short], "value1", [$-, ?SHORT(ShortArg)], "value1",
[$-, ShortInt#option.short], "100", [$-, ?SHORT(ShortInt)], "100",
[$-, Short2#option.short, Short3#option.short], [$-, ?SHORT(Short2), ?SHORT(Short3)],
"--long", "--long",
"--long-arg", "value2", "--long-arg", "value2",
"--long-int", "101", "--long-int", "101",
[$-, ShortLong#option.short], [$-, ?SHORT(ShortLong)],
"--short-long-arg", "value3", "--short-long-arg", "value3",
"--short-long-int", "103", "--short-long-int", "103",
"value4", "value4",
@ -153,22 +82,22 @@ parse_1_test_() ->
], ],
CombinedOpts = CombinedOpts =
[ [
Short#option.name, ?NAME(Short),
{ShortArg#option.name, "value1"}, {?NAME(ShortArg), "value1"},
{ShortInt#option.name, 100}, {?NAME(ShortInt), 100},
Short2#option.name, ?NAME(Short2),
Short3#option.name, ?NAME(Short3),
Long#option.name, ?NAME(Long),
{LongArg#option.name, "value2"}, {?NAME(LongArg), "value2"},
{LongInt#option.name, 101}, {?NAME(LongInt), 101},
ShortLong#option.name, ?NAME(ShortLong),
{ShortLongArg#option.name, "value3"}, {?NAME(ShortLongArg), "value3"},
{ShortLongInt#option.name, 103}, {?NAME(ShortLongInt), 103},
{NonOptArg#option.name, "value4"}, {?NAME(NonOptArg), "value4"},
{NonOptInt#option.name, 104}, {?NAME(NonOptInt), 104},
{ShortDefArg#option.name, "default-short"}, {?NAME(ShortDefArg), "default-short"},
{LongDefArg#option.name, "default-long"}, {?NAME(LongDefArg), "default-long"},
{ShortLongDefArg#option.name, "default-short-long"} {?NAME(ShortLongDefArg), "default-short-long"}
], ],
CombinedRest = ["dummy1", "dummy2"], CombinedRest = ["dummy1", "dummy2"],
@ -184,41 +113,41 @@ parse_1_test_() ->
{"Unused options and arguments", {"Unused options and arguments",
?_assertMatch({ok, {[], ["arg1", "arg2"]}}, parse([Short], ["arg1", "arg2"]))}, ?_assertMatch({ok, {[], ["arg1", "arg2"]}}, parse([Short], ["arg1", "arg2"]))},
%% Options with only the short form %% Options with only the short form
{Short#option.help, ?_assertMatch({ok, {[short], []}}, parse([Short], [[$-, Short#option.short]]))}, {?HELP(Short), ?_assertEqual({ok, {[short], []}}, parse([Short], [[$-, ?SHORT(Short)]]))},
{ShortArg#option.help, ?_assertMatch({ok, {[{short_arg, "value"}], []}}, parse([ShortArg], [[$-, ShortArg#option.short], "value"]))}, {?HELP(ShortArg), ?_assertEqual({ok, {[{short_arg, "value"}], []}}, parse([ShortArg], [[$-, ?SHORT(ShortArg)], "value"]))},
{ShortDefArg#option.help, ?_assertMatch({ok, {[{short_def_arg, "default-short"}], []}}, parse([ShortDefArg], []))}, {?HELP(ShortDefArg), ?_assertMatch({ok, {[{short_def_arg, "default-short"}], []}}, parse([ShortDefArg], []))},
{ShortInt#option.help, ?_assertMatch({ok, {[{short_int, 100}], []}}, parse([ShortInt], [[$-, ShortInt#option.short], "100"]))}, {?HELP(ShortInt), ?_assertEqual({ok, {[{short_int, 100}], []}}, parse([ShortInt], [[$-, ?SHORT(ShortInt)], "100"]))},
{"Unsorted multiple short form options and arguments in a single string", {"Unsorted multiple short form options and arguments in a single string",
?_assertMatch({ok, {[short, short2, short3], ["arg1", "arg2"]}}, parse([Short, Short2, Short3], "arg1 -abc arg2"))}, ?_assertMatch({ok, {[short, short2, short3], ["arg1", "arg2"]}}, parse([Short, Short2, Short3], "arg1 -abc arg2"))},
{"Short form option and arguments", {"Short form option and arguments",
?_assertMatch({ok, {[short], ["arg1", "arg2"]}}, parse([Short], [[$-, Short#option.short], "arg1", "arg2"]))}, ?_assertMatch({ok, {[short], ["arg1", "arg2"]}}, parse([Short], [[$-, ?SHORT(Short)], "arg1", "arg2"]))},
{"Short form option and arguments (unsorted)", {"Short form option and arguments (unsorted)",
?_assertMatch({ok, {[short], ["arg1", "arg2"]}}, parse([Short], ["arg1", [$-, Short#option.short], "arg2"]))}, ?_assertMatch({ok, {[short], ["arg1", "arg2"]}}, parse([Short], ["arg1", [$-, ?SHORT(Short)], "arg2"]))},
%% Options with only the long form %% Options with only the long form
{Long#option.help, ?_assertMatch({ok, {[long], []}}, parse([Long], ["--long"]))}, {?HELP(Long), ?_assertMatch({ok, {[long], []}}, parse([Long], ["--long"]))},
{LongArg#option.help, ?_assertMatch({ok, {[{long_arg, "value"}], []}}, parse([LongArg], ["--long-arg", "value"]))}, {?HELP(LongArg), ?_assertMatch({ok, {[{long_arg, "value"}], []}}, parse([LongArg], ["--long-arg", "value"]))},
{LongDefArg#option.help, ?_assertMatch({ok, {[{long_def_arg, "default-long"}], []}}, parse([LongDefArg], []))}, {?HELP(LongDefArg), ?_assertMatch({ok, {[{long_def_arg, "default-long"}], []}}, parse([LongDefArg], []))},
{LongInt#option.help, ?_assertMatch({ok, {[{long_int, 100}], []}}, parse([LongInt], ["--long-int", "100"]))}, {?HELP(LongInt), ?_assertMatch({ok, {[{long_int, 100}], []}}, parse([LongInt], ["--long-int", "100"]))},
{"Long form option and arguments", {"Long form option and arguments",
?_assertMatch({ok, {[long], ["arg1", "arg2"]}}, parse([Long], ["--long", "arg1", "arg2"]))}, ?_assertMatch({ok, {[long], ["arg1", "arg2"]}}, parse([Long], ["--long", "arg1", "arg2"]))},
{"Long form option and arguments (unsorted)", {"Long form option and arguments (unsorted)",
?_assertMatch({ok, {[long], ["arg1", "arg2"]}}, parse([Long], ["arg1", "--long", "arg2"]))}, ?_assertMatch({ok, {[long], ["arg1", "arg2"]}}, parse([Long], ["arg1", "--long", "arg2"]))},
%% Options with both the short and long form %% Options with both the short and long form
{ShortLong#option.help, ?_assertMatch({ok, {[short_long], []}}, parse([ShortLong], [[$-, ShortLong#option.short]]))}, {?HELP(ShortLong), ?_assertEqual({ok, {[short_long], []}}, parse([ShortLong], [[$-, ?SHORT(ShortLong)]]))},
{ShortLong#option.help, ?_assertMatch({ok, {[short_long], []}}, parse([ShortLong], ["--short-long"]))}, {?HELP(ShortLong), ?_assertMatch({ok, {[short_long], []}}, parse([ShortLong], ["--short-long"]))},
{ShortLongArg#option.help, ?_assertMatch({ok, {[{short_long_arg, "value"}], []}}, parse([ShortLongArg], [[$-, ShortLongArg#option.short], "value"]))}, {?HELP(ShortLongArg), ?_assertEqual({ok, {[{short_long_arg, "value"}], []}}, parse([ShortLongArg], [[$-, ?SHORT(ShortLongArg)], "value"]))},
{ShortLongArg#option.help, ?_assertMatch({ok, {[{short_long_arg, "value"}], []}}, parse([ShortLongArg], ["--short-long-arg", "value"]))}, {?HELP(ShortLongArg), ?_assertMatch({ok, {[{short_long_arg, "value"}], []}}, parse([ShortLongArg], ["--short-long-arg", "value"]))},
{ShortLongDefArg#option.help, ?_assertMatch({ok, {[{short_long_def_arg, "default-short-long"}], []}}, parse([ShortLongDefArg], []))}, {?HELP(ShortLongDefArg), ?_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"]))}, {?HELP(ShortLongInt), ?_assertEqual({ok, {[{short_long_int, 1234}], []}}, parse([ShortLongInt], [[$-, ?SHORT(ShortLongInt)], "1234"]))},
{ShortLongInt#option.help, ?_assertMatch({ok, {[{short_long_int, 1234}], []}}, parse([ShortLongInt], ["--short-long-int", "1234"]))}, {?HELP(ShortLongInt), ?_assertMatch({ok, {[{short_long_int, 1234}], []}}, parse([ShortLongInt], ["--short-long-int", "1234"]))},
%% Non-option arguments %% Non-option arguments
{NonOptArg#option.help, ?_assertMatch({ok, {[{non_opt_arg, "value"}], []}}, parse([NonOptArg], ["value"]))}, {?HELP(NonOptArg), ?_assertMatch({ok, {[{non_opt_arg, "value"}], []}}, parse([NonOptArg], ["value"]))},
{NonOptInt#option.help, ?_assertMatch({ok, {[{non_opt_int, 1234}], []}}, parse([NonOptInt], ["1234"]))}, {?HELP(NonOptInt), ?_assertMatch({ok, {[{non_opt_int, 1234}], []}}, parse([NonOptInt], ["1234"]))},
{"Declared and undeclared non-option arguments", {"Declared and undeclared non-option arguments",
?_assertMatch({ok, {[{non_opt_arg, "arg1"}], ["arg2", "arg3"]}}, parse([NonOptArg], ["arg1", "arg2", "arg3"]))}, ?_assertMatch({ok, {[{non_opt_arg, "arg1"}], ["arg2", "arg3"]}}, parse([NonOptArg], ["arg1", "arg2", "arg3"]))},
%% Combined %% Combined
{"Combined short, long and non-option arguments", {"Combined short, long and non-option arguments",
?_assertEqual({ok, {CombinedOpts, CombinedRest}}, parse(CombinedOptSpecs, CombinedArgs))}, ?_assertEqual({ok, {CombinedOpts, CombinedRest}}, parse(CombinedOptSpecs, CombinedArgs))},
{"Option with only short form and invalid integer argument", {"Option with only short form and invalid integer argument",
?_assertEqual({error, {invalid_option_arg, {ShortInt#option.name, "value"}}}, parse([ShortInt], [[$-, ShortInt#option.short], "value"]))} ?_assertEqual({error, {invalid_option_arg, {?NAME(ShortInt), "value"}}}, parse([ShortInt], [[$-, ?SHORT(ShortInt)], "value"]))}
]. ].