System.CommandLine 2.0.0-beta5+ 迁移指南

重要

System.CommandLine 目前为预览版。 本文档适用于版本 2.0 beta 5。 某些信息与预发布产品有关,该产品可能在发布前进行大幅修改。 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 之前,符号的名称和 别名 之间没有明确的分隔。 如果未为Option<T>构造函数提供name,符号将报告其名称为去掉前缀后的最长别名,其前缀如---/。 这令人困惑。

此外,若要获取分析的值,必须存储对选项或参数的引用,然后使用它从 ParseResult中获取值。

为了提升简单性和显式性,符号的名称现在是每个符号构造函数(包括 Argument<T>)的必需参数。 名称和别名的概念现在是独立的:别名只是别名,不包括符号的名称。 当然,它们是可选的。 因此,进行了以下更改:

  • name现在是Argument<T>Option<T>Command每个公共构造函数的必需参数。 在这种情况下 Argument<T>,它不用于分析,而是生成帮助。 在分析Option<T>Command期间,还用于标识符号,以及用于帮助和完成。
  • Symbol.Name 属性不再是 virtual;它现在是只读的,并且返回创建符号时提供的名称。 因此,Symbol.DefaultName已删除,Option.Name不再从最长别名中删除---/或任何其他前缀。
  • AliasesOption 公开的 Command 属性现在是一个可变集合。 此集合不再包含符号的名称。
  • System.CommandLine.Parsing.IdentifierSymbol已被删除(它是CommandOption的基类型)。

始终存在名称后,可以 按名称获取分析的值

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 构造函数接受一个解析委托和一个布尔值,以指示该委托是自定义解析器还是默认值提供程序,使人感到困惑。

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 编译期间花费的启动时间。

有关如何使用DefaultValueFactoryCustomParser的更多示例,请参阅如何在System.CommandLine中自定义解析和验证

分析和调用分离

在 2.0.0-beta4 中,可以分离分析和调用命令,但目前还不清楚如何执行此作。 Command未公开Parse方法,但CommandExtensions提供了针对CommandParseInvokeInvokeAsync扩展方法。 这令人困惑,因为不清楚使用哪种方法以及何时使用。 进行了以下更改以简化 API:

  • Command 现在公开一个 Parse 返回 ParseResult 对象的方法。 此方法用于分析命令行输入并返回分析作的结果。 此外,它明确表示不会调用命令,而是以同步方式进行分析。
  • ParseResult现在公开两种方法:InvokeInvokeAsync,可用于调用命令。 此模式表明,命令在分析后被调用,并允许同步和异步调用。
  • CommandExtensions 类已删除,因为它不再需要。

配置

在 2.0.0-beta5 之前,可以自定义分析,但只能使用一些公共 Parse 方法。 有一个 Parser 类公开了两个公共构造函数:一个接受 Command,另一个接受 CommandLineConfigurationCommandLineConfiguration 是不可变的,若要创建它,必须使用类公开的 CommandLineBuilder 生成器模式。 进行了以下更改以简化 API:

  • CommandLineConfiguration 被拆分为两个 可变 类(在 2.0.0-beta7 中): ParserConfigurationInvocationConfiguration。 创建调用配置现在和创建 InvocationConfiguration 实例并设置所需自定义的属性一样简单。
  • 现在,每个 Parse 方法都接受可用于自定义分析的可选 ParserConfiguration 参数。 如果未提供,则使用默认配置。
  • 为了避免名称冲突, Parser 已重命名为 CommandLineParser 消除其他分析程序类型的歧义。 由于它是无状态的,因此它现在是一个仅具有静态方法的静态类。 它公开两 Parse 种分析方法:一个接受一个 IReadOnlyList<string> args ,另一个接受一个 string args。 后者使用 CommandLineParser.SplitCommandLine(String) (也公共)将命令行输入拆分为 令牌 ,然后再对其进行分析。

CommandLineBuilderExtensions 也已删除。 下面介绍如何将其方法映射到新的 API:

  • CancelOnProcessTermination现在是InvocationConfiguration称为ProcessTerminationTimeout的属性。 默认启用,超时为 2 秒。 若要禁用它,请将其设置为 null. 有关详细信息,请参阅 进程终止超时

  • EnableDirectivesUseEnvironmentVariableDirectiveUseParseDirectiveUseSuggestDirective 已删除。 引入了新的 指令 类型, RootCommand 现在公开了一个 Directives 属性。 可以使用此集合添加、删除和迭代指令。 建议指令 默认包含;还可以使用其他指令,例如 DiagramDirectiveEnvironmentVariablesDirective

  • EnableLegacyDoubleDashBehavior 已删除。 所有不匹配的令牌现在都由 ParseResult.UnmatchedTokens 该属性公开。 有关详细信息,请参阅 不匹配的令牌

  • EnablePosixBundling 已删除。 绑定现在默认处于启用状态,可以通过将 ParserConfiguration.EnablePosixBundling 属性设置为 false 来禁用它。 有关详细信息,请参阅 EnablePosixBundling

  • RegisterWithDotnetSuggest 被删除,因为它执行了一项成本高昂的操作,通常发生在应用程序启动期间。 现在必须dotnet suggest手动注册命令。

  • UseExceptionHandler 已删除。 默认异常处理程序已默认启用, 可以通过将 InvocationConfiguration.EnableDefaultExceptionHandler 属性设置为 false. 如果希望以自定义方式处理异常,可以将 InvokeInvokeAsync 方法封装在 try-catch 块中,这样会非常有用。 有关详细信息,请参阅 EnableDefaultExceptionHandler

  • UseHelpUseVersion 已删除。 帮助和版本现在由HelpOptionVersionOption公共类型公开。 它们默认包含在 RootCommand 定义的选项中。 有关详细信息,请参阅 “自定义帮助输出版本”选项

  • UseHelpBuilder 已删除。 有关如何自定义帮助输出的详细信息,请参阅 如何自定义帮助 System.CommandLine

  • AddMiddleware 已删除。 它减慢了应用程序启动的速度,并且可以在不使用它的情况下表示功能。

  • UseParseErrorReportingUseTypoCorrections 已删除。 调用 ParseResult 时,现在会默认报告分析错误。 可以使用ParseErrorAction操作通过ParseResult.Action属性进行配置。

    ParseResult result = rootCommand.Parse("myArgs", config);
    if (result.Action is ParseErrorAction parseError)
    {
        parseError.ShowTypoCorrections = true;
        parseError.ShowHelp = false;
    }
    
  • UseLocalizationResourcesLocalizationResources 已删除。 此功能主要用于 dotnet CLI,将缺少的翻译添加到 System.CommandLine。 所有这些翻译都移动到了 System.CommandLine 本身,因此不再需要这一功能。 如果缺少语言支持,请 报告问题

  • UseTokenReplacer 已删除。 默认情况下启用响应文件 ,但可以通过将 ResponseFileTokenReplacer 属性设置为 来 null禁用它们。 您还可以提供一个自定义实现,以个性化响应文件的处理方式。

最后但同样重要的是,IConsole以及所有相关接口(IStandardOutIStandardErrorIStandardIn)都被移除。 InvocationConfiguration 公开两个 TextWriter 属性: OutputError。 可以将这些属性设置为任何 TextWriter 实例,例如 StringWriter可用于捕获用于测试的输出。 此更改的动机是公开更少的类型并重复使用现有抽象。

调用

在 2.0.0-beta4 中,ICommandHandler 接口公开了 InvokeInvokeAsync 方法,这些方法用于调用已分析的命令。 这样可以轻松地混合同步和异步代码,例如,为命令定义同步处理程序,然后异步调用它(这可能导致 死锁)。 此外,只能为命令定义处理程序,但无法为选项(例如显示帮助信息的“help”)或指令定义处理程序。

引入了一个新的抽象基类 CommandLineAction 和两个派生类, SynchronousCommandLineActionAsynchronousCommandLineAction已引入。 前者用于返回 int 退出代码的同步操作,而后者用于返回 Task<int> 退出代码的异步操作。

无需创建派生类型来定义动作。 可以使用Command.SetAction方法为命令设置动作。 同步操作可以是一个委派操作,此操作接受 System.CommandLine.ParseResult 参数并返回 int 退出代码(或者不返回任何内容,然后会返回默认的 0 退出代码)。 异步操作可以是一个委派操作,此操作使用 System.CommandLine.ParseResultCancellationToken 参数,并返回 Task<int>(或者 Task 以返回默认退出代码)。

rootCommand.SetAction(ParseResult parseResult =>
{
    FileInfo parsedFile = parseResult.GetValue(fileOption);
    ReadFile(parsedFile);
});

在过去,传递给 CancellationTokenInvokeAsync 通过 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 已删除。 SynchronousCommandLineActionAsynchronousCommandLineAction 被引入。
  • 方法 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%。

另请参阅