diff --git a/docs/input/cli/command-help.md b/docs/input/cli/command-help.md new file mode 100644 index 000000000..adc7a25b6 --- /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 585634e86..bef4be350 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 3c475f79d..b4dcd4e78 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 400616af5..8b8795453 100644 --- a/examples/Cli/Demo/Program.cs +++ b/examples/Cli/Demo/Program.cs @@ -15,25 +15,25 @@ public static int Main(string[] args) { 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 000000000..36b213b07 --- /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 000000000..51d14201e --- /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 000000000..79d00a8aa --- /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 000000000..1d4675f4a --- /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 9654ee80f..adba5e913 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 8445a581e..1a27e5aa3 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 8f8e5d453..f9a28b8b8 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; - -internal static class HelpWriter -{ +namespace Spectre.Console.Cli.Help; + +/// +/// 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 @@ public HelpArgument(string name, int position, bool required, string? descriptio 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 @@ public HelpOption(string? @short, string? @long, string? @value, bool? valueIsOp 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) - { - return WriteCommand(model, null, writeOptionsDefaultValues); - } - - public static IEnumerable WriteCommand(CommandModel model, CommandInfo? command, 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) { - 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 @@ private static IEnumerable GetDescription(CommandInfo? command) 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 @@ private static IEnumerable GetUsage(CommandModel model, CommandInfo } } - 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 @@ private static IEnumerable GetUsage(CommandModel model, CommandInfo } } - 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 @@ private static IEnumerable GetUsage(CommandModel model, CommandInfo { 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 @@ private static IEnumerable GetExamples(CommandModel model, CommandI } } - if (examples.Count > 0) + if (Math.Min(maxExamples, examples.Count) > 0) { var composer = new Composer(); composer.LineBreak(); @@ -221,7 +303,7 @@ private static IEnumerable GetExamples(CommandModel model, CommandI 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 @@ private static IEnumerable GetExamples(CommandModel model, CommandI } 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 @@ private static IEnumerable GetArguments(CommandInfo? command) 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 @@ private static IEnumerable GetOptions(CommandModel model, CommandIn }; 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 @@ static string GetOptionParts(HelpOption option) result.Add(grid); return result; - } - - private static IEnumerable GetCommands( - CommandModel model, - ICommandContainer command, - bool isDefaultCommand) - { - var commands = isDefaultCommand ? model.Commands : command.Commands; + } + + /// + /// 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; + + var commands = isDefaultCommand ? model.Commands : commandContainer.Commands; commands = commands.Where(x => !x.IsHidden).ToList(); if (commands.Count == 0) @@ -407,7 +507,7 @@ private static IEnumerable GetCommands( arguments.Space(); } - if (model.TrimTrailingPeriod) + if (TrimTrailingPeriod) { grid.AddRow( arguments.ToString().TrimEnd(), @@ -424,5 +524,16 @@ private static IEnumerable GetCommands( 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 000000000..abf064913 --- /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 000000000..21eb4b890 --- /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 000000000..aaba5ea03 --- /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 000000000..e91c8863f --- /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 000000000..e7fe5f728 --- /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 000000000..1247df820 --- /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 000000000..03bd7b641 --- /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 000000000..420dd0f38 --- /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 91af1784a..12fa7e14c 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 f6bbcb6d3..4f00c0b6e 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 @@ ICommandConfigurator AddAsyncDelegate(string name, FuncThe 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 6d463fc74..0c71b348c 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 3b7b735e9..0268a3cc1 100644 --- a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs +++ b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs @@ -8,85 +8,87 @@ public CommandExecutor(ITypeRegistrar registrar) { _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 @@ public async Task Execute(IConfiguration configuration, IEnumerable 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 426c349ce..278617ab9 100644 --- a/src/Spectre.Console.Cli/Internal/Composition/ComponentRegistry.cs +++ b/src/Spectre.Console.Cli/Internal/Composition/ComponentRegistry.cs @@ -35,11 +35,11 @@ public void Register(ComponentRegistration registration) 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 dc46992af..d1fba735b 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 @@ public CommandAppSettings(ITypeRegistrar registrar) { 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 3108b5dd7..2726a2cc6 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs @@ -19,6 +19,19 @@ public Configurator(ITypeRegistrar registrar) 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 14b65888d..537e48540 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs @@ -17,7 +17,7 @@ public void SetDescription(string description) _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 d0bb05c0f..c6b555baf 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 4152dc6d9..ed59e410d 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 @@ public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype) 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 526f0ee1f..81a4c5cb0 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 @@ public CommandModel( { 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 cecd1f9e6..29e113d5e 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 0c96b9811..74d66260b 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 0716be89b..70a6d6ae4 100644 --- a/src/Spectre.Console.Cli/Properties/Usings.cs +++ b/src/Spectre.Console.Cli/Properties/Usings.cs @@ -10,6 +10,7 @@ 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 0aa918902..b89634f68 100644 --- a/src/Spectre.Console.Testing/FakeTypeResolver.cs +++ b/src/Spectre.Console.Testing/FakeTypeResolver.cs @@ -35,10 +35,12 @@ public FakeTypeResolver( } 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 000000000..78c6d97a2 --- /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 000000000..f017bdf38 --- /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 c42baf342..000000000 --- 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 f36dc8905..ae0e15504 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 31c0da362..6e29ed297 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 000000000..5fba99cb5 --- /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 000000000..e51ef5fea --- /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 000000000..12898a8b4 --- /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 a45af87cb..000000000 --- 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 000000000..e4a56cd59 --- /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 000000000..c21811355 --- /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 000000000..ad99fbb63 --- /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 000000000..c21811355 --- /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 1ea12e364..aea49d4d1 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 15ed7459c..000000000 --- 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 000000000..e4a56cd59 --- /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 000000000..cd5b1e4fe --- /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 1ea12e364..aea49d4d1 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 3bdda1733..24018c08b 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 fe241adba..7288aefa5 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 000000000..3488e38c2 --- /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 6f5066a13..47e373aa3 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 000000000..3e5a6d939 --- /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 6f5066a13..3377d2a96 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 000000000..8924b5555 --- /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 7d3b86e39..8b753619b 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 000000000..63bded9a9 --- /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 000000000..53228a04c --- /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 000000000..07178cbc7 --- /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 be00c0442..c8e60a142 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.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 6bc5b2a49..e58ff863e 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 2edffb505..0c4caf09b 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 Task Should_Not_Trim_Description_Trailing_Period() } [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 Task Should_Output_Command_Correctly() }); // 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 Task Should_Output_Leaf_Correctly() }); // 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 Task Should_Output_Default_Command_And_Additional_Commands_When_Default_C } [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 Task Should_Not_Output_Default_Command_When_Command_Has_No_Required_Param // 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 Task Should_Output_Root_Examples_Defined_On_Root() // 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 Task Should_Output_Root_Examples_Defined_On_Direct_Children_If_Root_Have_ } [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,25 @@ public Task Should_Output_Root_Examples_Defined_On_Leaves_If_No_Other_Examples_A 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"); }); }); @@ -289,11 +590,93 @@ public Task Should_Output_Root_Examples_Defined_On_Leaves_If_No_Other_Examples_A // 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("CommandExamples")] - public Task Should_Only_Output_Command_Examples_Defined_On_Command() + [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(); @@ -302,8 +685,62 @@ public Task Should_Only_Output_Command_Examples_Defined_On_Command() configurator.SetApplicationName("myapp"); configurator.AddBranch("animal", animal => { - animal.SetDescription("The animal command."); - animal.AddExample(new[] { "animal", "--help" }); + 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"); + }); + }); + + // When + var result = fixture.Run("--help"); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Branch_Examples")] + public Task Should_Output_Examples_Defined_On_Branch() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddBranch("animal", animal => + { + 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 Task Should_Only_Output_Command_Examples_Defined_On_Command() // 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 e95081460..1a6d884d5 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 => + + // 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 => { - config.AddBranch("animal", animal => - { - animal.AddBranch("mammal", mammal => - { - mammal.AddCommand("dog"); - mammal.AddCommand("horse"); - }); + 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(Constants.VersionCommand); + var result = fixture.Run("--version"); // Then - result.Output.ShouldStartWith("Spectre.Cli version "); + 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 fed3504ce..83802f474 100644 --- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs @@ -362,7 +362,7 @@ public void Should_Overwrite_Property_Initializer_With_Argument_Value() }); // 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);