Fix usage output when a long option has a length that is bigger than a console line

This commit is contained in:
Juan Jose Comellas 2012-11-19 12:25:06 -03:00
parent 46ea9c7908
commit 98f3c6f123

View File

@ -48,6 +48,11 @@
%% Output streams %% Output streams
-type output_stream() :: 'standard_io' | 'standard_error'. -type output_stream() :: 'standard_io' | 'standard_error'.
%% For internal use
-type usage_line() :: {OptionText :: string(), HelpText :: string()}.
-type usage_line_with_length() :: {OptionLength :: non_neg_integer(), OptionText :: string(), HelpText :: string()}.
-export_type([arg_type/0, arg_value/0, arg_spec/0, simple_option/0, compound_option/0, option/0, option_spec/0]). -export_type([arg_type/0, arg_value/0, arg_spec/0, simple_option/0, compound_option/0, option/0, option_spec/0]).
@ -569,97 +574,115 @@ usage_cmd_line_option({Name, Short, Long, ArgSpec, _Help}) when is_tuple(ArgSpec
%% @doc Return a list of help messages to print for each of the options and arguments. %% @doc Return a list of help messages to print for each of the options and arguments.
-spec usage_options([option_spec()]) -> string(). -spec usage_options([option_spec()]) -> [string()].
usage_options(OptSpecList) -> usage_options(OptSpecList) ->
usage_options(OptSpecList, []). usage_options(OptSpecList, []).
%% @doc Return a list of help messages to print for each of the options and arguments. %% @doc Return a list of usage lines to print for each of the options and arguments.
-spec usage_options([option_spec()], [{OptionName :: string(), Help :: string()}]) -> string(). -spec usage_options([option_spec()], [{OptionName :: string(), Help :: string()}]) -> [string()].
usage_options(OptSpecList, CustomHelp) -> usage_options(OptSpecList, CustomHelp) ->
{MaxLength0, OptionHelpRows0} = add_option_spec_help_rows(OptSpecList, 0, []), %% Add the usage lines corresponding to the option specifications.
{MaxLength, OptionHelpRows} = add_custom_help_rows(CustomHelp, MaxLength0, OptionHelpRows0), {MaxOptionLength0, UsageLines0} = add_option_spec_help_lines(OptSpecList, 0, []),
%% Add the custom usage lines.
{MaxOptionLength, UsageLines} = add_custom_help_lines(CustomHelp, MaxOptionLength0, UsageLines0),
LineLength = line_length(), LineLength = line_length(),
lists:reverse([format_help_line(MaxLength + 1, LineLength, OptionHelp) || lists:reverse([format_usage_line(MaxOptionLength + 1, LineLength, UsageLine) || UsageLine <- UsageLines]).
OptionHelp <- OptionHelpRows]).
add_option_spec_help_rows([{Name, Short, Long, _ArgSpec, _Help} = OptSpec | Tail], MaxOptionLength, Acc) -> -spec add_option_spec_help_lines([option_spec()], PrevMaxOptionLength :: non_neg_integer(), [usage_line_with_length()]) ->
Option = {MaxOptionLength :: non_neg_integer(), [usage_line_with_length()]}.
case Long of add_option_spec_help_lines([OptSpec | Tail], PrevMaxOptionLength, Acc) ->
undefined -> OptionText = usage_option_text(OptSpec),
case Short of HelpText = usage_help_text(OptSpec),
%% Neither short nor long form (non-option argument). {MaxOptionLength, ColsWithLength} = get_max_option_length({OptionText, HelpText}, PrevMaxOptionLength),
undefined -> [$<, atom_to_list(Name), $>]; add_option_spec_help_lines(Tail, MaxOptionLength, [ColsWithLength | Acc]);
%% Only short form. add_option_spec_help_lines([], MaxOptionLength, Acc) ->
_ -> [$-, Short]
end;
_ ->
case Short of
%% Only long form.
undefined -> [$-, $- | Long];
%% Both short and long form.
_ -> [$-, Short, $,, $\s, $-, $- | Long]
end
end,
{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}. {MaxOptionLength, Acc}.
-spec usage_help(option_spec()) -> string(). -spec add_custom_help_lines([usage_line()], PrevMaxOptionLength :: non_neg_integer(), [usage_line_with_length()]) ->
usage_help({_Name, _Short, _Long, {_ArgType, ArgValue}, [_ | _] = Help}) -> {MaxOptionLength :: non_neg_integer(), [usage_line_with_length()]}.
add_custom_help_lines([CustomCols | Tail], PrevMaxOptionLength, Acc) ->
{MaxOptionLength, ColsWithLength} = get_max_option_length(CustomCols, PrevMaxOptionLength),
add_custom_help_lines(Tail, MaxOptionLength, [ColsWithLength | Acc]);
add_custom_help_lines([], MaxOptionLength, Acc) ->
{MaxOptionLength, Acc}.
-spec usage_option_text(option_spec()) -> string().
usage_option_text({Name, undefined, undefined, _ArgSpec, _Help}) ->
%% Neither short nor long form (non-option argument).
"<" ++ atom_to_list(Name) ++ ">";
usage_option_text({_Name, Short, undefined, _ArgSpec, _Help}) ->
%% Only short form.
[$-, Short];
usage_option_text({_Name, undefined, Long, _ArgSpec, _Help}) ->
%% Only long form.
[$-, $- | Long];
usage_option_text({_Name, Short, Long, _ArgSpec, _Help}) ->
%% Both short and long form.
[$-, Short, $,, $\s, $-, $- | Long].
-spec usage_help_text(option_spec()) -> string().
usage_help_text({_Name, _Short, _Long, {_ArgType, ArgValue}, [_ | _] = Help}) ->
Help ++ " [default: " ++ default_arg_value_to_string(ArgValue) ++ "]"; Help ++ " [default: " ++ default_arg_value_to_string(ArgValue) ++ "]";
usage_help({_Name, _Short, _Long, _ArgSpec, Help}) -> usage_help_text({_Name, _Short, _Long, _ArgSpec, Help}) ->
Help. Help.
add_custom_help_rows([CustomOptionHelp | Tail], MaxOptionLength, Acc) -> %% @doc Calculate the maximum width of the column that shows the option's short
{NewMaxOptionLength, CustomOptionHelpWithLength} = get_option_help_length(CustomOptionHelp, MaxOptionLength), %% and long form.
add_custom_help_rows(Tail, NewMaxOptionLength, [CustomOptionHelpWithLength | Acc]); -spec get_max_option_length(usage_line(), PrevMaxOptionLength :: non_neg_integer()) ->
add_custom_help_rows([], MaxOptionLength, Acc) -> {MaxOptionLength :: non_neg_integer(), usage_line_with_length()}.
{MaxOptionLength, Acc}. get_max_option_length({OptionText, HelpText}, PrevMaxOptionLength) ->
OptionLength = length(OptionText),
{erlang:max(OptionLength, PrevMaxOptionLength), {OptionLength, OptionText, HelpText}}.
get_option_help_length({UnflattenedOption, Help}, PrevMaxOptionLength) -> %% @doc Format the usage line that is shown for the options' usage. Each usage
Option = lists:flatten(UnflattenedOption), %% line has 2 columns. The first column shows the options in their short
OptionLength = length(Option), %% and long form. The second column shows the wrapped (if necessary) help
%% Calculate the maximum width of the column that shows the option's name. %% text lines associated with each option. e.g.:
MaxOptionLength = if %%
OptionLength > PrevMaxOptionLength -> OptionLength; %% -h, --host Database server host name or IP address; this is the
true -> PrevMaxOptionLength %% hostname of the server where the database is running
end, %% [default: localhost]
{MaxOptionLength, {OptionLength, Option, Help}}. %% -p, --port Database server port [default: 1000]
%%
-spec format_usage_line(MaxOptionLength :: non_neg_integer(), LineLength :: non_neg_integer(),
-spec format_help_line(MaxOptionLength :: non_neg_integer(), LineLength :: non_neg_integer(), usage_line_with_length()) -> iolist().
{OptionLength :: non_neg_integer(), Option :: string(), Help :: [string()]}) -> iolist(). format_usage_line(MaxOptionLength, LineLength, {OptionLength, OptionText, [_ | _] = HelpText})
format_help_line(MaxOptionLength, LineLength, {OptionLength, Option, [_ | _] = Help}) -> when MaxOptionLength < (LineLength div 2) ->
OptionColumnWidth = MaxOptionLength + 3, %% If the width of the column where the options are shown is smaller than
HelpLines = wrap_text_line(LineLength - OptionColumnWidth, Help), %% half the width of a console line then we show the help text line aligned
case OptionColumnWidth < LineLength of %% next to its corresponding option, with a separation of at least 2
true -> %% characters.
%% Lines in which the column with the options is narrower than that of the console [Head | Tail] = wrap_text_line(LineLength - MaxOptionLength - 3, HelpText),
FirstLineSep = lists:duplicate(MaxOptionLength - OptionLength + 1, $\s), FirstLineIndentation = lists:duplicate(MaxOptionLength - OptionLength + 1, $\s),
Indentation = [$\n | lists:duplicate(MaxOptionLength + 3, $\s)], Indentation = [$\n | lists:duplicate(MaxOptionLength + 3, $\s)],
[" ", Option, FirstLineSep, hd(HelpLines), [[Indentation, Line] || Line <- tl(HelpLines)], $\n]; [" ", OptionText, FirstLineIndentation, Head,
false -> [[Indentation, Line] || Line <- Tail], $\n];
[" ", Option, [["\n ", Line] || Line <- HelpLines], $\n] format_usage_line(_MaxOptionLength, LineLength, {_OptionLength, OptionText, [_ | _] = HelpText}) ->
end; %% If the width of the first column is bigger than the width of a console
format_help_line(_MaxOptionLength, _LineLength, {_OptionLength, Option, _Help}) -> %% line, we show the help text on the next line with an indentation of 6
[" ", Option, $\n]. %% characters.
HelpLines = wrap_text_line(LineLength - 6, HelpText),
[" ", OptionText, [["\n ", Line] || Line <- HelpLines], $\n];
format_usage_line(_MaxOptionLength, _LineLength, {_OptionLength, OptionText, _HelpText}) ->
[" ", OptionText, $\n].
%% @doc Wrap a text line converting it into several text lines so that the %% @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. %% length of each one of them is never over HelpLength characters.
-spec wrap_text_line(HelpLength :: non_neg_integer(), Help :: string()) -> [string()]. -spec wrap_text_line(Length :: non_neg_integer(), Text :: string()) -> [string()].
wrap_text_line(HelpLength, Help) -> wrap_text_line(Length, Text) ->
wrap_text_line(HelpLength, Help, [], 0, []). wrap_text_line(Length, Text, [], 0, []).
wrap_text_line(HelpLength, [Char | Tail], Acc, Count, CurrentLineAcc) when Count < HelpLength -> wrap_text_line(Length, [Char | Tail], Acc, Count, CurrentLineAcc) when Count < Length ->
wrap_text_line(HelpLength, Tail, Acc, Count + 1, [Char | CurrentLineAcc]); wrap_text_line(Length, Tail, Acc, Count + 1, [Char | CurrentLineAcc]);
wrap_text_line(HelpLength, [_ | _] = Help, Acc, Count, CurrentLineAcc) -> wrap_text_line(Length, [_ | _] = Help, Acc, Count, CurrentLineAcc) ->
%% Look for the first whitespace character in the current (reversed) line %% 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 %% buffer to get a wrapped line. If there is no whitespace just cut the
%% line at the position corresponding to the maximum length. %% line at the position corresponding to the maximum length.
@ -669,11 +692,11 @@ wrap_text_line(HelpLength, [_ | _] = Help, Acc, Count, CurrentLineAcc) ->
_ -> _ ->
{[], CurrentLineAcc} {[], CurrentLineAcc}
end, end,
wrap_text_line(HelpLength, Help, [lists:reverse(WrappedLine) | Acc], length(NextLineAcc), NextLineAcc); wrap_text_line(Length, Help, [lists:reverse(WrappedLine) | Acc], length(NextLineAcc), NextLineAcc);
wrap_text_line(_HelpLength, [], Acc, _Count, [_ | _] = CurrentLineAcc) -> wrap_text_line(_Length, [], Acc, _Count, [_ | _] = CurrentLineAcc) ->
%% If there was a non-empty line when we reached the buffer, add it to the accumulator %% If there was a non-empty line when we reached the buffer, add it to the accumulator
lists:reverse([lists:reverse(CurrentLineAcc) | Acc]); lists:reverse([lists:reverse(CurrentLineAcc) | Acc]);
wrap_text_line(_HelpLength, [], Acc, _Count, _CurrentLineAcc) -> wrap_text_line(_Length, [], Acc, _Count, _CurrentLineAcc) ->
lists:reverse(Acc). lists:reverse(Acc).