diff options
author | Tuncer Ayaz <tuncer.ayaz@gmail.com> | 2009-12-29 20:09:28 +0100 |
---|---|---|
committer | Tuncer Ayaz <tuncer.ayaz@gmail.com> | 2009-12-29 20:09:28 +0100 |
commit | b1b29e0858e4e7c6a358e982abc483c8e9091986 (patch) | |
tree | 1ccde340ad4010824602dda214650ae984786996 | |
parent | fe03778678fd98a62d3d2ca97011cb8261383394 (diff) |
Implemented getopt option support
-rw-r--r-- | ebin/rebar.app | 3 | ||||
-rw-r--r-- | src/getopt.erl | 314 | ||||
-rw-r--r-- | src/rebar_core.erl | 54 |
3 files changed, 366 insertions, 5 deletions
diff --git a/ebin/rebar.app b/ebin/rebar.app index 9598e8e..c2f7967 100644 --- a/ebin/rebar.app +++ b/ebin/rebar.app @@ -21,7 +21,8 @@ rebar_rel_utils, rebar_reltool, rebar_subdirs, - rebar_utils ]}, + rebar_utils, + getopt ]}, {registered, []}, {applications, [kernel, stdlib, diff --git a/src/getopt.erl b/src/getopt.erl new file mode 100644 index 0000000..b335f0c --- /dev/null +++ b/src/getopt.erl @@ -0,0 +1,314 @@ +%%%------------------------------------------------------------------- +%%% @author Juan Jose Comellas <juanjo@comellas.org> +%%% @copyright (C) 2009 Juan Jose Comellas +%%% @doc Parses command line options with a format similar to that of GNU getopt. +%%% @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(getopt). +-author('juanjo@comellas.org'). + +-define(TAB_LENGTH, 8). +%% Indentation of the help messages in number of tabs. +-define(INDENTATION, 3). + +-define(OPT_NAME, 1). +-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() :: { + Name :: atom(), + Short :: char() | undefined, + Long :: string() | undefined, + ArgSpec :: arg_spec(), + Help :: string() | undefined + }. + +-export([parse/2, usage/2]). + + +-spec parse([option_spec()], string() | [string()]) -> {ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: any()}}. +%%-------------------------------------------------------------------- +%% @spec parse(OptSpecList::[option_spec()], Args::string() | [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(OptSpecList, CmdLine) -> + try + Args = if + is_integer(hd(CmdLine)) -> + string:tokens(CmdLine, " \t\n"); + true -> + CmdLine + end, + parse(OptSpecList, [], [], 0, Args) + catch + throw: {error, {_Reason, _Data}} = Error -> + Error + 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, ?OPT_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, ?OPT_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, ?OPT_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, OptSpec} when ?IS_OPT_SPEC(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)}}. + + +-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, {Name, _Short, _Long, ArgSpec, _Help} = OptSpec} -> + case ArgSpec of + undefined -> + {Name, Tail}; + _ -> + case Tail of + [Arg | Tail1] -> + {convert_option_arg(OptSpec, Arg), Tail1}; + [] -> + throw({error, {missing_option_arg, Name}}) + end + end; + false -> + throw({error, {invalid_option, OptStr}}) + end. + +-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, {Name, _Short, _Long, undefined, _Help}} -> + Name; + {value, {Name, _Short, _Long, _ArgSpec, _Help}} -> + throw({error, {missing_option_arg, Name}}); + false -> + throw({error, {invalid_option, OptStr}}) + end. + +-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_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} = OptSpec | _Tail], 0) -> + {value, OptSpec}; +find_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} | 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()]) -> [option()]. +%% @doc Appends the default values of the options that are not present. +append_default_args([{Name, _Short, _Long, {_Type, DefaultArg}, _Help} | Tail], OptAcc) -> + append_default_args(Tail, + case lists:keymember(Name, 1, OptAcc) of + false -> + [{Name, DefaultArg} | OptAcc]; + _ -> + OptAcc + end); +%% For options with no default argument. +append_default_args([_Head | Tail], OptAcc) -> + append_default_args(Tail, OptAcc); +append_default_args([], OptAcc) -> + OptAcc. + + +-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, _Short, _Long, ArgSpec, _Help}, Arg) -> + try + Converted = case ArgSpec of + {Type, _DefaultArg} -> + to_type(Type, Arg); + Type when is_atom(Type) -> + to_type(Type, Arg) + end, + {Name, Converted} + catch + error:_ -> + throw({error, {invalid_option_arg, {Name, Arg}}}) + end. + +-spec to_type(atom(), string()) -> arg_value(). +to_type(binary, Arg) -> + list_to_binary(Arg); +to_type(atom, Arg) -> + list_to_atom(Arg); +to_type(integer, Arg) -> + list_to_integer(Arg); +to_type(float, Arg) -> + list_to_float(Arg); +to_type(boolean, Arg) -> + LowerArg = string:to_lower(Arg), + (LowerArg =:= "true") orelse (LowerArg =:= "t") orelse + (LowerArg =:= "yes") orelse (LowerArg =:= "y") orelse + (LowerArg =:= "on") orelse (LowerArg =:= "enabled"); +to_type(_Type, Arg) -> + Arg. + + +-spec usage([option_spec()], 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(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(). +%% @doc Return a string with the syntax for the command line options and +%% arguments. +usage_cmd_line(OptSpecList) -> + usage_cmd_line(OptSpecList, []). + +usage_cmd_line([{Name, Short, Long, ArgSpec, _Help} | Tail], Acc) -> + CmdLine = + case ArgSpec of + undefined -> + if + %% For options with short form and no argument. + Short =/= undefined -> + [$\s, $[, $-, Short, $]]; + %% For options with only long form and no argument. + Long =/= undefined -> + [$\s, $[, $-, $-, Long, $]]; + true -> + [] + end; + _ -> + 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) -> + lists:flatten(lists:reverse(Acc)). + + +-spec usage_options([option_spec()]) -> string(). +%% @doc Return a string with the help message for each of the options and +%% arguments. +usage_options(OptSpecList) -> + usage_options(OptSpecList, []). + +usage_options([{Name, Short, Long, _ArgSpec, _Help} = OptSpec | Tail], Acc) -> + Prefix = + case Long of + undefined -> + case Short of + %% Neither short nor long form (non-option argument). + undefined -> + [$<, atom_to_list(Name), $>]; + %% Only short form. + _ -> + [$-, Short] + 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) -> + lists:flatten(lists:reverse(Acc)). + +-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({_Name, _Short, _Long, _ArgSpec, Help}, Prefix, Acc) when is_list(Help), Help =/= [] -> + FlatPrefix = lists:flatten(Prefix), + case ((?INDENTATION * ?TAB_LENGTH) - 2 - length(FlatPrefix)) of + TabSize when TabSize > 0 -> + Tab = lists:duplicate(ceiling(TabSize / ?TAB_LENGTH), $\t), + [[$\s, $\s, FlatPrefix, Tab, Help, $\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. + + +-spec ceiling(float()) -> integer(). +%% @doc Return the smallest integral valur not less than the argument. +ceiling(X) -> + T = erlang:trunc(X), + case (X - T) of + %% Neg when Neg < 0 -> + %% T; + Pos when Pos > 0 -> + T + 1; + _ -> + T + end. diff --git a/src/rebar_core.erl b/src/rebar_core.erl index 06afc7f..e6cfa73 100644 --- a/src/rebar_core.erl +++ b/src/rebar_core.erl @@ -46,19 +46,41 @@ run(["version"]) -> ?CONSOLE("Version ~s built ~s\n", [Vsn, ?BUILD_TIME]), ok; run(Args) -> - %% Filter all the flags (i.e. string of form key=value) from the - %% command line arguments. What's left will be the commands to run. - Commands = filter_flags(Args, []), - %% Pre-load the rebar app so that we get default configuration ok = application:load(rebar), + %% Parse getopt options + case getopt:parse(option_spec_list(), Args) of + {ok, {Options, NonOptArgs}} -> + case proplists:get_bool(help, Options) of + true -> + %% display usage info + getopt:usage(option_spec_list(), "rebar"); + false -> + %% Set global variables based on getopt options + set_global_flag(Options, verbose), + set_global_flag(Options, quiet), + set_global_flag(Options, force), + + %% run rebar with supplied options + run2(NonOptArgs) + end; + {error, {Reason, Data}} -> + ?ERROR("Error: ~s ~p~n~n", [Reason, Data]), + getopt:usage(option_spec_list(), "rebar") + end. + +run2(Args) -> %% Make sure crypto is running crypto:start(), %% Initialize logging system rebar_log:init(), + %% Filter all the flags (i.e. string of form key=value) from the + %% command line arguments. What's left will be the commands to run. + Commands = filter_flags(Args, []), + %% Convert command strings to atoms CommandAtoms = [list_to_atom(C) || C <- Commands], @@ -71,6 +93,30 @@ run(Args) -> %% =================================================================== %% +%% set global flag based on getopt option boolean value +%% +set_global_flag(Options, Flag) -> + Value = case proplists:get_bool(Flag, Options) of + true -> + "1"; + false -> + "0" + end, + rebar_config:set_global(Flag, Value). + +%% +%% options accepted via getopt +%% +option_spec_list() -> + [ + %% {Name, ShortOpt, LongOpt, ArgSpec, HelpMsg} + {help, $h, "help", undefined, "Show the program options"}, + {verbose, $v, "verbose", undefined, "Be verbose about what gets done"}, + {quiet, $q, "quiet", undefined, "Be quiet about what gets done"}, + {force, $f, "force", undefined, "Force"} + ]. + +%% %% Seperate all commands (single-words) from flags (key=value) and store %% values into the rebar_config global storage. %% |