From e4b5b56d93526fe5e9df1566fae63677ceeda46a Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Sun, 25 May 2025 00:48:09 +0200 Subject: [PATCH] Update help output for required options --- src/Spectre.Console.Cli/Help/HelpProvider.cs | 87 +++++++++++++------ .../Help/HelpProviderStyles.cs | 11 ++- .../Help/ICommandParameter.cs | 2 +- .../Data/Settings/RequiredOptionsSettings.cs | 7 ++ ...ired_Options.Output.DotNet8_0.verified.txt | 6 ++ ...ired_Options.Output.DotNet9_0.verified.txt | 6 ++ ..._Description.Output.DotNet8_0.verified.txt | 6 ++ ..._Description.Output.DotNet9_0.verified.txt | 6 ++ .../Unit/CommandAppTests.Help.cs | 38 ++++++++ .../Unit/Testing/InteractiveCommandTests.cs | 2 +- 10 files changed, 138 insertions(+), 33 deletions(-) create mode 100644 src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options.Output.DotNet8_0.verified.txt create mode 100644 src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options.Output.DotNet9_0.verified.txt create mode 100644 src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options_No_Description.Output.DotNet8_0.verified.txt create mode 100644 src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options_No_Description.Output.DotNet9_0.verified.txt diff --git a/src/Spectre.Console.Cli/Help/HelpProvider.cs b/src/Spectre.Console.Cli/Help/HelpProvider.cs index f1c7daa..096c02d 100644 --- a/src/Spectre.Console.Cli/Help/HelpProvider.cs +++ b/src/Spectre.Console.Cli/Help/HelpProvider.cs @@ -53,8 +53,8 @@ public class HelpProvider : IHelpProvider { var arguments = new List(); arguments.AddRange(command?.Parameters?.OfType()?.Select( - x => new HelpArgument(x.Value, x.Position, x.Required, x.Description)) - ?? Array.Empty()); + x => new HelpArgument(x.Value, x.Position, x.IsRequired, x.Description)) + ?? Array.Empty()); return arguments; } } @@ -65,15 +65,20 @@ public class HelpProvider : IHelpProvider public string? Long { get; } public string? Value { get; } public bool? ValueIsOptional { get; } + public bool IsRequired { get; } public string? Description { 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; Long = @long; Value = value; ValueIsOptional = valueIsOptional; + IsRequired = isRequired; Description = description; DefaultValue = defaultValue; } @@ -85,7 +90,8 @@ public class HelpProvider : IHelpProvider { var parameters = new List { - 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. @@ -107,17 +113,18 @@ public class HelpProvider : IHelpProvider // Only show the version option if there is an application version set. 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().Where(o => !o.IsHidden).Select(o => - new HelpOption( - o.ShortNames.FirstOrDefault(), o.LongNames.FirstOrDefault(), - o.ValueName, o.ValueIsOptional, o.Description, - o.IsFlag && o.DefaultValue?.Value is false ? null : o.DefaultValue?.Value)) - ?? Array.Empty()); + new HelpOption( + o.ShortNames.FirstOrDefault(), o.LongNames.FirstOrDefault(), + o.ValueName, o.ValueIsOptional, o.IsRequired, o.Description, + o.IsFlag && o.DefaultValue?.Value is false ? null : o.DefaultValue?.Value)) + ?? Array.Empty()); return parameters; } } @@ -215,7 +222,8 @@ public class HelpProvider : IHelpProvider { 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 { @@ -228,38 +236,44 @@ public class HelpProvider : IHelpProvider if (isCurrent) { foreach (var argument in current.Parameters.OfType() - .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().Where(x => !x.Required).ToArray(); + var optionalArguments = current.Parameters.OfType().Where(x => !x.IsRequired) + .ToArray(); if (optionalArguments.Length > 0 || !isCurrent) { 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) { - 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) { // 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) { // We are on a branch with a default 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) { @@ -269,7 +283,8 @@ public class HelpProvider : IHelpProvider { // Commands other than the default are present // 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++) { 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(); } @@ -364,7 +380,8 @@ public class HelpProvider : IHelpProvider var result = new List { - 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(); @@ -407,7 +424,8 @@ public class HelpProvider : IHelpProvider var result = new List { - 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(); @@ -439,7 +457,15 @@ public class HelpProvider : IHelpProvider 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()); } @@ -470,7 +496,8 @@ public class HelpProvider : IHelpProvider var result = new List { - 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(); @@ -546,11 +573,11 @@ public class HelpProvider : IHelpProvider composer.Text(" "); if (option.ValueIsOptional ?? false) { - composer.Style(helpStyles?.Options?.OptionalOption ?? Style.Plain, $"[{option.Value}]"); + composer.Style(helpStyles?.Options?.OptionalOptionValue ?? Style.Plain, $"[{option.Value}]"); } 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(" "), "" => NewComposer().Text(" "), Array { Length: 0 } => NewComposer().Text(" "), - Array array => NewComposer().Join(", ", array.Cast().Select(o => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, o.ToString() ?? string.Empty))), - _ => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, defaultValue?.ToString() ?? string.Empty), + Array array => NewComposer().Join(", ", + array.Cast().Select(o => + NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, + o.ToString() ?? string.Empty))), + _ => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, + defaultValue?.ToString() ?? string.Empty), }; } diff --git a/src/Spectre.Console.Cli/Help/HelpProviderStyles.cs b/src/Spectre.Console.Cli/Help/HelpProviderStyles.cs index a4dee81..474f453 100644 --- a/src/Spectre.Console.Cli/Help/HelpProviderStyles.cs +++ b/src/Spectre.Console.Cli/Help/HelpProviderStyles.cs @@ -76,8 +76,8 @@ public sealed class HelpProviderStyle Header = "yellow", DefaultValueHeader = "lime", DefaultValue = "bold", - RequiredOption = "silver", - OptionalOption = "grey", + RequiredOptionValue = "silver", + OptionalOptionValue = "grey", }, }; } @@ -212,8 +212,13 @@ public sealed class OptionStyle /// public Style? RequiredOption { get; set; } + /// + /// Gets or sets the style for required option values. + /// + public Style? RequiredOptionValue { get; set; } + /// /// Gets or sets the style for optional options. /// - public Style? OptionalOption { get; set; } + public Style? OptionalOptionValue { get; set; } } diff --git a/src/Spectre.Console.Cli/Help/ICommandParameter.cs b/src/Spectre.Console.Cli/Help/ICommandParameter.cs index 161fa73..1177f86 100644 --- a/src/Spectre.Console.Cli/Help/ICommandParameter.cs +++ b/src/Spectre.Console.Cli/Help/ICommandParameter.cs @@ -13,7 +13,7 @@ public interface ICommandParameter /// /// Gets a value indicating whether the parameter is required. /// - bool Required { get; } + bool IsRequired { get; } /// /// Gets the description of the parameter. diff --git a/src/Tests/Spectre.Console.Cli.Tests/Data/Settings/RequiredOptionsSettings.cs b/src/Tests/Spectre.Console.Cli.Tests/Data/Settings/RequiredOptionsSettings.cs index ff14ed9..f05b92c 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Data/Settings/RequiredOptionsSettings.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Data/Settings/RequiredOptionsSettings.cs @@ -1,6 +1,13 @@ namespace Spectre.Console.Tests.Data; public class RequiredOptionsSettings : CommandSettings +{ + [CommandOption("--foo ", true)] + [Description("Foos the bars")] + public string Foo { get; set; } +} + +public class RequiredOptionsWithoutDescriptionSettings : CommandSettings { [CommandOption("--foo ", true)] public string Foo { get; set; } diff --git a/src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options.Output.DotNet8_0.verified.txt b/src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options.Output.DotNet8_0.verified.txt new file mode 100644 index 0000000..0ac93ae --- /dev/null +++ b/src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options.Output.DotNet8_0.verified.txt @@ -0,0 +1,6 @@ +USAGE: + myapp [OPTIONS] + +OPTIONS: + -h, --help Prints help information + --foo Foos the bars. Required \ No newline at end of file diff --git a/src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options.Output.DotNet9_0.verified.txt b/src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options.Output.DotNet9_0.verified.txt new file mode 100644 index 0000000..0ac93ae --- /dev/null +++ b/src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options.Output.DotNet9_0.verified.txt @@ -0,0 +1,6 @@ +USAGE: + myapp [OPTIONS] + +OPTIONS: + -h, --help Prints help information + --foo Foos the bars. Required \ No newline at end of file diff --git a/src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options_No_Description.Output.DotNet8_0.verified.txt b/src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options_No_Description.Output.DotNet8_0.verified.txt new file mode 100644 index 0000000..4f7fe31 --- /dev/null +++ b/src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options_No_Description.Output.DotNet8_0.verified.txt @@ -0,0 +1,6 @@ +USAGE: + myapp [OPTIONS] + +OPTIONS: + -h, --help Prints help information + --foo Required \ No newline at end of file diff --git a/src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options_No_Description.Output.DotNet9_0.verified.txt b/src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options_No_Description.Output.DotNet9_0.verified.txt new file mode 100644 index 0000000..4f7fe31 --- /dev/null +++ b/src/Tests/Spectre.Console.Cli.Tests/Expectations/Help/Required_Options_No_Description.Output.DotNet9_0.verified.txt @@ -0,0 +1,6 @@ +USAGE: + myapp [OPTIONS] + +OPTIONS: + -h, --help Prints help information + --foo Required \ No newline at end of file diff --git a/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs b/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs index 94fa8ab..81b434b 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs @@ -1061,5 +1061,43 @@ public sealed partial class CommandAppTests // Then return Verifier.Verify(result.Output); } + + [Fact] + [Expectation("Required_Options")] + public Task Should_Show_Required_Options() + { + // Given + var fixture = new CommandAppTester(); + fixture.SetDefaultCommand>(); + 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>(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + }); + + // When + var result = fixture.Run("--help"); + + // Then + return Verifier.Verify(result.Output); + } } } \ No newline at end of file diff --git a/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/InteractiveCommandTests.cs b/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/InteractiveCommandTests.cs index 0feb7e2..ec33c38 100644 --- a/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/InteractiveCommandTests.cs +++ b/src/Tests/Spectre.Console.Cli.Tests/Unit/Testing/InteractiveCommandTests.cs @@ -1,4 +1,4 @@ -namespace Spectre.Console.Cli.Tests.Unit.Testing; +namespace Spectre.Console.Tests.Unit.Cli; public sealed class InteractiveCommandTests {