dotnet-system-commandline

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

dotnet-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:
SetHandler
replaced by
SetAction
,
ICommandHandler
removed in favor of
SynchronousCommandLineAction
/
AsynchronousCommandLineAction
,
InvocationContext
removed (ParseResult passed directly),
CommandLineBuilder
and
AddMiddleware
removed,
IConsole
removed in favor of TextWriter properties, and the
System.CommandLine.Hosting
/
System.CommandLine.NamingConventionBinder
packages discontinued. Do not use beta-era patterns.
Out 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差异显著。主要变更包括:
SetHandler
SetAction
取代,
ICommandHandler
被移除,改为使用
SynchronousCommandLineAction
/
AsynchronousCommandLineAction
InvocationContext
被移除(直接传入ParseResult),
CommandLineBuilder
AddMiddleware
被移除,
IConsole
被移除,改为使用TextWriter属性,并且
System.CommandLine.Hosting
/
System.CommandLine.NamingConventionBinder
包已停止维护。请勿使用beta版本的模式。
超出范围: 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
System.CommandLine.Hosting
,
System.CommandLine.NamingConventionBinder
, and
System.CommandLine.Rendering
packages from the beta era are discontinued.

xml
<ItemGroup>
  <PackageReference Include="System.CommandLine" Version="2.0.*" />
</ItemGroup>
System.CommandLine 2.0目标框架为.NET 8+和.NET Standard 2.0。单个包提供所有功能——beta版本中单独的
System.CommandLine.Hosting
System.CommandLine.NamingConventionBinder
System.CommandLine.Rendering
包已停止维护。

RootCommand 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,
SetHandler
is replaced by
SetAction
. Actions receive a
ParseResult
directly (no
InvocationContext
).
在2.0.0正式版中,
SetHandler
SetAction
取代。Action直接接收
ParseResult
(无
InvocationContext
)。

Synchronous 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 SetAction

csharp
// 仅解析模式:检查结果而不运行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);
// 直接处理,无需SetAction

Custom Type Parsing

自定义类型解析

CustomParser Property

CustomParser属性

For types without built-in parsers, use the
CustomParser
property on
Option<T>
or
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>
上的
CustomParser
属性。
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);
    }
};

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,
CommandLineBuilder
is removed. Configuration uses
ParserConfiguration
(for parsing) and
InvocationConfiguration
(for invocation).
在2.0.0正式版中,
CommandLineBuilder
被移除。配置使用
ParserConfiguration
(用于解析)和
InvocationConfiguration
(用于调用)。

Parser 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
undefined

Bash -- 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
undefined
mycli [suggest:fish] | source
undefined

Custom 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

版本

--version
is automatically available on RootCommand via
VersionOption
. It reads from:
  1. AssemblyInformationalVersionAttribute
    (preferred -- includes SemVer metadata)
  2. AssemblyVersionAttribute
    (fallback)
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>
--version
通过
VersionOption
自动在RootCommand中可用。它从以下位置读取:
  1. AssemblyInformationalVersionAttribute
    (首选——包含SemVer元数据)
  2. AssemblyVersionAttribute
    (备选)
xml
<!-- 在.csproj中设置以自动输出--version -->
<PropertyGroup>
  <Version>1.2.3</Version>
  <!-- 或使用源链接/CI生成的版本 -->
  <InformationalVersion>1.2.3+abc123</InformationalVersion>
</PropertyGroup>

Help

帮助

Help is automatically provided via
HelpOption
on RootCommand. Descriptions from constructors and
Description
properties flow into help text.

帮助功能通过RootCommand上的
HelpOption
自动提供。构造函数和
Description
属性中的描述会流入帮助文本。

Directives

指令

Directives replace some beta-era
CommandLineBuilder
extensions. RootCommand exposes a
Directives
collection.
csharp
// 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版本的
CommandLineBuilder
扩展。RootCommand公开
Directives
集合。
csharp
// 内置指令(默认包含在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
System.CommandLine.Hosting
package is discontinued in 2.0.0 GA. For DI integration, use
Microsoft.Extensions.Hosting
directly and compose services before parsing.
csharp
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.Hosting
包在2.0.0正式版中已停止维护。如需集成DI,请直接使用
Microsoft.Extensions.Hosting
并在解析前组合服务。
csharp
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捕获)

IConsole
is removed in 2.0.0 GA. For testing, redirect output via
InvocationConfiguration
.
csharp
[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正式版中移除了
IConsole
。测试时,通过
InvocationConfiguration
重定向输出。
csharp
[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 (
@filename
) for passing large sets of arguments. Response file support is enabled by default; disable via
ParserConfiguration.ResponseFileTokenReplacer = null
.
bash
undefined
System.CommandLine支持响应文件(
@filename
),用于传递大量参数。响应文件支持默认启用;可通过
ParserConfiguration.ResponseFileTokenReplacer = null
禁用。
bash
undefined

args.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 API2.0.0 GA Replacement
command.SetHandler(...)
command.SetAction(...)
command.AddOption(opt)
command.Options.Add(opt)
command.AddCommand(sub)
command.Subcommands.Add(sub)
command.AddArgument(arg)
command.Arguments.Add(arg)
option.AddAlias("-x")
option.Aliases.Add("-x")
option.AddValidator(...)
option.Validators.Add(...)
option.IsRequired = true
option.Required = true
option.IsHidden = true
option.Hidden = true
InvocationContext context
ParseResult parseResult
(in SetAction)
context.GetCancellationToken()
CancellationToken ct
(second param in async SetAction)
context.Console
InvocationConfiguration.Output / .Error
IConsole
/
TestConsole
StringWriter
via
InvocationConfiguration
new CommandLineBuilder(root).UseDefaults().Build()
root.Parse(args)
(middleware built-in)
builder.AddMiddleware(...)
Removed -- use
ParseResult.Action
inspection or wrap
Invoke
CommandLineBuilder
ParserConfiguration
+
InvocationConfiguration
UseCommandHandler<T,T>
(Hosting)
Build host directly, resolve services in SetAction
Parser
class
CommandLineParser
(static class)
FindResultFor(symbol)
GetResult(symbol)
ErrorMessage = "..."
result.AddError("...")
getDefaultValue: () => val
DefaultValueFactory = _ => val
ParseArgument<T>
delegate
CustomParser
property

Beta4 API2.0.0正式版替代方案
command.SetHandler(...)
command.SetAction(...)
command.AddOption(opt)
command.Options.Add(opt)
command.AddCommand(sub)
command.Subcommands.Add(sub)
command.AddArgument(arg)
command.Arguments.Add(arg)
option.AddAlias("-x")
option.Aliases.Add("-x")
option.AddValidator(...)
option.Validators.Add(...)
option.IsRequired = true
option.Required = true
option.IsHidden = true
option.Hidden = true
InvocationContext context
ParseResult parseResult
(在SetAction中)
context.GetCancellationToken()
CancellationToken ct
(异步SetAction的第二个参数)
context.Console
InvocationConfiguration.Output / .Error
IConsole
/
TestConsole
InvocationConfiguration
搭配
StringWriter
new CommandLineBuilder(root).UseDefaults().Build()
root.Parse(args)
(中间件内置)
builder.AddMiddleware(...)
已移除——使用
ParseResult.Action
检查或包装
Invoke
CommandLineBuilder
ParserConfiguration
+
InvocationConfiguration
UseCommandHandler<T,T>
(Hosting)
直接构建主机,在SetAction中解析服务
Parser
CommandLineParser
(静态类)
FindResultFor(symbol)
GetResult(symbol)
ErrorMessage = "..."
result.AddError("...")
getDefaultValue: () => val
DefaultValueFactory = _ => val
ParseArgument<T>
委托
CustomParser
属性

Agent Gotchas

注意事项

  1. Do not use beta4 API patterns. The 2.0.0 GA API is fundamentally different. There is no
    SetHandler
    -- use
    SetAction
    . There is no
    InvocationContext
    -- actions receive
    ParseResult
    directly. There is no
    CommandLineBuilder
    -- configuration uses
    ParserConfiguration
    /
    InvocationConfiguration
    .
  2. Do not reference discontinued packages.
    System.CommandLine.Hosting
    ,
    System.CommandLine.NamingConventionBinder
    , and
    System.CommandLine.Rendering
    are discontinued. Use the single
    System.CommandLine
    package.
  3. Do not confuse
    Option<T>
    with
    Argument<T>
    .
    Options are named (
    --output file.txt
    ), arguments are positional (
    mycli file.txt
    ). Using the wrong type produces confusing parse errors.
  4. Do not use
    AddOption
    /
    AddCommand
    /
    AddAlias
    methods.
    These were replaced by mutable collection properties:
    Options.Add
    ,
    Subcommands.Add
    ,
    Aliases.Add
    . The old methods do not exist in 2.0.0.
  5. Do not use
    IConsole
    or
    TestConsole
    for testing.
    These interfaces were removed. Use
    InvocationConfiguration
    with
    StringWriter
    for
    Output
    /
    Error
    to capture test output.
  6. Do not ignore the
    CancellationToken
    in async actions.
    In 2.0.0 GA,
    CancellationToken
    is a mandatory second parameter for async
    SetAction
    delegates. The compiler warns (CA2016) when it is not propagated.
  7. Do not write
    Console.Out
    directly in command actions.
    Write to
    InvocationConfiguration.Output
    for testability. If no configuration is provided, output goes to
    Console.Out
    by default, but direct writes bypass test capture.
  8. Do not set default values via constructors. Use the
    DefaultValueFactory
    property instead. The old
    getDefaultValue
    constructor parameter does not exist in 2.0.0.

  1. 请勿使用beta4 API模式。2.0.0正式版API与beta版本完全不同。不再有
    SetHandler
    ——请使用
    SetAction
    。不再有
    InvocationContext
    ——Action直接接收
    ParseResult
    。不再有
    CommandLineBuilder
    ——配置使用
    ParserConfiguration
    /
    InvocationConfiguration
  2. 请勿引用已停止维护的包
    System.CommandLine.Hosting
    System.CommandLine.NamingConventionBinder
    System.CommandLine.Rendering
    已停止维护。请使用单个
    System.CommandLine
    包。
  3. 请勿混淆
    Option<T>
    Argument<T>
    。选项是命名的(
    --output file.txt
    ),参数是位置性的(
    mycli file.txt
    )。使用错误类型会导致难以理解的解析错误。
  4. 请勿使用
    AddOption
    /
    AddCommand
    /
    AddAlias
    方法
    。这些方法已被可变集合属性取代:
    Options.Add
    Subcommands.Add
    Aliases.Add
    。旧方法在2.0.0版本中不存在。
  5. 请勿在测试中使用
    IConsole
    TestConsole
    。这些接口已被移除。请使用
    InvocationConfiguration
    搭配
    StringWriter
    作为
    Output
    /
    Error
    来捕获测试输出。
  6. 请勿忽略异步Action中的
    CancellationToken
    。在2.0.0正式版中,
    CancellationToken
    是异步
    SetAction
    委托的必填第二个参数。编译器会在未传播时发出警告(CA2016)。
  7. 请勿在命令Action中直接写入
    Console.Out
    。为了可测试性,请写入
    InvocationConfiguration.Output
    。如果未提供配置,输出默认会到
    Console.Out
    ,但直接写入会绕过测试捕获。
  8. 请勿通过构造函数设置默认值。请改用
    DefaultValueFactory
    属性。旧的
    getDefaultValue
    构造函数参数在2.0.0版本中不存在。

References

参考资料

Attribution

致谢

Adapted from Aaronontheweb/dotnet-skills (MIT license).
改编自Aaronontheweb/dotnet-skills(MIT许可证)。