Expanding CLI docs

Adding new pages for commandApp, commands and settings.
This commit is contained in:
Phil Scott 2021-04-09 08:12:51 -04:00 committed by Patrik Svensson
parent 04efd1719c
commit 79af013bf2
6 changed files with 373 additions and 12 deletions

View File

@ -0,0 +1,82 @@
Title: CommandApp
Order: 2
---
`CommandApp` is the entry point for a `Spectre.Console.Cli` command line application. It is used to configure the settings and commands used for execution of the application. Most `Spectre.Console.Cli` applications will need to specify a custom configuration using the `Configure` method.
For example, the following configuration might be used to change the default behavior indicate that for `DEBUG` configuration's full exception stack traces should be outputted to the screen, and any examples defined for our commands will also be validated.
```csharp
var app = new CommandApp<FileSizeCommand>();
app.Configure(config =>
{
#if DEBUG
config.PropagateExceptions();
config.ValidateExamples();
#endif
});
```
# Multiple Commands
In the previous example we have a single command that is configured. For complex command line applications, it is common for them to have multiple commands (or verbs) defined. Examples of applications like this are `git`, `dotnet` and `gh`. For example, git would have an `commit` command and along with other commits like `add` or `rebase`. Each with their own settings and validation. With `Spectre.Console.Cli` we use the `Configure` method to add these commands.
For example, to add three different commands to the application:
```csharp
var app = new CommandApp();
app.Configure(config =>
{
config.AddCommand<AddCommand>("add");
config.AddCommand<CommitCommand>("commit");
config.AddCommand<RebaseCommand>("rebase");
});
```
This configuration would allow users to run `app.exe add`, `app.exe commit`, or `app.exe rebase` and have the settings routed the appropriate command.
For more complex command hierarchical configurations, they can also be composed via inheritance and branching. See [Composing Commands](./composing).
# 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.
``` csharp
var app = new CommandApp();
app.Configure(config =>
{
config.AddCommand<FileSizeCommand>("size")
.IsHidden()
.WithAlias("file-size")
.WithDescription("Gets the file size for a directory.")
.WithExample(new[] {"c:\\windows", "--pattern", "*.dll"});
});
```
# Dependency Injection
`CommandApp` takes care of instantiating commands upon execution. If given a custom type registrar, it will use that to resolve services defined in the command constructor.
```csharp
var registrations = new ServiceCollection();
registrations.AddSingleton<IGreeter, HelloWorldGreeter>();
// Create a type registrar and register any dependencies.
// A type registrar is an adapter for a DI framework.
var registrar = new TypeRegistrar(registrations);
// Create a new command app with the registrar
// and run it with the provided arguments.
var app = new CommandApp<DefaultCommand>(registrar);
return app.Run(args);
```
`TypeRegistrar` is a custom class that must be created by the user. This [example using `Microsoft.Extensions.DependencyInjection` as the container](https://github.com/spectreconsole/spectre.console/tree/main/examples/Cli/Injection) provides an example `TypeRegistrar` and `TypeResolver` that can be added to your application with small adjustments for your DI container.
# Interception
`CommandApp` also provides a `SetInterceptor` configuration. An interceptor is ran before all commands are executed. This is typically used for configuring logging or other infrastructure concerns.
All interceptors must implement `ICommandInterceptor`. Upon execution of a command, an instance of your interceptor will be called with the parsed settings. This provides an opportunity for configuring any infrastructure or modifying the settings.
For an example of using the interceptor to configure logging, see the [Serilog demo](https://github.com/spectreconsole/spectre.console/tree/main/examples/Cli/Logging).

View File

@ -0,0 +1,63 @@
Title: Creating Commands
Order: 6
---
Commands in `Spectre.Console.Cli` are defined by creating a class that inherits from either `Spectre.Console.Cli.Command<TSettings>` or `Spectre.Console.Cli.AsyncCommand<TSettings>`. `Command<TSettings>` must implement an `Execute` method that returns an int where as `AsyncCommand<TSettings>` must implement `ExecuteAsync` returning `Task<int>`.
```csharp
public class HelloCommand : Command<HelloCommand.Settings>
{
public class Settings : LogCommandSettings
{
[CommandArgument(0, "[Name]")]
public string Name { get; set; }
}
public override int Execute(CommandContext context, Settings settings)
{
AnsiConsole.MarkupLine($"Hello, [blue]{settings.Name}[/]");
return 0;
}
}
```
# Configuring.
Commands are configured via the [`CommandApp`](commandApp)'s `Configure` method.
```csharp
var app = new CommandApp();
app.Configure(config =>
{
config.AddCommand<HelloCommand>("hello")
.WithAlias("hola")
.WithDescription("Say hello")
.WithExample(new []{"hello", "Phil"})
.WithExample(new []{"hello", "Phil", "--count", "4"});
});
```
* `WithAlias` allows commands to have multiple names.
* `WithDescription` is used by the help renderer to give commands a description when displaying help.
* `WithExample` is used by the help renderer to provide examples to the user for running the commands. The parameters is a string array that matches the values passed in `Main(string[] args)`.
# Dependency Injection.
Constructor injection is supported on commands. See the [`CommandApp`](commandApp) documentation for further information on configuring `Spectre.Console` for your container.
# Validation.
While the settings can validate themselves, the command also provides a validation. For example, `IFileSystem` might be injected into the command which we want to use to validate that a path passed in exists.
```csharp
public override ValidationResult Validate(CommandContext context, Settings settings)
{
if (_fileSystem.IO.File.Exists(settings.Path))
{
return ValidationResult.Error($"Path not found - {settings.Path}");
}
return base.Validate(context, settings);
}
```

View File

@ -1,14 +1,9 @@
Title: Introduction
Order: 1
Title: Composing Commands
RedirectFrom: introduction
Order: 8
---
`Spectre.Console.Cli` is a modern library for parsing command line arguments. While it's extremely
opinionated in what it does, it tries to follow established industry conventions, and draws
its inspiration from applications you use everyday.
# How does it work?
The underlying philosophy behind `Spectre.Console.Cli` is to rely on the .NET type system to
The underlying philosophy behind `Spectre.Console.Cli` is to rely on the .NET type system to
declare the commands, but tie everything together via composition.
Imagine the following command structure:
@ -18,8 +13,8 @@ Imagine the following command structure:
* package `<PACKAGE_NAME>` --version `<VERSION>`
* reference `<PROJECT_REFERENCE>`
For this I would like to implement the commands (the different levels in the tree that
executes something) separately from the settings (the options, flags and arguments),
For this I would like to implement the commands (the different levels in the tree that
executes something) separately from the settings (the options, flags and arguments),
which I want to be able to inherit from each other.
## Specify the settings

View File

@ -0,0 +1,90 @@
Title: Getting Started
Order: 1
---
`Spectre.Console.Cli` is a modern library for parsing command line arguments. While it's extremely
opinionated in what it does, it tries to follow established industry conventions, and draws
its inspiration from applications you use everyday.
# How does it work?
A `Spectre.Console.Cli` app will be comprised of Commands and a matching Setting specification. The settings file will be the model for the command parameters. Upon execution, `Spectre.Console.Cli` will parse the `args[]` passed into your application and match them to the appropriate settings file giving you a strongly typed object to work with.
The following example demonstrates these concepts coming together.
```csharp
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Spectre.Console;
using Spectre.Console.Cli;
var app = new CommandApp<FileSizeCommand>();
return app.Run(args);
internal sealed class FileSizeCommand : Command<FileSizeCommand.Settings>
{
public sealed class Settings : CommandSettings
{
[Description("Path to search. Defaults to current directory.")]
[CommandArgument(0, "[searchPath]")]
public string? SearchPath { get; init; }
[CommandOption("-p|--pattern")]
public string? SearchPattern { get; init; }
[CommandOption("--hidden")]
[DefaultValue(true)]
public bool IncludeHidden { get; init; }
}
public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings)
{
var searchOptions = new EnumerationOptions
{
AttributesToSkip = settings.IncludeHidden
? FileAttributes.Hidden | FileAttributes.System
: FileAttributes.System
};
var searchPattern = settings.SearchPattern ?? "*.*";
var searchPath = settings.SearchPath ?? Directory.GetCurrentDirectory();
var files = new DirectoryInfo(searchPath)
.GetFiles(searchPattern, searchOptions);
var totalFileSize = files
.Sum(fileInfo => fileInfo.Length);
AnsiConsole.MarkupLine(
$"Total file size for [green]{searchPattern}[/] files in [green]{searchPath}[/]: [blue]{totalFileSize:N0}[/] bytes");
return 0;
}
}
```
In our `Main()` method, an instance of `Spectre.Console.Cli`'s `CommandApp` is instantiated specifying `FileSizeCommand` will be the app's default and only command. `FileSizeCommand` is defined as inheriting from `Spectre.Console.Cli`'s generic `Command` class specifying the settings for the command are `FileSizeCommand.Settings`. The settings are defined using a nested class, but they can exist anywhere in the project as long as they inherit from `CommandSettings`.
This command will have three parameters.
* The main parameter for the command will be the path. This is to be passed in as the first argument without needing to specify any command line flags. To configure that setting, use the `CommandArgument` attribute. The `[searchPath]` parameters of `CommandArgument` drives not only how the built in help display will render the help text, but the square brackets tells `Spectre.Console.Cli` that this argument is optional through convention.
* The second will be specified as a parameter option. The `CommandOption` attribute is used to specify this action along with the option command line flag. In the case of `SearchPattern` both `-p` and `--path` are valid.
* The third will also be a parameter option. Here `DefaultValue` is used to indicate the default value will be `true`. For boolean parameters these will be interpreted as flags which means the user can just specify `--hidden` rather than `-hidden true`.
When `args` is passed into the `CommandApp`'s run method, `Spectre.Console.Cli` will parse those arguments and populate an instance of your settings. Upon success, it will then pass those settings into an instance of the specified command's `Execute` method.
With this in place, we can the following commands will all work
```text
app.exe
app.exe c:\windows
app.exe c:\windows --pattern *.dll
app.exe c:\windows --hidden --pattern *.dll
```
Much more is possible. You can have multiple commands per application, settings can be customized and extended and the default behavior of the `CommandApp` can be extended and customized.
* See [CommandApp](./commandApp) for customizing how Spectre.Console.Cli builds the settings.
* See [Create Commands](./commands) for information about different command types and their configurations.
* See [Specifying Settings](./settings) for information about defining the settings.

View File

@ -1,5 +1,5 @@
Title: Migrate from Spectre.Cli
Order: 2
Order: 10
---
The functionality in `Spectre.Cli` has been moved into the `Spectre.Console`

131
docs/input/cli/settings.md Normal file
View File

@ -0,0 +1,131 @@
Title: Specifying Settings
Order: 5
---
Settings for `Spectre.Console.Cli` commands are defined via classes that inherit from `CommandSettings`. Attributes are used to indicate how the parser interprets the command line arguments and create a runtime instance of the settings to be used.
Example:
```csharp
public sealed class MyCommandSettings : CommandSettings
{
[CommandArgument(0, "[name]")]
public string Name { get; set; }
[CommandOption("-c|--count")]
public int Count { get; set; }
}
```
This setting file tells `Spectre.Console.Cli` that our command has two parameters. One is marked as a `CommandArgument`, the other is a `CommandOption`.
# `CommandArgument`.
Arguments have a position and a name. The name is not only used for generating help, but it's 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>`). Square brackets are required where as angle are optional. If neither are specified an exception will be thrown.
The position is used for scenarios where there could be more than one argument.
For example, we could split the above name argument into two values with an optional last name.
```csharp
[CommandArgument(0, "[firstName]")]
public string FirstName { get; set; }
[CommandArgument(1, "<lastName>")]
public string FirstName { get; set; }
```
# `CommandOption`.
`CommandOption` is used when you have options that are passed in command line switches. The attribute has one parameter - a pipe delimited string with the list of argument names. The following rules apply:
* As many names can be specified as you wish, they just can't conflict with other arguments.
* Options with a single character must be preceded by a single dash (e.g. `-c`).
* Multi-character options must be preceded by two dashes (e.g. `--count`).
## Flags.
There is a special mode for `CommandOptions` on boolean types. Typically all `CommandOptions` require a value to be included after the switch. For these only the switch needs to be specified to mark the value as true. This example would allow the user to run either `app.exe --debug`, or `app.exe --debug true`.
```csharp
[CommandOption("--debug")]
public bool Debug { get; set; }
```
# Description.
When rendering help the [`System.ComponentModel.Description`](https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.descriptionattribute?view=net-5.0) attribute is supported for specifying the text displayed to the user for both `CommandOption` and `CommandArgument`.
# DefaultValue.
The [`System.ComponentModel.DefaultValue`](https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.defaultvalueattribute?view=net-5.0) attribute supported to specify a default value for a command. For example, in the hello example displaying hello for a default count of zero wouldn't make sense. We can change this to a single hello:
```csharp
[CommandOption("-c|--count")]
[DefaultValue(1)]
public int Count { get; set; }
```
# TypeConverter.
`System.ComponentModel.TypeConverter` is supported for more complex arguments, such as mapping log levels to an enum via a [`TypeConverter`](https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.typeconverter?view=net-5.0).
# Arrays.
`CommandArgument` can be defined as arrays and any additional parameters will be included in the value. For example
```csharp
[CommandArgument(0, "[name]")]
public string[] Name { get; set; }
```
Would allow the user to run `app.exe Dwayne Elizondo "Mountain Dew" Herbert Camacho`. The settings passed to the command would have a 5 element array consisting of Dwayne, Elizondo, Mountain Dew, Herbert and Camacho.
# Constructors.
`Spectre.Console.Cli` supports constructor initialization and init only initialization. For constructor initialization, the parameter name of the constructor must match the name of the property name of the settings class. Order does not matter.
```csharp
public class Settings
{
public Settings(string[] name)
{
Name = name;
}
[Description("The name to display")]
[CommandArgument(0, "[Name]")]
public string Name { get; }
}
```
Also supported are init only properties.
```csharp
public class Settings
{
[Description("The name to display")]
[CommandArgument(0, "[Name]")]
public string Name { get; init; }
}
```
# Validation.
Simple type validation is performed automatically, but for scenarios where more complex validation is required, overriding the `Validate` method is supported. This method must return either `ValidationResult.Error` or `ValidationResult.Success`.
```csharp
public class Settings
{
[Description("The name to display")]
[CommandArgument(0, "[Name]")]
public string Name { get; init; }
public override ValidationResult Validate()
{
return Name.Length < 2
? ValidationResult.Error("Names must be at least two characters long")
: ValidationResult.Success();
}
}
```