dotnet-system-commandline
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesedotnet-system-commandline
dotnet-system-commandline
System.CommandLine 2.0 stable API for building .NET CLI applications. Covers RootCommand, Command, Option<T>, Argument<T>, SetAction for handler binding, ParseResult-based value access, custom type parsing, validation, tab completion, and testing with TextWriter capture.
Version assumptions: .NET 8.0+ baseline. System.CommandLine 2.0.0+ (stable NuGet package, GA since November 2025). All examples target the 2.0.0 GA API surface.
Breaking change note: System.CommandLine 2.0.0 GA differs significantly from the pre-release beta4 API. Key changes: replaced by , removed in favor of /, removed (ParseResult passed directly), and removed, removed in favor of TextWriter properties, and the / packages discontinued. Do not use beta-era patterns.
SetHandlerSetActionICommandHandlerSynchronousCommandLineActionAsynchronousCommandLineActionInvocationContextCommandLineBuilderAddMiddlewareIConsoleSystem.CommandLine.HostingSystem.CommandLine.NamingConventionBinderOut of scope: CLI application architecture patterns (layered command/handler/service design, configuration precedence, exit codes, stdin/stdout/stderr) -- see [skill:dotnet-cli-architecture]. Native AOT compilation -- see [skill:dotnet-native-aot]. CLI distribution strategy -- see [skill:dotnet-cli-distribution]. General CI/CD patterns -- see [skill:dotnet-gha-patterns] and [skill:dotnet-ado-patterns]. DI container mechanics -- see [skill:dotnet-csharp-dependency-injection]. General coding standards -- see [skill:dotnet-csharp-coding-standards].
Cross-references: [skill:dotnet-cli-architecture] for CLI design patterns, [skill:dotnet-native-aot] for AOT publishing CLI tools, [skill:dotnet-csharp-dependency-injection] for DI fundamentals, [skill:dotnet-csharp-configuration] for configuration integration, [skill:dotnet-csharp-coding-standards] for naming and style conventions.
System.CommandLine 2.0稳定版API,用于构建.NET CLI应用程序。涵盖RootCommand、Command、Option<T>、Argument<T>、用于处理程序绑定的SetAction、基于ParseResult的值访问、自定义类型解析、验证、补全以及通过TextWriter捕获进行测试。
版本要求: .NET 8.0+作为基准。System.CommandLine 2.0.0+(稳定版NuGet包,自2025年11月起正式发布)。所有示例均针对2.0.0正式版API。
重大变更说明: System.CommandLine 2.0.0正式版与预发布版beta4 API差异显著。主要变更包括:被取代,被移除,改为使用/,被移除(直接传入ParseResult),和被移除,被移除,改为使用TextWriter属性,并且/包已停止维护。请勿使用beta版本的模式。
SetHandlerSetActionICommandHandlerSynchronousCommandLineActionAsynchronousCommandLineActionInvocationContextCommandLineBuilderAddMiddlewareIConsoleSystem.CommandLine.HostingSystem.CommandLine.NamingConventionBinder超出范围: CLI应用程序架构模式(分层命令/处理程序/服务设计、配置优先级、退出码、标准输入/输出/错误)——请参见[skill:dotnet-cli-architecture]。Native AOT编译——请参见[skill:dotnet-native-aot]。CLI分发策略——请参见[skill:dotnet-cli-distribution]。通用CI/CD模式——请参见[skill:dotnet-gha-patterns]和[skill:dotnet-ado-patterns]。DI容器机制——请参见[skill:dotnet-csharp-dependency-injection]。通用编码标准——请参见[skill:dotnet-csharp-coding-standards]。
交叉引用:[skill:dotnet-cli-architecture]用于CLI设计模式,[skill:dotnet-native-aot]用于AOT发布CLI工具,[skill:dotnet-csharp-dependency-injection]用于DI基础,[skill:dotnet-csharp-configuration]用于配置集成,[skill:dotnet-csharp-coding-standards]用于命名和样式约定。
Package Reference
包引用
xml
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.*" />
</ItemGroup>System.CommandLine 2.0 targets .NET 8+ and .NET Standard 2.0. A single package provides all functionality -- the separate , , and packages from the beta era are discontinued.
System.CommandLine.HostingSystem.CommandLine.NamingConventionBinderSystem.CommandLine.Renderingxml
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.*" />
</ItemGroup>System.CommandLine 2.0目标框架为.NET 8+和.NET Standard 2.0。单个包提供所有功能——beta版本中单独的、和包已停止维护。
System.CommandLine.HostingSystem.CommandLine.NamingConventionBinderSystem.CommandLine.RenderingRootCommand and Command Hierarchy
RootCommand与命令层级结构
Basic Command Structure
基本命令结构
csharp
using System.CommandLine;
// Root command -- the entry point
var rootCommand = new RootCommand("My CLI tool description");
// Add a subcommand via mutable collection
var listCommand = new Command("list", "List all items");
rootCommand.Subcommands.Add(listCommand);
// Nested subcommands: mycli migrate up
var migrateCommand = new Command("migrate", "Database migrations");
var upCommand = new Command("up", "Apply pending migrations");
var downCommand = new Command("down", "Revert last migration");
migrateCommand.Subcommands.Add(upCommand);
migrateCommand.Subcommands.Add(downCommand);
rootCommand.Subcommands.Add(migrateCommand);csharp
using System.CommandLine;
// Root command -- 入口点
var rootCommand = new RootCommand("My CLI tool description");
// 通过可变集合添加子命令
var listCommand = new Command("list", "List all items");
rootCommand.Subcommands.Add(listCommand);
// 嵌套子命令:mycli migrate up
var migrateCommand = new Command("migrate", "Database migrations");
var upCommand = new Command("up", "Apply pending migrations");
var downCommand = new Command("down", "Revert last migration");
migrateCommand.Subcommands.Add(upCommand);
migrateCommand.Subcommands.Add(downCommand);
rootCommand.Subcommands.Add(migrateCommand);Collection Initializer Syntax
集合初始化语法
csharp
// Fluent collection initializer (commands, options, arguments)
RootCommand rootCommand = new("My CLI tool")
{
new Option<string>("--output", "-o") { Description = "Output file path" },
new Argument<FileInfo>("file") { Description = "Input file" },
new Command("list", "List all items")
{
new Option<int>("--limit") { Description = "Max items to return" }
}
};csharp
// 流畅式集合初始化(命令、选项、参数)
RootCommand rootCommand = new("My CLI tool")
{
new Option<string>("--output", "-o") { Description = "Output file path" },
new Argument<FileInfo>("file") { Description = "Input file" },
new Command("list", "List all items")
{
new Option<int>("--limit") { Description = "Max items to return" }
}
};Options and Arguments
选项与参数
Option<T> -- Named Parameters
Option<T> -- 命名参数
csharp
// Option<T> -- named parameter (--output, -o)
// name is the first parameter; additional params are aliases
var outputOption = new Option<FileInfo>("--output", "-o")
{
Description = "Output file path",
Required = true // was IsRequired in beta4
};
// Option with default value via DefaultValueFactory
var verbosityOption = new Option<int>("--verbosity")
{
Description = "Verbosity level (0-3)",
DefaultValueFactory = _ => 1
};csharp
// Option<T> -- 命名参数(--output, -o)
// 第一个构造函数参数是名称;其余是别名
var outputOption = new Option<FileInfo>("--output", "-o")
{
Description = "Output file path",
Required = true // beta4中为IsRequired
};
// 通过DefaultValueFactory设置默认值的选项
var verbosityOption = new Option<int>("--verbosity")
{
Description = "Verbosity level (0-3)",
DefaultValueFactory = _ => 1
};Argument<T> -- Positional Parameters
Argument<T> -- 位置参数
csharp
// Argument<T> -- positional parameter
// name is mandatory in 2.0 (used for help text)
var fileArgument = new Argument<FileInfo>("file")
{
Description = "Input file to process"
};
rootCommand.Arguments.Add(fileArgument);csharp
// Argument<T> -- 位置参数
// 2.0版本中名称是必填项(用于帮助文本)
var fileArgument = new Argument<FileInfo>("file")
{
Description = "Input file to process"
};
rootCommand.Arguments.Add(fileArgument);Constrained Values
约束值
csharp
var formatOption = new Option<string>("--format")
{
Description = "Output format"
};
formatOption.AcceptOnlyFromAmong("json", "csv", "table");
rootCommand.Options.Add(formatOption);csharp
var formatOption = new Option<string>("--format")
{
Description = "Output format"
};
formatOption.AcceptOnlyFromAmong("json", "csv", "table");
rootCommand.Options.Add(formatOption);Aliases
别名
csharp
// Aliases are separate from the name in 2.0
// First constructor param is the name; rest are aliases
var verboseOption = new Option<bool>("--verbose", "-v")
{
Description = "Enable verbose output"
};
// Or add aliases after construction
verboseOption.Aliases.Add("-V");csharp
// 2.0版本中别名与名称分离
// 第一个构造函数参数是名称;其余是别名
var verboseOption = new Option<bool>("--verbose", "-v")
{
Description = "Enable verbose output"
};
// 或者在构造后添加别名
verboseOption.Aliases.Add("-V");Global Options
全局选项
csharp
// Global options are inherited by all subcommands
var debugOption = new Option<bool>("--debug")
{
Description = "Enable debug mode",
Recursive = true // makes it global (inherited by subcommands)
};
rootCommand.Options.Add(debugOption);csharp
// 全局选项会被所有子命令继承
var debugOption = new Option<bool>("--debug")
{
Description = "Enable debug mode",
Recursive = true // 设为全局(被子命令继承)
};
rootCommand.Options.Add(debugOption);Setting Actions (Command Handlers)
设置Action(命令处理程序)
In 2.0.0 GA, is replaced by . Actions receive a directly (no ).
SetHandlerSetActionParseResultInvocationContext在2.0.0正式版中,被取代。Action直接接收(无)。
SetHandlerSetActionParseResultInvocationContextSynchronous Action
同步Action
csharp
var outputOption = new Option<FileInfo>("--output", "-o")
{
Description = "Output file path",
Required = true
};
var verbosityOption = new Option<int>("--verbosity")
{
DefaultValueFactory = _ => 1
};
rootCommand.Options.Add(outputOption);
rootCommand.Options.Add(verbosityOption);
rootCommand.SetAction(parseResult =>
{
var output = parseResult.GetValue(outputOption)!;
var verbosity = parseResult.GetValue(verbosityOption);
Console.WriteLine($"Output: {output.FullName}, Verbosity: {verbosity}");
return 0; // exit code
});csharp
var outputOption = new Option<FileInfo>("--output", "-o")
{
Description = "Output file path",
Required = true
};
var verbosityOption = new Option<int>("--verbosity")
{
DefaultValueFactory = _ => 1
};
rootCommand.Options.Add(outputOption);
rootCommand.Options.Add(verbosityOption);
rootCommand.SetAction(parseResult =>
{
var output = parseResult.GetValue(outputOption)!;
var verbosity = parseResult.GetValue(verbosityOption);
Console.WriteLine($"Output: {output.FullName}, Verbosity: {verbosity}");
return 0; // 退出码
});Asynchronous Action with CancellationToken
带CancellationToken的异步Action
csharp
// Async actions receive ParseResult AND CancellationToken
rootCommand.SetAction(async (ParseResult parseResult, CancellationToken ct) =>
{
var output = parseResult.GetValue(outputOption)!;
var verbosity = parseResult.GetValue(verbosityOption);
await ProcessAsync(output, verbosity, ct);
return 0;
});csharp
// 异步Action接收ParseResult和CancellationToken
rootCommand.SetAction(async (ParseResult parseResult, CancellationToken ct) =>
{
var output = parseResult.GetValue(outputOption)!;
var verbosity = parseResult.GetValue(verbosityOption);
await ProcessAsync(output, verbosity, ct);
return 0;
});Getting Values by Name
按名称获取值
csharp
// Values can also be retrieved by symbol name (requires type parameter)
rootCommand.SetAction(parseResult =>
{
int delay = parseResult.GetValue<int>("--delay");
string? message = parseResult.GetValue<string>("--message");
Console.WriteLine($"Delay: {delay}, Message: {message}");
});csharp
// 也可以通过符号名称检索值(需要类型参数)
rootCommand.SetAction(parseResult =>
{
int delay = parseResult.GetValue<int>("--delay");
string? message = parseResult.GetValue<string>("--message");
Console.WriteLine($"Delay: {delay}, Message: {message}");
});Parsing and Invoking
解析与调用
csharp
// Program.cs entry point -- parse then invoke
static int Main(string[] args)
{
var rootCommand = BuildCommand();
ParseResult parseResult = rootCommand.Parse(args);
return parseResult.Invoke();
}
// Async entry point
static async Task<int> Main(string[] args)
{
var rootCommand = BuildCommand();
ParseResult parseResult = rootCommand.Parse(args);
return await parseResult.InvokeAsync();
}csharp
// Program.cs入口点 -- 解析然后调用
static int Main(string[] args)
{
var rootCommand = BuildCommand();
ParseResult parseResult = rootCommand.Parse(args);
return parseResult.Invoke();
}
// 异步入口点
static async Task<int> Main(string[] args)
{
var rootCommand = BuildCommand();
ParseResult parseResult = rootCommand.Parse(args);
return await parseResult.InvokeAsync();
}Parse Without Invoking
仅解析不调用
csharp
// Parse-only mode: inspect results without running actions
ParseResult parseResult = rootCommand.Parse(args);
if (parseResult.Errors.Count > 0)
{
foreach (var error in parseResult.Errors)
{
Console.Error.WriteLine(error.Message);
}
return 1;
}
FileInfo? file = parseResult.GetValue(fileOption);
// Process directly without SetActioncsharp
// 仅解析模式:检查结果而不运行Action
ParseResult parseResult = rootCommand.Parse(args);
if (parseResult.Errors.Count > 0)
{
foreach (var error in parseResult.Errors)
{
Console.Error.WriteLine(error.Message);
}
return 1;
}
FileInfo? file = parseResult.GetValue(fileOption);
// 直接处理,无需SetActionCustom Type Parsing
自定义类型解析
CustomParser Property
CustomParser属性
For types without built-in parsers, use the property on or .
CustomParserOption<T>Argument<T>csharp
public record ConnectionInfo(string Host, int Port);
var connectionOption = new Option<ConnectionInfo?>("--connection")
{
Description = "Connection as host:port",
CustomParser = result =>
{
var raw = result.Tokens.SingleOrDefault()?.Value;
if (raw is null)
{
result.AddError("--connection requires a value");
return null;
}
var parts = raw.Split(':');
if (parts.Length != 2 || !int.TryParse(parts[1], out var port))
{
result.AddError("Expected format: host:port");
return null;
}
return new ConnectionInfo(parts[0], port);
}
};对于没有内置解析器的类型,使用或上的属性。
Option<T>Argument<T>CustomParsercsharp
public record ConnectionInfo(string Host, int Port);
var connectionOption = new Option<ConnectionInfo?>("--connection")
{
Description = "Connection as host:port",
CustomParser = result =>
{
var raw = result.Tokens.SingleOrDefault()?.Value;
if (raw is null)
{
result.AddError("--connection requires a value");
return null;
}
var parts = raw.Split(':');
if (parts.Length != 2 || !int.TryParse(parts[1], out var port))
{
result.AddError("Expected format: host:port");
return null;
}
return new ConnectionInfo(parts[0], port);
}
};DefaultValueFactory
DefaultValueFactory
csharp
var portOption = new Option<int>("--port")
{
Description = "Server port",
DefaultValueFactory = _ => 8080 // type-safe default
};csharp
var portOption = new Option<int>("--port")
{
Description = "Server port",
DefaultValueFactory = _ => 8080 // 类型安全的默认值
};Combining CustomParser with Validation
结合CustomParser与验证
csharp
var uriOption = new Option<Uri?>("--uri")
{
Description = "Target URI",
CustomParser = result =>
{
var raw = result.Tokens.SingleOrDefault()?.Value;
if (raw is null) return null;
if (!Uri.TryCreate(raw, UriKind.Absolute, out var uri))
{
result.AddError("Invalid URI format");
return null;
}
if (uri.Scheme != "https")
{
result.AddError("Only HTTPS URIs are accepted");
return null;
}
return uri;
}
};csharp
var uriOption = new Option<Uri?>("--uri")
{
Description = "Target URI",
CustomParser = result =>
{
var raw = result.Tokens.SingleOrDefault()?.Value;
if (raw is null) return null;
if (!Uri.TryCreate(raw, UriKind.Absolute, out var uri))
{
result.AddError("Invalid URI format");
return null;
}
if (uri.Scheme != "https")
{
result.AddError("Only HTTPS URIs are accepted");
return null;
}
return uri;
}
};Validation
验证
Option and Argument Validators
选项与参数验证器
csharp
// Validators use Validators.Add (not AddValidator in 2.0)
var portOption = new Option<int>("--port") { Description = "Port number" };
portOption.Validators.Add(result =>
{
var value = result.GetValue(portOption);
if (value < 1 || value > 65535)
{
result.AddError("Port must be between 1 and 65535");
}
});
// Arity constraints
var tagsOption = new Option<string[]>("--tag")
{
Arity = new ArgumentArity(1, 5), // 1 to 5 tags
AllowMultipleArgumentsPerToken = true
};csharp
// 验证器使用Validators.Add(2.0版本中不再是AddValidator)
var portOption = new Option<int>("--port") { Description = "Port number" };
portOption.Validators.Add(result =>
{
var value = result.GetValue(portOption);
if (value < 1 || value > 65535)
{
result.AddError("Port must be between 1 and 65535");
}
});
// 数量约束
var tagsOption = new Option<string[]>("--tag")
{
Arity = new ArgumentArity(1, 5), // 1到5个标签
AllowMultipleArgumentsPerToken = true
};Built-In Validators
内置验证器
csharp
// Accept only existing files/directories
var inputOption = new Option<FileInfo>("--input");
inputOption.AcceptExistingOnly();
// Accept only legal file names
var nameArg = new Argument<string>("name");
nameArg.AcceptLegalFileNamesOnly();
// Accept only from a set of values (moved from FromAmong)
var envOption = new Option<string>("--env");
envOption.AcceptOnlyFromAmong("dev", "staging", "prod");csharp
// 仅接受已存在的文件/目录
var inputOption = new Option<FileInfo>("--input");
inputOption.AcceptExistingOnly();
// 仅接受合法文件名
var nameArg = new Argument<string>("name");
nameArg.AcceptLegalFileNamesOnly();
// 仅接受一组指定值(从FromAmong迁移而来)
var envOption = new Option<string>("--env");
envOption.AcceptOnlyFromAmong("dev", "staging", "prod");Configuration
配置
In 2.0.0 GA, is removed. Configuration uses (for parsing) and (for invocation).
CommandLineBuilderParserConfigurationInvocationConfiguration在2.0.0正式版中,被移除。配置使用(用于解析)和(用于调用)。
CommandLineBuilderParserConfigurationInvocationConfigurationParser Configuration
解析器配置
csharp
using System.CommandLine;
var config = new ParserConfiguration
{
EnablePosixBundling = true, // -abc == -a -b -c (default: true)
};
// Response files enabled by default; disable with:
// config.ResponseFileTokenReplacer = null;
ParseResult parseResult = rootCommand.Parse(args, config);csharp
using System.CommandLine;
var config = new ParserConfiguration
{
EnablePosixBundling = true, // -abc == -a -b -c(默认:true)
};
// 响应文件默认启用;禁用方式:
// config.ResponseFileTokenReplacer = null;
ParseResult parseResult = rootCommand.Parse(args, config);Invocation Configuration
调用配置
csharp
var invocationConfig = new InvocationConfiguration
{
// Redirect output for testing or customization
Output = Console.Out,
Error = Console.Error,
// Process termination handling (default: 2 seconds)
ProcessTerminationTimeout = TimeSpan.FromSeconds(5),
// Disable default exception handler for custom try/catch
EnableDefaultExceptionHandler = false
};
int exitCode = parseResult.Invoke(invocationConfig);csharp
var invocationConfig = new InvocationConfiguration
{
// 重定向输出以进行测试或自定义
Output = Console.Out,
Error = Console.Error,
// 进程终止处理(默认:2秒)
ProcessTerminationTimeout = TimeSpan.FromSeconds(5),
// 禁用默认异常处理以使用自定义try/catch
EnableDefaultExceptionHandler = false
};
int exitCode = parseResult.Invoke(invocationConfig);Tab Completion
补全
Enabling Completion
启用补全
Tab completion is built into RootCommand via the SuggestDirective (included by default).
Users register completions for their shell:
bash
undefined补全功能通过SuggestDirective内置在RootCommand中(默认包含)。
用户需为其Shell注册补全:
bash
undefinedBash -- add to ~/.bashrc
Bash -- 添加到~/.bashrc
source <(mycli [suggest:bash])
source <(mycli [suggest:bash])
Zsh -- add to ~/.zshrc
Zsh -- 添加到~/.zshrc
source <(mycli [suggest:zsh])
source <(mycli [suggest:zsh])
PowerShell -- add to $PROFILE
PowerShell -- 添加到$PROFILE
mycli [suggest:powershell] | Out-String | Invoke-Expression
mycli [suggest:powershell] | Out-String | Invoke-Expression
Fish
Fish
mycli [suggest:fish] | source
undefinedmycli [suggest:fish] | source
undefinedCustom Completions
自定义补全
csharp
// Static completions
var envOption = new Option<string>("--environment");
envOption.CompletionSources.Add("development", "staging", "production");
// Dynamic completions
var branchOption = new Option<string>("--branch");
branchOption.CompletionSources.Add(ctx =>
[
new CompletionItem("main"),
new CompletionItem("develop"),
// Dynamically fetch branches
.. GetGitBranches().Select(b => new CompletionItem(b))
]);csharp
// 静态补全
var envOption = new Option<string>("--environment");
envOption.CompletionSources.Add("development", "staging", "production");
// 动态补全
var branchOption = new Option<string>("--branch");
branchOption.CompletionSources.Add(ctx =>
[
new CompletionItem("main"),
new CompletionItem("develop"),
// 动态获取分支
.. GetGitBranches().Select(b => new CompletionItem(b))
]);Automatic --version and --help
自动--version与--help
Version
版本
--versionVersionOption- (preferred -- includes SemVer metadata)
AssemblyInformationalVersionAttribute - (fallback)
AssemblyVersionAttribute
xml
<!-- Set in .csproj for automatic --version output -->
<PropertyGroup>
<Version>1.2.3</Version>
<!-- Or use source link / CI-generated version -->
<InformationalVersion>1.2.3+abc123</InformationalVersion>
</PropertyGroup>--versionVersionOption- (首选——包含SemVer元数据)
AssemblyInformationalVersionAttribute - (备选)
AssemblyVersionAttribute
xml
<!-- 在.csproj中设置以自动输出--version -->
<PropertyGroup>
<Version>1.2.3</Version>
<!-- 或使用源链接/CI生成的版本 -->
<InformationalVersion>1.2.3+abc123</InformationalVersion>
</PropertyGroup>Help
帮助
Help is automatically provided via on RootCommand. Descriptions from constructors and properties flow into help text.
HelpOptionDescription帮助功能通过RootCommand上的自动提供。构造函数和属性中的描述会流入帮助文本。
HelpOptionDescriptionDirectives
指令
Directives replace some beta-era extensions. RootCommand exposes a collection.
CommandLineBuilderDirectivescsharp
// Built-in directives (included by default on RootCommand):
// [suggest] -- tab completion suggestions
// Other available directives:
rootCommand.Directives.Add(new DiagramDirective()); // [diagram] -- shows parse tree
rootCommand.Directives.Add(new EnvironmentVariablesDirective()); // [env:VAR=value]指令替代了部分beta版本的扩展。RootCommand公开集合。
CommandLineBuilderDirectivescsharp
// 内置指令(默认包含在RootCommand中):
// [suggest] -- 补全建议
// 其他可用指令:
rootCommand.Directives.Add(new DiagramDirective()); // [diagram] -- 显示解析树
rootCommand.Directives.Add(new EnvironmentVariablesDirective()); // [env:VAR=value]Parse Error Handling
解析错误处理
csharp
// Customize parse error behavior
ParseResult result = rootCommand.Parse(args);
if (result.Action is ParseErrorAction parseError)
{
parseError.ShowTypoCorrections = true;
parseError.ShowHelp = false;
}
int exitCode = result.Invoke();csharp
// 自定义解析错误行为
ParseResult result = rootCommand.Parse(args);
if (result.Action is ParseErrorAction parseError)
{
parseError.ShowTypoCorrections = true;
parseError.ShowHelp = false;
}
int exitCode = result.Invoke();Dependency Injection Pattern
依赖注入模式
The package is discontinued in 2.0.0 GA. For DI integration, use directly and compose services before parsing.
System.CommandLine.HostingMicrosoft.Extensions.Hostingcsharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.CommandLine;
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddSingleton<ISyncService, SyncService>();
services.AddSingleton<IFileSystem, PhysicalFileSystem>();
})
.Build();
var serviceProvider = host.Services;
var sourceOption = new Option<string>("--source") { Description = "Source endpoint" };
var syncCommand = new Command("sync", "Synchronize data");
syncCommand.Options.Add(sourceOption);
syncCommand.SetAction(async (ParseResult parseResult, CancellationToken ct) =>
{
var syncService = serviceProvider.GetRequiredService<ISyncService>();
var source = parseResult.GetValue(sourceOption);
await syncService.SyncAsync(source!, ct);
return 0;
});
var rootCommand = new RootCommand("My CLI tool");
rootCommand.Subcommands.Add(syncCommand);
return await rootCommand.Parse(args).InvokeAsync();System.CommandLine.HostingMicrosoft.Extensions.Hostingcsharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.CommandLine;
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddSingleton<ISyncService, SyncService>();
services.AddSingleton<IFileSystem, PhysicalFileSystem>();
})
.Build();
var serviceProvider = host.Services;
var sourceOption = new Option<string>("--source") { Description = "Source endpoint" };
var syncCommand = new Command("sync", "Synchronize data");
syncCommand.Options.Add(sourceOption);
syncCommand.SetAction(async (ParseResult parseResult, CancellationToken ct) =>
{
var syncService = serviceProvider.GetRequiredService<ISyncService>();
var source = parseResult.GetValue(sourceOption);
await syncService.SyncAsync(source!, ct);
return 0;
});
var rootCommand = new RootCommand("My CLI tool");
rootCommand.Subcommands.Add(syncCommand);
return await rootCommand.Parse(args).InvokeAsync();Testing
测试
Testing with InvocationConfiguration (TextWriter Capture)
使用InvocationConfiguration进行测试(TextWriter捕获)
IConsoleInvocationConfigurationcsharp
[Fact]
public void ListCommand_WritesItems_ToOutput()
{
// Arrange
var outputWriter = new StringWriter();
var errorWriter = new StringWriter();
var config = new InvocationConfiguration
{
Output = outputWriter,
Error = errorWriter
};
var rootCommand = BuildRootCommand();
// Act
ParseResult parseResult = rootCommand.Parse("list --format json");
int exitCode = parseResult.Invoke(config);
// Assert
Assert.Equal(0, exitCode);
Assert.Contains("json", outputWriter.ToString());
Assert.Empty(errorWriter.ToString());
}2.0.0正式版中移除了。测试时,通过重定向输出。
IConsoleInvocationConfigurationcsharp
[Fact]
public void ListCommand_WritesItems_ToOutput()
{
// 准备
var outputWriter = new StringWriter();
var errorWriter = new StringWriter();
var config = new InvocationConfiguration
{
Output = outputWriter,
Error = errorWriter
};
var rootCommand = BuildRootCommand();
// 执行
ParseResult parseResult = rootCommand.Parse("list --format json");
int exitCode = parseResult.Invoke(config);
// 断言
Assert.Equal(0, exitCode);
Assert.Contains("json", outputWriter.ToString());
Assert.Empty(errorWriter.ToString());
}Testing Parsed Values Without Invocation
不调用的情况下测试解析值
csharp
[Fact]
public void ParseResult_ExtractsOptionValues()
{
var portOption = new Option<int>("--port") { DefaultValueFactory = _ => 8080 };
var rootCommand = new RootCommand { portOption };
ParseResult result = rootCommand.Parse("--port 3000");
Assert.Equal(3000, result.GetValue(portOption));
Assert.Empty(result.Errors);
}
[Fact]
public void ParseResult_ReportsErrors_ForInvalidInput()
{
var portOption = new Option<int>("--port");
var rootCommand = new RootCommand { portOption };
ParseResult result = rootCommand.Parse("--port not-a-number");
Assert.NotEmpty(result.Errors);
}csharp
[Fact]
public void ParseResult_ExtractsOptionValues()
{
var portOption = new Option<int>("--port") { DefaultValueFactory = _ => 8080 };
var rootCommand = new RootCommand { portOption };
ParseResult result = rootCommand.Parse("--port 3000");
Assert.Equal(3000, result.GetValue(portOption));
Assert.Empty(result.Errors);
}
[Fact]
public void ParseResult_ReportsErrors_ForInvalidInput()
{
var portOption = new Option<int>("--port");
var rootCommand = new RootCommand { portOption };
ParseResult result = rootCommand.Parse("--port not-a-number");
Assert.NotEmpty(result.Errors);
}Testing Custom Parsers
测试自定义解析器
csharp
[Fact]
public void CustomParser_ParsesConnectionInfo()
{
var connOption = new Option<ConnectionInfo?>("--connection")
{
CustomParser = result =>
{
var parts = result.Tokens.Single().Value.Split(':');
return new ConnectionInfo(parts[0], int.Parse(parts[1]));
}
};
var rootCommand = new RootCommand { connOption };
ParseResult result = rootCommand.Parse("--connection localhost:5432");
var conn = result.GetValue(connOption);
Assert.Equal("localhost", conn!.Host);
Assert.Equal(5432, conn.Port);
}csharp
[Fact]
public void CustomParser_ParsesConnectionInfo()
{
var connOption = new Option<ConnectionInfo?>("--connection")
{
CustomParser = result =>
{
var parts = result.Tokens.Single().Value.Split(':');
return new ConnectionInfo(parts[0], int.Parse(parts[1]));
}
};
var rootCommand = new RootCommand { connOption };
ParseResult result = rootCommand.Parse("--connection localhost:5432");
var conn = result.GetValue(connOption);
Assert.Equal("localhost", conn!.Host);
Assert.Equal(5432, conn.Port);
}Testing with DI Services
使用DI服务进行测试
csharp
[Fact]
public async Task SyncCommand_CallsService()
{
var mockService = new Mock<ISyncService>();
var services = new ServiceCollection()
.AddSingleton(mockService.Object)
.BuildServiceProvider();
var sourceOption = new Option<string>("--source");
var syncCommand = new Command("sync") { sourceOption };
syncCommand.SetAction(async (ParseResult pr, CancellationToken ct) =>
{
var svc = services.GetRequiredService<ISyncService>();
await svc.SyncAsync(pr.GetValue(sourceOption)!, ct);
return 0;
});
var root = new RootCommand { syncCommand };
int exitCode = await root.Parse("sync --source https://api.example.com")
.InvokeAsync();
Assert.Equal(0, exitCode);
mockService.Verify(s => s.SyncAsync("https://api.example.com",
It.IsAny<CancellationToken>()), Times.Once);
}csharp
[Fact]
public async Task SyncCommand_CallsService()
{
var mockService = new Mock<ISyncService>();
var services = new ServiceCollection()
.AddSingleton(mockService.Object)
.BuildServiceProvider();
var sourceOption = new Option<string>("--source");
var syncCommand = new Command("sync") { sourceOption };
syncCommand.SetAction(async (ParseResult pr, CancellationToken ct) =>
{
var svc = services.GetRequiredService<ISyncService>();
await svc.SyncAsync(pr.GetValue(sourceOption)!, ct);
return 0;
});
var root = new RootCommand { syncCommand };
int exitCode = await root.Parse("sync --source https://api.example.com")
.InvokeAsync();
Assert.Equal(0, exitCode);
mockService.Verify(s => s.SyncAsync("https://api.example.com",
It.IsAny<CancellationToken>()), Times.Once);
}Response Files
响应文件
System.CommandLine supports response files () for passing large sets of arguments. Response file support is enabled by default; disable via .
@filenameParserConfiguration.ResponseFileTokenReplacer = nullbash
undefinedSystem.CommandLine支持响应文件(),用于传递大量参数。响应文件支持默认启用;可通过禁用。
@filenameParserConfiguration.ResponseFileTokenReplacer = nullbash
undefinedargs.rsp
args.rsp
--source https://api.example.com
--output /tmp/results.json
--verbose
--source https://api.example.com
--output /tmp/results.json
--verbose
Invoke with response file
使用响应文件调用
mycli sync @args.rsp
---mycli sync @args.rsp
---Migration from Beta4 to 2.0.0 GA
从Beta4迁移到2.0.0正式版
| Beta4 API | 2.0.0 GA Replacement |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Removed -- use |
| |
| Build host directly, resolve services in SetAction |
| |
| |
| |
| |
| |
| Beta4 API | 2.0.0正式版替代方案 |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 已移除——使用 |
| |
| 直接构建主机,在SetAction中解析服务 |
| |
| |
| |
| |
| |
Agent Gotchas
注意事项
- Do not use beta4 API patterns. The 2.0.0 GA API is fundamentally different. There is no -- use
SetHandler. There is noSetAction-- actions receiveInvocationContextdirectly. There is noParseResult-- configuration usesCommandLineBuilder/ParserConfiguration.InvocationConfiguration - Do not reference discontinued packages. ,
System.CommandLine.Hosting, andSystem.CommandLine.NamingConventionBinderare discontinued. Use the singleSystem.CommandLine.Renderingpackage.System.CommandLine - Do not confuse with
Option<T>. Options are named (Argument<T>), arguments are positional (--output file.txt). Using the wrong type produces confusing parse errors.mycli file.txt - Do not use /
AddOption/AddCommandmethods. These were replaced by mutable collection properties:AddAlias,Options.Add,Subcommands.Add. The old methods do not exist in 2.0.0.Aliases.Add - Do not use or
IConsolefor testing. These interfaces were removed. UseTestConsolewithInvocationConfigurationforStringWriter/Outputto capture test output.Error - Do not ignore the in async actions. In 2.0.0 GA,
CancellationTokenis a mandatory second parameter for asyncCancellationTokendelegates. The compiler warns (CA2016) when it is not propagated.SetAction - Do not write directly in command actions. Write to
Console.Outfor testability. If no configuration is provided, output goes toInvocationConfiguration.Outputby default, but direct writes bypass test capture.Console.Out - Do not set default values via constructors. Use the property instead. The old
DefaultValueFactoryconstructor parameter does not exist in 2.0.0.getDefaultValue
- 请勿使用beta4 API模式。2.0.0正式版API与beta版本完全不同。不再有——请使用
SetHandler。不再有SetAction——Action直接接收InvocationContext。不再有ParseResult——配置使用CommandLineBuilder/ParserConfiguration。InvocationConfiguration - 请勿引用已停止维护的包。、
System.CommandLine.Hosting和System.CommandLine.NamingConventionBinder已停止维护。请使用单个System.CommandLine.Rendering包。System.CommandLine - 请勿混淆与
Option<T>。选项是命名的(Argument<T>),参数是位置性的(--output file.txt)。使用错误类型会导致难以理解的解析错误。mycli file.txt - 请勿使用/
AddOption/AddCommand方法。这些方法已被可变集合属性取代:AddAlias、Options.Add、Subcommands.Add。旧方法在2.0.0版本中不存在。Aliases.Add - 请勿在测试中使用或
IConsole。这些接口已被移除。请使用TestConsole搭配InvocationConfiguration作为StringWriter/Output来捕获测试输出。Error - 请勿忽略异步Action中的。在2.0.0正式版中,
CancellationToken是异步CancellationToken委托的必填第二个参数。编译器会在未传播时发出警告(CA2016)。SetAction - 请勿在命令Action中直接写入。为了可测试性,请写入
Console.Out。如果未提供配置,输出默认会到InvocationConfiguration.Output,但直接写入会绕过测试捕获。Console.Out - 请勿通过构造函数设置默认值。请改用属性。旧的
DefaultValueFactory构造函数参数在2.0.0版本中不存在。getDefaultValue
References
参考资料
Attribution
致谢
Adapted from Aaronontheweb/dotnet-skills (MIT license).
改编自Aaronontheweb/dotnet-skills(MIT许可证)。