diff options
| -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.  %% | 
