重要
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
不再从最长别名中删除--
-
/
或任何其他前缀。 - 由
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");
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
提供了Parse
Invoke
和InvokeAsync
扩展方法Command
。 这令人困惑,因为不清楚使用哪种方法以及何时使用。 进行了以下更改以简化 API:
-
Command
现在公开一个Parse
返回ParseResult
对象的方法。 此方法用于分析命令行输入并返回分析作的结果。 此外,它明确表示,不会调用命令,但只以同步方式进行分析和仅同步方式。 -
ParseResult
现在公开Invoke
InvokeAsync
可用于调用命令的方法。 这清楚地表明,命令在分析后被调用,并允许同步和异步调用。 - 该
CommandExtensions
类已删除,因为它不再需要。
配置
在 2.0.0-beta5 之前,可以自定义分析,但只能使用一些公共 Parse
方法。 有一个类公开了两个公共构造函数:一个 Parser
接受一 Command
个,另一个接受一个 CommandLineConfiguration
。
CommandLineConfiguration
是不可变的,若要创建它,必须使用类公开的 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
禁用它。EnableDirectives
、UseEnvironmentVariableDirective
、UseParseDirective
和UseSuggestDirective
已删除。 引入了新的 指令 类型, RootCommand 现在公开System.CommandLine.RootCommand.Directives
属性。 可以使用此集合添加、删除和迭代指令。 建议指令 默认包含;还可以使用其他指令,例如 DiagramDirective 或EnvironmentVariablesDirective
。EnableLegacyDoubleDashBehavior
已删除。 所有不匹配的令牌现在都由 ParseResult.UnmatchedTokens 属性公开。EnablePosixBundling
已删除。 绑定现已默认启用,可以通过将 CommandLineConfiguration.EnableBundling 属性设置为false
禁用它。RegisterWithDotnetSuggest
在执行成本高昂的作(通常在应用程序启动期间)被删除。 现在必须手动注册命令dotnet suggest
。UseExceptionHandler
已删除。 默认的异常处理程序现已启用,可以通过将 CommandLineConfiguration.EnableDefaultExceptionHandler 属性设置为false
禁用它。 如果想要以自定义方式处理异常,只需在 try-catch 块中包装Invoke
或InvokeAsync
方法,即可执行此作。UseHelp
并UseVersion
已删除。 帮助和版本现在由 HelpOption 和 VersionOption 公共类型公开。 它们默认包含在 RootCommand 定义的选项中。UseHelpBuilder
已删除。 有关如何自定义帮助输出的详细信息,请参阅 如何自定义帮助 System.CommandLine。AddMiddleware
已删除。 它减慢了应用程序启动的速度,并且可以在不使用它的情况下表示功能。UseParseErrorReporting
并UseTypoCorrections
已删除。 调用时ParseResult
,分析错误现在默认报告。 可以使用公开的属性ParseResult.Action
对其进行ParseErrorAction
配置。ParseResult result = rootCommand.Parse("myArgs", config); if (result.Action is ParseErrorAction parseError) { parseError.ShowTypoCorrections = true; parseError.ShowHelp = false; }
UseLocalizationResources
并LocalizationResources
已删除。 此功能主要用于 CLIdotnet
将缺少的翻译添加到System.CommandLine
其中。 所有这些翻译都移动到 System.CommandLine 了自身,因此不再需要此功能。 如果缺少对语言的支持,请 报告问题。UseTokenReplacer
已删除。 默认情况下启用响应文件 ,但可以通过将System.CommandLine.CommandLineConfiguration.ResponseFileTokenReplacer
属性设置为 来null
禁用它们。 还可以提供自定义实现来自定义响应文件的处理方式。
最后但至少 IConsole
删除了所有相关接口(IStandardOut
、 IStandardError
和 IStandardIn
)。
System.CommandLine.CommandLineConfiguration
公开两个 TextWriter
属性: Output
和 Error
。 这些实例可以设置为任何 TextWriter
实例,例如 StringWriter
可用于捕获用于测试的输出。 我们的动机是公开更少的类型并重复使用现有抽象。
调用
在 2.0.0-beta4 中ICommandHandler
,接口公开,Invoke
InvokeAsync
以及用于调用已分析命令的方法。 这样可以轻松地混合同步和异步代码,例如,为命令定义同步处理程序,然后异步调用它(这可能导致 死锁)。 此外,只能为命令定义处理程序,但不能为选项(如显示帮助的帮助)或指令定义处理程序。
新的抽象基类 System.CommandLine.CommandLineAction
和两个派生类: System.CommandLine.SynchronousCommandLineAction
并 System.CommandLine.AsynchronousCommandLineAction
已引入。 前者用于返回 int
退出代码的同步作,而后者用于返回退出代码的 Task<int>
异步作。
无需创建派生类型来定义作。 可以使用该方法 System.CommandLine.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);
});
过去,传递给InvokeAsync
的CancellationToken
是通过以下方法向处理程序公开的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
作将直接传递给该作。
有关如何使用作的更多详细信息,请参阅 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%。