diff --git a/docs/input/cli/command-help.md b/docs/input/cli/command-help.md new file mode 100644 index 0000000..adc7a25 --- /dev/null +++ b/docs/input/cli/command-help.md @@ -0,0 +1,47 @@ +Title: Command Help +Order: 13 +Description: "Console applications built with *Spectre.Console.Cli* include automatically generated help command line help." +--- + +Console applications built with `Spectre.Console.Cli` include automatically generated help which is displayed when `-h` or `--help` has been specified on the command line. + +The automatically generated help is derived from the configured commands and their command settings. + +The help is also context aware and tailored depending on what has been specified on the command line before it. For example, + +1. When `-h` or `--help` appears immediately after the application name (eg. `application.exe --help`), then the help displayed is a high-level summary of the application, including any command line examples and a listing of all possible commands the user can execute. + +2. When `-h` or `--help` appears immediately after a command has been specified (eg. `application.exe command --help`), then the help displayed is specific to the command and includes information about command specific switches and any default values. + +`HelpProvider` is the `Spectre.Console` class responsible for determining context and preparing the help text to write to the console. It is an implementation of the public interface `IHelpProvider`. + +## Custom help providers + +Whilst it shouldn't be common place to implement your own help provider, it is however possible. + +You are able to implement your own `IHelpProvider` and configure a `CommandApp` to use that instead of the Spectre.Console help provider. + +```csharp +using Spectre.Console.Cli; + +namespace Help; + +public static class Program +{ + public static int Main(string[] args) + { + var app = new CommandApp(); + + app.Configure(config => + { + // Register the custom help provider + config.SetHelpProvider(new CustomHelpProvider(config.Settings)); + }); + + return app.Run(args); + } +} +``` + +There is a working [example of a custom help provider](https://github.com/spectreconsole/spectre.console/tree/main/examples/Cli/Help) demonstrating this. + diff --git a/docs/input/cli/commandApp.md b/docs/input/cli/commandApp.md index 585634e..bef4be3 100644 --- a/docs/input/cli/commandApp.md +++ b/docs/input/cli/commandApp.md @@ -43,7 +43,7 @@ For more complex command hierarchical configurations, they can also be composed ## Customizing Command Configurations -The `Configure` method is also used to change how help for the commands is generated. This configuration will give our command an additional alias of `file-size` and a description to be used when displaying the help. Additional, an example is specified that will be parsed and displayed for users asking for help. Multiple examples can be provided. Commands can also be marked as hidden. With this option they are still executable, but will not be displayed in help screens. +The `Configure` method is also used to change how help for the commands is generated. This configuration will give our command an additional alias of `file-size` and a description to be used when displaying the help. Additionally, an example is specified that will be parsed and displayed for users asking for help. Multiple examples can be provided. Commands can also be marked as hidden. With this option they are still executable, but will not be displayed in help screens. ``` csharp var app = new CommandApp(); diff --git a/docs/input/cli/settings.md b/docs/input/cli/settings.md index 3c475f7..b4dcd4e 100644 --- a/docs/input/cli/settings.md +++ b/docs/input/cli/settings.md @@ -26,7 +26,7 @@ This setting file tells `Spectre.Console.Cli` that our command has two parameter ## CommandArgument -Arguments have a position and a name. The name is not only used for generating help, but its formatting is used to determine whether or not the argument is optional. The name must either be surrounded by square brackets (e.g. `[name]`) or angle brackets (e.g. ``). Angle brackets denote required whereas square brackets denote optional. If neither are specified an exception will be thrown. +Arguments have a position and a name. The name is not only used for generating help, but its formatting is used to determine whether or not the argument is optional. Angle brackets denote a required argument (e.g. ``) whereas square brackets denote an optional argument (e.g. `[name]`). If neither are specified an exception will be thrown. The position is used for scenarios where there could be more than one argument. diff --git a/examples/Cli/Demo/Program.cs b/examples/Cli/Demo/Program.cs index 400616a..8b87954 100644 --- a/examples/Cli/Demo/Program.cs +++ b/examples/Cli/Demo/Program.cs @@ -15,25 +15,25 @@ public static class Program { config.SetApplicationName("fake-dotnet"); config.ValidateExamples(); - config.AddExample(new[] { "run", "--no-build" }); - - // Run - config.AddCommand("run"); - - // Add - config.AddBranch("add", add => - { - add.SetDescription("Add a package or reference to a .NET project"); - add.AddCommand("package"); - add.AddCommand("reference"); + config.AddExample("run", "--no-build"); + + // Run + config.AddCommand("run"); + + // Add + config.AddBranch("add", add => + { + add.SetDescription("Add a package or reference to a .NET project"); + add.AddCommand("package"); + add.AddCommand("reference"); + }); + + // Serve + config.AddCommand("serve") + .WithExample("serve", "-o", "firefox") + .WithExample("serve", "--port", "80", "-o", "firefox"); }); - // Serve - config.AddCommand("serve") - .WithExample(new[] { "serve", "-o", "firefox" }) - .WithExample(new[] { "serve", "--port", "80", "-o", "firefox" }); - }); - return app.Run(args); } } diff --git a/examples/Cli/Help/CustomHelpProvider.cs b/examples/Cli/Help/CustomHelpProvider.cs new file mode 100644 index 0000000..36b213b --- /dev/null +++ b/examples/Cli/Help/CustomHelpProvider.cs @@ -0,0 +1,30 @@ +using System.Linq; +using Spectre.Console; +using Spectre.Console.Cli; +using Spectre.Console.Cli.Help; +using Spectre.Console.Rendering; + +namespace Help; + +/// +/// Example showing how to extend the built-in Spectre.Console help provider +/// by rendering a custom banner at the top of the help information +/// +internal class CustomHelpProvider : HelpProvider +{ + public CustomHelpProvider(ICommandAppSettings settings) + : base(settings) + { + } + + public override IEnumerable GetHeader(ICommandModel model, ICommandInfo? command) + { + return new[] + { + new Text("--------------------------------------"), Text.NewLine, + new Text("--- CUSTOM HELP PROVIDER ---"), Text.NewLine, + new Text("--------------------------------------"), Text.NewLine, + Text.NewLine, + }; + } +} \ No newline at end of file diff --git a/examples/Cli/Help/DefaultCommand.cs b/examples/Cli/Help/DefaultCommand.cs new file mode 100644 index 0000000..51d1420 --- /dev/null +++ b/examples/Cli/Help/DefaultCommand.cs @@ -0,0 +1,20 @@ +using Spectre.Console; +using Spectre.Console.Cli; + +namespace Help; + +public sealed class DefaultCommand : Command +{ + private IAnsiConsole _console; + + public DefaultCommand(IAnsiConsole console) + { + _console = console; + } + + public override int Execute(CommandContext context) + { + _console.WriteLine("Hello world"); + return 0; + } +} \ No newline at end of file diff --git a/examples/Cli/Help/Help.csproj b/examples/Cli/Help/Help.csproj new file mode 100644 index 0000000..79d00a8 --- /dev/null +++ b/examples/Cli/Help/Help.csproj @@ -0,0 +1,18 @@ + + + + Exe + net7.0 + enable + enable + Help + Demonstrates how to extend the built-in Spectre.Console help provider to render a custom banner at the top of the help information. + Cli + false + + + + + + + diff --git a/examples/Cli/Help/Program.cs b/examples/Cli/Help/Program.cs new file mode 100644 index 0000000..1d4675f --- /dev/null +++ b/examples/Cli/Help/Program.cs @@ -0,0 +1,19 @@ +using Spectre.Console.Cli; + +namespace Help; + +public static class Program +{ + public static int Main(string[] args) + { + var app = new CommandApp(); + + app.Configure(config => + { + // Register the custom help provider + config.SetHelpProvider(new CustomHelpProvider(config.Settings)); + }); + + return app.Run(args); + } +} diff --git a/examples/Examples.sln b/examples/Examples.sln index 9654ee8..adba5e9 100644 --- a/examples/Examples.sln +++ b/examples/Examples.sln @@ -83,6 +83,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Json", "Console\Json\Json.c EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Json", "..\src\Spectre.Console.Json\Spectre.Console.Json.csproj", "{91A5637F-1F89-48B3-A0BA-6CC629807393}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Help", "Cli\Help\Help.csproj", "{BAB490D6-FF8D-462B-B2B0-933384D629DB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -549,6 +551,18 @@ Global {91A5637F-1F89-48B3-A0BA-6CC629807393}.Release|x64.Build.0 = Release|Any CPU {91A5637F-1F89-48B3-A0BA-6CC629807393}.Release|x86.ActiveCfg = Release|Any CPU {91A5637F-1F89-48B3-A0BA-6CC629807393}.Release|x86.Build.0 = Release|Any CPU + {BAB490D6-FF8D-462B-B2B0-933384D629DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAB490D6-FF8D-462B-B2B0-933384D629DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAB490D6-FF8D-462B-B2B0-933384D629DB}.Debug|x64.ActiveCfg = Debug|Any CPU + {BAB490D6-FF8D-462B-B2B0-933384D629DB}.Debug|x64.Build.0 = Debug|Any CPU + {BAB490D6-FF8D-462B-B2B0-933384D629DB}.Debug|x86.ActiveCfg = Debug|Any CPU + {BAB490D6-FF8D-462B-B2B0-933384D629DB}.Debug|x86.Build.0 = Debug|Any CPU + {BAB490D6-FF8D-462B-B2B0-933384D629DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAB490D6-FF8D-462B-B2B0-933384D629DB}.Release|Any CPU.Build.0 = Release|Any CPU + {BAB490D6-FF8D-462B-B2B0-933384D629DB}.Release|x64.ActiveCfg = Release|Any CPU + {BAB490D6-FF8D-462B-B2B0-933384D629DB}.Release|x64.Build.0 = Release|Any CPU + {BAB490D6-FF8D-462B-B2B0-933384D629DB}.Release|x86.ActiveCfg = Release|Any CPU + {BAB490D6-FF8D-462B-B2B0-933384D629DB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -564,6 +578,7 @@ Global {A127CE7D-A5A7-4745-9809-EBD7CB12CEE7} = {2571F1BD-6556-4F96-B27B-B6190E1BF13A} {EFAADF6A-C77D-41EC-83F5-BBB4FFC5A6D7} = {2571F1BD-6556-4F96-B27B-B6190E1BF13A} {91A5637F-1F89-48B3-A0BA-6CC629807393} = {2571F1BD-6556-4F96-B27B-B6190E1BF13A} + {BAB490D6-FF8D-462B-B2B0-933384D629DB} = {4682E9B7-B54C-419D-B92F-470DA4E5674C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3EE724C5-CAB4-410D-AC63-8D4260EF83ED} diff --git a/src/Spectre.Console.Cli/ConfiguratorExtensions.cs b/src/Spectre.Console.Cli/ConfiguratorExtensions.cs index 8445a58..1a27e5a 100644 --- a/src/Spectre.Console.Cli/ConfiguratorExtensions.cs +++ b/src/Spectre.Console.Cli/ConfiguratorExtensions.cs @@ -5,7 +5,42 @@ namespace Spectre.Console.Cli; /// and . /// public static class ConfiguratorExtensions -{ +{ + /// + /// Sets the help provider for the application. + /// + /// The configurator. + /// The help provider to use. + /// A configurator that can be used to configure the application further. + public static IConfigurator SetHelpProvider(this IConfigurator configurator, IHelpProvider helpProvider) + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.SetHelpProvider(helpProvider); + return configurator; + } + + /// + /// Sets the help provider for the application. + /// + /// The configurator. + /// The type of the help provider to instantiate at runtime and use. + /// A configurator that can be used to configure the application further. + public static IConfigurator SetHelpProvider(this IConfigurator configurator) + where T : IHelpProvider + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.SetHelpProvider(); + return configurator; + } + /// /// Sets the name of the application. /// diff --git a/src/Spectre.Console.Cli/Internal/HelpWriter.cs b/src/Spectre.Console.Cli/Help/HelpProvider.cs similarity index 55% rename from src/Spectre.Console.Cli/Internal/HelpWriter.cs rename to src/Spectre.Console.Cli/Help/HelpProvider.cs index 8f8e5d4..f9a28b8 100644 --- a/src/Spectre.Console.Cli/Internal/HelpWriter.cs +++ b/src/Spectre.Console.Cli/Help/HelpProvider.cs @@ -1,7 +1,28 @@ -namespace Spectre.Console.Cli; +namespace Spectre.Console.Cli.Help; -internal static class HelpWriter -{ +/// +/// The help provider for Spectre.Console. +/// +/// +/// Other IHelpProvider implementations can be injected into the CommandApp, if desired. +/// +public class HelpProvider : IHelpProvider +{ + /// + /// Gets a value indicating how many examples from direct children to show in the help text. + /// + protected virtual int MaximumIndirectExamples { get; } + + /// + /// Gets a value indicating whether any default values for command options are shown in the help text. + /// + protected virtual bool ShowOptionDefaultValues { get; } + + /// + /// Gets a value indicating whether a trailing period of a command description is trimmed in the help text. + /// + protected virtual bool TrimTrailingPeriod { get; } + private sealed class HelpArgument { public string Name { get; } @@ -17,10 +38,10 @@ internal static class HelpWriter Description = description; } - public static IReadOnlyList Get(CommandInfo? command) + public static IReadOnlyList Get(ICommandInfo? command) { var arguments = new List(); - arguments.AddRange(command?.Parameters?.OfType()?.Select( + arguments.AddRange(command?.Parameters?.OfType()?.Select( x => new HelpArgument(x.Value, x.Position, x.Required, x.Description)) ?? Array.Empty()); return arguments; @@ -46,49 +67,75 @@ internal static class HelpWriter DefaultValue = defaultValue; } - public static IReadOnlyList Get(CommandModel model, CommandInfo? command) + public static IReadOnlyList Get(ICommandInfo? command) { var parameters = new List(); - 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", null)); + parameters.Add(new HelpOption("h", "help", null, null, "Prints help information", null)); + + // Version information applies to the entire application + // Include the "-v" option in the help when at the root of the command line application + // Don't allow the "-v" option if users have specified one or more sub-commands + if ((command == null || command?.Parent == null) && !(command?.IsBranch ?? false)) + { + parameters.Add(new HelpOption("v", "version", null, null, "Prints version information", null)); } - parameters.AddRange(command?.Parameters.OfType().Where(o => !o.IsHidden).Select(o => + 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.ParameterKind == ParameterKind.Flag && o.DefaultValue?.Value is false ? null : o.DefaultValue?.Value)) + o.IsFlag && o.DefaultValue?.Value is false ? null : o.DefaultValue?.Value)) ?? Array.Empty()); return parameters; } - } - - public static IEnumerable Write(CommandModel model, bool writeOptionsDefaultValues) + } + + /// + /// Initializes a new instance of the class. + /// + /// The command line application settings used for configuration. + public HelpProvider(ICommandAppSettings settings) + { + this.ShowOptionDefaultValues = settings.ShowOptionDefaultValues; + this.MaximumIndirectExamples = settings.MaximumIndirectExamples; + this.TrimTrailingPeriod = settings.TrimTrailingPeriod; + } + + /// + public virtual IEnumerable Write(ICommandModel model, ICommandInfo? command) { - return WriteCommand(model, null, writeOptionsDefaultValues); - } - - public static IEnumerable WriteCommand(CommandModel model, CommandInfo? command, bool writeOptionsDefaultValues) - { - var container = command as ICommandContainer ?? model; - var isDefaultCommand = command?.IsDefaultCommand ?? false; - - var result = new List(); - result.AddRange(GetDescription(command)); + var result = new List(); + + result.AddRange(GetHeader(model, command)); + result.AddRange(GetDescription(model, command)); result.AddRange(GetUsage(model, command)); result.AddRange(GetExamples(model, command)); - result.AddRange(GetArguments(command)); - result.AddRange(GetOptions(model, command, writeOptionsDefaultValues)); - result.AddRange(GetCommands(model, container, isDefaultCommand)); + result.AddRange(GetArguments(model, command)); + result.AddRange(GetOptions(model, command)); + result.AddRange(GetCommands(model, command)); + result.AddRange(GetFooter(model, command)); return result; - } - - private static IEnumerable GetDescription(CommandInfo? command) + } + + /// + /// Gets the header for the help information. + /// + /// The command model to write help for. + /// The command for which to write help information (optional). + /// An enumerable collection of objects. + public virtual IEnumerable GetHeader(ICommandModel model, ICommandInfo? command) + { + yield break; + } + + /// + /// Gets the description section of the help information. + /// + /// The command model to write help for. + /// The command for which to write help information (optional). + /// An enumerable collection of objects. + public virtual IEnumerable GetDescription(ICommandModel model, ICommandInfo? command) { if (command?.Description == null) { @@ -99,13 +146,19 @@ internal static class HelpWriter composer.Style("yellow", "DESCRIPTION:").LineBreak(); composer.Text(command.Description).LineBreak(); yield return composer.LineBreak(); - } - - private static IEnumerable GetUsage(CommandModel model, CommandInfo? command) + } + + /// + /// Gets the usage section of the help information. + /// + /// The command model to write help for. + /// The command for which to write help information (optional). + /// An enumerable collection of objects. + public virtual IEnumerable GetUsage(ICommandModel model, ICommandInfo? command) { var composer = new Composer(); composer.Style("yellow", "USAGE:").LineBreak(); - composer.Tab().Text(model.GetApplicationName()); + composer.Tab().Text(model.ApplicationName); var parameters = new List(); @@ -132,18 +185,18 @@ internal static class HelpWriter } } - if (current.Parameters.OfType().Any()) + if (current.Parameters.OfType().Any()) { if (isCurrent) { - foreach (var argument in current.Parameters.OfType() + foreach (var argument in current.Parameters.OfType() .Where(a => a.Required).OrderBy(a => a.Position).ToArray()) { parameters.Add($"[aqua]<{argument.Value.EscapeMarkup()}>[/]"); } } - var optionalArguments = current.Parameters.OfType().Where(x => !x.Required).ToArray(); + var optionalArguments = current.Parameters.OfType().Where(x => !x.Required).ToArray(); if (optionalArguments.Length > 0 || !isCurrent) { foreach (var optionalArgument in optionalArguments) @@ -159,9 +212,27 @@ internal static class HelpWriter } } - if (command.IsBranch) - { + if (command.IsBranch && command.DefaultCommand == null) + { + // The user must specify the command parameters.Add("[aqua][/]"); + } + 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("[aqua][[COMMAND]][/]"); + } + else if (command.IsDefaultCommand) + { + var commands = model.Commands.Where(x => !x.IsHidden && !x.IsDefaultCommand).ToList(); + + if (commands.Count > 0) + { + // Commands other than the default are present + // So make these optional in the usage statement + parameters.Add("[aqua][[COMMAND]][/]"); + } } } @@ -172,37 +243,48 @@ internal static class HelpWriter { composer, }; - } - - private static IEnumerable GetExamples(CommandModel model, CommandInfo? command) + } + + /// + /// Gets the examples section of the help information. + /// + /// The command model to write help for. + /// The command for which to write help information (optional). + /// An enumerable collection of objects. + /// + /// Examples from the command's direct children are used + /// if no examples have been set on the specified command or model. + /// + public virtual IEnumerable GetExamples(ICommandModel model, ICommandInfo? command) { var maxExamples = int.MaxValue; - var examples = command?.Examples ?? model.Examples ?? new List(); + var examples = command?.Examples?.ToList() ?? model.Examples?.ToList() ?? new List(); if (examples.Count == 0) { // Since we're not checking direct examples, // make sure that we limit the number of examples. - maxExamples = 5; + maxExamples = MaximumIndirectExamples; - // Get the current root command. - var root = command ?? (ICommandContainer)model; - var queue = new Queue(new[] { root }); + // Start at the current command (if exists) + // or alternatively commence at the model. + var commandContainer = command ?? (ICommandContainer)model; + var queue = new Queue(new[] { commandContainer }); - // Traverse the command tree and look for examples. + // Traverse the command tree and look for examples. // As soon as a node contains commands, bail. while (queue.Count > 0) { var current = queue.Dequeue(); - foreach (var cmd in current.Commands.Where(x => !x.IsHidden)) + foreach (var child in current.Commands.Where(x => !x.IsHidden)) { - if (cmd.Examples.Count > 0) + if (child.Examples.Count > 0) { - examples.AddRange(cmd.Examples); + examples.AddRange(child.Examples); } - queue.Enqueue(cmd); + queue.Enqueue(child); } if (examples.Count >= maxExamples) @@ -212,7 +294,7 @@ internal static class HelpWriter } } - if (examples.Count > 0) + if (Math.Min(maxExamples, examples.Count) > 0) { var composer = new Composer(); composer.LineBreak(); @@ -221,7 +303,7 @@ internal static class HelpWriter for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++) { var args = string.Join(" ", examples[index]); - composer.Tab().Text(model.GetApplicationName()).Space().Style("grey", args); + composer.Tab().Text(model.ApplicationName).Space().Style("grey", args); composer.LineBreak(); } @@ -229,9 +311,15 @@ internal static class HelpWriter } return Array.Empty(); - } - - private static IEnumerable GetArguments(CommandInfo? command) + } + + /// + /// Gets the arguments section of the help information. + /// + /// The command model to write help for. + /// The command for which to write help information (optional). + /// An enumerable collection of objects. + public virtual IEnumerable GetArguments(ICommandModel model, ICommandInfo? command) { var arguments = HelpArgument.Get(command); if (arguments.Count == 0) @@ -267,12 +355,18 @@ internal static class HelpWriter result.Add(grid); return result; - } - - private static IEnumerable GetOptions(CommandModel model, CommandInfo? command, bool writeDefaultValues) + } + + /// + /// Gets the options section of the help information. + /// + /// The command model to write help for. + /// The command for which to write help information (optional). + /// An enumerable collection of objects. + public virtual IEnumerable GetOptions(ICommandModel model, ICommandInfo? command) { // Collect all options into a single structure. - var parameters = HelpOption.Get(model, command); + var parameters = HelpOption.Get(command); if (parameters.Count == 0) { return Array.Empty(); @@ -286,7 +380,7 @@ internal static class HelpWriter }; var helpOptions = parameters.ToArray(); - var defaultValueColumn = writeDefaultValues && helpOptions.Any(e => e.DefaultValue != null); + var defaultValueColumn = ShowOptionDefaultValues && helpOptions.Any(e => e.DefaultValue != null); var grid = new Grid(); grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true }); @@ -369,14 +463,20 @@ internal static class HelpWriter result.Add(grid); return result; - } + } + + /// + /// Gets the commands section of the help information. + /// + /// The command model to write help for. + /// The command for which to write help information (optional). + /// An enumerable collection of objects. + public virtual IEnumerable GetCommands(ICommandModel model, ICommandInfo? command) + { + var commandContainer = command ?? (ICommandContainer)model; + bool isDefaultCommand = command?.IsDefaultCommand ?? false; - private static IEnumerable GetCommands( - CommandModel model, - ICommandContainer command, - bool isDefaultCommand) - { - var commands = isDefaultCommand ? model.Commands : command.Commands; + var commands = isDefaultCommand ? model.Commands : commandContainer.Commands; commands = commands.Where(x => !x.IsHidden).ToList(); if (commands.Count == 0) @@ -407,7 +507,7 @@ internal static class HelpWriter arguments.Space(); } - if (model.TrimTrailingPeriod) + if (TrimTrailingPeriod) { grid.AddRow( arguments.ToString().TrimEnd(), @@ -424,5 +524,16 @@ internal static class HelpWriter result.Add(grid); return result; + } + + /// + /// Gets the footer for the help information. + /// + /// The command model to write help for. + /// The command for which to write help information (optional). + /// An enumerable collection of objects. + public virtual IEnumerable GetFooter(ICommandModel model, ICommandInfo? command) + { + yield break; } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Help/ICommandArgument.cs b/src/Spectre.Console.Cli/Help/ICommandArgument.cs new file mode 100644 index 0000000..abf0649 --- /dev/null +++ b/src/Spectre.Console.Cli/Help/ICommandArgument.cs @@ -0,0 +1,17 @@ +namespace Spectre.Console.Cli.Help; + +/// +/// Represents a command argument. +/// +public interface ICommandArgument : ICommandParameter +{ + /// + /// Gets the value of the argument. + /// + string Value { get; } + + /// + /// Gets the position of the argument. + /// + int Position { get; } +} \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Help/ICommandContainer.cs b/src/Spectre.Console.Cli/Help/ICommandContainer.cs new file mode 100644 index 0000000..21eb4b8 --- /dev/null +++ b/src/Spectre.Console.Cli/Help/ICommandContainer.cs @@ -0,0 +1,25 @@ +namespace Spectre.Console.Cli.Help; + +/// +/// Represents a command container. +/// +public interface ICommandContainer +{ + /// + /// Gets all the examples for the container. + /// + IReadOnlyList Examples { get; } + + /// + /// Gets all commands in the container. + /// + IReadOnlyList Commands { get; } + + /// + /// Gets the default command for the container. + /// + /// + /// Returns null if a default command has not been set. + /// + ICommandInfo? DefaultCommand { get; } +} diff --git a/src/Spectre.Console.Cli/Help/ICommandInfo.cs b/src/Spectre.Console.Cli/Help/ICommandInfo.cs new file mode 100644 index 0000000..aaba5ea --- /dev/null +++ b/src/Spectre.Console.Cli/Help/ICommandInfo.cs @@ -0,0 +1,42 @@ +namespace Spectre.Console.Cli.Help; + +/// +/// Represents an executable command. +/// +public interface ICommandInfo : ICommandContainer +{ + /// + /// Gets the name of the command. + /// + string Name { get; } + + /// + /// Gets the description of the command. + /// + string? Description { get; } + + /// + /// Gets a value indicating whether the command is a branch. + /// + bool IsBranch { get; } + + /// + /// Gets a value indicating whether the command is the default command within its container. + /// + bool IsDefaultCommand { get; } + + /// + /// Gets a value indicating whether the command is hidden. + /// + bool IsHidden { get; } + + /// + /// Gets the parameters associated with the command. + /// + IReadOnlyList Parameters { get; } + + /// + /// Gets the parent command, if any. + /// + ICommandInfo? Parent { get; } +} \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Help/ICommandInfoExtensions.cs b/src/Spectre.Console.Cli/Help/ICommandInfoExtensions.cs new file mode 100644 index 0000000..e91c886 --- /dev/null +++ b/src/Spectre.Console.Cli/Help/ICommandInfoExtensions.cs @@ -0,0 +1,23 @@ +namespace Spectre.Console.Cli.Help; + +internal static class ICommandInfoExtensions +{ + /// + /// Walks up the command.Parent tree, adding each command into a list as it goes. + /// + /// The first command added to the list is the current (ie. this one). + /// The list of commands from current to root, as traversed by . + public static List Flatten(this ICommandInfo commandInfo) + { + var result = new Stack(); + + var current = commandInfo; + while (current != null) + { + result.Push(current); + current = current.Parent; + } + + return result.ToList(); + } +} diff --git a/src/Spectre.Console.Cli/Help/ICommandModel.cs b/src/Spectre.Console.Cli/Help/ICommandModel.cs new file mode 100644 index 0000000..e7fe5f7 --- /dev/null +++ b/src/Spectre.Console.Cli/Help/ICommandModel.cs @@ -0,0 +1,12 @@ +namespace Spectre.Console.Cli.Help; + +/// +/// Represents a command model. +/// +public interface ICommandModel : ICommandContainer +{ + /// + /// Gets the name of the application. + /// + string ApplicationName { get; } +} diff --git a/src/Spectre.Console.Cli/Help/ICommandOption.cs b/src/Spectre.Console.Cli/Help/ICommandOption.cs new file mode 100644 index 0000000..1247df8 --- /dev/null +++ b/src/Spectre.Console.Cli/Help/ICommandOption.cs @@ -0,0 +1,27 @@ +namespace Spectre.Console.Cli.Help; + +/// +/// Represents a command option. +/// +public interface ICommandOption : ICommandParameter +{ + /// + /// Gets the long names of the option. + /// + IReadOnlyList LongNames { get; } + + /// + /// Gets the short names of the option. + /// + IReadOnlyList ShortNames { get; } + + /// + /// Gets the value name of the option, if applicable. + /// + string? ValueName { get; } + + /// + /// Gets a value indicating whether the option value is optional. + /// + bool ValueIsOptional { get; } +} \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Help/ICommandParameter.cs b/src/Spectre.Console.Cli/Help/ICommandParameter.cs new file mode 100644 index 0000000..03bd7b6 --- /dev/null +++ b/src/Spectre.Console.Cli/Help/ICommandParameter.cs @@ -0,0 +1,32 @@ +namespace Spectre.Console.Cli.Help; + +/// +/// Represents a command parameter. +/// +public interface ICommandParameter +{ + /// + /// Gets a value indicating whether the parameter is a flag. + /// + bool IsFlag { get; } + + /// + /// Gets a value indicating whether the parameter is required. + /// + bool Required { get; } + + /// + /// Gets the description of the parameter. + /// + string? Description { get; } + + /// + /// Gets the default value of the parameter, if specified. + /// + DefaultValueAttribute? DefaultValue { get; } + + /// + /// Gets a value indicating whether the parameter is hidden. + /// + bool IsHidden { get; } +} \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Help/IHelpProvider.cs b/src/Spectre.Console.Cli/Help/IHelpProvider.cs new file mode 100644 index 0000000..420dd0f --- /dev/null +++ b/src/Spectre.Console.Cli/Help/IHelpProvider.cs @@ -0,0 +1,20 @@ +namespace Spectre.Console.Cli.Help; + +/// +/// The help provider interface for Spectre.Console. +/// +/// +/// Implementations of this interface are responsbile +/// for writing command help to the terminal when the +/// `-h` or `--help` has been specified on the command line. +/// +public interface IHelpProvider +{ + /// + /// Writes help information for the application. + /// + /// The command model to write help for. + /// The command for which to write help information (optional). + /// An enumerable collection of objects representing the help information. + IEnumerable Write(ICommandModel model, ICommandInfo? command); +} diff --git a/src/Spectre.Console.Cli/ICommandAppSettings.cs b/src/Spectre.Console.Cli/ICommandAppSettings.cs index 91af178..12fa7e1 100644 --- a/src/Spectre.Console.Cli/ICommandAppSettings.cs +++ b/src/Spectre.Console.Cli/ICommandAppSettings.cs @@ -13,12 +13,22 @@ public interface ICommandAppSettings /// /// Gets or sets the application version (use it to override auto-detected value). /// - string? ApplicationVersion { get; set; } - - /// - /// Gets or sets a value indicating whether any default values for command options are shown in the help text. - /// - bool ShowOptionDefaultValues { get; set; } + string? ApplicationVersion { get; set; } + + /// + /// Gets or sets a value indicating how many examples from direct children to show in the help text. + /// + int MaximumIndirectExamples { get; set; } + + /// + /// Gets or sets a value indicating whether any default values for command options are shown in the help text. + /// + bool ShowOptionDefaultValues { get; set; } + + /// + /// Gets or sets a value indicating whether a trailing period of a command description is trimmed in the help text. + /// + bool TrimTrailingPeriod { get; set; } /// /// Gets or sets the . @@ -41,11 +51,6 @@ public interface ICommandAppSettings /// CaseSensitivity CaseSensitivity { get; set; } - /// - /// Gets or sets a value indicating whether trailing period of a description is trimmed. - /// - bool TrimTrailingPeriod { get; set; } - /// /// Gets or sets a value indicating whether or not parsing is strict. /// diff --git a/src/Spectre.Console.Cli/IConfigurator.cs b/src/Spectre.Console.Cli/IConfigurator.cs index f6bbcb6..4f00c0b 100644 --- a/src/Spectre.Console.Cli/IConfigurator.cs +++ b/src/Spectre.Console.Cli/IConfigurator.cs @@ -4,7 +4,20 @@ namespace Spectre.Console.Cli; /// Represents a configurator. /// public interface IConfigurator -{ +{ + /// + /// Sets the help provider for the application. + /// + /// The help provider to use. + public void SetHelpProvider(IHelpProvider helpProvider); + + /// + /// Sets the help provider for the application. + /// + /// The type of the help provider to instantiate at runtime and use. + public void SetHelpProvider() + where T : IHelpProvider; + /// /// Gets the command app settings. /// @@ -53,5 +66,5 @@ public interface IConfigurator /// The command branch configurator. /// A branch configurator that can be used to configure the branch further. IBranchConfigurator AddBranch(string name, Action> action) - where TSettings : CommandSettings; + where TSettings : CommandSettings; } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/IConfiguratorOfT.cs b/src/Spectre.Console.Cli/IConfiguratorOfT.cs index 6d463fc..0c71b34 100644 --- a/src/Spectre.Console.Cli/IConfiguratorOfT.cs +++ b/src/Spectre.Console.Cli/IConfiguratorOfT.cs @@ -17,7 +17,7 @@ public interface IConfigurator /// Adds an example of how to use the branch. /// /// The example arguments. - void AddExample(string[] args); + void AddExample(params string[] args); /// /// Adds a default command. diff --git a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs index 3b7b735..0268a3c 100644 --- a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs +++ b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs @@ -8,85 +8,87 @@ internal sealed class CommandExecutor { _registrar = registrar ?? throw new ArgumentNullException(nameof(registrar)); _registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor)); - } - + } + public async Task Execute(IConfiguration configuration, IEnumerable args) { if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); - } - - _registrar.RegisterInstance(typeof(IConfiguration), configuration); - _registrar.RegisterLazy(typeof(IAnsiConsole), () => configuration.Settings.Console.GetConsole()); + } + + args ??= new List(); + + _registrar.RegisterInstance(typeof(IConfiguration), configuration); + _registrar.RegisterLazy(typeof(IAnsiConsole), () => configuration.Settings.Console.GetConsole()); + + // Register the help provider + var defaultHelpProvider = new HelpProvider(configuration.Settings); + _registrar.RegisterInstance(typeof(IHelpProvider), defaultHelpProvider); // Create the command model. var model = CommandModelBuilder.Build(configuration); _registrar.RegisterInstance(typeof(CommandModel), model); - _registrar.RegisterDependencies(model); - - // No default command? - if (model.DefaultCommand == null) - { - // Got at least one argument? - var firstArgument = args.FirstOrDefault(); - if (firstArgument != null) - { - // Asking for version? Kind of a hack, but it's alright. - // We should probably make this a bit better in the future. - if (firstArgument.Equals("--version", StringComparison.OrdinalIgnoreCase) || - firstArgument.Equals("-v", StringComparison.OrdinalIgnoreCase)) - { - var console = configuration.Settings.Console.GetConsole(); - console.WriteLine(ResolveApplicationVersion(configuration)); - return 0; - } - } - } + _registrar.RegisterDependencies(model); + + // Asking for version? Kind of a hack, but it's alright. + // We should probably make this a bit better in the future. + if (args.Contains("-v") || args.Contains("--version")) + { + var console = configuration.Settings.Console.GetConsole(); + console.WriteLine(ResolveApplicationVersion(configuration)); + return 0; + } // Parse and map the model against the arguments. - var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args); - - // Currently the root? - if (parsedResult?.Tree == null) - { - // Display help. - configuration.Settings.Console.SafeRender(HelpWriter.Write(model, configuration.Settings.ShowOptionDefaultValues)); - return 0; - } - - // Get the command to execute. - var leaf = parsedResult.Tree.GetLeafCommand(); - if (leaf.Command.IsBranch || leaf.ShowHelp) - { - // Branches can't be executed. Show help. - 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.ShowOptionDefaultValues)); - return 1; - } - - // Register the arguments with the container. + var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args); + + // Register the arguments with the container. _registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult); _registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining); - // Create the resolver and the context. + // Create the resolver. using (var resolver = new TypeResolverAdapter(_registrar.Build())) - { + { + // Get the registered help provider, falling back to the default provider + // registered above if no custom implementations have been registered. + var helpProvider = resolver.Resolve(typeof(IHelpProvider)) as IHelpProvider ?? defaultHelpProvider; + + // Currently the root? + if (parsedResult?.Tree == null) + { + // Display help. + configuration.Settings.Console.SafeRender(helpProvider.Write(model, null)); + return 0; + } + + // Get the command to execute. + var leaf = parsedResult.Tree.GetLeafCommand(); + if (leaf.Command.IsBranch || leaf.ShowHelp) + { + // Branches can't be executed. Show help. + configuration.Settings.Console.SafeRender(helpProvider.Write(model, leaf.Command)); + 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(helpProvider.Write(model, leaf.Command)); + return 1; + } + + // Create the content. var context = new CommandContext(parsedResult.Remaining, leaf.Command.Name, leaf.Command.Data); // Execute the command tree. return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false); } - } + } - private CommandTreeParserResult? ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable args) +#pragma warning disable CS8603 // Possible null reference return. + private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable args) { var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments); @@ -113,7 +115,8 @@ internal sealed class CommandExecutor return parsedResult; } - +#pragma warning restore CS8603 // Possible null reference return. + private static string ResolveApplicationVersion(IConfiguration configuration) { return diff --git a/src/Spectre.Console.Cli/Internal/Composition/ComponentRegistry.cs b/src/Spectre.Console.Cli/Internal/Composition/ComponentRegistry.cs index 426c349..278617a 100644 --- a/src/Spectre.Console.Cli/Internal/Composition/ComponentRegistry.cs +++ b/src/Spectre.Console.Cli/Internal/Composition/ComponentRegistry.cs @@ -35,11 +35,11 @@ internal sealed class ComponentRegistry : IDisposable foreach (var type in new HashSet(registration.RegistrationTypes)) { if (!_registrations.ContainsKey(type)) - { - _registrations.Add(type, new HashSet()); + { + // Only add each registration type once. + _registrations.Add(type, new HashSet()); + _registrations[type].Add(registration); } - - _registrations[type].Add(registration); } } diff --git a/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs b/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs index dc46992..d1fba73 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs @@ -4,7 +4,8 @@ internal sealed class CommandAppSettings : ICommandAppSettings { public string? ApplicationName { get; set; } public string? ApplicationVersion { get; set; } - public bool ShowOptionDefaultValues { get; set; } + public int MaximumIndirectExamples { get; set; } + public bool ShowOptionDefaultValues { get; set; } public IAnsiConsole? Console { get; set; } public ICommandInterceptor? Interceptor { get; set; } public ITypeRegistrarFrontend Registrar { get; set; } @@ -24,7 +25,8 @@ internal sealed class CommandAppSettings : ICommandAppSettings { Registrar = new TypeRegistrar(registrar); CaseSensitivity = CaseSensitivity.All; - ShowOptionDefaultValues = true; + ShowOptionDefaultValues = true; + MaximumIndirectExamples = 5; } public bool IsTrue(Func func, string environmentVariableName) diff --git a/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs b/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs index 3108b5d..2726a2c 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs @@ -19,6 +19,19 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig Settings = new CommandAppSettings(registrar); Examples = new List(); } + + public void SetHelpProvider(IHelpProvider helpProvider) + { + // Register the help provider + _registrar.RegisterInstance(typeof(IHelpProvider), helpProvider); + } + + public void SetHelpProvider() + where T : IHelpProvider + { + // Register the help provider + _registrar.Register(typeof(IHelpProvider), typeof(T)); + } public void AddExample(params string[] args) { diff --git a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs index 14b6588..537e485 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs @@ -17,7 +17,7 @@ internal sealed class Configurator : IUnsafeBranchConfigurator, IConf _command.Description = description; } - public void AddExample(string[] args) + public void AddExample(params string[] args) { _command.Examples.Add(args); } diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandArgument.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandArgument.cs index d0bb05c..c6b555b 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandArgument.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandArgument.cs @@ -1,6 +1,6 @@ namespace Spectre.Console.Cli; -internal sealed class CommandArgument : CommandParameter +internal sealed class CommandArgument : CommandParameter, ICommandArgument { public string Value { get; } public int Position { get; set; } diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs index 4152dc6..ed59e41 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs @@ -1,6 +1,6 @@ namespace Spectre.Console.Cli; - -internal sealed class CommandInfo : ICommandContainer + +internal sealed class CommandInfo : ICommandContainer, ICommandInfo { public string Name { get; } public HashSet Aliases { get; } @@ -20,8 +20,14 @@ internal sealed class CommandInfo : ICommandContainer // only branches can have a default command public CommandInfo? DefaultCommand => IsBranch ? Children.FirstOrDefault(c => c.IsDefaultCommand) : null; - public bool IsHidden { get; } - + public bool IsHidden { get; } + + IReadOnlyList Help.ICommandContainer.Commands => Children.Cast().ToList(); + ICommandInfo? Help.ICommandContainer.DefaultCommand => DefaultCommand; + IReadOnlyList ICommandInfo.Parameters => Parameters.Cast().ToList(); + ICommandInfo? ICommandInfo.Parent => Parent; + IReadOnlyList Help.ICommandContainer.Examples => (IReadOnlyList)Examples; + public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype) { Parent = parent; @@ -48,19 +54,5 @@ internal sealed class CommandInfo : ICommandContainer Description = description.Description; } } - } - - public List Flatten() - { - var result = new Stack(); - - var current = this; - while (current != null) - { - result.Push(current); - current = current.Parent; - } - - return result.ToList(); - } + } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandModel.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandModel.cs index 526f0ee..81a4c5c 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandModel.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandModel.cs @@ -1,14 +1,18 @@ namespace Spectre.Console.Cli; - -internal sealed class CommandModel : ICommandContainer + +internal sealed class CommandModel : ICommandContainer, ICommandModel { public string? ApplicationName { get; } public ParsingMode ParsingMode { get; } public IList Commands { get; } public IList Examples { get; } - public bool TrimTrailingPeriod { get; } - public CommandInfo? DefaultCommand => Commands.FirstOrDefault(c => c.IsDefaultCommand); + public CommandInfo? DefaultCommand => Commands.FirstOrDefault(c => c.IsDefaultCommand); + + string ICommandModel.ApplicationName => GetApplicationName(ApplicationName); + IReadOnlyList Help.ICommandContainer.Commands => Commands.Cast().ToList(); + ICommandInfo? Help.ICommandContainer.DefaultCommand => DefaultCommand; + IReadOnlyList Help.ICommandContainer.Examples => (IReadOnlyList)Examples; public CommandModel( CommandAppSettings settings, @@ -17,22 +21,32 @@ internal sealed class CommandModel : ICommandContainer { ApplicationName = settings.ApplicationName; ParsingMode = settings.ParsingMode; - TrimTrailingPeriod = settings.TrimTrailingPeriod; Commands = new List(commands ?? Array.Empty()); Examples = new List(examples ?? Array.Empty()); - } - - public string GetApplicationName() + } + + /// + /// Gets the name of the application. + /// If the provided is not null or empty, + /// it is returned. Otherwise the name of the current application + /// is determined based on the executable file's name. + /// + /// The optional name of the application. + /// + /// The name of the application, or a default value of "?" if no valid application name can be determined. + /// + private static string GetApplicationName(string? applicationName) { return - ApplicationName ?? + applicationName ?? Path.GetFileName(GetApplicationFile()) ?? // null is propagated by GetFileName "?"; } private static string? GetApplicationFile() { - var location = Assembly.GetEntryAssembly()?.Location; + var location = Assembly.GetEntryAssembly()?.Location; + if (string.IsNullOrWhiteSpace(location)) { // this is special case for single file executable diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandOption.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandOption.cs index cecd1f9..29e113d 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandOption.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandOption.cs @@ -1,6 +1,6 @@ namespace Spectre.Console.Cli; -internal sealed class CommandOption : CommandParameter +internal sealed class CommandOption : CommandParameter, ICommandOption { public IReadOnlyList LongNames { get; } public IReadOnlyList ShortNames { get; } diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandParameter.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandParameter.cs index 0c96b98..74d6626 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandParameter.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandParameter.cs @@ -1,6 +1,6 @@ namespace Spectre.Console.Cli; - -internal abstract class CommandParameter : ICommandParameterInfo + +internal abstract class CommandParameter : ICommandParameterInfo, ICommandParameter { public Guid Id { get; } public Type ParameterType { get; } @@ -17,8 +17,10 @@ internal abstract class CommandParameter : ICommandParameterInfo public string PropertyName => Property.Name; public virtual bool WantRawValue => ParameterType.IsPairDeconstructable() - && (PairDeconstructor != null || Converter == null); - + && (PairDeconstructor != null || Converter == null); + + public bool IsFlag => ParameterKind == ParameterKind.Flag; + protected CommandParameter( Type parameterType, ParameterKind parameterKind, PropertyInfo property, string? description, TypeConverterAttribute? converter, diff --git a/src/Spectre.Console.Cli/Properties/Usings.cs b/src/Spectre.Console.Cli/Properties/Usings.cs index 0716be8..70a6d6a 100644 --- a/src/Spectre.Console.Cli/Properties/Usings.cs +++ b/src/Spectre.Console.Cli/Properties/Usings.cs @@ -10,6 +10,7 @@ global using System.Linq; global using System.Reflection; global using System.Text; global using System.Threading.Tasks; -global using System.Xml; +global using System.Xml; +global using Spectre.Console.Cli.Help; global using Spectre.Console.Cli.Unsafe; -global using Spectre.Console.Rendering; +global using Spectre.Console.Rendering; \ No newline at end of file diff --git a/src/Spectre.Console.Testing/FakeTypeResolver.cs b/src/Spectre.Console.Testing/FakeTypeResolver.cs index 0aa9189..b89634f 100644 --- a/src/Spectre.Console.Testing/FakeTypeResolver.cs +++ b/src/Spectre.Console.Testing/FakeTypeResolver.cs @@ -35,10 +35,12 @@ public sealed class FakeTypeResolver : ITypeResolver } if (_registrations.TryGetValue(type, out var registrations)) - { + { + // The type might be an interface, but the registration should be a class. + // So call CreateInstance on the first registration rather than the type. return registrations.Count == 0 - ? null - : Activator.CreateInstance(type); + ? null + : Activator.CreateInstance(registrations[0]); } return null; diff --git a/test/Spectre.Console.Cli.Tests/Data/Help/CustomHelpProvider.cs b/test/Spectre.Console.Cli.Tests/Data/Help/CustomHelpProvider.cs new file mode 100644 index 0000000..78c6d97 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Data/Help/CustomHelpProvider.cs @@ -0,0 +1,34 @@ +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli.Tests.Data.Help; + +internal class CustomHelpProvider : HelpProvider +{ + private readonly string version; + + public CustomHelpProvider(ICommandAppSettings settings, string version) + : base(settings) + { + this.version = version; + } + + public override IEnumerable GetHeader(ICommandModel model, ICommandInfo command) + { + return new IRenderable[] + { + new Text("--------------------------------------"), Text.NewLine, + new Text("--- CUSTOM HELP PROVIDER ---"), Text.NewLine, + new Text("--------------------------------------"), Text.NewLine, + Text.NewLine, + }; + } + + public override IEnumerable GetFooter(ICommandModel model, ICommandInfo command) + { + return new IRenderable[] + { + Text.NewLine, + new Text($"Version {version}"), + }; + } +} diff --git a/test/Spectre.Console.Cli.Tests/Data/Help/RedirectHelpProvider.cs b/test/Spectre.Console.Cli.Tests/Data/Help/RedirectHelpProvider.cs new file mode 100644 index 0000000..f017bdf --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Data/Help/RedirectHelpProvider.cs @@ -0,0 +1,21 @@ +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli.Tests.Data.Help; + +internal class RedirectHelpProvider : IHelpProvider +{ + public virtual IEnumerable Write(ICommandModel model) + { + return Write(model, null); + } +#nullable enable + public virtual IEnumerable Write(ICommandModel model, ICommandInfo? command) +#nullable disable + { + return new[] + { + new Text("Help has moved online. Please see: http://www.example.com"), + Text.NewLine, + }; + } +} \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Data/Settings/EmptySettings.cs b/test/Spectre.Console.Cli.Tests/Data/Settings/EmptySettings.cs deleted file mode 100644 index c42baf3..0000000 --- a/test/Spectre.Console.Cli.Tests/Data/Settings/EmptySettings.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Spectre.Console.Tests.Data; - -public sealed class EmptySettings : CommandSettings -{ -} diff --git a/test/Spectre.Console.Cli.Tests/Data/Settings/OptionalArgumentWithDefaultValueSettings.cs b/test/Spectre.Console.Cli.Tests/Data/Settings/OptionalArgumentWithDefaultValueSettings.cs index f36dc89..ae0e155 100644 --- a/test/Spectre.Console.Cli.Tests/Data/Settings/OptionalArgumentWithDefaultValueSettings.cs +++ b/test/Spectre.Console.Cli.Tests/Data/Settings/OptionalArgumentWithDefaultValueSettings.cs @@ -15,7 +15,7 @@ public sealed class OptionalArgumentWithPropertyInitializerSettings : CommandSet [CommandOption("-c")] public int Count { get; set; } = 1; - [CommandOption("-v")] + [CommandOption("--value")] public int Value { get; set; } = 0; } diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/ArgumentOrder.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/ArgumentOrder.Output.verified.txt index 31c0da3..6e29ed2 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/ArgumentOrder.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/ArgumentOrder.Output.verified.txt @@ -9,4 +9,5 @@ ARGUMENTS: [QUX] OPTIONS: - -h, --help Prints help information \ No newline at end of file + -h, --help Prints help information + -v, --version Prints version information \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Command.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Branch.Output.verified.txt similarity index 100% rename from test/Spectre.Console.Cli.Tests/Expectations/Help/Command.Output.verified.txt rename to test/Spectre.Console.Cli.Tests/Expectations/Help/Branch.Output.verified.txt diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Branch_Called_Without_Help.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Branch_Called_Without_Help.Output.verified.txt new file mode 100644 index 0000000..5fba99c --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Branch_Called_Without_Help.Output.verified.txt @@ -0,0 +1,18 @@ +DESCRIPTION: +Contains settings for a cat. + +USAGE: + myapp cat [LEGS] [OPTIONS] + +ARGUMENTS: + [LEGS] The number of legs + +OPTIONS: + DEFAULT + -h, --help Prints help information + -a, --alive Indicates whether or not the animal is alive + -n, --name + --agility 10 The agility between 0 and 100 + +COMMANDS: + lion The lion command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Branch_Default_Greeter.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Branch_Default_Greeter.Output.verified.txt new file mode 100644 index 0000000..e51ef5f --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Branch_Default_Greeter.Output.verified.txt @@ -0,0 +1,11 @@ +USAGE: + myapp branch [GREETING] [OPTIONS] [COMMAND] + +ARGUMENTS: + [GREETING] + +OPTIONS: + -h, --help Prints help information + +COMMANDS: + greeter \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Branch_Examples.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Branch_Examples.Output.verified.txt new file mode 100644 index 0000000..12898a8 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Branch_Examples.Output.verified.txt @@ -0,0 +1,30 @@ +DESCRIPTION: +The animal command. + +USAGE: + myapp animal [LEGS] [OPTIONS] + +EXAMPLES: + myapp animal dog --name Rufus --age 12 --good-boy + myapp animal dog --name Luna + myapp animal dog --name Charlie + myapp animal dog --name Bella + myapp animal dog --name Daisy + myapp animal dog --name Milo + myapp animal horse --name Brutus + myapp animal horse --name Sugar --IsAlive false + myapp animal horse --name Cash + myapp animal horse --name Dakota + myapp animal horse --name Cisco + myapp animal horse --name Spirit + +ARGUMENTS: + [LEGS] The number of legs + +OPTIONS: + -h, --help Prints help information + -a, --alive Indicates whether or not the animal is alive + +COMMANDS: + dog The dog command + horse The horse command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/CommandExamples.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/CommandExamples.Output.verified.txt deleted file mode 100644 index a45af87..0000000 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/CommandExamples.Output.verified.txt +++ /dev/null @@ -1,19 +0,0 @@ -DESCRIPTION: -The animal command. - -USAGE: - myapp animal [LEGS] [OPTIONS] - -EXAMPLES: - myapp animal --help - -ARGUMENTS: - [LEGS] The number of legs - -OPTIONS: - -h, --help Prints help information - -a, --alive Indicates whether or not the animal is alive - -COMMANDS: - dog The dog command - horse The horse command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Custom_Help_Configured_By_Instance.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Custom_Help_Configured_By_Instance.Output.verified.txt new file mode 100644 index 0000000..e4a56cd --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Custom_Help_Configured_By_Instance.Output.verified.txt @@ -0,0 +1,15 @@ +-------------------------------------- +--- CUSTOM HELP PROVIDER --- +-------------------------------------- + +USAGE: + myapp [OPTIONS] + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + dog The dog command + +Version 1.0 \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Custom_Help_Configured_By_Type.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Custom_Help_Configured_By_Type.Output.verified.txt new file mode 100644 index 0000000..c218113 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Custom_Help_Configured_By_Type.Output.verified.txt @@ -0,0 +1 @@ +Help has moved online. Please see: http://www.example.com \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Custom_Help_Registered_By_Instance.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Custom_Help_Registered_By_Instance.Output.verified.txt new file mode 100644 index 0000000..ad99fbb --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Custom_Help_Registered_By_Instance.Output.verified.txt @@ -0,0 +1,15 @@ +-------------------------------------- +--- CUSTOM HELP PROVIDER --- +-------------------------------------- + +USAGE: + myapp [OPTIONS] + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + dog The dog command + +Version 1.0 \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Custom_Help_Registered_By_Type.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Custom_Help_Registered_By_Type.Output.verified.txt new file mode 100644 index 0000000..c218113 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Custom_Help_Registered_By_Type.Output.verified.txt @@ -0,0 +1 @@ +Help has moved online. Please see: http://www.example.com \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default.Output.verified.txt index 1ea12e3..aea49d4 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default.Output.verified.txt @@ -1,4 +1,4 @@ -DESCRIPTION: +DESCRIPTION: The lion command. USAGE: @@ -10,7 +10,8 @@ ARGUMENTS: OPTIONS: DEFAULT - -h, --help Prints help information + -h, --help Prints help information + -v, --version Prints version information -a, --alive Indicates whether or not the animal is alive -n, --name --agility 10 The agility between 0 and 100 diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/DefaultExamples.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/DefaultExamples.Output.verified.txt deleted file mode 100644 index 15ed745..0000000 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/DefaultExamples.Output.verified.txt +++ /dev/null @@ -1,21 +0,0 @@ -DESCRIPTION: -The lion command. - -USAGE: - myapp [LEGS] [OPTIONS] - -EXAMPLES: - myapp 12 -c 3 - -ARGUMENTS: - 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 - --agility 10 The agility between 0 and 100 - -c The number of children the lion has - -d Monday, Thursday The days the lion goes hunting \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Custom_Help_Provider.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Custom_Help_Provider.Output.verified.txt new file mode 100644 index 0000000..e4a56cd --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Custom_Help_Provider.Output.verified.txt @@ -0,0 +1,15 @@ +-------------------------------------- +--- CUSTOM HELP PROVIDER --- +-------------------------------------- + +USAGE: + myapp [OPTIONS] + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + dog The dog command + +Version 1.0 \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Examples.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Examples.Output.verified.txt new file mode 100644 index 0000000..cd5b1e4 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Examples.Output.verified.txt @@ -0,0 +1,24 @@ +DESCRIPTION: +The dog command. + +USAGE: + myapp [LEGS] [OPTIONS] + +EXAMPLES: + myapp --name Rufus --age 12 --good-boy + myapp --name Luna + myapp --name Charlie + myapp --name Bella + myapp --name Daisy + myapp --name Milo + +ARGUMENTS: + + [LEGS] The number of legs + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + -a, --alive Indicates whether or not the animal is alive + -n, --name + -g, --good-boy \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Greeter_Default.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Greeter.Output.verified.txt similarity index 100% rename from test/Spectre.Console.Cli.Tests/Expectations/Help/Greeter_Default.Output.verified.txt rename to test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Greeter.Output.verified.txt diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args.Output.verified.txt index 1ea12e3..aea49d4 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args.Output.verified.txt @@ -1,4 +1,4 @@ -DESCRIPTION: +DESCRIPTION: The lion command. USAGE: @@ -10,7 +10,8 @@ ARGUMENTS: OPTIONS: DEFAULT - -h, --help Prints help information + -h, --help Prints help information + -v, --version Prints version information -a, --alive Indicates whether or not the animal is alive -n, --name --agility 10 The agility between 0 and 100 diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output.verified.txt index 3bdda17..24018c0 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output.verified.txt @@ -1,8 +1,8 @@ -DESCRIPTION: +DESCRIPTION: The lion command. USAGE: - myapp [LEGS] [OPTIONS] + myapp [LEGS] [OPTIONS] [COMMAND] ARGUMENTS: The number of teeth the lion has @@ -10,7 +10,8 @@ ARGUMENTS: OPTIONS: DEFAULT - -h, --help Prints help information + -h, --help Prints help information + -v, --version Prints version information -a, --alive Indicates whether or not the animal is alive -n, --name --agility 10 The agility between 0 and 100 diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Hidden_Command_Options.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Hidden_Command_Options.Output.verified.txt index fe241ad..7288aef 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/Hidden_Command_Options.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Hidden_Command_Options.Output.verified.txt @@ -5,5 +5,6 @@ ARGUMENTS: Dummy argument FOO OPTIONS: - -h, --help Prints help information - --baz Dummy option BAZ \ No newline at end of file + -h, --help Prints help information + -v, --version Prints version information + --baz Dummy option BAZ \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples.Output.verified.txt new file mode 100644 index 0000000..3488e38 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples.Output.verified.txt @@ -0,0 +1,24 @@ +USAGE: + myapp [OPTIONS] + +EXAMPLES: + myapp dog --name Rufus --age 12 --good-boy + myapp dog --name Luna + myapp dog --name Charlie + myapp dog --name Bella + myapp dog --name Daisy + myapp dog --name Milo + myapp horse --name Brutus + myapp horse --name Sugar --IsAlive false + myapp horse --name Cash + myapp horse --name Dakota + myapp horse --name Cisco + myapp horse --name Spirit + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + dog The dog command + horse The horse command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/RootExamples.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Children.Output.verified.txt similarity index 71% rename from test/Spectre.Console.Cli.Tests/Expectations/Help/RootExamples.Output.verified.txt rename to test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Children.Output.verified.txt index 6f5066a..47e373a 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/RootExamples.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Children.Output.verified.txt @@ -3,7 +3,10 @@ USAGE: EXAMPLES: myapp dog --name Rufus --age 12 --good-boy - myapp horse --name Brutus + myapp dog --name Luna + myapp dog --name Charlie + myapp dog --name Bella + myapp dog --name Daisy OPTIONS: -h, --help Prints help information diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Children_Eight.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Children_Eight.Output.verified.txt new file mode 100644 index 0000000..3e5a6d9 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Children_Eight.Output.verified.txt @@ -0,0 +1,20 @@ +USAGE: + myapp [OPTIONS] + +EXAMPLES: + myapp dog --name Rufus --age 12 --good-boy + myapp dog --name Luna + myapp dog --name Charlie + myapp dog --name Bella + myapp dog --name Daisy + myapp dog --name Milo + myapp horse --name Brutus + myapp horse --name Sugar --IsAlive false + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + dog The dog command + horse The horse command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/RootExamples_Children.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Children_None.Output.verified.txt similarity index 71% rename from test/Spectre.Console.Cli.Tests/Expectations/Help/RootExamples_Children.Output.verified.txt rename to test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Children_None.Output.verified.txt index 6f5066a..3377d2a 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/RootExamples_Children.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Children_None.Output.verified.txt @@ -1,14 +1,10 @@ -USAGE: - myapp [OPTIONS] - -EXAMPLES: - myapp dog --name Rufus --age 12 --good-boy - myapp horse --name Brutus - -OPTIONS: - -h, --help Prints help information - -v, --version Prints version information - -COMMANDS: - dog The dog command +USAGE: + myapp [OPTIONS] + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + dog The dog command horse The horse command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Children_Twelve.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Children_Twelve.Output.verified.txt new file mode 100644 index 0000000..8924b55 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Children_Twelve.Output.verified.txt @@ -0,0 +1,24 @@ +USAGE: + myapp [OPTIONS] + +EXAMPLES: + myapp dog --name Rufus --age 12 --good-boy + myapp dog --name Luna + myapp dog --name Charlie + myapp dog --name Bella + myapp dog --name Daisy + myapp dog --name Milo + myapp horse --name Brutus + myapp horse --name Sugar --IsAlive false + myapp horse --name Cash + myapp horse --name Dakota + myapp horse --name Cisco + myapp horse --name Spirit + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + dog The dog command + horse The horse command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/RootExamples_Leafs.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Leafs.Output.verified.txt similarity index 64% rename from test/Spectre.Console.Cli.Tests/Expectations/Help/RootExamples_Leafs.Output.verified.txt rename to test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Leafs.Output.verified.txt index 7d3b86e..8b75361 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/RootExamples_Leafs.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Leafs.Output.verified.txt @@ -3,7 +3,10 @@ USAGE: EXAMPLES: myapp animal dog --name Rufus --age 12 --good-boy - myapp animal horse --name Brutus + myapp animal dog --name Luna + myapp animal dog --name Charlie + myapp animal dog --name Bella + myapp animal dog --name Daisy OPTIONS: -h, --help Prints help information diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Leafs_Eight.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Leafs_Eight.Output.verified.txt new file mode 100644 index 0000000..63bded9 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Leafs_Eight.Output.verified.txt @@ -0,0 +1,19 @@ +USAGE: + myapp [OPTIONS] + +EXAMPLES: + myapp animal dog --name Rufus --age 12 --good-boy + myapp animal dog --name Luna + myapp animal dog --name Charlie + myapp animal dog --name Bella + myapp animal dog --name Daisy + myapp animal dog --name Milo + myapp animal horse --name Brutus + myapp animal horse --name Sugar --IsAlive false + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + animal The animal command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Leafs_None.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Leafs_None.Output.verified.txt new file mode 100644 index 0000000..53228a0 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Leafs_None.Output.verified.txt @@ -0,0 +1,9 @@ +USAGE: + myapp [OPTIONS] + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + animal The animal command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Leafs_Twelve.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Leafs_Twelve.Output.verified.txt new file mode 100644 index 0000000..07178cb --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Examples_Leafs_Twelve.Output.verified.txt @@ -0,0 +1,23 @@ +USAGE: + myapp [OPTIONS] + +EXAMPLES: + myapp animal dog --name Rufus --age 12 --good-boy + myapp animal dog --name Luna + myapp animal dog --name Charlie + myapp animal dog --name Bella + myapp animal dog --name Daisy + myapp animal dog --name Milo + myapp animal horse --name Brutus + myapp animal horse --name Sugar --IsAlive false + myapp animal horse --name Cash + myapp animal horse --name Dakota + myapp animal horse --name Cisco + myapp animal horse --name Spirit + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + animal The animal command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Properties/Usings.cs b/test/Spectre.Console.Cli.Tests/Properties/Usings.cs index be00c04..c8e60a1 100644 --- a/test/Spectre.Console.Cli.Tests/Properties/Usings.cs +++ b/test/Spectre.Console.Cli.Tests/Properties/Usings.cs @@ -7,7 +7,8 @@ global using System.Linq; global using System.Runtime.CompilerServices; global using System.Threading.Tasks; global using Shouldly; -global using Spectre.Console.Cli; +global using Spectre.Console.Cli; +global using Spectre.Console.Cli.Help; global using Spectre.Console.Cli.Unsafe; global using Spectre.Console.Testing; global using Spectre.Console.Tests.Data; diff --git a/test/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj b/test/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj index 6bc5b2a..e58ff86 100644 --- a/test/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj +++ b/test/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj @@ -34,7 +34,7 @@ $([System.String]::Copy('%(FileName)').Split('.')[0]) %(ParentFile).cs - + $([System.String]::Copy('%(FileName)').Split('.')[0]) %(ParentFile).cs diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs index 2edffb5..0c4caf0 100644 --- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs @@ -1,3 +1,5 @@ +using Spectre.Console.Cli.Tests.Data.Help; + namespace Spectre.Console.Tests.Unit.Cli; public sealed partial class CommandAppTests @@ -75,8 +77,8 @@ public sealed partial class CommandAppTests } [Fact] - [Expectation("Command")] - public Task Should_Output_Command_Correctly() + [Expectation("Branch")] + public Task Should_Output_Branch_Correctly() { // Given var fixture = new CommandAppTester(); @@ -91,7 +93,53 @@ public sealed partial class CommandAppTests }); // When - var result = fixture.Run("cat", "--help"); + var result = fixture.Run("cat", "--help"); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Branch_Called_Without_Help")] + public Task Should_Output_Branch_When_Called_Without_Help_Option() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddBranch("cat", animal => + { + animal.SetDescription("Contains settings for a cat."); + animal.AddCommand("lion"); + }); + }); + + // When + var result = fixture.Run("cat"); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Branch_Default_Greeter")] + public Task Should_Output_Branch_With_Default_Correctly() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddBranch("branch", animal => + { + animal.SetDefaultCommand(); + animal.AddCommand("greeter"); + }); + }); + + // When + var result = fixture.Run("branch", "--help"); // Then return Verifier.Verify(result.Output); @@ -138,7 +186,7 @@ public sealed partial class CommandAppTests }); // When - var result = fixture.Run("cat", "lion", "--help"); + var result = fixture.Run("cat", "lion", "--help"); // Then return Verifier.Verify(result.Output); @@ -203,7 +251,7 @@ public sealed partial class CommandAppTests } [Fact] - [Expectation("Greeter_Default")] + [Expectation("Default_Greeter")] public Task Should_Not_Output_Default_Command_When_Command_Has_No_Required_Parameters_And_Is_Called_Without_Args() { // Given @@ -219,19 +267,131 @@ public sealed partial class CommandAppTests // Then return Verifier.Verify(result.Output); - } + } + + [Fact] + [Expectation("Custom_Help_Registered_By_Instance")] + public Task Should_Output_Custom_Help_When_Registered_By_Instance() + { + var registrar = new DefaultTypeRegistrar(); + + // Given + var fixture = new CommandAppTester(registrar); + fixture.Configure(configurator => + { + // Create the custom help provider + var helpProvider = new CustomHelpProvider(configurator.Settings, "1.0"); + + // Register the custom help provider instance + registrar.RegisterInstance(typeof(IHelpProvider), helpProvider); + + configurator.SetApplicationName("myapp"); + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run(); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Custom_Help_Registered_By_Type")] + public Task Should_Output_Custom_Help_When_Registered_By_Type() + { + var registrar = new DefaultTypeRegistrar(); + + // Given + var fixture = new CommandAppTester(registrar); + fixture.Configure(configurator => + { + // Register the custom help provider type + registrar.Register(typeof(IHelpProvider), typeof(RedirectHelpProvider)); + + configurator.SetApplicationName("myapp"); + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run(); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Custom_Help_Configured_By_Instance")] + public Task Should_Output_Custom_Help_When_Configured_By_Instance() + { + var registrar = new DefaultTypeRegistrar(); + + // Given + var fixture = new CommandAppTester(registrar); + fixture.Configure(configurator => + { + // Configure the custom help provider instance + configurator.SetHelpProvider(new CustomHelpProvider(configurator.Settings, "1.0")); + + configurator.SetApplicationName("myapp"); + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run(); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Custom_Help_Configured_By_Type")] + public Task Should_Output_Custom_Help_When_Configured_By_Type() + { + var registrar = new DefaultTypeRegistrar(); + + // Given + var fixture = new CommandAppTester(registrar); + fixture.Configure(configurator => + { + // Configure the custom help provider type + configurator.SetHelpProvider(); + + configurator.SetApplicationName("myapp"); + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run(); + + // Then + return Verifier.Verify(result.Output); + } [Fact] - [Expectation("RootExamples")] - public Task Should_Output_Root_Examples_Defined_On_Root() + [Expectation("Root_Examples")] + public Task Should_Output_Examples_Defined_On_Root() { // Given var fixture = new CommandAppTester(); fixture.Configure(configurator => { configurator.SetApplicationName("myapp"); - configurator.AddExample("dog", "--name", "Rufus", "--age", "12", "--good-boy"); - configurator.AddExample("horse", "--name", "Brutus"); + + // All root examples should be shown + configurator.AddExample("dog", "--name", "Rufus", "--age", "12", "--good-boy"); + configurator.AddExample("dog", "--name", "Luna"); + configurator.AddExample("dog", "--name", "Charlie"); + configurator.AddExample("dog", "--name", "Bella"); + configurator.AddExample("dog", "--name", "Daisy"); + configurator.AddExample("dog", "--name", "Milo"); + configurator.AddExample("horse", "--name", "Brutus"); + configurator.AddExample("horse", "--name", "Sugar", "--IsAlive", "false"); + configurator.AddExample("horse", "--name", "Cash"); + configurator.AddExample("horse", "--name", "Dakota"); + configurator.AddExample("horse", "--name", "Cisco"); + configurator.AddExample("horse", "--name", "Spirit"); + configurator.AddCommand("dog"); configurator.AddCommand("horse"); }); @@ -241,21 +401,147 @@ public sealed partial class CommandAppTests // Then return Verifier.Verify(result.Output); - } + } [Fact] - [Expectation("RootExamples_Children")] - public Task Should_Output_Root_Examples_Defined_On_Direct_Children_If_Root_Have_No_Examples() + [Expectation("Root_Examples_Children")] + [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:SingleLineCommentsMustNotBeFollowedByBlankLine", Justification = "Single line comment is relevant to several code blocks that follow.")] + public Task Should_Output_Examples_Defined_On_Direct_Children_If_Root_Has_No_Examples() { // Given var fixture = new CommandAppTester(); fixture.Configure(configurator => { - configurator.SetApplicationName("myapp"); + configurator.SetApplicationName("myapp"); + + // It should be capped to the first 5 examples by default + configurator.AddCommand("dog") - .WithExample("dog", "--name", "Rufus", "--age", "12", "--good-boy"); + .WithExample("dog", "--name", "Rufus", "--age", "12", "--good-boy") + .WithExample("dog", "--name", "Luna") + .WithExample("dog", "--name", "Charlie") + .WithExample("dog", "--name", "Bella") + .WithExample("dog", "--name", "Daisy") + .WithExample("dog", "--name", "Milo"); + configurator.AddCommand("horse") - .WithExample("horse", "--name", "Brutus"); + .WithExample("horse", "--name", "Brutus") + .WithExample("horse", "--name", "Sugar", "--IsAlive", "false") + .WithExample("horse", "--name", "Cash") + .WithExample("horse", "--name", "Dakota") + .WithExample("horse", "--name", "Cisco") + .WithExample("horse", "--name", "Spirit"); + }); + + // When + var result = fixture.Run("--help"); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Root_Examples_Children_Eight")] + public Task Should_Output_Eight_Examples_Defined_On_Direct_Children_If_Root_Has_No_Examples() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + + // Show the first 8 examples defined on the direct children + configurator.Settings.MaximumIndirectExamples = 8; + + configurator.AddCommand("dog") + .WithExample("dog", "--name", "Rufus", "--age", "12", "--good-boy") + .WithExample("dog", "--name", "Luna") + .WithExample("dog", "--name", "Charlie") + .WithExample("dog", "--name", "Bella") + .WithExample("dog", "--name", "Daisy") + .WithExample("dog", "--name", "Milo"); + + configurator.AddCommand("horse") + .WithExample("horse", "--name", "Brutus") + .WithExample("horse", "--name", "Sugar", "--IsAlive", "false") + .WithExample("horse", "--name", "Cash") + .WithExample("horse", "--name", "Dakota") + .WithExample("horse", "--name", "Cisco") + .WithExample("horse", "--name", "Spirit"); + }); + + // When + var result = fixture.Run("--help"); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Root_Examples_Children_Twelve")] + public Task Should_Output_All_Examples_Defined_On_Direct_Children_If_Root_Has_No_Examples() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + + // Show all examples defined on the direct children + configurator.Settings.MaximumIndirectExamples = int.MaxValue; + + configurator.AddCommand("dog") + .WithExample("dog", "--name", "Rufus", "--age", "12", "--good-boy") + .WithExample("dog", "--name", "Luna") + .WithExample("dog", "--name", "Charlie") + .WithExample("dog", "--name", "Bella") + .WithExample("dog", "--name", "Daisy") + .WithExample("dog", "--name", "Milo"); + + configurator.AddCommand("horse") + .WithExample("horse", "--name", "Brutus") + .WithExample("horse", "--name", "Sugar", "--IsAlive", "false") + .WithExample("horse", "--name", "Cash") + .WithExample("horse", "--name", "Dakota") + .WithExample("horse", "--name", "Cisco") + .WithExample("horse", "--name", "Spirit"); + }); + + // When + var result = fixture.Run("--help"); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Root_Examples_Children_None")] + public Task Should_Not_Output_Examples_Defined_On_Direct_Children_If_Root_Has_No_Examples() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + + // Do not show examples defined on the direct children + configurator.Settings.MaximumIndirectExamples = 0; + + configurator.AddCommand("dog") + .WithExample("dog", "--name", "Rufus", "--age", "12", "--good-boy") + .WithExample("dog", "--name", "Luna") + .WithExample("dog", "--name", "Charlie") + .WithExample("dog", "--name", "Bella") + .WithExample("dog", "--name", "Daisy") + .WithExample("dog", "--name", "Milo"); + + configurator.AddCommand("horse") + .WithExample("horse", "--name", "Brutus") + .WithExample("horse", "--name", "Sugar", "--IsAlive", "false") + .WithExample("horse", "--name", "Cash") + .WithExample("horse", "--name", "Dakota") + .WithExample("horse", "--name", "Cisco") + .WithExample("horse", "--name", "Spirit"); }); // When @@ -266,8 +552,9 @@ public sealed partial class CommandAppTests } [Fact] - [Expectation("RootExamples_Leafs")] - public Task Should_Output_Root_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found() + [Expectation("Root_Examples_Leafs")] + [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:SingleLineCommentsMustNotBeFollowedByBlankLine", Justification = "Single line comment is relevant to several code blocks that follow.")] + public Task Should_Output_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found() { // Given var fixture = new CommandAppTester(); @@ -276,11 +563,148 @@ public sealed partial class CommandAppTests configurator.SetApplicationName("myapp"); configurator.AddBranch("animal", animal => { - animal.SetDescription("The animal command."); + animal.SetDescription("The animal command."); + + // It should be capped to the first 5 examples by default + animal.AddCommand("dog") - .WithExample("animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy"); + .WithExample("animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy") + .WithExample("animal", "dog", "--name", "Luna") + .WithExample("animal", "dog", "--name", "Charlie") + .WithExample("animal", "dog", "--name", "Bella") + .WithExample("animal", "dog", "--name", "Daisy") + .WithExample("animal", "dog", "--name", "Milo"); + animal.AddCommand("horse") - .WithExample("animal", "horse", "--name", "Brutus"); + .WithExample("animal", "horse", "--name", "Brutus") + .WithExample("animal", "horse", "--name", "Sugar", "--IsAlive", "false") + .WithExample("animal", "horse", "--name", "Cash") + .WithExample("animal", "horse", "--name", "Dakota") + .WithExample("animal", "horse", "--name", "Cisco") + .WithExample("animal", "horse", "--name", "Spirit"); + }); + }); + + // When + var result = fixture.Run("--help"); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Root_Examples_Leafs_Eight")] + public Task Should_Output_Eight_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddBranch("animal", animal => + { + animal.SetDescription("The animal command."); + + // Show the first 8 examples defined on the direct children + configurator.Settings.MaximumIndirectExamples = 8; + + animal.AddCommand("dog") + .WithExample("animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy") + .WithExample("animal", "dog", "--name", "Luna") + .WithExample("animal", "dog", "--name", "Charlie") + .WithExample("animal", "dog", "--name", "Bella") + .WithExample("animal", "dog", "--name", "Daisy") + .WithExample("animal", "dog", "--name", "Milo"); + + animal.AddCommand("horse") + .WithExample("animal", "horse", "--name", "Brutus") + .WithExample("animal", "horse", "--name", "Sugar", "--IsAlive", "false") + .WithExample("animal", "horse", "--name", "Cash") + .WithExample("animal", "horse", "--name", "Dakota") + .WithExample("animal", "horse", "--name", "Cisco") + .WithExample("animal", "horse", "--name", "Spirit"); + }); + }); + + // When + var result = fixture.Run("--help"); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Root_Examples_Leafs_Twelve")] + public Task Should_Output_All_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddBranch("animal", animal => + { + animal.SetDescription("The animal command."); + + // Show all examples defined on the direct children + configurator.Settings.MaximumIndirectExamples = int.MaxValue; + + animal.AddCommand("dog") + .WithExample("animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy") + .WithExample("animal", "dog", "--name", "Luna") + .WithExample("animal", "dog", "--name", "Charlie") + .WithExample("animal", "dog", "--name", "Bella") + .WithExample("animal", "dog", "--name", "Daisy") + .WithExample("animal", "dog", "--name", "Milo"); + + animal.AddCommand("horse") + .WithExample("animal", "horse", "--name", "Brutus") + .WithExample("animal", "horse", "--name", "Sugar", "--IsAlive", "false") + .WithExample("animal", "horse", "--name", "Cash") + .WithExample("animal", "horse", "--name", "Dakota") + .WithExample("animal", "horse", "--name", "Cisco") + .WithExample("animal", "horse", "--name", "Spirit"); + }); + }); + + // When + var result = fixture.Run("--help"); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Root_Examples_Leafs_None")] + public Task Should_Not_Output_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddBranch("animal", animal => + { + animal.SetDescription("The animal command."); + + // Do not show examples defined on the direct children + configurator.Settings.MaximumIndirectExamples = 0; + + animal.AddCommand("dog") + .WithExample("animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy") + .WithExample("animal", "dog", "--name", "Luna") + .WithExample("animal", "dog", "--name", "Charlie") + .WithExample("animal", "dog", "--name", "Bella") + .WithExample("animal", "dog", "--name", "Daisy") + .WithExample("animal", "dog", "--name", "Milo"); + + animal.AddCommand("horse") + .WithExample("animal", "horse", "--name", "Brutus") + .WithExample("animal", "horse", "--name", "Sugar", "--IsAlive", "false") + .WithExample("animal", "horse", "--name", "Cash") + .WithExample("animal", "horse", "--name", "Dakota") + .WithExample("animal", "horse", "--name", "Cisco") + .WithExample("animal", "horse", "--name", "Spirit"); }); }); @@ -292,18 +716,31 @@ public sealed partial class CommandAppTests } [Fact] - [Expectation("CommandExamples")] - public Task Should_Only_Output_Command_Examples_Defined_On_Command() + [Expectation("Branch_Examples")] + public Task Should_Output_Examples_Defined_On_Branch() { // Given var fixture = new CommandAppTester(); fixture.Configure(configurator => { - configurator.SetApplicationName("myapp"); + configurator.SetApplicationName("myapp"); configurator.AddBranch("animal", animal => { - animal.SetDescription("The animal command."); - animal.AddExample(new[] { "animal", "--help" }); + animal.SetDescription("The animal command."); + + // All branch examples should be shown + animal.AddExample("animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy"); + animal.AddExample("animal", "dog", "--name", "Luna"); + animal.AddExample("animal", "dog", "--name", "Charlie"); + animal.AddExample("animal", "dog", "--name", "Bella"); + animal.AddExample("animal", "dog", "--name", "Daisy"); + animal.AddExample("animal", "dog", "--name", "Milo"); + animal.AddExample("animal", "horse", "--name", "Brutus"); + animal.AddExample("animal", "horse", "--name", "Sugar", "--IsAlive", "false"); + animal.AddExample("animal", "horse", "--name", "Cash"); + animal.AddExample("animal", "horse", "--name", "Dakota"); + animal.AddExample("animal", "horse", "--name", "Cisco"); + animal.AddExample("animal", "horse", "--name", "Spirit"); animal.AddCommand("dog") .WithExample("animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy"); @@ -317,19 +754,26 @@ public sealed partial class CommandAppTests // Then return Verifier.Verify(result.Output); - } + } [Fact] - [Expectation("DefaultExamples")] - public Task Should_Output_Root_Examples_If_Default_Command_Is_Specified() + [Expectation("Default_Examples")] + public Task Should_Output_Examples_Defined_On_Root_If_Default_Command_Is_Specified() { // Given var fixture = new CommandAppTester(); - fixture.SetDefaultCommand(); + fixture.SetDefaultCommand(); fixture.Configure(configurator => { configurator.SetApplicationName("myapp"); - configurator.AddExample("12", "-c", "3"); + + // All root examples should be shown + configurator.AddExample("--name", "Rufus", "--age", "12", "--good-boy"); + configurator.AddExample("--name", "Luna"); + configurator.AddExample("--name", "Charlie"); + configurator.AddExample("--name", "Bella"); + configurator.AddExample("--name", "Daisy"); + configurator.AddExample("--name", "Milo"); }); // When diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Version.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Version.cs index e950814..1a6d884 100644 --- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Version.cs +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Version.cs @@ -5,27 +5,92 @@ public sealed partial class CommandAppTests public sealed class Version { [Fact] - public void Should_Output_The_Version_To_The_Console() + public void Should_Output_CLI_Version_To_The_Console() { // Given var fixture = new CommandAppTester(); - fixture.Configure(config => - { - config.AddBranch("animal", animal => - { - animal.AddBranch("mammal", mammal => - { - mammal.AddCommand("dog"); - mammal.AddCommand("horse"); - }); - }); - }); // When var result = fixture.Run(Constants.VersionCommand); // Then result.Output.ShouldStartWith("Spectre.Cli version "); + } + + [Fact] + public void Should_Output_Application_Version_To_The_Console_With_No_Command() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configurator => + { + configurator.SetApplicationVersion("1.0"); + }); + + // When + var result = fixture.Run("--version"); + + // Then + result.Output.ShouldBe("1.0"); + } + + [Fact] + public void Should_Output_Application_Version_To_The_Console_With_Command() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configurator => + { + configurator.SetApplicationVersion("1.0"); + + configurator.AddCommand("empty"); + }); + + // When + var result = fixture.Run("empty", "--version"); + + // Then + result.Output.ShouldBe("1.0"); + } + + [Fact] + public void Should_Output_Application_Version_To_The_Console_With_Default_Command() + { + // Given + var fixture = new CommandAppTester(); + fixture.SetDefaultCommand(); + fixture.Configure(configurator => + { + configurator.SetApplicationVersion("1.0"); + }); + + // When + var result = fixture.Run("--version"); + + // Then + result.Output.ShouldBe("1.0"); + } + + [Fact] + public void Should_Output_Application_Version_To_The_Console_With_Branch_Default_Command() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configurator => + { + configurator.SetApplicationVersion("1.0"); + + configurator.AddBranch("branch", branch => + { + branch.SetDefaultCommand(); + }); + }); + + // When + var result = fixture.Run("--version"); + + // Then + result.Output.ShouldBe("1.0"); } } } diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs index fed3504..83802f4 100644 --- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs @@ -362,7 +362,7 @@ public sealed partial class CommandAppTests }); // When - var result = app.Run("-c", "0", "-v", "50", "ABBA", "Herreys"); + var result = app.Run("-c", "0", "--value", "50", "ABBA", "Herreys"); // Then result.ExitCode.ShouldBe(0);