Skip to content

Commit

Permalink
Allow custom help providers (#1259)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
FrankRay78 authored Sep 8, 2023
1 parent 813a53c commit 131b37f
Show file tree
Hide file tree
Showing 70 changed files with 1,647 additions and 331 deletions.
47 changes: 47 additions & 0 deletions docs/input/cli/command-help.md
Original file line number Diff line number Diff line change
@@ -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:/spectreconsole/spectre.console/tree/main/examples/Cli/Help) demonstrating this.

2 changes: 1 addition & 1 deletion docs/input/cli/commandApp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion docs/input/cli/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
34 changes: 17 additions & 17 deletions examples/Cli/Demo/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,25 @@ public static int Main(string[] args)
{
config.SetApplicationName("fake-dotnet");
config.ValidateExamples();
config.AddExample(new[] { "run", "--no-build" });
// Run
config.AddCommand<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);
}
}
30 changes: 30 additions & 0 deletions examples/Cli/Help/CustomHelpProvider.cs
Original file line number Diff line number Diff line change
@@ -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,
};
}
}
20 changes: 20 additions & 0 deletions examples/Cli/Help/DefaultCommand.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
18 changes: 18 additions & 0 deletions examples/Cli/Help/Help.csproj
Original file line number Diff line number Diff line change
@@ -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>
19 changes: 19 additions & 0 deletions examples/Cli/Help/Program.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
15 changes: 15 additions & 0 deletions examples/Examples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
37 changes: 36 additions & 1 deletion src/Spectre.Console.Cli/ConfiguratorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Loading

0 comments on commit 131b37f

Please sign in to comment.