Introduction
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
declare the commands, but tie everything together via composition.
Imagine the following command structure:
- dotnet (executable)
- add
[PROJECT]
- package
<PACKAGE_NAME>
--version<VERSION>
- reference
<PROJECT_REFERENCE>
- package
- add
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
We start by creating some settings that represents the options, flags and arguments that we want to act upon.
public class AddSettings : CommandSettings
{
[CommandArgument(0, "[PROJECT]")]
public string Project { get; set; }
}
public class AddPackageSettings : AddSettings
{
[CommandArgument(0, "<PACKAGE_NAME>")]
public string PackageName { get; set; }
[CommandOption("-v|--version <VERSION>")]
public string Version { get; set; }
}
public class AddReferenceSettings : AddSettings
{
[CommandArgument(0, "<PROJECT_REFERENCE>")]
public string ProjectReference { get; set; }
}
Specify the commands
Now it's time to specify the commands that act on the settings we created in the previous step.
public class AddPackageCommand : Command<AddPackageSettings>
{
public override int Execute(CommandContext context, AddPackageSettings settings)
{
// Omitted
return 0;
}
}
public class AddReferenceCommand : Command<AddReferenceSettings>
{
public override int Execute(CommandContext context, AddReferenceSettings settings)
{
// Omitted
return 0;
}
}
You can use AsyncCommand
if you need async support.
Let's tie it together
Now when we have our commands and settings implemented, we can compose a command tree that tells the parser how to interpret user input.
using Spectre.Console.Cli;
namespace MyApp
{
public static class Program
{
public static int Main(string[] args)
{
var app = new CommandApp();
app.Configure(config =>
{
config.AddBranch<AddSettings>("add", add =>
{
add.AddCommand<AddPackageCommand>("package");
add.AddCommand<AddReferenceCommand>("reference");
});
});
return app.Run(args);
}
}
}
So why this way?
Now you might wonder, why do things like this? Well, for starters the different parts of the application are separated, while still having the option to share things like options, flags and arguments between them.
This makes the resulting code very clean and easy to navigate, not to mention to unit test.
And most importantly at all, the type system guides me to do the right thing. I can't configure
commands in non-compatible ways, and if I want to add a new top-level add-package
command
(or move the command completely), it's just a single line to change. This makes it easy to
experiment and makes the CLI experience a first class citizen of your application.