Description
System.CommandLine 2.0 Beta 2
It's been a while since the last beta release of System.CommandLine. We’re happy to be able to say that it's almost time for a non-beta release of System.CommandLine 2.0. The library is stable in the sense of being robust and relatively bug free, but it was too easy to make mistakes with some of the APIs. In this release, we're providing improved APIs, offering a lightweight injection approach, making it easier for you to customize help, improving tab completion, working on documentation, and making big performance improvements. We’ve delayed going to GA because we want feedback on the changes. We anticipate this release to be the last before one or two stabilizing RCs and, at last, a stable 2.0 release.
These updates have required a number of breaking changes over the last few months. Rather than trickling them out, we’ve batched most of them up into a single release. The changes are too significant to jump into a release candidate without giving you time to comment. You can see the list here. The most important changes are summarized below. Please give the latest packages a try and let us know your thoughts.
Command handler improvements
Introducing System.CommandLine.NamingConventionBinder
Since the early days of the System.CommandLine 2.0 effort, one API has stood out as particularly troublesome, which is the binding of option and argument values to command handler parameters by name. In this release, we introduce a new approach (described below) and move the naming convention to a separate NuGet package, System.CommandLine.NamingConventionBinder.
Here's an example of the naming convention-based approach to binding, which you'll be familiar with if you've been using System.CommandLine Beta 1:
var rootCommand = new RootCommand
{
new Option<int>("-i"),
new Argument<string>("-s"),
};
rootCommand.Handler = CommandHandler.Create<int, string>((i, s) => { });
The parameters in the handler delegate will only be populated if they are in fact named i
and s
. Otherwise, they'll be set to 0
and null
with no indication that anything is wrong. We thought this would be intuitive because this convention is similar to name-based route value binding in ASP.NET MVC. We were wrong. This has been the source of the majority of the issues people have had using System.CommandLine.
Moving the name-based binding APIs into a separate package encourages the use of the newer command handler APIs and will eventually make System.CommandLine trimmable. If you want to continue using them, you can find them in System.CommandLine.NamingConventionBinder. This package is where you'll also now find the support for convention-based model binding of complex types. If you want to continue using these APIs, do the following and everything will continue to work:
- In your project, add a reference to
System.CommandLine.NamingConventionBinder
. - In your code, change references to the
System.CommandLine.Invocation
namespace to useSystem.CommandLine.NamingConventionBinder
, where theCommandHandler.Create
methods are now found. (There’s no longer aCommandHandler
type in System.CommandLine, so after you update you’ll get compilation errors until you referenceSystem.CommandLine.NamingConventionBinder
.)
The new command handler API
The recommended way to set a command handler using the core System.CommandLine library now looks like this:
// This is the same example as above.
var option = new Option<int>("-i");
var argument = new Argument<string>("-s");
var rootCommand = new RootCommand
{
option,
argument
};
// This is new!
rootCommand.SetHandler(
(int someInt, string someString) => { /* Do something exciting! */ },
option, argument);
The parameter names (someInt
and someString
) no longer need to match option or argument names. They're now bound to the options or arguments passed to CommandHandler.Create
in the order in which they're provided to the SetHandler
method. There are overloads supporting up to sixteen parameters, with both synchronous and asynchronous signatures.
As with the older CommandHandler.Create
methods, there are various Task
-returning Func
overloads if you need to do asynchronous work. If you return a Task<int>
from these handlers, it's used to set the process exit code. If you don't have asynchronous work to do, you can use the Action
overloads. You can still set the process exit code with these by accepting a parameter of type InvocationContext
and setting InvocationContext.ExitCode
to the desired value. If you don't explicitly set it and your handler exits normally, then the exit code will be set to 0
. If an exception is thrown, then the exit code will be set to 1
.
Going to more than sixteen options or arguments using custom types
If you have a complex CLI, you might have more than sixteen options or arguments whose values need to find their way into your handler method. The new SetHandler
API lets you specify a custom binder that can be used to combine multiple option or argument values into a more complex type and pass that into a single handler parameter. This can be done by creating a class derived from BinderBase<T>
, where T
is the type to construct based on command line input. You can also use this approach to support complex types.
Let's suppose you have a custom type that you want to use:
public class MyCustomType
{
public int IntValue { get; set; }
public string StringValue { get; set; }
}
With a custom binder, you can get your custom type passed to your handler the same way you get values for options and arguments:
var customBinder = new MyCustomBinder(stringArg, intOption);
command.SetHandler(
(MyCustomType instance) => { /* Do something custom! */ },
customBinder);
You can pass as many custom binder instances as you need and you can use them in combination with any number of Option<T>
and Argument<T>
instances.
Implementing a custom binder
Here's what the implementation for MyCustomBinder
looks like:
public class MyCustomBinder : BinderBase<MyCustomType>
{
private readonly Option<int> _intOption;
private readonly Argument<string> _stringArg;
public MyCustomBinder(Option<int> intOption, Argument<string> stringArg)
{
_intOption = intOption;
_stringArg = stringArg;
}
protected override MyCustomType GetBoundValue(BindingContext bindingContext) =>
new MyCustomType
{
IntValue = bindingContext.ParseResult.GetValueForOption(_intOption),
StringValue = bindingContext.ParseResult.GetValueForArgument(_stringArg)
};
}
The BindingContext
also gives you access to a number of other objects, so this approach can be used to compose both parsed values and injected values in a single place.
Injecting System.CommandLine types
System.CommandLine allows you to use a few types in your handlers simply by adding parameters for them to your handler signature. The available types are:
CancellationToken
InvocationContext
ParseResult
IConsole
HelpBuilder
BindingContext
Consuming one or more of these types is straightforward with SetHandler
. Here's an example using a few of them:
rootCommand.SetHandler(
( int i,
string s,
InvocationContext ctx,
HelpBuilder helpBuilder,
CancellationToken cancellationToken ) => { /* Do something with dependencies! */ },
option, argument);
When the handler is invoked, the current InvocationContext
, HelpBuilder
and CancellationToken
instances will be passed.
Injecting custom dependencies
We've received a good number of questions about how to use dependency injection for custom types in command line apps built with System.CommandLine. The new custom binder support provides a simpler way to do this than was available in Beta 1.
There has been a very simplistic IServiceProvider
built into the BindingContext
for some time, but configuring it can be awkward. This is intentional. Unlike longer-lived web or GUI apps where a dependency injection container is typically configured once and the startup cost isn't paid on every user gesture, command line apps are often short-lived processes. This particularly important when System.CommandLine calculates tab completions. Also, when a command line app that has multiple subcommands is run, only one of those subcommands will be executed. If you configure dependencies for the ones that don't run, it's wasted work. For this reason, we've recommended handler-specific dependency configurations.
Putting that together with the SetHandler
methods described above, you might have guessed the recommended approach to dependency injection in the latest version of System.CommandLine.
rootCommand.SetHandler(
( int i,
string s,
ILogger logger ) => { /* Do something with dependencies! */ },
option, argument, new MyCustomBinder<ILogger>());
We'll leave the possible implementations of MyCustomBinder<ILogger>
to you to explore. It will follow the same pattern as shown in the section Implementing a custom binder.
Customizing help
People ask very frequently how they can customize the help for their command line tools. Until a few months ago, the best answer we had was to implement your own IHelpBuilder
and replace the default one using the CommandLineBuilder.UseHelpBuilder
method. While this gave people complete control over the output, it made it awkward to reuse existing functionality such as column formatting, word wrapping, and usage diagrams. It's difficult to come up with an API that can address the myriad ways that people want to customize help. We realized early on that a templating engine might solve the problem more thoroughly, and that idea was the start of the System.CommandLine.Rendering experiment. Ultimately though, that approach was too complex.
After some rethinking, we think we've found a reasonable middle ground. It addresses the two most common needs that come up when customizing help and while also letting you use functionality from HelpBuilder
that you don't want to have to reimplement. You can now customize help for a specific symbol and you can add or replace whole help sections.
The sample CLI for the help examples
Let's look at a small sample program.
var durationOption = new Option<int>(
"--duration",
description: "The duration of the beep measured in milliseconds",
getDefaultValue: () => 1000);
var frequencyOption = new Option<int>(
"--frequency",
description: "The frequency of the beep, ranging from 37 to 32767 hertz",
getDefaultValue: () => 4200);
var rootCommand = new RootCommand("beep")
{
durationOption,
frequencyOption
};
rootCommand.SetHandler(
(int frequency, int duration) =>
{
Console.Beep(frequency, duration);
},
frequencyOption,
durationOption);
When help is requested using the default configuration (e.g. by calling rootCommand.Invoke("-h")
), the following output is produced:
Description:
beep
Usage:
beep [options]
Options:
--duration <duration> The duration of the beep measured in milliseconds [default: 1000]
--frequency <frequency> The frequency of the beep, ranging from 37 to 32767 hertz [default: 4200]
--version Show version information
-?, -h, --help Show help and usage information
Let's take a look at two common ways we might want to customize this help output.
Customizing help for a single option or argument
One common need is to replace the help for a specific option or argument. You can do this using HelpBuilder.CustomizeSymbol
, which lets you customize any of three different parts of the typical help output: the first column text, the second column text, and the way a default value is described.
In our sample, the --duration
option is pretty self-explanatory, but people might be less familiar with how the frequency range corresponds to common the common range of what people can hear. Let's customize the help output to be a bit more informative using HelpBuilder.CustomizeSymbol
.
var parser = new CommandLineBuilder(rootCommand)
.UseDefaults()
.UseHelp(ctx =>
{
ctx.HelpBuilder.CustomizeSymbol(frequencyOption,
firstColumnText: "--frequency <37 - 32767>\n Bats:\n Cats:\n Dogs:\n Humans:",
secondColumnText: "What frequency do you want your beep? For reference:\n 15 kHz to 90 kHz\n 500 Hz to 32 kHz\n 67 Hz to 45 kHz\n 20 to 20,000 Hz");
})
.Build();
parser.Invoke("-h");
Our program now produces the following help output:
Description:
beep
Usage:
beep [options]
Options:
--duration <duration> The duration of the beep measured in milliseconds [default: 1000]
--frequency <37 - 32767> What frequency do you want your beep? For reference:
Bats: 15 kHz to 90 kHz
Cats: 500 Hz to 32 kHz
Dogs: 67 Hz to 45 kHz
Humans: 20 to 20,000 Hz
--version Show version information
-?, -h, --help Show help and usage information
Only the output for the --frequency
option was changed by this customization. It's also worth noting that the firstColumnText
and secondColumnText
support word wrapping within their columns.
This API can also be used for Command
and Argument
objects.
Adding or replacing help sections
Another thing that people have asked for is the ability to add or replace a whole section of the help output. Maybe the description section in the help output above needs to be a little flashier, like this:
_ _
| |__ ___ ___ _ __ | |
| '_ \ / _ \ / _ \ | '_ \ | |
| |_) | | __/ | __/ | |_) | |_|
|_.__/ \___| \___| | .__/ (_)
|_|
Usage:
beep [options]
Options:
--duration <duration> The duration of the beep measured in milliseconds [default: 1000]
--frequency <37 - 32767> What frequency do you want your beep? For reference:
Bats: 15 kHz to 90 kHz
Cats: 500 Hz to 32 kHz
Dogs: 67 Hz to 45 kHz
Humans: 20 to 20,000 Hz
--version Show version information
-?, -h, --help Show help and usage information
You can change the layout by adding a call to HelpBuilder.CustomizeLayout
in the lambda passed to the CommandLineBuilder.UseHelp
method:
ctx.HelpBuilder.CustomizeLayout(
_ =>
HelpBuilder.Default
.GetLayout()
.Skip(1) /* Skip the boring default description section... */
.Prepend(
_ => Spectre.Console.AnsiConsole.Write(new FigletText("beep!"))
));
The HelpBuilder.Default
class has a number of methods that allow you to reuse pieces of existing help formatting functionality and compose them into your custom help.
Making suggestions completions richer
One of the great things about System.CommandLine is that it provides tab completions by default. We’ve updated it to make more advanced scenarios easier.
If your users aren't getting tab completion, remind them to enable it by installing the dotnet-suggest
tool and doing a one-time addition of the appropriate shim scripts in their shell profile. The details are here.
The completions model found in previous releases of System.CommandLine has provided tab completion output as a sequence of strings. This is the way that bash and PowerShell accept completions and it got the job done. But as we've started using System.CommandLine in richer applications such as for magic commands in .NET Interactive Notebooks, and as we've started looking at the capabilities of other shells, we realized this wasn't the most forward-looking design. We've replaced the string
value for completions with the CompletionItem
type, adopting a model that uses the same concepts as the Language Server Protocol used by many editors including Visual Studio Code.
In order to align to the naming found in common shell APIs and the Language Server Protocol, we've renamed the suggestion APIs to use the term "completion". However, the dotnet-suggest
tool and the [suggest]
directive that's sent to get completions from your System.CommandLine-powered apps have not been renamed, as this would be a breaking change for your end users.
Documentation
The public API for System.CommandLine now has XML documentation throughout, and we've started work on official online documentation and samples. If you find places where the XML documentation could be clearer or needs more details, please open an issue or, better yet, a pull request!
Deprecating System.CommandLine.Rendering
The System.CommandLine.Rendering project grew out of the realization that no help API could cover all of the ways in which someone might want to customize help. We started exploring approaches to rendering output that would look good in both older Windows command prompts that lack VT support as well as Linux and Mac terminals and the new Windows Terminal. This led to discussions about first-class support for VT codes and the lack of a separation of a terminal concept in System.Console
. That discussion is ongoing and has implications well beyond the scope of this library. In the meantime, many of the objectives of System.CommandLine.Rendering were realized beautifully by the Spectre.Console project.
What about DragonFruit?
The System.CommandLine.DragonFruit library started as an experiment in what a simple-as-possible command line programming model could look like. It's been popular because of its simplicity. With C# now supporting top-level code, and with the arrival of source generators, we think we can simplify DragonFruit further while also filling in some of the gaps in its capabilities, such as its lack of support for subcommands. It's being worked on and we'll be ready to talk more about it after System.CommandLine reaches a stable 2.0 release.
The path to a stable 2.0 release
So what's next? When we've received enough feedback to feel confident about the latest changes, and depending on how much needs to be fixed, we'll publish an RC version or two. A stable release will follow. In the meantime, we're working on major performance improvements that will introduce a few additional breaking changes. The timeline will be driven by your feedback and our ability to respond to it.
This project has depended on community contributions of design ideas and code from the very beginning. No one has said we need to ship a stable version by any specific date. But it's our hope that we can be stable at last by March. You can help by downloading the latest version, upgrading your existing System.CommandLine apps or building some new ones, and letting us know what you think.
Thanks!