Allow custom help providers (#1259)

Allow custom help providers

* Version option will show in help even with a default command

* Reserve `-v` and `--version` as special Spectre.Console command line arguments (nb. breaking change for Spectre.Console users who have a default command with a settings class that uses either of these switches).

* Help writer correctly determines if trailing commands exist and whether to display them as optional or mandatory in the usage statement.

* Ability to control the number of indirect commands to display in the help text when the command itself doesn't have any examples of its own. Defaults to 5 (for backward compatibility) but can be set to any integer or zero to disable completely.

* Significant increase in unit test coverage for the help writer.

* Minor grammatical improvements to website documentation.
This commit is contained in:
Frank Ray 2023-09-08 08:51:33 +01:00 committed by GitHub
parent 813a53cdfa
commit 131b37fff8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 1646 additions and 330 deletions

View File

@ -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<DefaultCommand>();
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.

View File

@ -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();

View File

@ -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. `<name>`). 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. `<name>`) 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.

View File

@ -15,25 +15,25 @@ public static class Program
{
config.SetApplicationName("fake-dotnet");
config.ValidateExamples();
config.AddExample(new[] { "run", "--no-build" });
// Run
config.AddCommand<RunCommand>("run");
// Add
config.AddBranch<AddSettings>("add", add =>
{
add.SetDescription("Add a package or reference to a .NET project");
add.AddCommand<AddPackageCommand>("package");
add.AddCommand<AddReferenceCommand>("reference");
config.AddExample("run", "--no-build");
// Run
config.AddCommand<RunCommand>("run");
// Add
config.AddBranch<AddSettings>("add", add =>
{
add.SetDescription("Add a package or reference to a .NET project");
add.AddCommand<AddPackageCommand>("package");
add.AddCommand<AddReferenceCommand>("reference");
});
// Serve
config.AddCommand<ServeCommand>("serve")
.WithExample("serve", "-o", "firefox")
.WithExample("serve", "--port", "80", "-o", "firefox");
});
// Serve
config.AddCommand<ServeCommand>("serve")
.WithExample(new[] { "serve", "-o", "firefox" })
.WithExample(new[] { "serve", "--port", "80", "-o", "firefox" });
});
return app.Run(args);
}
}

View File

@ -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;
/// <summary>
/// Example showing how to extend the built-in Spectre.Console help provider
/// by rendering a custom banner at the top of the help information
/// </summary>
internal class CustomHelpProvider : HelpProvider
{
public CustomHelpProvider(ICommandAppSettings settings)
: base(settings)
{
}
public override IEnumerable<IRenderable> GetHeader(ICommandModel model, ICommandInfo? command)
{
return new[]
{
new Text("--------------------------------------"), Text.NewLine,
new Text("--- CUSTOM HELP PROVIDER ---"), Text.NewLine,
new Text("--------------------------------------"), Text.NewLine,
Text.NewLine,
};
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ExampleName>Help</ExampleName>
<ExampleDescription>Demonstrates how to extend the built-in Spectre.Console help provider to render a custom banner at the top of the help information.</ExampleDescription>
<ExampleGroup>Cli</ExampleGroup>
<ExampleVisible>false</ExampleVisible>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Shared\Shared.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,19 @@
using Spectre.Console.Cli;
namespace Help;
public static class Program
{
public static int Main(string[] args)
{
var app = new CommandApp<DefaultCommand>();
app.Configure(config =>
{
// Register the custom help provider
config.SetHelpProvider(new CustomHelpProvider(config.Settings));
});
return app.Run(args);
}
}

View File

@ -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}

View File

@ -5,7 +5,42 @@ namespace Spectre.Console.Cli;
/// and <see cref="IConfigurator{TSettings}"/>.
/// </summary>
public static class ConfiguratorExtensions
{
{
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <param name="helpProvider">The help provider to use.</param>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator SetHelpProvider(this IConfigurator configurator, IHelpProvider helpProvider)
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
configurator.SetHelpProvider(helpProvider);
return configurator;
}
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <typeparam name="T">The type of the help provider to instantiate at runtime and use.</typeparam>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator SetHelpProvider<T>(this IConfigurator configurator)
where T : IHelpProvider
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
configurator.SetHelpProvider<T>();
return configurator;
}
/// <summary>
/// Sets the name of the application.
/// </summary>

View File

@ -1,7 +1,28 @@
namespace Spectre.Console.Cli;
namespace Spectre.Console.Cli.Help;
internal static class HelpWriter
{
/// <summary>
/// The help provider for Spectre.Console.
/// </summary>
/// <remarks>
/// Other IHelpProvider implementations can be injected into the CommandApp, if desired.
/// </remarks>
public class HelpProvider : IHelpProvider
{
/// <summary>
/// Gets a value indicating how many examples from direct children to show in the help text.
/// </summary>
protected virtual int MaximumIndirectExamples { get; }
/// <summary>
/// Gets a value indicating whether any default values for command options are shown in the help text.
/// </summary>
protected virtual bool ShowOptionDefaultValues { get; }
/// <summary>
/// Gets a value indicating whether a trailing period of a command description is trimmed in the help text.
/// </summary>
protected virtual bool TrimTrailingPeriod { get; }
private sealed class HelpArgument
{
public string Name { get; }
@ -17,10 +38,10 @@ internal static class HelpWriter
Description = description;
}
public static IReadOnlyList<HelpArgument> Get(CommandInfo? command)
public static IReadOnlyList<HelpArgument> Get(ICommandInfo? command)
{
var arguments = new List<HelpArgument>();
arguments.AddRange(command?.Parameters?.OfType<CommandArgument>()?.Select(
arguments.AddRange(command?.Parameters?.OfType<ICommandArgument>()?.Select(
x => new HelpArgument(x.Value, x.Position, x.Required, x.Description))
?? Array.Empty<HelpArgument>());
return arguments;
@ -46,49 +67,75 @@ internal static class HelpWriter
DefaultValue = defaultValue;
}
public static IReadOnlyList<HelpOption> Get(CommandModel model, CommandInfo? command)
public static IReadOnlyList<HelpOption> Get(ICommandInfo? command)
{
var parameters = new List<HelpOption>();
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<CommandOption>().Where(o => !o.IsHidden).Select(o =>
parameters.AddRange(command?.Parameters.OfType<ICommandOption>().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<HelpOption>());
return parameters;
}
}
public static IEnumerable<IRenderable> Write(CommandModel model, bool writeOptionsDefaultValues)
}
/// <summary>
/// Initializes a new instance of the <see cref="HelpProvider"/> class.
/// </summary>
/// <param name="settings">The command line application settings used for configuration.</param>
public HelpProvider(ICommandAppSettings settings)
{
this.ShowOptionDefaultValues = settings.ShowOptionDefaultValues;
this.MaximumIndirectExamples = settings.MaximumIndirectExamples;
this.TrimTrailingPeriod = settings.TrimTrailingPeriod;
}
/// <inheritdoc/>
public virtual IEnumerable<IRenderable> Write(ICommandModel model, ICommandInfo? command)
{
return WriteCommand(model, null, writeOptionsDefaultValues);
}
public static IEnumerable<IRenderable> WriteCommand(CommandModel model, CommandInfo? command, bool writeOptionsDefaultValues)
{
var container = command as ICommandContainer ?? model;
var isDefaultCommand = command?.IsDefaultCommand ?? false;
var result = new List<IRenderable>();
result.AddRange(GetDescription(command));
var result = new List<IRenderable>();
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<IRenderable> GetDescription(CommandInfo? command)
}
/// <summary>
/// Gets the header for the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetHeader(ICommandModel model, ICommandInfo? command)
{
yield break;
}
/// <summary>
/// Gets the description section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetDescription(ICommandModel model, ICommandInfo? command)
{
if (command?.Description == null)
{
@ -99,13 +146,19 @@ internal static class HelpWriter
composer.Style("yellow", "DESCRIPTION:").LineBreak();
composer.Text(command.Description).LineBreak();
yield return composer.LineBreak();
}
private static IEnumerable<IRenderable> GetUsage(CommandModel model, CommandInfo? command)
}
/// <summary>
/// Gets the usage section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> 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<string>();
@ -132,18 +185,18 @@ internal static class HelpWriter
}
}
if (current.Parameters.OfType<CommandArgument>().Any())
if (current.Parameters.OfType<ICommandArgument>().Any())
{
if (isCurrent)
{
foreach (var argument in current.Parameters.OfType<CommandArgument>()
foreach (var argument in current.Parameters.OfType<ICommandArgument>()
.Where(a => a.Required).OrderBy(a => a.Position).ToArray())
{
parameters.Add($"[aqua]<{argument.Value.EscapeMarkup()}>[/]");
}
}
var optionalArguments = current.Parameters.OfType<CommandArgument>().Where(x => !x.Required).ToArray();
var optionalArguments = current.Parameters.OfType<ICommandArgument>().Where(x => !x.Required).ToArray();
if (optionalArguments.Length > 0 || !isCurrent)
{
foreach (var optionalArgument in optionalArguments)
@ -159,9 +212,27 @@ internal static class HelpWriter
}
}
if (command.IsBranch)
{
if (command.IsBranch && command.DefaultCommand == null)
{
// The user must specify the command
parameters.Add("[aqua]<COMMAND>[/]");
}
else if (command.IsBranch && command.DefaultCommand != null && command.Commands.Count > 0)
{
// We are on a branch with a default command
// The user can optionally specify the command
parameters.Add("[aqua][[COMMAND]][/]");
}
else if (command.IsDefaultCommand)
{
var commands = model.Commands.Where(x => !x.IsHidden && !x.IsDefaultCommand).ToList();
if (commands.Count > 0)
{
// Commands other than the default are present
// So make these optional in the usage statement
parameters.Add("[aqua][[COMMAND]][/]");
}
}
}
@ -172,37 +243,48 @@ internal static class HelpWriter
{
composer,
};
}
private static IEnumerable<IRenderable> GetExamples(CommandModel model, CommandInfo? command)
}
/// <summary>
/// Gets the examples section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
/// <remarks>
/// Examples from the command's direct children are used
/// if no examples have been set on the specified command or model.
/// </remarks>
public virtual IEnumerable<IRenderable> GetExamples(ICommandModel model, ICommandInfo? command)
{
var maxExamples = int.MaxValue;
var examples = command?.Examples ?? model.Examples ?? new List<string[]>();
var examples = command?.Examples?.ToList() ?? model.Examples?.ToList() ?? new List<string[]>();
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<ICommandContainer>(new[] { root });
// Start at the current command (if exists)
// or alternatively commence at the model.
var commandContainer = command ?? (ICommandContainer)model;
var queue = new Queue<ICommandContainer>(new[] { commandContainer });
// Traverse the command tree and look for examples.
// Traverse the command tree and look for examples.
// As soon as a node contains commands, bail.
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var cmd in current.Commands.Where(x => !x.IsHidden))
foreach (var child in current.Commands.Where(x => !x.IsHidden))
{
if (cmd.Examples.Count > 0)
if (child.Examples.Count > 0)
{
examples.AddRange(cmd.Examples);
examples.AddRange(child.Examples);
}
queue.Enqueue(cmd);
queue.Enqueue(child);
}
if (examples.Count >= maxExamples)
@ -212,7 +294,7 @@ internal static class HelpWriter
}
}
if (examples.Count > 0)
if (Math.Min(maxExamples, examples.Count) > 0)
{
var composer = new Composer();
composer.LineBreak();
@ -221,7 +303,7 @@ internal static class HelpWriter
for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++)
{
var args = string.Join(" ", examples[index]);
composer.Tab().Text(model.GetApplicationName()).Space().Style("grey", args);
composer.Tab().Text(model.ApplicationName).Space().Style("grey", args);
composer.LineBreak();
}
@ -229,9 +311,15 @@ internal static class HelpWriter
}
return Array.Empty<IRenderable>();
}
private static IEnumerable<IRenderable> GetArguments(CommandInfo? command)
}
/// <summary>
/// Gets the arguments section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetArguments(ICommandModel model, ICommandInfo? command)
{
var arguments = HelpArgument.Get(command);
if (arguments.Count == 0)
@ -267,12 +355,18 @@ internal static class HelpWriter
result.Add(grid);
return result;
}
private static IEnumerable<IRenderable> GetOptions(CommandModel model, CommandInfo? command, bool writeDefaultValues)
}
/// <summary>
/// Gets the options section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> 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<IRenderable>();
@ -286,7 +380,7 @@ internal static class HelpWriter
};
var helpOptions = parameters.ToArray();
var defaultValueColumn = writeDefaultValues && helpOptions.Any(e => e.DefaultValue != null);
var defaultValueColumn = ShowOptionDefaultValues && helpOptions.Any(e => e.DefaultValue != null);
var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true });
@ -369,14 +463,20 @@ internal static class HelpWriter
result.Add(grid);
return result;
}
}
/// <summary>
/// Gets the commands section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetCommands(ICommandModel model, ICommandInfo? command)
{
var commandContainer = command ?? (ICommandContainer)model;
bool isDefaultCommand = command?.IsDefaultCommand ?? false;
private static IEnumerable<IRenderable> GetCommands(
CommandModel model,
ICommandContainer command,
bool isDefaultCommand)
{
var commands = isDefaultCommand ? model.Commands : command.Commands;
var commands = isDefaultCommand ? model.Commands : commandContainer.Commands;
commands = commands.Where(x => !x.IsHidden).ToList();
if (commands.Count == 0)
@ -407,7 +507,7 @@ internal static class HelpWriter
arguments.Space();
}
if (model.TrimTrailingPeriod)
if (TrimTrailingPeriod)
{
grid.AddRow(
arguments.ToString().TrimEnd(),
@ -424,5 +524,16 @@ internal static class HelpWriter
result.Add(grid);
return result;
}
/// <summary>
/// Gets the footer for the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetFooter(ICommandModel model, ICommandInfo? command)
{
yield break;
}
}

View File

@ -0,0 +1,17 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Represents a command argument.
/// </summary>
public interface ICommandArgument : ICommandParameter
{
/// <summary>
/// Gets the value of the argument.
/// </summary>
string Value { get; }
/// <summary>
/// Gets the position of the argument.
/// </summary>
int Position { get; }
}

View File

@ -0,0 +1,25 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Represents a command container.
/// </summary>
public interface ICommandContainer
{
/// <summary>
/// Gets all the examples for the container.
/// </summary>
IReadOnlyList<string[]> Examples { get; }
/// <summary>
/// Gets all commands in the container.
/// </summary>
IReadOnlyList<ICommandInfo> Commands { get; }
/// <summary>
/// Gets the default command for the container.
/// </summary>
/// <remarks>
/// Returns null if a default command has not been set.
/// </remarks>
ICommandInfo? DefaultCommand { get; }
}

View File

@ -0,0 +1,42 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Represents an executable command.
/// </summary>
public interface ICommandInfo : ICommandContainer
{
/// <summary>
/// Gets the name of the command.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the description of the command.
/// </summary>
string? Description { get; }
/// <summary>
/// Gets a value indicating whether the command is a branch.
/// </summary>
bool IsBranch { get; }
/// <summary>
/// Gets a value indicating whether the command is the default command within its container.
/// </summary>
bool IsDefaultCommand { get; }
/// <summary>
/// Gets a value indicating whether the command is hidden.
/// </summary>
bool IsHidden { get; }
/// <summary>
/// Gets the parameters associated with the command.
/// </summary>
IReadOnlyList<ICommandParameter> Parameters { get; }
/// <summary>
/// Gets the parent command, if any.
/// </summary>
ICommandInfo? Parent { get; }
}

View File

@ -0,0 +1,23 @@
namespace Spectre.Console.Cli.Help;
internal static class ICommandInfoExtensions
{
/// <summary>
/// Walks up the command.Parent tree, adding each command into a list as it goes.
/// </summary>
/// <remarks>The first command added to the list is the current (ie. this one).</remarks>
/// <returns>The list of commands from current to root, as traversed by <see cref="CommandInfo.Parent"/>.</returns>
public static List<ICommandInfo> Flatten(this ICommandInfo commandInfo)
{
var result = new Stack<Help.ICommandInfo>();
var current = commandInfo;
while (current != null)
{
result.Push(current);
current = current.Parent;
}
return result.ToList();
}
}

View File

@ -0,0 +1,12 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Represents a command model.
/// </summary>
public interface ICommandModel : ICommandContainer
{
/// <summary>
/// Gets the name of the application.
/// </summary>
string ApplicationName { get; }
}

View File

@ -0,0 +1,27 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Represents a command option.
/// </summary>
public interface ICommandOption : ICommandParameter
{
/// <summary>
/// Gets the long names of the option.
/// </summary>
IReadOnlyList<string> LongNames { get; }
/// <summary>
/// Gets the short names of the option.
/// </summary>
IReadOnlyList<string> ShortNames { get; }
/// <summary>
/// Gets the value name of the option, if applicable.
/// </summary>
string? ValueName { get; }
/// <summary>
/// Gets a value indicating whether the option value is optional.
/// </summary>
bool ValueIsOptional { get; }
}

View File

@ -0,0 +1,32 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Represents a command parameter.
/// </summary>
public interface ICommandParameter
{
/// <summary>
/// Gets a value indicating whether the parameter is a flag.
/// </summary>
bool IsFlag { get; }
/// <summary>
/// Gets a value indicating whether the parameter is required.
/// </summary>
bool Required { get; }
/// <summary>
/// Gets the description of the parameter.
/// </summary>
string? Description { get; }
/// <summary>
/// Gets the default value of the parameter, if specified.
/// </summary>
DefaultValueAttribute? DefaultValue { get; }
/// <summary>
/// Gets a value indicating whether the parameter is hidden.
/// </summary>
bool IsHidden { get; }
}

View File

@ -0,0 +1,20 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// The help provider interface for Spectre.Console.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public interface IHelpProvider
{
/// <summary>
/// Writes help information for the application.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects representing the help information.</returns>
IEnumerable<IRenderable> Write(ICommandModel model, ICommandInfo? command);
}

View File

@ -13,12 +13,22 @@ public interface ICommandAppSettings
/// <summary>
/// Gets or sets the application version (use it to override auto-detected value).
/// </summary>
string? ApplicationVersion { get; set; }
/// <summary>
/// Gets or sets a value indicating whether any default values for command options are shown in the help text.
/// </summary>
bool ShowOptionDefaultValues { get; set; }
string? ApplicationVersion { get; set; }
/// <summary>
/// Gets or sets a value indicating how many examples from direct children to show in the help text.
/// </summary>
int MaximumIndirectExamples { get; set; }
/// <summary>
/// Gets or sets a value indicating whether any default values for command options are shown in the help text.
/// </summary>
bool ShowOptionDefaultValues { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a trailing period of a command description is trimmed in the help text.
/// </summary>
bool TrimTrailingPeriod { get; set; }
/// <summary>
/// Gets or sets the <see cref="IAnsiConsole"/>.
@ -41,11 +51,6 @@ public interface ICommandAppSettings
/// </summary>
CaseSensitivity CaseSensitivity { get; set; }
/// <summary>
/// Gets or sets a value indicating whether trailing period of a description is trimmed.
/// </summary>
bool TrimTrailingPeriod { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not parsing is strict.
/// </summary>

View File

@ -4,7 +4,20 @@ namespace Spectre.Console.Cli;
/// Represents a configurator.
/// </summary>
public interface IConfigurator
{
{
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <param name="helpProvider">The help provider to use.</param>
public void SetHelpProvider(IHelpProvider helpProvider);
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <typeparam name="T">The type of the help provider to instantiate at runtime and use.</typeparam>
public void SetHelpProvider<T>()
where T : IHelpProvider;
/// <summary>
/// Gets the command app settings.
/// </summary>
@ -53,5 +66,5 @@ public interface IConfigurator
/// <param name="action">The command branch configurator.</param>
/// <returns>A branch configurator that can be used to configure the branch further.</returns>
IBranchConfigurator AddBranch<TSettings>(string name, Action<IConfigurator<TSettings>> action)
where TSettings : CommandSettings;
where TSettings : CommandSettings;
}

View File

@ -17,7 +17,7 @@ public interface IConfigurator<in TSettings>
/// Adds an example of how to use the branch.
/// </summary>
/// <param name="args">The example arguments.</param>
void AddExample(string[] args);
void AddExample(params string[] args);
/// <summary>
/// Adds a default command.

View File

@ -8,85 +8,87 @@ internal sealed class CommandExecutor
{
_registrar = registrar ?? throw new ArgumentNullException(nameof(registrar));
_registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor));
}
}
public async Task<int> Execute(IConfiguration configuration, IEnumerable<string> 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<string>();
_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<string> args)
#pragma warning disable CS8603 // Possible null reference return.
private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable<string> args)
{
var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments);
@ -113,7 +115,8 @@ internal sealed class CommandExecutor
return parsedResult;
}
#pragma warning restore CS8603 // Possible null reference return.
private static string ResolveApplicationVersion(IConfiguration configuration)
{
return

View File

@ -35,11 +35,11 @@ internal sealed class ComponentRegistry : IDisposable
foreach (var type in new HashSet<Type>(registration.RegistrationTypes))
{
if (!_registrations.ContainsKey(type))
{
_registrations.Add(type, new HashSet<ComponentRegistration>());
{
// Only add each registration type once.
_registrations.Add(type, new HashSet<ComponentRegistration>());
_registrations[type].Add(registration);
}
_registrations[type].Add(registration);
}
}

View File

@ -4,7 +4,8 @@ internal sealed class CommandAppSettings : ICommandAppSettings
{
public string? ApplicationName { get; set; }
public string? ApplicationVersion { get; set; }
public bool ShowOptionDefaultValues { get; set; }
public int MaximumIndirectExamples { get; set; }
public bool ShowOptionDefaultValues { get; set; }
public IAnsiConsole? Console { get; set; }
public ICommandInterceptor? Interceptor { get; set; }
public ITypeRegistrarFrontend Registrar { get; set; }
@ -24,7 +25,8 @@ internal sealed class CommandAppSettings : ICommandAppSettings
{
Registrar = new TypeRegistrar(registrar);
CaseSensitivity = CaseSensitivity.All;
ShowOptionDefaultValues = true;
ShowOptionDefaultValues = true;
MaximumIndirectExamples = 5;
}
public bool IsTrue(Func<CommandAppSettings, bool> func, string environmentVariableName)

View File

@ -19,6 +19,19 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
Settings = new CommandAppSettings(registrar);
Examples = new List<string[]>();
}
public void SetHelpProvider(IHelpProvider helpProvider)
{
// Register the help provider
_registrar.RegisterInstance(typeof(IHelpProvider), helpProvider);
}
public void SetHelpProvider<T>()
where T : IHelpProvider
{
// Register the help provider
_registrar.Register(typeof(IHelpProvider), typeof(T));
}
public void AddExample(params string[] args)
{

View File

@ -17,7 +17,7 @@ internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConf
_command.Description = description;
}
public void AddExample(string[] args)
public void AddExample(params string[] args)
{
_command.Examples.Add(args);
}

View File

@ -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; }

View File

@ -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<string> 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<ICommandInfo> Help.ICommandContainer.Commands => Children.Cast<ICommandInfo>().ToList();
ICommandInfo? Help.ICommandContainer.DefaultCommand => DefaultCommand;
IReadOnlyList<ICommandParameter> ICommandInfo.Parameters => Parameters.Cast<ICommandParameter>().ToList();
ICommandInfo? ICommandInfo.Parent => Parent;
IReadOnlyList<string[]> Help.ICommandContainer.Examples => (IReadOnlyList<string[]>)Examples;
public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype)
{
Parent = parent;
@ -48,19 +54,5 @@ internal sealed class CommandInfo : ICommandContainer
Description = description.Description;
}
}
}
public List<CommandInfo> Flatten()
{
var result = new Stack<CommandInfo>();
var current = this;
while (current != null)
{
result.Push(current);
current = current.Parent;
}
return result.ToList();
}
}
}

View File

@ -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<CommandInfo> Commands { get; }
public IList<string[]> 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<ICommandInfo> Help.ICommandContainer.Commands => Commands.Cast<ICommandInfo>().ToList();
ICommandInfo? Help.ICommandContainer.DefaultCommand => DefaultCommand;
IReadOnlyList<string[]> Help.ICommandContainer.Examples => (IReadOnlyList<string[]>)Examples;
public CommandModel(
CommandAppSettings settings,
@ -17,22 +21,32 @@ internal sealed class CommandModel : ICommandContainer
{
ApplicationName = settings.ApplicationName;
ParsingMode = settings.ParsingMode;
TrimTrailingPeriod = settings.TrimTrailingPeriod;
Commands = new List<CommandInfo>(commands ?? Array.Empty<CommandInfo>());
Examples = new List<string[]>(examples ?? Array.Empty<string[]>());
}
public string GetApplicationName()
}
/// <summary>
/// Gets the name of the application.
/// If the provided <paramref name="applicationName"/> is not null or empty,
/// it is returned. Otherwise the name of the current application
/// is determined based on the executable file's name.
/// </summary>
/// <param name="applicationName">The optional name of the application.</param>
/// <returns>
/// The name of the application, or a default value of "?" if no valid application name can be determined.
/// </returns>
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

View File

@ -1,6 +1,6 @@
namespace Spectre.Console.Cli;
internal sealed class CommandOption : CommandParameter
internal sealed class CommandOption : CommandParameter, ICommandOption
{
public IReadOnlyList<string> LongNames { get; }
public IReadOnlyList<string> ShortNames { get; }

View File

@ -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,

View File

@ -10,6 +10,7 @@ global using System.Linq;
global using System.Reflection;
global using System.Text;
global using System.Threading.Tasks;
global using System.Xml;
global using System.Xml;
global using Spectre.Console.Cli.Help;
global using Spectre.Console.Cli.Unsafe;
global using Spectre.Console.Rendering;
global using Spectre.Console.Rendering;

View File

@ -35,10 +35,12 @@ public sealed class FakeTypeResolver : ITypeResolver
}
if (_registrations.TryGetValue(type, out var registrations))
{
{
// The type might be an interface, but the registration should be a class.
// So call CreateInstance on the first registration rather than the type.
return registrations.Count == 0
? null
: Activator.CreateInstance(type);
? null
: Activator.CreateInstance(registrations[0]);
}
return null;

View File

@ -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<IRenderable> 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<IRenderable> GetFooter(ICommandModel model, ICommandInfo command)
{
return new IRenderable[]
{
Text.NewLine,
new Text($"Version {version}"),
};
}
}

View File

@ -0,0 +1,21 @@
using Spectre.Console.Rendering;
namespace Spectre.Console.Cli.Tests.Data.Help;
internal class RedirectHelpProvider : IHelpProvider
{
public virtual IEnumerable<IRenderable> Write(ICommandModel model)
{
return Write(model, null);
}
#nullable enable
public virtual IEnumerable<IRenderable> Write(ICommandModel model, ICommandInfo? command)
#nullable disable
{
return new[]
{
new Text("Help has moved online. Please see: http://www.example.com"),
Text.NewLine,
};
}
}

View File

@ -1,5 +0,0 @@
namespace Spectre.Console.Tests.Data;
public sealed class EmptySettings : CommandSettings
{
}

View File

@ -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;
}

View File

@ -9,4 +9,5 @@ ARGUMENTS:
[QUX]
OPTIONS:
-h, --help Prints help information
-h, --help Prints help information
-v, --version Prints version information

View File

@ -0,0 +1,18 @@
DESCRIPTION:
Contains settings for a cat.
USAGE:
myapp cat [LEGS] [OPTIONS] <COMMAND>
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 <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
COMMANDS:
lion <TEETH> The lion command

View File

@ -0,0 +1,11 @@
USAGE:
myapp branch [GREETING] [OPTIONS] [COMMAND]
ARGUMENTS:
[GREETING]
OPTIONS:
-h, --help Prints help information
COMMANDS:
greeter

View File

@ -0,0 +1,30 @@
DESCRIPTION:
The animal command.
USAGE:
myapp animal [LEGS] [OPTIONS] <COMMAND>
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 <AGE> The dog command
horse The horse command

View File

@ -1,19 +0,0 @@
DESCRIPTION:
The animal command.
USAGE:
myapp animal [LEGS] [OPTIONS] <COMMAND>
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 <AGE> The dog command
horse The horse command

View File

@ -0,0 +1,15 @@
--------------------------------------
--- CUSTOM HELP PROVIDER ---
--------------------------------------
USAGE:
myapp [OPTIONS] <COMMAND>
OPTIONS:
-h, --help Prints help information
-v, --version Prints version information
COMMANDS:
dog <AGE> The dog command
Version 1.0

View File

@ -0,0 +1 @@
Help has moved online. Please see: http://www.example.com

View File

@ -0,0 +1,15 @@
--------------------------------------
--- CUSTOM HELP PROVIDER ---
--------------------------------------
USAGE:
myapp [OPTIONS] <COMMAND>
OPTIONS:
-h, --help Prints help information
-v, --version Prints version information
COMMANDS:
dog <AGE> The dog command
Version 1.0

View File

@ -0,0 +1 @@
Help has moved online. Please see: http://www.example.com

View File

@ -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 <VALUE>
--agility <VALUE> 10 The agility between 0 and 100

View File

@ -1,21 +0,0 @@
DESCRIPTION:
The lion command.
USAGE:
myapp <TEETH> [LEGS] [OPTIONS]
EXAMPLES:
myapp 12 -c 3
ARGUMENTS:
<TEETH> The number of teeth the lion has
[LEGS] The number of legs
OPTIONS:
DEFAULT
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting

View File

@ -0,0 +1,15 @@
--------------------------------------
--- CUSTOM HELP PROVIDER ---
--------------------------------------
USAGE:
myapp [OPTIONS] <COMMAND>
OPTIONS:
-h, --help Prints help information
-v, --version Prints version information
COMMANDS:
dog <AGE> The dog command
Version 1.0

View File

@ -0,0 +1,24 @@
DESCRIPTION:
The dog command.
USAGE:
myapp <AGE> [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:
<AGE>
[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 <VALUE>
-g, --good-boy

View File

@ -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 <VALUE>
--agility <VALUE> 10 The agility between 0 and 100

View File

@ -1,8 +1,8 @@
DESCRIPTION:
DESCRIPTION:
The lion command.
USAGE:
myapp <TEETH> [LEGS] [OPTIONS]
myapp <TEETH> [LEGS] [OPTIONS] [COMMAND]
ARGUMENTS:
<TEETH> 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 <VALUE>
--agility <VALUE> 10 The agility between 0 and 100

View File

@ -5,5 +5,6 @@ ARGUMENTS:
<FOO> Dummy argument FOO
OPTIONS:
-h, --help Prints help information
--baz Dummy option BAZ
-h, --help Prints help information
-v, --version Prints version information
--baz Dummy option BAZ

View File

@ -0,0 +1,24 @@
USAGE:
myapp [OPTIONS] <COMMAND>
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 <AGE> The dog command
horse The horse command

View File

@ -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

View File

@ -0,0 +1,20 @@
USAGE:
myapp [OPTIONS] <COMMAND>
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 <AGE> The dog command
horse The horse command

View File

@ -1,14 +1,10 @@
USAGE:
myapp [OPTIONS] <COMMAND>
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 <AGE> The dog command
USAGE:
myapp [OPTIONS] <COMMAND>
OPTIONS:
-h, --help Prints help information
-v, --version Prints version information
COMMANDS:
dog <AGE> The dog command
horse The horse command

View File

@ -0,0 +1,24 @@
USAGE:
myapp [OPTIONS] <COMMAND>
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 <AGE> The dog command
horse The horse command

View File

@ -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

View File

@ -0,0 +1,19 @@
USAGE:
myapp [OPTIONS] <COMMAND>
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

View File

@ -0,0 +1,9 @@
USAGE:
myapp [OPTIONS] <COMMAND>
OPTIONS:
-h, --help Prints help information
-v, --version Prints version information
COMMANDS:
animal The animal command

View File

@ -0,0 +1,23 @@
USAGE:
myapp [OPTIONS] <COMMAND>
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

View File

@ -7,7 +7,8 @@ global using System.Linq;
global using System.Runtime.CompilerServices;
global using System.Threading.Tasks;
global using Shouldly;
global using Spectre.Console.Cli;
global using Spectre.Console.Cli;
global using Spectre.Console.Cli.Help;
global using Spectre.Console.Cli.Unsafe;
global using Spectre.Console.Testing;
global using Spectre.Console.Tests.Data;

View File

@ -34,7 +34,7 @@
<ParentFile>$([System.String]::Copy('%(FileName)').Split('.')[0])</ParentFile>
<DependentUpon>%(ParentFile).cs</DependentUpon>
</None>
<None Update="Expectations\Help\Greeter_Default.Output.verified.txt">
<None Update="Expectations\Help\Default_Greeter.Output.verified.txt">
<ParentFile>$([System.String]::Copy('%(FileName)').Split('.')[0])</ParentFile>
<DependentUpon>%(ParentFile).cs</DependentUpon>
</None>

View File

@ -1,3 +1,5 @@
using Spectre.Console.Cli.Tests.Data.Help;
namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests
@ -75,8 +77,8 @@ public sealed partial class CommandAppTests
}
[Fact]
[Expectation("Command")]
public Task Should_Output_Command_Correctly()
[Expectation("Branch")]
public Task Should_Output_Branch_Correctly()
{
// Given
var fixture = new CommandAppTester();
@ -91,7 +93,53 @@ public sealed partial class CommandAppTests
});
// When
var result = fixture.Run("cat", "--help");
var result = fixture.Run("cat", "--help");
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Branch_Called_Without_Help")]
public Task Should_Output_Branch_When_Called_Without_Help_Option()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
configurator.AddBranch<CatSettings>("cat", animal =>
{
animal.SetDescription("Contains settings for a cat.");
animal.AddCommand<LionCommand>("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<OptionalArgumentWithDefaultValueSettings>("branch", animal =>
{
animal.SetDefaultCommand<GreeterCommand>();
animal.AddCommand<GreeterCommand>("greeter");
});
});
// When
var result = fixture.Run("branch", "--help");
// Then
return Verifier.Verify(result.Output);
@ -138,7 +186,7 @@ public sealed partial class CommandAppTests
});
// When
var result = fixture.Run("cat", "lion", "--help");
var result = fixture.Run("cat", "lion", "--help");
// Then
return Verifier.Verify(result.Output);
@ -203,7 +251,7 @@ public sealed partial class CommandAppTests
}
[Fact]
[Expectation("Greeter_Default")]
[Expectation("Default_Greeter")]
public Task Should_Not_Output_Default_Command_When_Command_Has_No_Required_Parameters_And_Is_Called_Without_Args()
{
// Given
@ -219,19 +267,131 @@ public sealed partial class CommandAppTests
// Then
return Verifier.Verify(result.Output);
}
}
[Fact]
[Expectation("Custom_Help_Registered_By_Instance")]
public Task Should_Output_Custom_Help_When_Registered_By_Instance()
{
var registrar = new DefaultTypeRegistrar();
// Given
var fixture = new CommandAppTester(registrar);
fixture.Configure(configurator =>
{
// Create the custom help provider
var helpProvider = new CustomHelpProvider(configurator.Settings, "1.0");
// Register the custom help provider instance
registrar.RegisterInstance(typeof(IHelpProvider), helpProvider);
configurator.SetApplicationName("myapp");
configurator.AddCommand<DogCommand>("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<DogCommand>("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<DogCommand>("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<RedirectHelpProvider>();
configurator.SetApplicationName("myapp");
configurator.AddCommand<DogCommand>("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<DogCommand>("dog");
configurator.AddCommand<HorseCommand>("horse");
});
@ -241,21 +401,147 @@ public sealed partial class CommandAppTests
// Then
return Verifier.Verify(result.Output);
}
}
[Fact]
[Expectation("RootExamples_Children")]
public Task Should_Output_Root_Examples_Defined_On_Direct_Children_If_Root_Have_No_Examples()
[Expectation("Root_Examples_Children")]
[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:SingleLineCommentsMustNotBeFollowedByBlankLine", Justification = "Single line comment is relevant to several code blocks that follow.")]
public Task Should_Output_Examples_Defined_On_Direct_Children_If_Root_Has_No_Examples()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
configurator.SetApplicationName("myapp");
// It should be capped to the first 5 examples by default
configurator.AddCommand<DogCommand>("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<HorseCommand>("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<DogCommand>("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<HorseCommand>("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<DogCommand>("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<HorseCommand>("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<DogCommand>("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<HorseCommand>("horse")
.WithExample("horse", "--name", "Brutus")
.WithExample("horse", "--name", "Sugar", "--IsAlive", "false")
.WithExample("horse", "--name", "Cash")
.WithExample("horse", "--name", "Dakota")
.WithExample("horse", "--name", "Cisco")
.WithExample("horse", "--name", "Spirit");
});
// When
@ -266,8 +552,9 @@ public sealed partial class CommandAppTests
}
[Fact]
[Expectation("RootExamples_Leafs")]
public Task Should_Output_Root_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found()
[Expectation("Root_Examples_Leafs")]
[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:SingleLineCommentsMustNotBeFollowedByBlankLine", Justification = "Single line comment is relevant to several code blocks that follow.")]
public Task Should_Output_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found()
{
// Given
var fixture = new CommandAppTester();
@ -276,11 +563,148 @@ public sealed partial class CommandAppTests
configurator.SetApplicationName("myapp");
configurator.AddBranch<AnimalSettings>("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<DogCommand>("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<HorseCommand>("horse")
.WithExample("animal", "horse", "--name", "Brutus");
.WithExample("animal", "horse", "--name", "Brutus")
.WithExample("animal", "horse", "--name", "Sugar", "--IsAlive", "false")
.WithExample("animal", "horse", "--name", "Cash")
.WithExample("animal", "horse", "--name", "Dakota")
.WithExample("animal", "horse", "--name", "Cisco")
.WithExample("animal", "horse", "--name", "Spirit");
});
});
// When
var result = fixture.Run("--help");
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Root_Examples_Leafs_Eight")]
public Task Should_Output_Eight_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
configurator.AddBranch<AnimalSettings>("animal", animal =>
{
animal.SetDescription("The animal command.");
// Show the first 8 examples defined on the direct children
configurator.Settings.MaximumIndirectExamples = 8;
animal.AddCommand<DogCommand>("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<HorseCommand>("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<AnimalSettings>("animal", animal =>
{
animal.SetDescription("The animal command.");
// Show all examples defined on the direct children
configurator.Settings.MaximumIndirectExamples = int.MaxValue;
animal.AddCommand<DogCommand>("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<HorseCommand>("horse")
.WithExample("animal", "horse", "--name", "Brutus")
.WithExample("animal", "horse", "--name", "Sugar", "--IsAlive", "false")
.WithExample("animal", "horse", "--name", "Cash")
.WithExample("animal", "horse", "--name", "Dakota")
.WithExample("animal", "horse", "--name", "Cisco")
.WithExample("animal", "horse", "--name", "Spirit");
});
});
// When
var result = fixture.Run("--help");
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Root_Examples_Leafs_None")]
public Task Should_Not_Output_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
configurator.AddBranch<AnimalSettings>("animal", animal =>
{
animal.SetDescription("The animal command.");
// Do not show examples defined on the direct children
configurator.Settings.MaximumIndirectExamples = 0;
animal.AddCommand<DogCommand>("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<HorseCommand>("horse")
.WithExample("animal", "horse", "--name", "Brutus")
.WithExample("animal", "horse", "--name", "Sugar", "--IsAlive", "false")
.WithExample("animal", "horse", "--name", "Cash")
.WithExample("animal", "horse", "--name", "Dakota")
.WithExample("animal", "horse", "--name", "Cisco")
.WithExample("animal", "horse", "--name", "Spirit");
});
});
@ -292,18 +716,31 @@ public sealed partial class CommandAppTests
}
[Fact]
[Expectation("CommandExamples")]
public Task Should_Only_Output_Command_Examples_Defined_On_Command()
[Expectation("Branch_Examples")]
public Task Should_Output_Examples_Defined_On_Branch()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
configurator.SetApplicationName("myapp");
configurator.AddBranch<AnimalSettings>("animal", animal =>
{
animal.SetDescription("The animal command.");
animal.AddExample(new[] { "animal", "--help" });
animal.SetDescription("The animal command.");
// All branch examples should be shown
animal.AddExample("animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy");
animal.AddExample("animal", "dog", "--name", "Luna");
animal.AddExample("animal", "dog", "--name", "Charlie");
animal.AddExample("animal", "dog", "--name", "Bella");
animal.AddExample("animal", "dog", "--name", "Daisy");
animal.AddExample("animal", "dog", "--name", "Milo");
animal.AddExample("animal", "horse", "--name", "Brutus");
animal.AddExample("animal", "horse", "--name", "Sugar", "--IsAlive", "false");
animal.AddExample("animal", "horse", "--name", "Cash");
animal.AddExample("animal", "horse", "--name", "Dakota");
animal.AddExample("animal", "horse", "--name", "Cisco");
animal.AddExample("animal", "horse", "--name", "Spirit");
animal.AddCommand<DogCommand>("dog")
.WithExample("animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy");
@ -317,19 +754,26 @@ public sealed partial class CommandAppTests
// Then
return Verifier.Verify(result.Output);
}
}
[Fact]
[Expectation("DefaultExamples")]
public Task Should_Output_Root_Examples_If_Default_Command_Is_Specified()
[Expectation("Default_Examples")]
public Task Should_Output_Examples_Defined_On_Root_If_Default_Command_Is_Specified()
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<LionCommand>();
fixture.SetDefaultCommand<DogCommand>();
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

View File

@ -5,27 +5,92 @@ public sealed partial class CommandAppTests
public sealed class Version
{
[Fact]
public void Should_Output_The_Version_To_The_Console()
public void Should_Output_CLI_Version_To_The_Console()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(config =>
{
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddBranch<MammalSettings>("mammal", mammal =>
{
mammal.AddCommand<DogCommand>("dog");
mammal.AddCommand<HorseCommand>("horse");
});
});
});
// When
var result = fixture.Run(Constants.VersionCommand);
// Then
result.Output.ShouldStartWith("Spectre.Cli version ");
}
[Fact]
public void Should_Output_Application_Version_To_The_Console_With_No_Command()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
});
// When
var result = fixture.Run("--version");
// Then
result.Output.ShouldBe("1.0");
}
[Fact]
public void Should_Output_Application_Version_To_The_Console_With_Command()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
configurator.AddCommand<EmptyCommand>("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<EmptyCommand>();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
});
// When
var result = fixture.Run("--version");
// Then
result.Output.ShouldBe("1.0");
}
[Fact]
public void Should_Output_Application_Version_To_The_Console_With_Branch_Default_Command()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
configurator.AddBranch<EmptyCommandSettings>("branch", branch =>
{
branch.SetDefaultCommand<EmptyCommand>();
});
});
// When
var result = fixture.Run("--version");
// Then
result.Output.ShouldBe("1.0");
}
}
}

View File

@ -362,7 +362,7 @@ public sealed partial class CommandAppTests
});
// When
var result = app.Run("-c", "0", "-v", "50", "ABBA", "Herreys");
var result = app.Run("-c", "0", "--value", "50", "ABBA", "Herreys");
// Then
result.ExitCode.ShouldBe(0);