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-beta4 中,并非所有类型和成员都遵循 命名准则。 有些与命名约定不一致,例如使用布尔属性的 Is 前缀。 在 2.0.0-beta5 中,某些类型和成员已重命名。 下表显示了旧名称和新名称:

旧名称 新名称
System.CommandLine.Parsing.Parser System.CommandLine.Parsing.CommandLineParser
System.CommandLine.Parsing.OptionResult.IsImplicit System.CommandLine.Parsing.OptionResult.Implicit
System.CommandLine.Option.IsRequired System.CommandLine.Option.Required
System.CommandLine.Symbol.IsHidden System.CommandLine.Symbol.Hidden
System.CommandLine.Option.ArgumentHelpName System.CommandLine.Option.HelpName
System.CommandLine.Parsing.OptionResult.Token System.CommandLine.Parsing.OptionResult.IdentifierToken
System.CommandLine.Parsing.ParseResult.FindResultFor System.CommandLine.Parsing.ParseResult.GetResult
System.CommandLine.Parsing.SymbolResult.ErrorMessage System.CommandLine.Parsing.SymbolResult.AddError

若要允许报告同一符号的多个错误,该 ErrorMessage 属性已转换为方法并重命名为 AddError

公开可变集合

版本 2.0.0-beta4 具有许多 Add 方法,这些方法用于将项添加到集合,例如参数、选项、子命令、验证器和完成。 其中一些集合通过属性作为只读集合公开。 因此,无法从这些集合中删除项。

在 2.0.0-beta5 中,我们更改了 API 以公开可变集合,而不是 Add 方法(有时)只读集合。 这样,不仅可以添加项或枚举项,还可以删除它们。 下表显示了旧方法和新属性名称:

旧方法名称 新属性
System.CommandLine.Command.AddArgument System.CommandLine.Command.Arguments.Add
System.CommandLine.Command.AddOption System.CommandLine.Command.Options.Add
System.CommandLine.Command.AddCommand System.CommandLine.Command.Subcommands.Add
System.CommandLine.Command.AddValidator System.CommandLine.Command.Validators.Add
System.CommandLine.Option.AddValidator System.CommandLine.Option.Validators.Add
System.CommandLine.Argument.AddValidator System.CommandLine.Argument.Validators.Add
System.CommandLine.Command.AddCompletions System.CommandLine.Command.CompletionSources.Add
System.CommandLine.Option.AddCompletions System.CommandLine.Option.CompletionSources.Add
System.CommandLine.Argument.AddCompletions System.CommandLine.Argument.CompletionSources.Add
System.CommandLine.Command.AddAlias System.CommandLine.Command.Aliases.Add
System.CommandLine.Option.AddAlias System.CommandLine.Option.Aliases.Add

RemoveAlias此外,还删除了方法和HasAlias方法,因为Aliases该属性现在是可变集合。 可以使用该方法 Remove 从集合中删除别名。 Contains使用该方法检查别名是否存在。

名称和别名

在 2.0.0-beta5 之前,符号的名称和 别名 之间没有明确的分隔。 未为构造函数提供时name,符号将报告其名称为前缀最长的别名(例如---或删除/)。Option<T> 这令人困惑。

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

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

  • 现在是每个公共构造函数和 /> 的必需参数。 在这种情况下 Argument<T>,它不用于分析,而是生成帮助。 在分析Option<T>Command期间以及用于帮助和完成期间标识符号。
  • Symbol.Name 属性不再 virtual;它现在为只读,并返回创建符号时提供的名称。 因此,Symbol.DefaultName已删除,Option.Name不再从最长别名中删除---/或任何其他前缀。
  • AliasesOption该属性公开,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");
option.SetDefaultValue("text"); // This is not type-safe, as the value is a string, not an int.

此外,某些 Option 构造 Argument 函数接受分析委托和布尔值,指示委托是自定义分析程序还是默认值提供程序。 这令人困惑。

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

有关如何使用验证的更多示例,请参阅>如何自定义分析和验证。

分析和调用的分离

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

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

配置

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

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

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

  • CancelOnProcessTermination现在是名为 ProcessTerminationTimeout 的属性CommandLineConfiguration。 默认启用,超时为 2 秒。 将其设置为 null 禁用它。

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

  • EnableLegacyDoubleDashBehavior 已删除。 所有不匹配的令牌现在都由 ParseResult.UnmatchedTokens 属性公开。

  • EnablePosixBundling 已删除。 绑定现已默认启用,可以通过将 CommandLineConfiguration.EnableBundling 属性设置为 false禁用它。

  • RegisterWithDotnetSuggest 在执行成本高昂的作(通常在应用程序启动期间)被删除。 现在必须手动注册命令dotnet suggest

  • UseExceptionHandler 已删除。 默认的异常处理程序现已启用,可以通过将 CommandLineConfiguration.EnableDefaultExceptionHandler 属性设置为 false禁用它。 如果想要以自定义方式处理异常,只需在 try-catch 块中包装 InvokeInvokeAsync 方法,即可执行此作。

  • UseHelpUseVersion 已删除。 帮助和版本现在由 HelpOptionVersionOption 公共类型公开。 它们默认包含在 RootCommand 定义的选项中。

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

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

  • UseParseErrorReportingUseTypoCorrections 已删除。 调用时 ParseResult,分析错误现在默认报告。 可以使用公开的属性ParseResult.Action对其进行ParseErrorAction配置。

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

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

最后但至少 IConsole 删除了所有相关接口(IStandardOutIStandardErrorIStandardIn)。 System.CommandLine.CommandLineConfiguration 公开两个 TextWriter 属性: OutputError。 这些实例可以设置为任何 TextWriter 实例,例如 StringWriter可用于捕获用于测试的输出。 我们的动机是公开更少的类型并重复使用现有抽象。

调用

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

新的抽象基类 System.CommandLine.CommandLineAction和两个派生类: System.CommandLine.SynchronousCommandLineActionSystem.CommandLine.AsynchronousCommandLineAction 已引入。 前者用于返回 int 退出代码的同步作,而后者用于返回退出代码的 Task<int> 异步作。

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

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

过去,传递给InvokeAsyncCancellationToken是通过以下方法向处理程序公开的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 作将直接传递给该作。

有关如何使用作的更多详细信息,请参阅 How to parse and invoke 命令。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 应用的大小减少 20%:

Option<bool> boolOption = new Option<bool>(new[] { "--bool", "-b" }, "Bool option");
Option<string> stringOption = new Option<string>(new[] { "--string", "-s" }, "String option");

RootCommand command = new RootCommand
{
    boolOption,
    stringOption
};

command.SetHandler<bool, string>(Run, boolOption, stringOption);

return new CommandLineBuilder(command).UseDefaults().Build().Invoke(args);

static void Run(bool boolean, string text)
{
    Console.WriteLine($"Bool option: {text}");
    Console.WriteLine($"String option: {boolean}");
}
Option<bool> boolOption = new Option<bool>("--bool", "-b") { Description = "Bool option" };
Option<string> stringOption = new Option<string>("--string", "-s") { Description = "String option" };

RootCommand command = new ()
{
    boolOption,
    stringOption,
};

command.SetAction(parseResult => Run(parseResult.GetValue(boolOption), parseResult.GetValue(stringOption)));

return new CommandLineConfiguration(command).Invoke(args);

static void Run(bool boolean, string text)
{
    Console.WriteLine($"Bool option: {text}");
    Console.WriteLine($"String option: {boolean}");
}

简单性也提高了库的性能(这是工作的副作用,而不是它的主要目标)。 基准测试表明,对命令进行分析和调用的速度现在快于 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 编译应用,则它的速度仅为 3 毫秒,而 NativeAOT 应用根本不分析参数(上表中的 EmptyAOT)。 此外,当我们排除空应用(63.58 毫秒)的开销时,分析速度比 2.0.0-beta4(22.22 毫秒 vs 13.66 毫秒)快 40%。

另请参阅