Update help output for required options

This commit is contained in:
Patrik Svensson
2025-05-25 00:48:09 +02:00
committed by Patrik Svensson
parent 67c3909bbb
commit e4b5b56d93
10 changed files with 138 additions and 33 deletions

View File

@ -53,8 +53,8 @@ public class HelpProvider : IHelpProvider
{ {
var arguments = new List<HelpArgument>(); var arguments = new List<HelpArgument>();
arguments.AddRange(command?.Parameters?.OfType<ICommandArgument>()?.Select( arguments.AddRange(command?.Parameters?.OfType<ICommandArgument>()?.Select(
x => new HelpArgument(x.Value, x.Position, x.Required, x.Description)) x => new HelpArgument(x.Value, x.Position, x.IsRequired, x.Description))
?? Array.Empty<HelpArgument>()); ?? Array.Empty<HelpArgument>());
return arguments; return arguments;
} }
} }
@ -65,15 +65,20 @@ public class HelpProvider : IHelpProvider
public string? Long { get; } public string? Long { get; }
public string? Value { get; } public string? Value { get; }
public bool? ValueIsOptional { get; } public bool? ValueIsOptional { get; }
public bool IsRequired { get; }
public string? Description { get; } public string? Description { get; }
public object? DefaultValue { get; } public object? DefaultValue { get; }
private HelpOption(string? @short, string? @long, string? @value, bool? valueIsOptional, string? description, object? defaultValue) private HelpOption(
string? @short, string? @long, string? @value,
bool? valueIsOptional, bool isRequired,
string? description, object? defaultValue)
{ {
Short = @short; Short = @short;
Long = @long; Long = @long;
Value = value; Value = value;
ValueIsOptional = valueIsOptional; ValueIsOptional = valueIsOptional;
IsRequired = isRequired;
Description = description; Description = description;
DefaultValue = defaultValue; DefaultValue = defaultValue;
} }
@ -85,7 +90,8 @@ public class HelpProvider : IHelpProvider
{ {
var parameters = new List<HelpOption> var parameters = new List<HelpOption>
{ {
new HelpOption("h", "help", null, null, resources.PrintHelpDescription, null), new HelpOption("h", "help", null, null, false,
resources.PrintHelpDescription, null),
}; };
// Version information applies to the entire CLI application. // Version information applies to the entire CLI application.
@ -107,17 +113,18 @@ public class HelpProvider : IHelpProvider
// Only show the version option if there is an application version set. // Only show the version option if there is an application version set.
if (model.ApplicationVersion != null) if (model.ApplicationVersion != null)
{ {
parameters.Add(new HelpOption("v", "version", null, null, resources.PrintVersionDescription, null)); parameters.Add(new HelpOption("v", "version", null, null, false,
resources.PrintVersionDescription, null));
} }
} }
} }
parameters.AddRange(command?.Parameters.OfType<ICommandOption>().Where(o => !o.IsHidden).Select(o => parameters.AddRange(command?.Parameters.OfType<ICommandOption>().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.IsRequired, o.Description,
o.IsFlag && o.DefaultValue?.Value is false ? null : o.DefaultValue?.Value)) o.IsFlag && o.DefaultValue?.Value is false ? null : o.DefaultValue?.Value))
?? Array.Empty<HelpOption>()); ?? Array.Empty<HelpOption>());
return parameters; return parameters;
} }
} }
@ -215,7 +222,8 @@ public class HelpProvider : IHelpProvider
{ {
if (isCurrent) if (isCurrent)
{ {
parameters.Add(NewComposer().Style(helpStyles?.Usage?.CurrentCommand ?? Style.Plain, $"{current.Name}")); parameters.Add(NewComposer().Style(helpStyles?.Usage?.CurrentCommand ?? Style.Plain,
$"{current.Name}"));
} }
else else
{ {
@ -228,38 +236,44 @@ public class HelpProvider : IHelpProvider
if (isCurrent) if (isCurrent)
{ {
foreach (var argument in current.Parameters.OfType<ICommandArgument>() foreach (var argument in current.Parameters.OfType<ICommandArgument>()
.Where(a => a.Required).OrderBy(a => a.Position).ToArray()) .Where(a => a.IsRequired).OrderBy(a => a.Position).ToArray())
{ {
parameters.Add(NewComposer().Style(helpStyles?.Usage?.RequiredArgument ?? Style.Plain, $"<{argument.Value}>")); parameters.Add(NewComposer().Style(helpStyles?.Usage?.RequiredArgument ?? Style.Plain,
$"<{argument.Value}>"));
} }
} }
var optionalArguments = current.Parameters.OfType<ICommandArgument>().Where(x => !x.Required).ToArray(); var optionalArguments = current.Parameters.OfType<ICommandArgument>().Where(x => !x.IsRequired)
.ToArray();
if (optionalArguments.Length > 0 || !isCurrent) if (optionalArguments.Length > 0 || !isCurrent)
{ {
foreach (var optionalArgument in optionalArguments) foreach (var optionalArgument in optionalArguments)
{ {
parameters.Add(NewComposer().Style(helpStyles?.Usage?.OptionalArgument ?? Style.Plain, $"[{optionalArgument.Value}]")); parameters.Add(NewComposer().Style(helpStyles?.Usage?.OptionalArgument ?? Style.Plain,
$"[{optionalArgument.Value}]"));
} }
} }
} }
if (isCurrent) if (isCurrent)
{ {
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]")); parameters.Add(NewComposer()
.Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]"));
} }
} }
if (command.IsBranch && command.DefaultCommand == null) if (command.IsBranch && command.DefaultCommand == null)
{ {
// The user must specify the command // The user must specify the command
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"<{resources.Command}>")); parameters.Add(NewComposer()
.Style(helpStyles?.Usage?.Command ?? Style.Plain, $"<{resources.Command}>"));
} }
else if (command.IsBranch && command.DefaultCommand != null && command.Commands.Count > 0) else if (command.IsBranch && command.DefaultCommand != null && command.Commands.Count > 0)
{ {
// We are on a branch with a default command // We are on a branch with a default command
// The user can optionally specify the command // The user can optionally specify the command
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]")); parameters.Add(NewComposer()
.Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
} }
else if (command.IsDefaultCommand) else if (command.IsDefaultCommand)
{ {
@ -269,7 +283,8 @@ public class HelpProvider : IHelpProvider
{ {
// Commands other than the default are present // Commands other than the default are present
// So make these optional in the usage statement // So make these optional in the usage statement
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]")); parameters.Add(NewComposer()
.Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
} }
} }
} }
@ -338,7 +353,8 @@ public class HelpProvider : IHelpProvider
for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++) for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++)
{ {
var args = string.Join(" ", examples[index]); var args = string.Join(" ", examples[index]);
composer.Tab().Text(model.ApplicationName).Space().Style(helpStyles?.Examples?.Arguments ?? Style.Plain, args); composer.Tab().Text(model.ApplicationName).Space()
.Style(helpStyles?.Examples?.Arguments ?? Style.Plain, args);
composer.LineBreak(); composer.LineBreak();
} }
@ -364,7 +380,8 @@ public class HelpProvider : IHelpProvider
var result = new List<IRenderable> var result = new List<IRenderable>
{ {
NewComposer().LineBreak().Style(helpStyles?.Arguments?.Header ?? Style.Plain, $"{resources.Arguments}:").LineBreak(), NewComposer().LineBreak().Style(helpStyles?.Arguments?.Header ?? Style.Plain, $"{resources.Arguments}:")
.LineBreak(),
}; };
var grid = new Grid(); var grid = new Grid();
@ -407,7 +424,8 @@ public class HelpProvider : IHelpProvider
var result = new List<IRenderable> var result = new List<IRenderable>
{ {
NewComposer().LineBreak().Style(helpStyles?.Options?.Header ?? Style.Plain, $"{resources.Options}:").LineBreak(), NewComposer().LineBreak().Style(helpStyles?.Options?.Header ?? Style.Plain, $"{resources.Options}:")
.LineBreak(),
}; };
var helpOptions = parameters.ToArray(); var helpOptions = parameters.ToArray();
@ -439,7 +457,15 @@ public class HelpProvider : IHelpProvider
columns.Add(GetDefaultValueForOption(option.DefaultValue)); columns.Add(GetDefaultValueForOption(option.DefaultValue));
} }
columns.Add(NewComposer().Text(NormalizeDescription(option.Description))); var description = option.Description;
if (option.IsRequired)
{
description = string.IsNullOrWhiteSpace(description)
? "[i]Required[/]"
: description.TrimEnd('.') + ". [i]Required[/]";
}
columns.Add(NewComposer().Text(NormalizeDescription(description)));
grid.AddRow(columns.ToArray()); grid.AddRow(columns.ToArray());
} }
@ -470,7 +496,8 @@ public class HelpProvider : IHelpProvider
var result = new List<IRenderable> var result = new List<IRenderable>
{ {
NewComposer().LineBreak().Style(helpStyles?.Commands?.Header ?? Style.Plain, $"{resources.Commands}:").LineBreak(), NewComposer().LineBreak().Style(helpStyles?.Commands?.Header ?? Style.Plain, $"{resources.Commands}:")
.LineBreak(),
}; };
var grid = new Grid(); var grid = new Grid();
@ -546,11 +573,11 @@ public class HelpProvider : IHelpProvider
composer.Text(" "); composer.Text(" ");
if (option.ValueIsOptional ?? false) if (option.ValueIsOptional ?? false)
{ {
composer.Style(helpStyles?.Options?.OptionalOption ?? Style.Plain, $"[{option.Value}]"); composer.Style(helpStyles?.Options?.OptionalOptionValue ?? Style.Plain, $"[{option.Value}]");
} }
else else
{ {
composer.Style(helpStyles?.Options?.RequiredOption ?? Style.Plain, $"<{option.Value}>"); composer.Style(helpStyles?.Options?.RequiredOptionValue ?? Style.Plain, $"<{option.Value}>");
} }
} }
@ -564,8 +591,12 @@ public class HelpProvider : IHelpProvider
null => NewComposer().Text(" "), null => NewComposer().Text(" "),
"" => NewComposer().Text(" "), "" => NewComposer().Text(" "),
Array { Length: 0 } => NewComposer().Text(" "), Array { Length: 0 } => NewComposer().Text(" "),
Array array => NewComposer().Join(", ", array.Cast<object>().Select(o => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, o.ToString() ?? string.Empty))), Array array => NewComposer().Join(", ",
_ => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, defaultValue?.ToString() ?? string.Empty), array.Cast<object>().Select(o =>
NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain,
o.ToString() ?? string.Empty))),
_ => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain,
defaultValue?.ToString() ?? string.Empty),
}; };
} }

View File

@ -76,8 +76,8 @@ public sealed class HelpProviderStyle
Header = "yellow", Header = "yellow",
DefaultValueHeader = "lime", DefaultValueHeader = "lime",
DefaultValue = "bold", DefaultValue = "bold",
RequiredOption = "silver", RequiredOptionValue = "silver",
OptionalOption = "grey", OptionalOptionValue = "grey",
}, },
}; };
} }
@ -212,8 +212,13 @@ public sealed class OptionStyle
/// </summary> /// </summary>
public Style? RequiredOption { get; set; } public Style? RequiredOption { get; set; }
/// <summary>
/// Gets or sets the style for required option values.
/// </summary>
public Style? RequiredOptionValue { get; set; }
/// <summary> /// <summary>
/// Gets or sets the style for optional options. /// Gets or sets the style for optional options.
/// </summary> /// </summary>
public Style? OptionalOption { get; set; } public Style? OptionalOptionValue { get; set; }
} }

View File

@ -13,7 +13,7 @@ public interface ICommandParameter
/// <summary> /// <summary>
/// Gets a value indicating whether the parameter is required. /// Gets a value indicating whether the parameter is required.
/// </summary> /// </summary>
bool Required { get; } bool IsRequired { get; }
/// <summary> /// <summary>
/// Gets the description of the parameter. /// Gets the description of the parameter.

View File

@ -1,6 +1,13 @@
namespace Spectre.Console.Tests.Data; namespace Spectre.Console.Tests.Data;
public class RequiredOptionsSettings : CommandSettings public class RequiredOptionsSettings : CommandSettings
{
[CommandOption("--foo <VALUE>", true)]
[Description("Foos the bars")]
public string Foo { get; set; }
}
public class RequiredOptionsWithoutDescriptionSettings : CommandSettings
{ {
[CommandOption("--foo <VALUE>", true)] [CommandOption("--foo <VALUE>", true)]
public string Foo { get; set; } public string Foo { get; set; }

View File

@ -0,0 +1,6 @@
USAGE:
myapp [OPTIONS]
OPTIONS:
-h, --help Prints help information
--foo <VALUE> Foos the bars. Required

View File

@ -0,0 +1,6 @@
USAGE:
myapp [OPTIONS]
OPTIONS:
-h, --help Prints help information
--foo <VALUE> Foos the bars. Required

View File

@ -0,0 +1,6 @@
USAGE:
myapp [OPTIONS]
OPTIONS:
-h, --help Prints help information
--foo <VALUE> Required

View File

@ -0,0 +1,6 @@
USAGE:
myapp [OPTIONS]
OPTIONS:
-h, --help Prints help information
--foo <VALUE> Required

View File

@ -1061,5 +1061,43 @@ public sealed partial class CommandAppTests
// Then // Then
return Verifier.Verify(result.Output); return Verifier.Verify(result.Output);
} }
[Fact]
[Expectation("Required_Options")]
public Task Should_Show_Required_Options()
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<GenericCommand<RequiredOptionsSettings>>();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
});
// When
var result = fixture.Run("--help");
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Required_Options_No_Description")]
public Task Should_Show_Required_Options_Without_Description()
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<GenericCommand<RequiredOptionsWithoutDescriptionSettings>>();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
});
// When
var result = fixture.Run("--help");
// Then
return Verifier.Verify(result.Output);
}
} }
} }

View File

@ -1,4 +1,4 @@
namespace Spectre.Console.Cli.Tests.Unit.Testing; namespace Spectre.Console.Tests.Unit.Cli;
public sealed class InteractiveCommandTests public sealed class InteractiveCommandTests
{ {