Automatically display default values of options in the help page (#1032)

Fixes #973
This commit is contained in:
Cédric Luthi 2022-12-28 21:28:41 +01:00 committed by GitHub
parent 4a8a4ab048
commit 3e6e0990c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 154 additions and 62 deletions

View File

@ -40,6 +40,23 @@ public static class ConfiguratorExtensions
return configurator;
}
/// <summary>
/// Hides the <c>DEFAULT</c> column that lists default values coming from the
/// <see cref="DefaultValueAttribute"/> in the options help text.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator HideOptionDefaultValues(this IConfigurator configurator)
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
configurator.Settings.ShowOptionDefaultValues = false;
return configurator;
}
/// <summary>
/// Configures the console.
/// </summary>

View File

@ -15,6 +15,11 @@ public interface ICommandAppSettings
/// </summary>
string? ApplicationVersion { get; set; }
/// <summary>
/// Gets or sets a value indicating whether any default values for command options are shown in the help text.
/// </summary>
bool ShowOptionDefaultValues { get; set; }
/// <summary>
/// Gets or sets the <see cref="IAnsiConsole"/>.
/// </summary>
@ -34,11 +39,11 @@ public interface ICommandAppSettings
/// <summary>
/// Gets or sets case sensitivity.
/// </summary>
CaseSensitivity CaseSensitivity { get; set; }
/// <summary>
/// Gets or sets a value indicating whether trailing period of a description is trimmed.
/// </summary>
CaseSensitivity CaseSensitivity { get; set; }
/// <summary>
/// Gets or sets a value indicating whether trailing period of a description is trimmed.
/// </summary>
bool TrimTrailingPeriod { get; set; }
/// <summary>

View File

@ -53,7 +53,7 @@ internal sealed class CommandExecutor
if (parsedResult.Tree == null)
{
// Display help.
configuration.Settings.Console.SafeRender(HelpWriter.Write(model));
configuration.Settings.Console.SafeRender(HelpWriter.Write(model, configuration.Settings.ShowOptionDefaultValues));
return 0;
}
@ -62,7 +62,7 @@ internal sealed class CommandExecutor
if (leaf.Command.IsBranch || leaf.ShowHelp)
{
// Branches can't be executed. Show help.
configuration.Settings.Console.SafeRender(HelpWriter.WriteCommand(model, leaf.Command));
configuration.Settings.Console.SafeRender(HelpWriter.WriteCommand(model, leaf.Command, configuration.Settings.ShowOptionDefaultValues));
return leaf.ShowHelp ? 0 : 1;
}
@ -70,7 +70,7 @@ internal sealed class CommandExecutor
if (leaf.Command.IsDefaultCommand && args.Count() == 0 && leaf.Command.Parameters.Any(p => p.Required))
{
// Display help for default command.
configuration.Settings.Console.SafeRender(HelpWriter.WriteCommand(model, leaf.Command));
configuration.Settings.Console.SafeRender(HelpWriter.WriteCommand(model, leaf.Command, configuration.Settings.ShowOptionDefaultValues));
return 1;
}

View File

@ -4,6 +4,7 @@ internal sealed class CommandAppSettings : ICommandAppSettings
{
public string? ApplicationName { get; set; }
public string? ApplicationVersion { get; set; }
public bool ShowOptionDefaultValues { get; set; }
public IAnsiConsole? Console { get; set; }
public ICommandInterceptor? Interceptor { get; set; }
public ITypeRegistrarFrontend Registrar { get; set; }
@ -22,6 +23,7 @@ internal sealed class CommandAppSettings : ICommandAppSettings
{
Registrar = new TypeRegistrar(registrar);
CaseSensitivity = CaseSensitivity.All;
ShowOptionDefaultValues = true;
}
public bool IsTrue(Func<CommandAppSettings, bool> func, string environmentVariableName)

View File

@ -34,42 +34,45 @@ internal static class HelpWriter
public string? Value { get; }
public bool? ValueIsOptional { get; }
public string? Description { get; }
public object? DefaultValue { get; }
public HelpOption(string? @short, string? @long, string? @value, bool? valueIsOptional, string? description)
public HelpOption(string? @short, string? @long, string? @value, bool? valueIsOptional, string? description, object? defaultValue)
{
Short = @short;
Long = @long;
Value = value;
ValueIsOptional = valueIsOptional;
Description = description;
DefaultValue = defaultValue;
}
public static IReadOnlyList<HelpOption> Get(CommandModel model, CommandInfo? command)
{
var parameters = new List<HelpOption>();
parameters.Add(new HelpOption("h", "help", null, null, "Prints help information"));
parameters.Add(new HelpOption("h", "help", null, null, "Prints help information", null));
// At the root and no default command?
if (command == null && model?.DefaultCommand == null)
{
parameters.Add(new HelpOption("v", "version", null, null, "Prints version information"));
parameters.Add(new HelpOption("v", "version", null, null, "Prints version information", null));
}
parameters.AddRange(command?.Parameters.OfType<CommandOption>().Where(o => !o.IsHidden).Select(o =>
new HelpOption(
o.ShortNames.FirstOrDefault(), o.LongNames.FirstOrDefault(),
o.ValueName, o.ValueIsOptional, o.Description))
o.ValueName, o.ValueIsOptional, o.Description,
o.ParameterKind == ParameterKind.Flag && o.DefaultValue?.Value is false ? null : o.DefaultValue?.Value))
?? Array.Empty<HelpOption>());
return parameters;
}
}
public static IEnumerable<IRenderable> Write(CommandModel model)
public static IEnumerable<IRenderable> Write(CommandModel model, bool writeOptionsDefaultValues)
{
return WriteCommand(model, null);
return WriteCommand(model, null, writeOptionsDefaultValues);
}
public static IEnumerable<IRenderable> WriteCommand(CommandModel model, CommandInfo? command)
public static IEnumerable<IRenderable> WriteCommand(CommandModel model, CommandInfo? command, bool writeOptionsDefaultValues)
{
var container = command as ICommandContainer ?? model;
var isDefaultCommand = command?.IsDefaultCommand ?? false;
@ -79,7 +82,7 @@ internal static class HelpWriter
result.AddRange(GetUsage(model, command));
result.AddRange(GetExamples(model, command));
result.AddRange(GetArguments(command));
result.AddRange(GetOptions(model, command));
result.AddRange(GetOptions(model, command, writeOptionsDefaultValues));
result.AddRange(GetCommands(model, container, isDefaultCommand));
return result;
@ -266,7 +269,7 @@ internal static class HelpWriter
return result;
}
private static IEnumerable<IRenderable> GetOptions(CommandModel model, CommandInfo? command)
private static IEnumerable<IRenderable> GetOptions(CommandModel model, CommandInfo? command, bool writeDefaultValues)
{
// Collect all options into a single structure.
var parameters = HelpOption.Get(model, command);
@ -282,8 +285,16 @@ internal static class HelpWriter
new Markup(Environment.NewLine),
};
var helpOptions = parameters.ToArray();
var defaultValueColumn = writeDefaultValues && helpOptions.Any(e => e.DefaultValue != null);
var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true });
if (defaultValueColumn)
{
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0, 4, 0) });
}
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) });
static string GetOptionParts(HelpOption option)
@ -327,11 +338,22 @@ internal static class HelpWriter
return builder.ToString();
}
foreach (var option in parameters.ToArray())
if (defaultValueColumn)
{
grid.AddRow(
GetOptionParts(option),
option.Description?.TrimEnd('.') ?? " ");
grid.AddRow(" ", "[lime]DEFAULT[/]", " ");
}
foreach (var option in helpOptions)
{
var columns = new List<string> { GetOptionParts(option) };
if (defaultValueColumn)
{
columns.Add(option.DefaultValue == null ? " " : $"[bold]{option.DefaultValue.ToString().EscapeMarkup()}[/]");
}
columns.Add(option.Description?.TrimEnd('.') ?? " ");
grid.AddRow(columns.ToArray());
}
result.Add(grid);
@ -373,19 +395,19 @@ internal static class HelpWriter
{
arguments.Style("silver", $"<{argument.Name.EscapeMarkup()}>");
arguments.Space();
}
}
if (model.TrimTrailingPeriod)
{
if (model.TrimTrailingPeriod)
{
grid.AddRow(
arguments.ToString().TrimEnd(),
child.Description?.TrimEnd('.') ?? " ");
}
else
{
grid.AddRow(
arguments.ToString().TrimEnd(),
child.Description ?? " ");
}
else
{
grid.AddRow(
arguments.ToString().TrimEnd(),
child.Description ?? " ");
}
}

View File

@ -8,10 +8,11 @@ ARGUMENTS:
[LEGS] The number of legs
OPTIONS:
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
DEFAULT
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> The agility between 0 and 100
--agility <VALUE> 10 The agility between 0 and 100
COMMANDS:
lion <TEETH> The lion command

View File

@ -0,0 +1,17 @@
DESCRIPTION:
Contains settings for a cat.
USAGE:
myapp cat [LEGS] [OPTIONS] <COMMAND>
ARGUMENTS:
[LEGS] The number of legs
OPTIONS:
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> The agility between 0 and 100
COMMANDS:
lion <TEETH> The lion command

View File

@ -9,8 +9,9 @@ ARGUMENTS:
[LEGS] The number of legs
OPTIONS:
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
DEFAULT
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has

View File

@ -12,8 +12,9 @@ ARGUMENTS:
[LEGS] The number of legs
OPTIONS:
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
DEFAULT
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has

View File

@ -1,16 +1,17 @@
DESCRIPTION:
The lion command.
USAGE:
myapp <TEETH> [LEGS] [OPTIONS]
ARGUMENTS:
<TEETH> The number of teeth the lion has
[LEGS] The number of legs
OPTIONS:
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
DESCRIPTION:
The lion command.
USAGE:
myapp <TEETH> [LEGS] [OPTIONS]
ARGUMENTS:
<TEETH> The number of teeth the lion has
[LEGS] The number of legs
OPTIONS:
DEFAULT
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has

View File

@ -1,4 +1,4 @@
DESCRIPTION:
DESCRIPTION:
The lion command.
USAGE:
@ -9,11 +9,12 @@ ARGUMENTS:
[LEGS] The number of legs
OPTIONS:
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
DEFAULT
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
COMMANDS:
giraffe <LENGTH> The giraffe command

View File

@ -99,6 +99,30 @@ public sealed partial class CommandAppTests
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Command_Hide_Default")]
public Task Should_Not_Print_Default_Column()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
configurator.AddBranch<CatSettings>("cat", animal =>
{
animal.SetDescription("Contains settings for a cat.");
animal.AddCommand<LionCommand>("lion");
});
configurator.HideOptionDefaultValues();
});
// When
var result = fixture.Run("cat", "--help");
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Leaf")]
public Task Should_Output_Leaf_Correctly()