Skip to content

Commit 131b37f

Browse files
authored
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.
1 parent 813a53c commit 131b37f

File tree

70 files changed

+1647
-331
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+1647
-331
lines changed

docs/input/cli/command-help.md

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Title: Command Help
2+
Order: 13
3+
Description: "Console applications built with *Spectre.Console.Cli* include automatically generated help command line help."
4+
---
5+
6+
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.
7+
8+
The automatically generated help is derived from the configured commands and their command settings.
9+
10+
The help is also context aware and tailored depending on what has been specified on the command line before it. For example,
11+
12+
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.
13+
14+
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.
15+
16+
`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`.
17+
18+
## Custom help providers
19+
20+
Whilst it shouldn't be common place to implement your own help provider, it is however possible.
21+
22+
You are able to implement your own `IHelpProvider` and configure a `CommandApp` to use that instead of the Spectre.Console help provider.
23+
24+
```csharp
25+
using Spectre.Console.Cli;
26+
27+
namespace Help;
28+
29+
public static class Program
30+
{
31+
public static int Main(string[] args)
32+
{
33+
var app = new CommandApp<DefaultCommand>();
34+
35+
app.Configure(config =>
36+
{
37+
// Register the custom help provider
38+
config.SetHelpProvider(new CustomHelpProvider(config.Settings));
39+
});
40+
41+
return app.Run(args);
42+
}
43+
}
44+
```
45+
46+
There is a working [example of a custom help provider](https://github.com/spectreconsole/spectre.console/tree/main/examples/Cli/Help) demonstrating this.
47+

docs/input/cli/commandApp.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ For more complex command hierarchical configurations, they can also be composed
4343

4444
## Customizing Command Configurations
4545

46-
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.
46+
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.
4747

4848
``` csharp
4949
var app = new CommandApp();

docs/input/cli/settings.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ This setting file tells `Spectre.Console.Cli` that our command has two parameter
2626

2727
## CommandArgument
2828

29-
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.
29+
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.
3030

3131
The position is used for scenarios where there could be more than one argument.
3232

examples/Cli/Demo/Program.cs

+17-17
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,25 @@ public static int Main(string[] args)
1515
{
1616
config.SetApplicationName("fake-dotnet");
1717
config.ValidateExamples();
18-
config.AddExample(new[] { "run", "--no-build" });
19-
20-
// Run
21-
config.AddCommand<RunCommand>("run");
22-
23-
// Add
24-
config.AddBranch<AddSettings>("add", add =>
25-
{
26-
add.SetDescription("Add a package or reference to a .NET project");
27-
add.AddCommand<AddPackageCommand>("package");
28-
add.AddCommand<AddReferenceCommand>("reference");
18+
config.AddExample("run", "--no-build");
19+
20+
// Run
21+
config.AddCommand<RunCommand>("run");
22+
23+
// Add
24+
config.AddBranch<AddSettings>("add", add =>
25+
{
26+
add.SetDescription("Add a package or reference to a .NET project");
27+
add.AddCommand<AddPackageCommand>("package");
28+
add.AddCommand<AddReferenceCommand>("reference");
29+
});
30+
31+
// Serve
32+
config.AddCommand<ServeCommand>("serve")
33+
.WithExample("serve", "-o", "firefox")
34+
.WithExample("serve", "--port", "80", "-o", "firefox");
2935
});
3036

31-
// Serve
32-
config.AddCommand<ServeCommand>("serve")
33-
.WithExample(new[] { "serve", "-o", "firefox" })
34-
.WithExample(new[] { "serve", "--port", "80", "-o", "firefox" });
35-
});
36-
3737
return app.Run(args);
3838
}
3939
}
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Linq;
2+
using Spectre.Console;
3+
using Spectre.Console.Cli;
4+
using Spectre.Console.Cli.Help;
5+
using Spectre.Console.Rendering;
6+
7+
namespace Help;
8+
9+
/// <summary>
10+
/// Example showing how to extend the built-in Spectre.Console help provider
11+
/// by rendering a custom banner at the top of the help information
12+
/// </summary>
13+
internal class CustomHelpProvider : HelpProvider
14+
{
15+
public CustomHelpProvider(ICommandAppSettings settings)
16+
: base(settings)
17+
{
18+
}
19+
20+
public override IEnumerable<IRenderable> GetHeader(ICommandModel model, ICommandInfo? command)
21+
{
22+
return new[]
23+
{
24+
new Text("--------------------------------------"), Text.NewLine,
25+
new Text("--- CUSTOM HELP PROVIDER ---"), Text.NewLine,
26+
new Text("--------------------------------------"), Text.NewLine,
27+
Text.NewLine,
28+
};
29+
}
30+
}

examples/Cli/Help/DefaultCommand.cs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Spectre.Console;
2+
using Spectre.Console.Cli;
3+
4+
namespace Help;
5+
6+
public sealed class DefaultCommand : Command
7+
{
8+
private IAnsiConsole _console;
9+
10+
public DefaultCommand(IAnsiConsole console)
11+
{
12+
_console = console;
13+
}
14+
15+
public override int Execute(CommandContext context)
16+
{
17+
_console.WriteLine("Hello world");
18+
return 0;
19+
}
20+
}

examples/Cli/Help/Help.csproj

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net7.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<ExampleName>Help</ExampleName>
9+
<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>
10+
<ExampleGroup>Cli</ExampleGroup>
11+
<ExampleVisible>false</ExampleVisible>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\..\Shared\Shared.csproj" />
16+
</ItemGroup>
17+
18+
</Project>

examples/Cli/Help/Program.cs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Spectre.Console.Cli;
2+
3+
namespace Help;
4+
5+
public static class Program
6+
{
7+
public static int Main(string[] args)
8+
{
9+
var app = new CommandApp<DefaultCommand>();
10+
11+
app.Configure(config =>
12+
{
13+
// Register the custom help provider
14+
config.SetHelpProvider(new CustomHelpProvider(config.Settings));
15+
});
16+
17+
return app.Run(args);
18+
}
19+
}

examples/Examples.sln

+15
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Json", "Console\Json\Json.c
8383
EndProject
8484
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Json", "..\src\Spectre.Console.Json\Spectre.Console.Json.csproj", "{91A5637F-1F89-48B3-A0BA-6CC629807393}"
8585
EndProject
86+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Help", "Cli\Help\Help.csproj", "{BAB490D6-FF8D-462B-B2B0-933384D629DB}"
87+
EndProject
8688
Global
8789
GlobalSection(SolutionConfigurationPlatforms) = preSolution
8890
Debug|Any CPU = Debug|Any CPU
@@ -549,6 +551,18 @@ Global
549551
{91A5637F-1F89-48B3-A0BA-6CC629807393}.Release|x64.Build.0 = Release|Any CPU
550552
{91A5637F-1F89-48B3-A0BA-6CC629807393}.Release|x86.ActiveCfg = Release|Any CPU
551553
{91A5637F-1F89-48B3-A0BA-6CC629807393}.Release|x86.Build.0 = Release|Any CPU
554+
{BAB490D6-FF8D-462B-B2B0-933384D629DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
555+
{BAB490D6-FF8D-462B-B2B0-933384D629DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
556+
{BAB490D6-FF8D-462B-B2B0-933384D629DB}.Debug|x64.ActiveCfg = Debug|Any CPU
557+
{BAB490D6-FF8D-462B-B2B0-933384D629DB}.Debug|x64.Build.0 = Debug|Any CPU
558+
{BAB490D6-FF8D-462B-B2B0-933384D629DB}.Debug|x86.ActiveCfg = Debug|Any CPU
559+
{BAB490D6-FF8D-462B-B2B0-933384D629DB}.Debug|x86.Build.0 = Debug|Any CPU
560+
{BAB490D6-FF8D-462B-B2B0-933384D629DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
561+
{BAB490D6-FF8D-462B-B2B0-933384D629DB}.Release|Any CPU.Build.0 = Release|Any CPU
562+
{BAB490D6-FF8D-462B-B2B0-933384D629DB}.Release|x64.ActiveCfg = Release|Any CPU
563+
{BAB490D6-FF8D-462B-B2B0-933384D629DB}.Release|x64.Build.0 = Release|Any CPU
564+
{BAB490D6-FF8D-462B-B2B0-933384D629DB}.Release|x86.ActiveCfg = Release|Any CPU
565+
{BAB490D6-FF8D-462B-B2B0-933384D629DB}.Release|x86.Build.0 = Release|Any CPU
552566
EndGlobalSection
553567
GlobalSection(SolutionProperties) = preSolution
554568
HideSolutionNode = FALSE
@@ -564,6 +578,7 @@ Global
564578
{A127CE7D-A5A7-4745-9809-EBD7CB12CEE7} = {2571F1BD-6556-4F96-B27B-B6190E1BF13A}
565579
{EFAADF6A-C77D-41EC-83F5-BBB4FFC5A6D7} = {2571F1BD-6556-4F96-B27B-B6190E1BF13A}
566580
{91A5637F-1F89-48B3-A0BA-6CC629807393} = {2571F1BD-6556-4F96-B27B-B6190E1BF13A}
581+
{BAB490D6-FF8D-462B-B2B0-933384D629DB} = {4682E9B7-B54C-419D-B92F-470DA4E5674C}
567582
EndGlobalSection
568583
GlobalSection(ExtensibilityGlobals) = postSolution
569584
SolutionGuid = {3EE724C5-CAB4-410D-AC63-8D4260EF83ED}

src/Spectre.Console.Cli/ConfiguratorExtensions.cs

+36-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,42 @@ namespace Spectre.Console.Cli;
55
/// and <see cref="IConfigurator{TSettings}"/>.
66
/// </summary>
77
public static class ConfiguratorExtensions
8-
{
8+
{
9+
/// <summary>
10+
/// Sets the help provider for the application.
11+
/// </summary>
12+
/// <param name="configurator">The configurator.</param>
13+
/// <param name="helpProvider">The help provider to use.</param>
14+
/// <returns>A configurator that can be used to configure the application further.</returns>
15+
public static IConfigurator SetHelpProvider(this IConfigurator configurator, IHelpProvider helpProvider)
16+
{
17+
if (configurator == null)
18+
{
19+
throw new ArgumentNullException(nameof(configurator));
20+
}
21+
22+
configurator.SetHelpProvider(helpProvider);
23+
return configurator;
24+
}
25+
26+
/// <summary>
27+
/// Sets the help provider for the application.
28+
/// </summary>
29+
/// <param name="configurator">The configurator.</param>
30+
/// <typeparam name="T">The type of the help provider to instantiate at runtime and use.</typeparam>
31+
/// <returns>A configurator that can be used to configure the application further.</returns>
32+
public static IConfigurator SetHelpProvider<T>(this IConfigurator configurator)
33+
where T : IHelpProvider
34+
{
35+
if (configurator == null)
36+
{
37+
throw new ArgumentNullException(nameof(configurator));
38+
}
39+
40+
configurator.SetHelpProvider<T>();
41+
return configurator;
42+
}
43+
944
/// <summary>
1045
/// Sets the name of the application.
1146
/// </summary>

0 commit comments

Comments
 (0)