Description
System.CommandLine 2.0 Beta 3
We've been getting great feedback on the changes we made for the System.CommandLine Beta 2 release. (The announcement is here if you'd like to catch up.) The Beta 3 release incorporates feedback, bug fixes, and more performance improvements. Please give it a try and let us know your thoughts.
Here's an overview of the most significant changes in this release.
Removed unnecessary interfaces
A number of interfaces have been removed in this release. While this was done during our performance work over the last couple of months, performance wasn't the main motivation for this change. These interfaces were initially introduced to provide an API that would discourage changing the configuration of a Parser
once it's been instantiated and to leave open the possibility of making the underlying model immutable. An immutable model wasn't worth the cost or complexity in the end. Meanwhile, no compelling use case arose for people to create their own implementations of ICommand
(for example) as opposed to inheriting Command
. So while removing these interfaces does save a little time during JIT compilation and removed the need for numerous casts, the main reason they've been removed is to reduce the complexity of a redundant extension point.
These are the interfaces that have been removed and the corresponding classes that you can reference in their place :
Removed interface | Class you should reference instead |
---|---|
IArgument |
Argument |
ICommand |
Command |
IDirectiveCollection |
DirectiveCollection |
IIdentifierSymbol |
IdentifierSymbol |
IOption |
Option |
ISymbol |
Symbol |
Command line configuration is now validated on demand rather than up-front
Up until this release, validation of a command line configuration would happen immediately as symbols were added. The third line of the following code would throw an exception:
var root = new RootCommand();
root.Add(new Option<string>("--duplicate"));
root.Add(new Option<string>("--duplicate"));
System.ArgumentException: Alias '--duplicate' is already in use.
In typical usage this validation is wasteful because once compiled, the parser will be configured the same way every time your app starts up. If it was valid when you compiled it, it will always be valid. We identified this as a place to improve performance by moving validation to a method that you can run when appropriate: CommandLineConfiguration.ThrowIfInvalid
. Maybe you'll want to use it at runtime in specialized cases such as configuring a parser based on an external configuration. We strongly encourage you use it in unit testing. Here's an example:
[Fact]
public void Parser_is_configured_correctly()
{
var command = MyApp.CreateRootCommand(); // exposing your root command for testing is very helpful
var configuration = new CommandLineConfiguration(command);
configuration.ThrowIfInvalid();
}
Removed SymbolSet
The SymbolSet
class was originally created to support name-based lookups and up-front checking for duplicate command or option aliases that could lead to an ambiguous parser configuration. Once we had moved the name-based binding functionality out of the core System.CommandLine library and moved validation to CommandLineConfiguration.ThrowIfInvalid
, SymbolSet
no longer had a role to play, so it has been removed.
Removed the [debug]
directive
The [debug]
directive provided a consistent way to attach a debugger to your System.CommandLine-driven application and stop on a breakpoint in your startup code. While this is clearly useful, we weren't satisfied with the security of the design. Until this is improved, we’ve removed support for this directive from the library. We plan to bring it back in the future when a better design is in place. If you have ideas or use cases for it, please let us know!
A simpler validator API
Since the earliest versions of System.CommandLine, it's been possible to add custom validation logic to a symbol using the AddValidator
method on a Command
, Option
, or Argument
, like this:
var option = new Option<int>("-x");
option.AddValidator((OptionResult result) =>
{
// validation logic here
});
Originally, the way to signal a validation error was to return a string
from your validator delegate, while returning null
indicated there was no error. This was a pretty unusual interface and had been on the list of things to fix for a long time. To make matters worse, it became ambiguous with the introduction of the SymbolResult.ErrorMessage
property which can be set to indicate an error. If you were to set this to one value and return a different value, which should take precedence?
In this release we've resolved these oddities by making the ValidateSymbolResult<T>
delegate void
-returning. Now, the only way to set an error message from a custom validator is by setting SymbolResult.ErrorMessage
. Here's an example that ensures the value of the option is a number from 1 to 100:
var option = new Option<int>("-x");
option.AddValidator((OptionResult result) =>
{
if (result.GetValueForOption(option) <= 0)
{
result.ErrorMessage = "The value of -x must be greater than 0.";
}
});
Support for trimming
In .NET 6, full support was added for publishing trimmed, self-contained applications. Previous versions of System.CommandLine would produce trim warnings due to a reliance on certain reflection APIs. This is no longer the case. If you would like to use trimming to publish smaller command line apps using System.CommandLine, please give it a try and let us know what you think.
One notable breaking change that resulted from removing much of the reflection code in System.CommandLine is that we've had to remove the conventions that allowed instantiating types when:
- the type has a constructor that accepts a single string parameter, or
- the type has an associated
TypeConverter
.
This will be a breaking change for many custom types. A few common types will still work because we've added special cases for them, including DirectoryInfo
, FileInfo
, FileSystemInfo
, Uri
, and the common numeric types. For other types that were previously relying on these conventions, you can continue to parse them by passing a ParseArgument<T>
to the Option<T>
or Argument<T>
constructors.