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; 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> /// <summary>
/// Configures the console. /// Configures the console.
/// </summary> /// </summary>

View File

@ -15,6 +15,11 @@ public interface ICommandAppSettings
/// </summary> /// </summary>
string? ApplicationVersion { get; set; } 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> /// <summary>
/// Gets or sets the <see cref="IAnsiConsole"/>. /// Gets or sets the <see cref="IAnsiConsole"/>.
/// </summary> /// </summary>

View File

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

View File

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

View File

@ -34,42 +34,45 @@ internal static class HelpWriter
public string? Value { get; } public string? Value { get; }
public bool? ValueIsOptional { get; } public bool? ValueIsOptional { get; }
public string? Description { 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; Short = @short;
Long = @long; Long = @long;
Value = value; Value = value;
ValueIsOptional = valueIsOptional; ValueIsOptional = valueIsOptional;
Description = description; Description = description;
DefaultValue = defaultValue;
} }
public static IReadOnlyList<HelpOption> Get(CommandModel model, CommandInfo? command) public static IReadOnlyList<HelpOption> Get(CommandModel model, CommandInfo? command)
{ {
var parameters = new List<HelpOption>(); 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? // At the root and no default command?
if (command == null && model?.DefaultCommand == null) 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 => parameters.AddRange(command?.Parameters.OfType<CommandOption>().Where(o => !o.IsHidden).Select(o =>
new HelpOption( new HelpOption(
o.ShortNames.FirstOrDefault(), o.LongNames.FirstOrDefault(), 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>()); ?? Array.Empty<HelpOption>());
return parameters; 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 container = command as ICommandContainer ?? model;
var isDefaultCommand = command?.IsDefaultCommand ?? false; var isDefaultCommand = command?.IsDefaultCommand ?? false;
@ -79,7 +82,7 @@ internal static class HelpWriter
result.AddRange(GetUsage(model, command)); result.AddRange(GetUsage(model, command));
result.AddRange(GetExamples(model, command)); result.AddRange(GetExamples(model, command));
result.AddRange(GetArguments(command)); result.AddRange(GetArguments(command));
result.AddRange(GetOptions(model, command)); result.AddRange(GetOptions(model, command, writeOptionsDefaultValues));
result.AddRange(GetCommands(model, container, isDefaultCommand)); result.AddRange(GetCommands(model, container, isDefaultCommand));
return result; return result;
@ -266,7 +269,7 @@ internal static class HelpWriter
return result; 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. // Collect all options into a single structure.
var parameters = HelpOption.Get(model, command); var parameters = HelpOption.Get(model, command);
@ -282,8 +285,16 @@ internal static class HelpWriter
new Markup(Environment.NewLine), new Markup(Environment.NewLine),
}; };
var helpOptions = parameters.ToArray();
var defaultValueColumn = writeDefaultValues && helpOptions.Any(e => e.DefaultValue != null);
var grid = new Grid(); var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true }); 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) }); grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) });
static string GetOptionParts(HelpOption option) static string GetOptionParts(HelpOption option)
@ -327,11 +338,22 @@ internal static class HelpWriter
return builder.ToString(); return builder.ToString();
} }
foreach (var option in parameters.ToArray()) if (defaultValueColumn)
{ {
grid.AddRow( grid.AddRow(" ", "[lime]DEFAULT[/]", " ");
GetOptionParts(option), }
option.Description?.TrimEnd('.') ?? " ");
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); result.Add(grid);

View File

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

View File

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

View File

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

View File

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

View File

@ -99,6 +99,30 @@ public sealed partial class CommandAppTests
return Verifier.Verify(result.Output); 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] [Fact]
[Expectation("Leaf")] [Expectation("Leaf")]
public Task Should_Output_Leaf_Correctly() public Task Should_Output_Leaf_Correctly()