Description
System.CommandLine 2.0 Beta 4
We've been working through feedback on the beta 2 and beta 3 releases of System.CommandLine and now we're ready with beta 4. Here's a quick overview of what's new.
Microsoft Docs
Probably the most important update isn't in the API itself but in the release of preview documentation for System.CommandLine. It's been live now since April and, like the library, is in preview. There's a good deal of API to cover so if there's something that's missing or unclear, please let us know.
SetHandler
simplification
The most common feedback we've heard about the SetHandler
API introduced in beta 2 is that it's confusing, mainly because you can pass any number of IValueDescriptor<T>
instances (e.g. Argument<T>
, Option<T>
, or BinderBase<T>
) to the params IValueDescriptor[]
parameter on most of the overloads, and this number can differ from the count of the handler's generic parameters.
I'll use this simple parser setup in the following examples:
var option = new Option<int>("-i");
var argument = new Argument<string>("-s");
var rootCommand = new RootCommand
{
option,
argument
};
Here's an example of the old SetHandler
signature:
// ⚠️ Now outdated
public static void SetHandler<T1, T2>(
this Command command,
Action<T1, T2> handle,
params IValueDescriptor[] symbols);
The expectation was that in the common case, you would call this method like this:
rootCommand.SetHandler(
(int someInt, string someString) => { /* Do something exciting! */ },
option,
argument);
But the fact that the final parameter to the old SetHandler
methods was a params
array meant that the following would compile and then fail at runtime:
// ⚠️ Now outdated
rootCommand.SetHandler((int someInt, string someString) => { /* Do something exciting! */ });
The logic behind this method signature was that any handler parameters without a matching IValueDescriptor
would be satisfied instead by falling back to BindingContext.GetService
. This has been a longstanding way that dependency injection was supported for both System.CommandLine types such as ParseResult
as well as for external types. But it's not the common case, and it's confusing.
We've changed the SetHandler
methods to require a single IValueDescriptor<T>
for each generic parameter T
, rather than the previous params
array:
public static void SetHandler<T1, T2>(
this Command command,
Action<T1, T2> handle,
IValueDescriptor<T1> symbol1,
IValueDescriptor<T2> symbol2);
This has a number of benefits. In addition to making it clearer that you also need to pass the Option<T>
and Argument<T>
instances corresponding to the generic parameters, it also works with C# type inference to make your code a little more concise. You no longer need to restate the parameter types, because they're inferred:
rootCommand.SetHandler(
(someInt, someString) => { /* Do something exciting! */ },
option,
argument);
We've also added overloads accepting InvocationContext
, so if you want more detailed access for DI or larger numbers of options and arguments, you can do this:
rootCommand.SetHandler(
(context) => // InvocationContext, but it's inferred so you don't need to specify it.
{
var i = context.ParseResult.GetValueForOption(option);
var s = context.ParseResult.GetValueForArgument(argument);
var someDependency = context.GetService(typeof(ILogger)) as ILogger;
/* Do something exciting! */
});
Finally, we've reduced the number of SetHandler
overloads. Where before there were overloads supporting up to sixteen generic parameters, they now only go up to eight.
Option
and Argument
(non-generic) are now abstract
Support has existed since the earliest previews of System.CommandLine for creating non-generic Option
and Argument
instances that could be late-bound to a handler parameter. For example, an Argument
could be created that could later be bound to either a string
or an int
parameter. This relied heavily on the reflection code that powered CommandHandler.Create
, which was moved out of System.CommandLine and into the System.CommandLine.NamingConventionBinder package in beta 2. Fully removing support for this late-binding behavior by making Option
and Argument
abstract has helped simplify some code, clarify the API, and provided additional performance benefits.
Response file format unification
Response file support is a common feature of a number of command line tools, allowing a token within the command line to be replaced by inlining the contents of a file. You can think of a response file as a sort of configuration file that uses the same grammar as the command line tool itself. For example, given a file called response_file.rsp
, the following command line will interpolate the contents of that file into the command line as if you had run it directly:
> myapp @response_file.rsp
(The .rsp
file extension is another common convention, though you can use any file extension you like).
Until this release, System.CommandLine supported two different formats for response files: line-delimited and space-delimited. Line-delimited response files are more common and thus were the default. If response_file.rsp
contained the following:
# Make it verbose
--verbosity
very
then it would be treated as though you had run this:
> myapp --verbosity very
(Note following a #
, the rest of the text on that line is treated as a comment and the parser will ignore it.)
This also provided a mechanism for avoiding the sometimes confusing rules of escaping quotes, which differ from one shell to another. You could produce a single token with a line-delimited response file by putting multiple words on a single line.
If response_file.rsp
contained the following:
--greeting
Good morning!
then it would be treated as though you had run this:
> myapp --greeting "Good morning!"
System.CommandLine also supported another response file format, space-separated, where the content of the response file was expected to be all on a single line and was interpolated verbatim into the command line. This was not commonly used and we decided it would simplify the developer choice and make the behavior clearer for end users to unify the format.
The rules for the new format are as follows:
- Files can have many lines and content is treated as though concatenated into a single line.
- One or more tokens can appear on a single line.
- A line containing
Good morning!
will be treated as two tokens,Good
andmorning!
. - If multiple tokens are intended to be passed as a single token (e.g.
"Good morning!"
), you must enclose them in quotes. - Shell-specific escaping rules are not used for the contents of a response file.
- Any text between a
#
symbol and the end of the line is treated as a comment and ignored. - Tokens prefixed with
@
can reference additional response files.
Custom token replacement
While we were trying to figure out how to make response file handling simpler, we saw that there are so many variations on how to parse response files that we could never provide a single implementation that could address them all. This could easily become a deal-breaker for people who have older apps that they would like to reimplement using System.CommandLine. So while we were consolidating response file handling, we also decided to generalize and hopefully future-proof the solution by adding a new extensibility point. After all, the core logic of response files is simple: during tokenization, a token beginning with @
is (possibly) replaced by zero or more other tokens.
If the default response file handling doesn't work for you, you can now replace it using the CommandLineBuilder.UseTokenReplacer
method. The following example configures the parser to replace @
-prefixed tokens with other tokens.
var parser = new CommandLineBuilder(command)
.UseTokenReplacer((string tokenToReplace, out IReadOnlyList<string> replacementTokens, out string errorMessage) =>
{
if (tokenToReplace == "fruits") // note that the leading '@' will already have been removed
{
replacementTokens = new [] { "apple", "banana", "cherry" };
errorMessage = null;
return true;
}
else
{
replacementTokens
errorMessage = null; // no parse error is created
return false; // tokenToReplace is not replaced
}
})
.Build();
This token replacement happens prior to parsing, so just as with response files, the new tokens can be parsed as commands, options, or arguments.
If you return true
from the token replacer, it will cause the @
-prefixed token to be replaced. If you return false
, it will be passed along to the parser unchanged, and any value you assign to the errorMessage
parameter will be displayed to the user.
Given the above token replacement logic, an app will parse as follows:
> myapp @fruits # is parsed as: myapp apple banana cherry
> myapp @not-fruits # is parsed as: myapp @not-fruits
It's worth noting that this behavior is recursive, just as with response files. If a replacement token begins with @
, the token replacer will be called again to potentially replace that token.
Added support for NativeAOT compilation
Continuing the work done in beta 3 to make apps built using System.CommandLine trimmable, they can now also be compiled with Native AOT. For more details, you can read about Native AOT compilation in the .NET 7 Preview 3 announcement here: https://devblogs.microsoft.com/dotnet/announcing-dotnet-7-preview-3/#faster-lighter-apps-with-native-aot.