diff --git a/src/getopt.erl b/src/getopt.erl index f4773bd..0983797 100644 --- a/src/getopt.erl +++ b/src/getopt.erl @@ -12,18 +12,9 @@ -author('juanjo@comellas.org'). -export([parse/2, usage/2, usage/3, usage/4, tokenize/1]). +-export([usage_cmd_line/2]). --export_type([arg_type/0, - arg_value/0, - arg_spec/0, - simple_option/0, - compound_option/0, - option/0, - option_spec/0]). - --define(TAB_LENGTH, 8). -%% Indentation of the help messages in number of tabs. --define(INDENTATION, 3). +-define(LINE_LENGTH, 75). %% Position of each field in the option specification tuple. -define(OPT_NAME, 1). @@ -33,7 +24,8 @@ -define(OPT_HELP, 5). -define(IS_OPT_SPEC(Opt), (tuple_size(Opt) =:= ?OPT_HELP)). - +-define(IS_WHITESPACE(Char), ((Char) =:= $\s orelse (Char) =:= $\t orelse + (Char) =:= $\n orelse (Char) =:= $\r)). %% Atom indicating the data type that an argument can be converted to. -type arg_type() :: 'atom' | 'binary' | 'boolean' | 'float' | 'integer' | 'string'. @@ -56,19 +48,19 @@ %% Output streams -type output_stream() :: 'standard_io' | 'standard_error'. +-export_type([arg_type/0, arg_value/0, arg_spec/0, simple_option/0, compound_option/0, option/0, option_spec/0]). + %% @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. -spec parse([option_spec()], string() | [string()]) -> - {ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: any()}}. -parse(OptSpecList, CmdLine) -> + {ok, {[option()], [string()]}} | {error, {Reason :: atom(), Data :: any()}}. +parse(OptSpecList, CmdLine) when is_list(CmdLine) -> try Args = if - is_integer(hd(CmdLine)) -> - tokenize(CmdLine); - true -> - CmdLine + is_integer(hd(CmdLine)) -> tokenize(CmdLine); + true -> CmdLine end, parse(OptSpecList, [], [], 0, Args) catch @@ -78,7 +70,7 @@ parse(OptSpecList, CmdLine) -> -spec parse([option_spec()], [option()], [string()], integer(), [string()]) -> - {ok, {[option()], [string()]}}. + {ok, {[option()], [string()]}}. %% Process the option terminator. parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, ["--" | Tail]) -> %% Any argument present after the terminator is not considered an option. @@ -110,7 +102,7 @@ parse(OptSpecList, OptAcc, ArgAcc, _ArgPos, []) -> %% --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()) -> - {ok, {[option()], [string()]}}. + {ok, {[option()], [string()]}}. parse_long_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) -> case split_assigned_arg(OptArg) of {Long, Arg} -> @@ -197,7 +189,7 @@ parse_long_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _Sh %% -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()) -> - {ok, {[option()], [string()]}}. + {ok, {[option()], [string()]}}. parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, OptArg) -> parse_short_option(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, OptStr, first, OptArg). @@ -258,7 +250,7 @@ parse_short_option_next_arg(OptSpecList, OptAcc, ArgAcc, ArgPos, Args, {Name, _S %% Pos argument. -spec find_non_option_arg([option_spec()], integer()) -> {value, option_spec()} | false. find_non_option_arg([{_Name, undefined, undefined, _ArgSpec, _Help} = OptSpec | _Tail], 0) -> - {value, OptSpec}; + {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) -> @@ -272,12 +264,12 @@ find_non_option_arg([], _Pos) -> -spec append_default_options([option_spec()], [option()]) -> [option()]. append_default_options([{Name, _Short, _Long, {_Type, DefaultArg}, _Help} | Tail], OptAcc) -> append_default_options(Tail, - case lists:keymember(Name, 1, OptAcc) of - false -> - [{Name, DefaultArg} | OptAcc]; - _ -> - OptAcc - end); + case lists:keymember(Name, 1, OptAcc) of + false -> + [{Name, DefaultArg} | OptAcc]; + _ -> + OptAcc + end); %% For options with no default argument. append_default_options([_Head | Tail], OptAcc) -> append_default_options(Tail, OptAcc); @@ -470,140 +462,231 @@ is_non_neg_float_arg([]) -> %% arguments that are supported by the program. -spec usage([option_spec()], string()) -> ok. usage(OptSpecList, ProgramName) -> - usage(OptSpecList, ProgramName, standard_error). + usage(OptSpecList, ProgramName, standard_error). %% @doc Show a message on standard_error or standard_io indicating the command line options and %% arguments that are supported by the program. -spec usage([option_spec()], string(), output_stream() | string()) -> ok. usage(OptSpecList, ProgramName, OutputStream) when is_atom(OutputStream) -> - io:format(OutputStream, "Usage: ~s~s~n~n~s~n", - [ProgramName, usage_cmd_line(OptSpecList), usage_options(OptSpecList)]); + io:format(OutputStream, "~s~n~n~s~n", + [usage_cmd_line(ProgramName, OptSpecList), usage_options(OptSpecList)]); %% @doc Show a message on standard_error indicating the command line options and %% arguments that are supported by the program. The CmdLineTail argument %% is a string that is added to the end of the usage command line. usage(OptSpecList, ProgramName, CmdLineTail) -> - usage(OptSpecList, ProgramName, CmdLineTail, standard_error). + usage(OptSpecList, ProgramName, CmdLineTail, standard_error). %% @doc Show a message on standard_error or standard_io indicating the command line options and %% arguments that are supported by the program. The CmdLineTail argument %% is a string that is added to the end of the usage command line. --spec usage([option_spec()], string(), string(), output_stream() | [{string(), string()}]) -> ok. +-spec usage([option_spec()], ProgramName :: string(), CmdLineTail :: string(), output_stream() | [{string(), string()}]) -> ok. usage(OptSpecList, ProgramName, CmdLineTail, OutputStream) when is_atom(OutputStream) -> - io:format(OutputStream, "Usage: ~s~s ~s~n~n~s~n", - [ProgramName, usage_cmd_line(OptSpecList), CmdLineTail, usage_options(OptSpecList)]); + io:format(OutputStream, "~s ~s~n~n~s~n", + [usage_cmd_line(ProgramName, OptSpecList), CmdLineTail, usage_options(OptSpecList)]); %% @doc Show a message on standard_error indicating the command line options and %% arguments that are supported by the program. The CmdLineTail and OptionsTail %% arguments are a string that is added to the end of the usage command line %% and a list of tuples that are added to the end of the options' help lines. usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail) -> - usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail, standard_error). + usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail, standard_error). %% @doc Show a message on standard_error or standard_io indicating the command line options and %% arguments that are supported by the program. The CmdLineTail and OptionsTail %% arguments are a string that is added to the end of the usage command line %% and a list of tuples that are added to the end of the options' help lines. --spec usage([option_spec()], string(), string(), [{string(), string()}], output_stream()) -> ok. +-spec usage([option_spec()], ProgramName :: string(), CmdLineTail :: string(), + [{OptionName :: string(), Help :: string()}], output_stream()) -> ok. usage(OptSpecList, ProgramName, CmdLineTail, OptionsTail, OutputStream) -> - UsageOptions = lists:foldl( - fun ({Prefix, Help}, Acc) -> - add_option_help(Prefix, Help, Acc) - end, usage_options_reverse(OptSpecList, []), OptionsTail), - io:format(OutputStream, "Usage: ~s~s ~s~n~n~s~n", - [ProgramName, usage_cmd_line(OptSpecList), CmdLineTail, - lists:flatten(lists:reverse(UsageOptions))]). + io:format(OutputStream, "~s ~s~n~n~s~n", + [usage_cmd_line(ProgramName, OptSpecList), CmdLineTail, usage_options(OptSpecList, OptionsTail)]). -%% @doc Return a string with the syntax for the command line options and -%% arguments. --spec usage_cmd_line([option_spec()]) -> string(). -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_cmd_line(ProgramName :: string(), [option_spec()]) -> iolist(). +usage_cmd_line(ProgramName, OptSpecList) -> + Prefix = "Usage: " ++ ProgramName, + PrefixLength = length(Prefix), + Indentation = lists:duplicate(PrefixLength, $\s), + [Prefix | usage_cmd_line_options(line_length() - PrefixLength, Indentation, OptSpecList)]. -%% @doc Return a string with the help message for each of the options and -%% arguments. +%% @doc Return a string with the syntax for the command line options and arguments. +-spec usage_cmd_line_options(LineLength :: non_neg_integer(), Indentation :: string(), [option_spec()]) -> iolist(). +usage_cmd_line_options(LineLength, Indentation, OptSpecList) -> + usage_cmd_line_options(LineLength, Indentation, OptSpecList, [], 0, []). + +usage_cmd_line_options(LineLength, Indentation, [OptSpec | Tail], LineAcc, AccLength, Acc) -> + Option = lists:flatten(usage_cmd_line_option(OptSpec)), + OptionLength = length(Option), + %% We add the length of this command line option together with the corresponding + %% space to the accumulated length, + NewAccLength = AccLength + OptionLength + 1, + if + NewAccLength < LineLength -> + usage_cmd_line_options(LineLength, Indentation, Tail, [Option | LineAcc], NewAccLength, Acc); + true -> + usage_cmd_line_options(LineLength, Indentation, Tail, [Option, Indentation], + length(Option) + 1, [lists:reverse([$\n | LineAcc]) | Acc]) + end; +usage_cmd_line_options(LineLength, Indentation, [] = OptSpecList, [_ | _] = LineAcc, AccLength, Acc) -> + usage_cmd_line_options(LineLength, Indentation, OptSpecList, [], + AccLength, [lists:reverse(LineAcc) | Acc]); +usage_cmd_line_options(_LineLength, _Indentation, [], [], _AccLength, Acc) -> + lists:reverse(Acc). + + +-spec usage_cmd_line_option(option_spec()) -> string(). +usage_cmd_line_option({_Name, Short, _Long, undefined, _Help}) when Short =/= undefined -> + %% For options with short form and no argument. + [$\s, $[, $-, Short, $]]; +usage_cmd_line_option({_Name, _Short, Long, undefined, _Help}) when Long =/= undefined -> + %% For options with only long form and no argument. + [$\s, $[, $-, $-, Long, $]]; +usage_cmd_line_option({_Name, _Short, _Long, undefined, _Help}) -> + []; +usage_cmd_line_option({Name, Short, Long, ArgSpec, _Help}) when is_atom(ArgSpec) -> + %% For options with no default argument. + 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; +usage_cmd_line_option({Name, Short, Long, ArgSpec, _Help}) when is_tuple(ArgSpec) -> + %% For options with default argument. + if + %% For options with short form and default argument. + Short =/= undefined -> [$\s, $[, $-, Short, $\s, $[, $<, atom_to_list(Name), $>, $], $]]; + %% For options with only long form and default argument. + Long =/= undefined -> [$\s, $[, $-, $-, Long, $\s, $[, $<, atom_to_list(Name), $>, $], $]]; + %% For options with neither short nor long form and default argument. + true -> [$\s, $[, $<, atom_to_list(Name), $>, $]] + end. + + +%% @doc Return a list of help messages to print for each of the options and arguments. -spec usage_options([option_spec()]) -> string(). usage_options(OptSpecList) -> - lists:flatten(lists:reverse(usage_options_reverse(OptSpecList, []))). + usage_options(OptSpecList, []). -usage_options_reverse([{Name, Short, Long, _ArgSpec, Help} | Tail], Acc) -> - Prefix = + +%% @doc Return a list of help messages to print for each of the options and arguments. +-spec usage_options([option_spec()], [{OptionName :: string(), Help :: string()}]) -> string(). +usage_options(OptSpecList, CustomHelp) -> + {MaxLength0, OptionHelpRows0} = add_option_spec_help_rows(OptSpecList, 0, []), + {MaxLength, OptionHelpRows} = add_custom_help_rows(CustomHelp, MaxLength0, OptionHelpRows0), + LineLength = line_length(), + lists:reverse([format_help_line(MaxLength + 1, LineLength, OptionHelp) || + OptionHelp <- OptionHelpRows]). + + +add_option_spec_help_rows([{Name, Short, Long, _ArgSpec, _Help} = OptSpec | Tail], MaxOptionLength, Acc) -> + Option = case Long of undefined -> case Short of %% Neither short nor long form (non-option argument). - undefined -> - [$<, atom_to_list(Name), $>]; + undefined -> [$<, atom_to_list(Name), $>]; %% Only short form. - _ -> - [$-, Short] + _ -> [$-, Short] end; _ -> case Short of %% Only long form. - undefined -> - [$-, $- | Long]; + undefined -> [$-, $- | Long]; %% Both short and long form. - _ -> - [$-, Short, $,, $\s, $-, $- | Long] + _ -> [$-, Short, $,, $\s, $-, $- | Long] end end, - usage_options_reverse(Tail, add_option_help(Prefix, Help, Acc)); -usage_options_reverse([], Acc) -> - Acc. + {NewMaxOptionLength, OptionSpecHelpWithLength} = get_option_help_length({Option, usage_help(OptSpec)}, MaxOptionLength), + add_option_spec_help_rows(Tail, NewMaxOptionLength, [OptionSpecHelpWithLength | Acc]); +add_option_spec_help_rows([], MaxOptionLength, Acc) -> + {MaxOptionLength, Acc}. -%% @doc Add the help message corresponding to an option specification to a list -%% with the correct indentation. --spec add_option_help(Prefix :: string(), Help :: string(), Acc :: string()) -> string(). -add_option_help(Prefix, Help, 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] +-spec usage_help(option_spec()) -> string(). +usage_help({_Name, _Short, _Long, {_ArgType, ArgValue}, [_ | _] = Help}) -> + Help ++ " [default: " ++ default_arg_value_to_string(ArgValue) ++ "]"; +usage_help({_Name, _Short, _Long, _ArgSpec, Help}) -> + Help. + + +add_custom_help_rows([CustomOptionHelp | Tail], MaxOptionLength, Acc) -> + {NewMaxOptionLength, CustomOptionHelpWithLength} = get_option_help_length(CustomOptionHelp, MaxOptionLength), + add_custom_help_rows(Tail, NewMaxOptionLength, [CustomOptionHelpWithLength | Acc]); +add_custom_help_rows([], MaxOptionLength, Acc) -> + {MaxOptionLength, Acc}. + + +get_option_help_length({UnflattenedOption, Help}, PrevMaxOptionLength) -> + Option = lists:flatten(UnflattenedOption), + OptionLength = length(Option), + %% Calculate the maximum width of the column that shows the option's name. + MaxOptionLength = if + OptionLength > PrevMaxOptionLength -> OptionLength; + true -> PrevMaxOptionLength + end, + {MaxOptionLength, {OptionLength, Option, Help}}. + + +-spec format_help_line(MaxOptionLength :: non_neg_integer(), LineLength :: non_neg_integer(), + {OptionLength :: non_neg_integer(), Option :: string(), Help :: [string()]}) -> iolist(). +format_help_line(MaxOptionLength, LineLength, {OptionLength, Option, [_ | _] = Help}) -> + OptionColumnWidth = MaxOptionLength + 3, + HelpLines = wrap_text_line(LineLength - OptionColumnWidth, Help), + case OptionColumnWidth < LineLength of + true -> + %% Lines in which the column with the options is narrower than that of the console + FirstLineSep = lists:duplicate(MaxOptionLength - OptionLength + 1, $\s), + Indentation = [$\n | lists:duplicate(MaxOptionLength + 3, $\s)], + [" ", Option, FirstLineSep, hd(HelpLines), [[Indentation, Line] || Line <- tl(HelpLines)], $\n]; + false -> + [" ", Option, [["\n ", Line] || Line <- HelpLines], $\n] end; -add_option_help(_Opt, _Prefix, Acc) -> - Acc. +format_help_line(_MaxOptionLength, _LineLength, {_OptionLength, Option, _Help}) -> + [" ", Option, $\n]. + + +%% @doc Wrap a text line converting it into several text lines so that the +%% length of each one of them is never over HelpLength characters. +-spec wrap_text_line(HelpLength :: non_neg_integer(), Help :: string()) -> [string()]. +wrap_text_line(HelpLength, Help) -> + wrap_text_line(HelpLength, Help, [], 0, []). + +wrap_text_line(HelpLength, [Char | Tail], Acc, Count, CurrentLineAcc) when Count < HelpLength -> + wrap_text_line(HelpLength, Tail, Acc, Count + 1, [Char | CurrentLineAcc]); +wrap_text_line(HelpLength, [_ | _] = Help, Acc, Count, CurrentLineAcc) -> + %% Look for the first whitespace character in the current (reversed) line + %% buffer to get a wrapped line. If there is no whitespace just cut the + %% line at the position corresponding to the maximum length. + {NextLineAcc, WrappedLine} = case string:cspan(CurrentLineAcc, " \t") of + WhitespacePos when WhitespacePos < Count -> + lists:split(WhitespacePos, CurrentLineAcc); + _ -> + {[], CurrentLineAcc} + end, + wrap_text_line(HelpLength, Help, [lists:reverse(WrappedLine) | Acc], length(NextLineAcc), NextLineAcc); +wrap_text_line(_HelpLength, [], Acc, _Count, [_ | _] = CurrentLineAcc) -> + %% If there was a non-empty line when we reached the buffer, add it to the accumulator + lists:reverse([lists:reverse(CurrentLineAcc) | Acc]); +wrap_text_line(_HelpLength, [], Acc, _Count, _CurrentLineAcc) -> + lists:reverse(Acc). + + +default_arg_value_to_string(Value) when is_atom(Value) -> + atom_to_list(Value); +default_arg_value_to_string(Value) when is_binary(Value) -> + binary_to_list(Value); +default_arg_value_to_string(Value) when is_integer(Value) -> + integer_to_list(Value); +default_arg_value_to_string(Value) when is_float(Value) -> + float_to_list(Value); +default_arg_value_to_string(Value) -> + Value. %% @doc Tokenize a command line string with support for single and double @@ -611,12 +694,12 @@ add_option_help(_Opt, _Prefix, Acc) -> %% The function also supports the expansion of environment variables in %% both the Unix (${VAR}; $VAR) and Windows (%VAR%) formats. It does NOT %% support wildcard expansion of paths. --spec tokenize(CmdLine :: string()) -> [string()]. +-spec tokenize(CmdLine :: string()) -> [nonempty_string()]. tokenize(CmdLine) -> tokenize(CmdLine, [], []). -spec tokenize(CmdLine :: string(), Acc :: [string()], ArgAcc :: string()) -> [string()]. -tokenize([Sep | Tail], Acc, ArgAcc) when Sep =:= $\s; Sep =:= $\t; Sep =:= $\n -> +tokenize([Sep | Tail], Acc, ArgAcc) when ?IS_WHITESPACE(Sep) -> NewAcc = case ArgAcc of [_ | _] -> %% Found separator: add to the list of arguments. @@ -660,7 +743,7 @@ tokenize_quoted_arg(_QuotationMark, CmdLine, Acc, ArgAcc) -> tokenize(CmdLine, Acc, ArgAcc). --spec expand_env_var(CmdLine :: string()) -> {string(), string()}. +-spec expand_env_var(CmdLine :: nonempty_string()) -> {string(), string()}. expand_env_var(CmdLine) -> case CmdLine of "${" ++ Tail -> @@ -703,15 +786,13 @@ get_env_var(Prefix, Suffix, []) -> Prefix ++ Suffix. -%% @doc Return the smallest integral value not less than the argument. --spec ceiling(float()) -> integer(). -ceiling(X) -> - T = erlang:trunc(X), - case (X - T) of - % Neg when Neg < 0 -> - % T; - Pos when Pos > 0 -> - T + 1; +-spec line_length() -> non_neg_integer(). +line_length() -> + case io:columns() of + {ok, Columns} when Columns >= ?LINE_LENGTH -> + Columns - 5; + {ok, Columns} -> + Columns; _ -> - T + ?LINE_LENGTH end.