Skip to content

Commit

Permalink
Automatically display default values of options in the help page (#1032)
Browse files Browse the repository at this point in the history
Fixes #973
  • Loading branch information
0xced authored Dec 28, 2022
1 parent 4a8a4ab commit 3e6e099
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 62 deletions.
17 changes: 17 additions & 0 deletions src/Spectre.Console.Cli/ConfiguratorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ public static IConfigurator SetApplicationVersion(this IConfigurator configurato
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>
Expand Down
15 changes: 10 additions & 5 deletions src/Spectre.Console.Cli/ICommandAppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
6 changes: 3 additions & 3 deletions src/Spectre.Console.Cli/Internal/CommandExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task<int> Execute(IConfiguration configuration, IEnumerable<string>
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;
}

Expand All @@ -62,15 +62,15 @@ public async Task<int> Execute(IConfiguration configuration, IEnumerable<string>
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;
}

// Is this the default and is it called without arguments when there are required arguments?
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -22,6 +23,7 @@ public CommandAppSettings(ITypeRegistrar registrar)
{
Registrar = new TypeRegistrar(registrar);
CaseSensitivity = CaseSensitivity.All;
ShowOptionDefaultValues = true;
}

public bool IsTrue(Func<CommandAppSettings, bool> func, string environmentVariableName)
Expand Down
66 changes: 44 additions & 22 deletions src/Spectre.Console.Cli/Internal/HelpWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,42 +34,45 @@ private sealed class HelpOption
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;
Expand All @@ -79,7 +82,7 @@ public static IEnumerable<IRenderable> WriteCommand(CommandModel model, CommandI
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;
Expand Down Expand Up @@ -266,7 +269,7 @@ private static IEnumerable<IRenderable> GetArguments(CommandInfo? command)
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);
Expand All @@ -282,8 +285,16 @@ private static IEnumerable<IRenderable> GetOptions(CommandModel model, CommandIn
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)
Expand Down Expand Up @@ -327,11 +338,22 @@ static string GetOptionParts(HelpOption option)
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);
Expand Down Expand Up @@ -373,19 +395,19 @@ private static IEnumerable<IRenderable> GetCommands(
{
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 ?? " ");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
DESCRIPTION:
DESCRIPTION:
The lion command.

USAGE:
Expand All @@ -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
24 changes: 24 additions & 0 deletions test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,30 @@ public Task Should_Output_Command_Correctly()
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()
Expand Down

0 comments on commit 3e6e099

Please sign in to comment.