重要
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不再从最长别名中删除--
-
/
或任何其他前缀。 -
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
构造函数接受一个解析委托和一个布尔值,以指示该委托是自定义解析器还是默认值提供程序,使人感到困惑。
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
提供了针对Command
的Parse
、Invoke
和InvokeAsync
扩展方法。 这令人困惑,因为不清楚使用哪种方法以及何时使用。 进行了以下更改以简化 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
已删除。 此功能主要用于dotnet
CLI,将缺少的翻译添加到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%。