重要
              System.CommandLine 目前为预览版。 本文档适用于版本 2.0 beta 7。
某些信息与预发布产品有关,该产品可能在发布前进行大幅修改。 Microsoft对此处提供的信息不作任何明示或暗示的保证。
2.0.0-beta5 版本的主要重点是改进 API,并采取措施发布稳定版本的 API System.CommandLine。 API 已简化,并更加一致且与 框架设计准则保持一致。 本文介绍在 2.0.0-beta5 和 2.0.0-beta7 中做出的重大更改,以及它们背后的原因。
重 命名
在 2.0.0-beta4 中,并非所有类型和成员都遵循 命名准则。 有些与命名约定不一致,例如对布尔属性使用 Is 前缀。 在 2.0.0-beta5 中,某些类型和成员已重命名。 下表显示了旧名称和新名称:
| 旧名称 | 新名称 | 
|---|---|
System.CommandLine.Parsing.Parser | 
CommandLineParser | 
System.CommandLine.Parsing.OptionResult.IsImplicit | 
Implicit | 
System.CommandLine.Option.IsRequired | 
Required | 
System.CommandLine.Symbol.IsHidden | 
Hidden | 
System.CommandLine.Option.ArgumentHelpName | 
HelpName | 
System.CommandLine.Parsing.OptionResult.Token | 
IdentifierToken | 
System.CommandLine.Parsing.ParseResult.FindResultFor | 
GetResult | 
System.CommandLine.Parsing.SymbolResult.ErrorMessage | 
AddError(String)† | 
†允许报告同一符号的多个错误,该 ErrorMessage 属性已转换为方法并重命名为 AddError。
选项和验证器的可变集合
版本 2.0.0-beta4 具有许多 Add 方法,这些方法用于将项添加到集合,例如参数、选项、子命令、验证器和完成。 其中一些集合通过属性作为只读集合公开。 因此,无法从这些集合中删除项。
在 2.0.0-beta5 中,API 已更改为公开可变集合,而不是 Add 方法(有时)只读集合。 这样,不仅可以添加项或枚举项,还可以删除它们。 下表显示了旧方法和新属性名称:
| 旧方法名称 | 新属性 | 
|---|---|
Command.AddArgument | 
Command.Arguments.Add | 
Command.AddOption | 
Command.Options.Add | 
Command.AddCommand | 
Command.Subcommands.Add | 
Command.AddValidator | 
Command.Validators.Add | 
Option.AddValidator | 
Option.Validators.Add | 
Argument.AddValidator | 
Argument.Validators.Add | 
Command.AddCompletions | 
Command.CompletionSources.Add | 
Option.AddCompletions | 
Option.CompletionSources.Add | 
Argument.AddCompletions | 
Argument.CompletionSources.Add | 
Command.AddAlias | 
Command.Aliases.Add | 
Option.AddAlias | 
Option.Aliases.Add | 
              RemoveAlias方法和HasAlias方法也被移除,因为Aliases属性现在是可变集合。 可以使用该方法 Remove 从集合中删除别名。 
              Contains使用该方法检查别名是否存在。
名称和别名
在 2.0.0-beta5 之前,符号的名称和 别名 之间没有明确的分隔。 如果未为name构造函数提供Option<T>,符号将报告其名称为去掉前缀后的最长别名,其前缀如--、-或/。 这令人困惑。
此外,若要获取分析的值,必须存储对选项或参数的引用,然后使用它从 ParseResult中获取值。
为了提升简单性和显式性,符号的名称现在是每个符号构造函数(包括 Argument<T>)的必需参数。 名称和别名的概念现在是独立的:别名只是别名,不包括符号的名称。 当然,它们是可选的。 因此,进行了以下更改:
- 
              
name现在是Argument<T>、Option<T>和Command每个公共构造函数的必需参数。 在这种情况下Argument<T>,它不用于分析,而是生成帮助。 在分析Option<T>Command期间,还用于标识符号,以及用于帮助和完成。 - 该 Symbol.Name 属性不再是 
virtual;它现在是只读的,并且返回创建符号时提供的名称。 因此,Symbol.DefaultName已删除,Option.Name不再从最长别名中删除---/或任何其他前缀。 - 
              
Aliases和Option公开的Command属性现在是一个可变集合。 此集合不再包含符号的名称。 - 
              
System.CommandLine.Parsing.IdentifierSymbol已被删除(它是Command和Option的基类型)。 
始终存在名称后,可以 按名称获取分析的值:
RootCommand command = new("The description.")
{
    new Option<int>("--number")
};
ParseResult parseResult = command.Parse(args);
int number = parseResult.GetValue<int>("--number");
使用别名创建选项
过去, Option<T> 公开了许多构造函数,其中一些构造函数接受该名称。 由于该名称现在是必需的,并且经常为别名 Option<T>提供,因此只有一个构造函数。 它接受名称和 params 别名数组。
在 2.0.0-beta5 之前, Option<T> 有一个构造函数,该构造函数具有名称和说明。 因此,第二个参数现在可能被视为别名而不是说明。 这是 API 中唯一不会导致编译器错误的已知中断性变更。
更新向构造函数传递说明的任何代码,以使用采用名称和别名的新构造函数,然后单独设置 Description 属性。 例如:
Option<bool> beta4 = new("--help", "An option with aliases.");
beta4b.Aliases.Add("-h");
beta4b.Aliases.Add("/h");
Option<bool> beta5 = new("--help", "-h", "/h")
{
    Description = "An option with aliases."
};
默认值和自定义分析
在 2.0.0-beta4 中,可以使用方法为选项和参数 SetDefaultValue 设置默认值。 这些方法接受的 object 值不是类型安全的,如果该值与选项或参数类型不兼容,可能会导致运行时错误。
Option<int> option = new("--number");
// This is not type safe, as the value is a string, not an int:
option.SetDefaultValue("text");
此外,某些 Option 构造 Argument 函数接受分析委托()和布尔值parse(isDefault),指示委托是自定义分析程序还是默认值提供程序,这令人困惑。
              Option<T> 和 Argument<T> 类现在有一个属性,可用于设置一个 DefaultValueFactory 委托,可以调用该委托来获取选项或参数的默认值。 在分析的命令行输入中找不到选项或参数时,将调用此委托。
Option<int> number = new("--number")
{
    DefaultValueFactory = _ => 42
};
              Argument<T> 和 Option<T> 都配有 CustomParser 属性,可用于设置符号的自定义解析器:
Argument<Uri> uri = new("arg")
{
    CustomParser = result =>
    {
        if (!Uri.TryCreate(result.Tokens.Single().Value, UriKind.RelativeOrAbsolute, out var uriValue))
        {
            result.AddError("Invalid URI format.");
            return null;
        }
        return uriValue;
    }
};
此外,CustomParser 接受一个类型为Func<ParseResult,T>的委托,而不是以前的ParseArgument委托。 删除了这一点和其他几个自定义委托,以简化 API 并减少 API 公开的类型数,从而减少在 JIT 编译期间花费的启动时间。
有关如何使用DefaultValueFactory和CustomParser的更多示例,请参阅如何在System.CommandLine中自定义解析和验证。
分析和调用分离
在 2.0.0-beta4 中,可以分离分析和调用命令,但目前还不清楚如何执行此作。 
              Command未公开Parse方法,但CommandExtensions提供了针对Parse的Invoke、InvokeAsync和Command扩展方法。 这令人困惑,因为不清楚使用哪种方法以及何时使用。 进行了以下更改以简化 API:
- 
              Command 现在公开一个 
Parse返回ParseResult对象的方法。 此方法用于分析命令行输入并返回分析作的结果。 此外,它明确表示不会调用命令,而是以同步方式进行分析。 - 
              
ParseResult现在公开两种方法:Invoke和InvokeAsync,可用于调用命令。 此模式表明,命令在分析后被调用,并允许同步和异步调用。 - 该 
CommandExtensions类已删除,因为它不再需要。 
配置
在 2.0.0-beta5 之前,可以自定义分析,但只能使用一些公共 Parse 方法。 有一个 Parser 类公开了两个公共构造函数:一个接受 Command,另一个接受 CommandLineConfiguration。 
              CommandLineConfiguration 是不可变的,若要创建它,必须使用类公开的 CommandLineBuilder 生成器模式。 进行了以下更改以简化 API:
- 
              
CommandLineConfiguration被拆分为两个 可变 类(在 2.0.0-beta7 中): ParserConfiguration 和 InvocationConfiguration。 创建调用配置现在和创建InvocationConfiguration实例并设置所需自定义的属性一样简单。 - 现在,每个 
Parse方法都接受可用于自定义分析的可选 ParserConfiguration 参数。 如果未提供,则使用默认配置。 - 为了避免名称冲突, 
Parser已重命名为 CommandLineParser 消除其他分析程序类型的歧义。 由于它是无状态的,因此它现在是一个仅具有静态方法的静态类。 它公开两Parse种分析方法:一个接受一个IReadOnlyList<string> args,另一个接受一个string args。 后者使用 CommandLineParser.SplitCommandLine(String) (也公共)将命令行输入拆分为 令牌 ,然后再对其进行分析。 
              CommandLineBuilderExtensions 也已删除。 下面介绍如何将其方法映射到新的 API:
CancelOnProcessTermination现在是InvocationConfiguration称为ProcessTerminationTimeout的属性。 默认启用,超时为 2 秒。 若要禁用它,请将其设置为null. 有关详细信息,请参阅 进程终止超时。EnableDirectives、UseEnvironmentVariableDirective、UseParseDirective和UseSuggestDirective已删除。 引入了新的 指令 类型, RootCommand 现在公开了一个 Directives 属性。 可以使用此集合添加、删除和迭代指令。 建议指令 默认包含;还可以使用其他指令,例如 DiagramDirective 或 EnvironmentVariablesDirective。EnableLegacyDoubleDashBehavior已删除。 所有不匹配的令牌现在都由 ParseResult.UnmatchedTokens 该属性公开。 有关详细信息,请参阅 不匹配的令牌。EnablePosixBundling已删除。 绑定现在默认处于启用状态,可以通过将 ParserConfiguration.EnablePosixBundling 属性设置为false来禁用它。 有关详细信息,请参阅 EnablePosixBundling。RegisterWithDotnetSuggest被删除,因为它执行了一项成本高昂的操作,通常发生在应用程序启动期间。 现在必须dotnet suggest手动注册命令。UseExceptionHandler已删除。 默认异常处理程序已默认启用, 可以通过将 InvocationConfiguration.EnableDefaultExceptionHandler 属性设置为false. 如果希望以自定义方式处理异常,可以将Invoke或InvokeAsync方法封装在 try-catch 块中,这样会非常有用。 有关详细信息,请参阅 EnableDefaultExceptionHandler。UseHelp并UseVersion已删除。 帮助和版本现在由HelpOptionVersionOption公共类型公开。 它们默认包含在 RootCommand 定义的选项中。 有关详细信息,请参阅 “自定义帮助输出 和 版本”选项。UseHelpBuilder已删除。 有关如何自定义帮助输出的详细信息,请参阅 如何自定义帮助 System.CommandLine。AddMiddleware已删除。 它减慢了应用程序启动的速度,并且可以在不使用它的情况下表示功能。UseParseErrorReporting并UseTypoCorrections已删除。 调用ParseResult时,现在会默认报告分析错误。 可以使用ParseErrorAction操作通过ParseResult.Action属性进行配置。ParseResult result = rootCommand.Parse("myArgs", config); if (result.Action is ParseErrorAction parseError) { parseError.ShowTypoCorrections = true; parseError.ShowHelp = false; }UseLocalizationResources并LocalizationResources已删除。 此功能主要用于dotnetCLI,将缺少的翻译添加到System.CommandLine。 所有这些翻译都移动到了 System.CommandLine 本身,因此不再需要这一功能。 如果缺少语言支持,请 报告问题。UseTokenReplacer已删除。 默认情况下启用响应文件 ,但可以通过将 ResponseFileTokenReplacer 属性设置为 来null禁用它们。 您还可以提供一个自定义实现,以个性化响应文件的处理方式。
最后但同样重要的是,IConsole以及所有相关接口(IStandardOut、IStandardError和IStandardIn)都被移除。 
              InvocationConfiguration 公开两个 TextWriter 属性: Output 和 Error。 可以将这些属性设置为任何 TextWriter 实例,例如 StringWriter可用于捕获用于测试的输出。 此更改的动机是公开更少的类型并重复使用现有抽象。
调用
在 2.0.0-beta4 中,ICommandHandler 接口公开了 Invoke 和 InvokeAsync 方法,这些方法用于调用已分析的命令。 这样可以轻松地混合同步和异步代码,例如,为命令定义同步处理程序,然后异步调用它(这可能导致 死锁)。 此外,只能为命令定义处理程序,但无法为选项(例如显示帮助信息的“help”)或指令定义处理程序。
引入了一个新的抽象基类 CommandLineAction 和两个派生类, SynchronousCommandLineAction 并 AsynchronousCommandLineAction已引入。 前者用于返回 int 退出代码的同步操作,而后者用于返回 Task<int> 退出代码的异步操作。
无需创建派生类型来定义动作。 可以使用Command.SetAction方法为命令设置动作。 同步操作可以是一个委派操作,此操作接受 System.CommandLine.ParseResult 参数并返回 int 退出代码(或者不返回任何内容,然后会返回默认的 0 退出代码)。 异步操作可以是一个委派操作,此操作使用 System.CommandLine.ParseResult 和 CancellationToken 参数,并返回 Task<int>(或者 Task 以返回默认退出代码)。
rootCommand.SetAction(ParseResult parseResult =>
{
    FileInfo parsedFile = parseResult.GetValue(fileOption);
    ReadFile(parsedFile);
});
在过去,传递给 CancellationToken 的 InvokeAsync 通过 InvocationContext 的方法公开给处理程序。
rootCommand.SetHandler(async (InvocationContext context) =>
{
    string? urlOptionValue = context.ParseResult.GetValueForOption(urlOption);
    var token = context.GetCancellationToken();
    returnCode = await DoRootCommand(urlOptionValue, token);
});
大多数用户未获取此令牌并进一步传递。 
              CancellationToken 现在是异步作的必需参数,因此编译器在未进一步传递时生成警告(请参阅 CA2016)。
rootCommand.SetAction((ParseResult parseResult, CancellationToken token) =>
{
    string? urlOptionValue = parseResult.GetValue(urlOption);
    return DoRootCommandAsync(urlOptionValue, token);
});
由于这些更改和其他上述更改,类 InvocationContext 也被删除。 现在,该ParseResult直接传递给动作,因此您可以直接从中访问已解析的值和选项。
总结这些更改:
- 接口 
ICommandHandler已删除。SynchronousCommandLineAction和AsynchronousCommandLineAction被引入。 - 方法 
Command.SetHandler已重命名为 SetAction. - 该 
Command.Handler属性已重命名为 Command.Action.Option已扩展为 Option.Action. - 
              
InvocationContext已删除。 现在,ParseResult直接传送给此操作。 
有关如何使用操作的更多详细信息,请参阅如何在 System.CommandLine 中分析和调用命令
简化的 API 的优点
2.0.0-beta5 中的更改使 API 更加一致、未来且更易于用于现有和新用户。
新用户需要了解更少的概念和类型,因为公共接口的数量从 11 个减少到 0,公共类(和结构)从 56 个减少到 38 个。 公共方法的数量从 378 个下降到 235 个,公共属性的数量从 118 个下降到 99 个。
System.CommandLine 引用的程序集数量已从 11 个减少到 6 个:
System.Collections
- System.Collections.Concurrent
- System.ComponentModel
System.Console
- System.Diagnostics.Process
System.Linq
System.Memory
- System.Net.Primitives
System.Runtime
- System.Runtime.Serialization.Formatters
+ System.Runtime.InteropServices
- System.Threading
库的大小会减少(32%),因此使用库的 NativeAOT 应用的大小也是如此。
简单性也提高了库的性能(这是工作的副作用,而不是它的主要目标)。 基准测试表明,对命令进行分析和调用的速度现在快于 2.0.0-beta4,尤其是对于具有许多选项和参数的大型命令。 同步和异步方案中都可以看到性能改进。
前面介绍的最简单应用生成以下结果:
BenchmarkDotNet v0.15.0, Windows 11 (10.0.26100.4061/24H2/2024Update/HudsonValley)
AMD Ryzen Threadripper PRO 3945WX 12-Cores 3.99GHz, 1 CPU, 24 logical and 12 physical cores
.NET SDK 9.0.300
  [Host]     : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2
  Job-JJVAFK : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2
EvaluateOverhead=False  OutlierMode=DontRemove  InvocationCount=1
IterationCount=100  UnrollFactor=1  WarmupCount=3
| Method                  | Args           | Mean      | StdDev   | Ratio |
|------------------------ |--------------- |----------:|---------:|------:|
| Empty                   | --bool -s test |  63.58 ms | 0.825 ms |  0.83 |
| EmptyAOT                | --bool -s test |  14.39 ms | 0.507 ms |  0.19 |
| SystemCommandLineBeta4  | --bool -s test |  85.80 ms | 1.007 ms |  1.12 |
| SystemCommandLineNow    | --bool -s test |  76.74 ms | 1.099 ms |  1.00 |
| SystemCommandLineNowR2R | --bool -s test |  69.35 ms | 1.127 ms |  0.90 |
| SystemCommandLineNowAOT | --bool -s test |  17.35 ms | 0.487 ms |  0.23 |
如你所看到的,启动时间(基准测试报告运行给定可执行文件所需的时间)已比 2.0.0-beta4 提高了 12%。 如果使用 NativeAOT 编译应用,则它的速度仅比 NativeAOT 应用慢 3 毫秒,它根本不分析参数(上表中的 EmptyAOT)。 此外,如果排除空应用的开销(63.58 毫秒),则分析速度比 2.0.0-beta4(22.22 ms vs 13.66 毫秒)快 40%。