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.
Scope
- RootCommand, Command, Option, Argument hierarchy
- SetAction handler binding (sync and async)
- ParseResult-based value access
- Custom type parsing and validation
- Tab completion and directives
- Testing with InvocationConfiguration and TextWriter capture
- Migration from beta4 to 2.0.0 GA
- Dependency injection integration without System.CommandLine.Hosting
Out of scope
- CLI application architecture patterns (layered design, 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]
- CLI packaging for Homebrew, apt, winget -- see [skill:dotnet-cli-packaging]
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.
Package Reference
<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.*" />
</ItemGroup>
```bash
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.
---
## RootCommand and Command Hierarchy
### 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);
```bash
### 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" }
}
};
```text
---
## Options and Arguments
### Option\<T\> -- Named Parameters
```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
};
```text
### Argument\<T\> -- Positional Parameters
```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);
```bash
### Constrained Values
```csharp
var formatOption = new Option<string>("--format")
{
Description = "Output format"
};
formatOption.AcceptOnlyFromAmong("json", "csv", "table");
rootCommand.Options.Add(formatOption);
```bash
### 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");
```text
### 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);
```bash
---
## Setting Actions (Command Handlers)
In 2.0.0 GA, `SetHandler` is replaced by `SetAction`. Actions receive a `ParseResult` directly (no `InvocationContext`).
### Synchronous 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
});
```text
### Asynchronous Action with CancellationToken
```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;
});
```text
### 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}");
});
```text
### 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();
}
```bash
### 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
```text
---
For detailed examples (custom parsing, validation, configuration, tab completion, DI, testing, migration), see `examples.md` in this skill directory.
## 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.
---
## References
- [System.CommandLine overview](https://learn.microsoft.com/en-us/dotnet/standard/commandline/)
- [System.CommandLine migration guide (beta5+)](https://learn.microsoft.com/en-us/dotnet/standard/commandline/migration-guide-2.0.0-beta5)
- [How to parse and invoke](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-parse-and-invoke)
- [How to customize parsing and validation](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-customize-parsing-and-validation)
- [System.CommandLine GitHub](https://github.com/dotnet/command-line-api)
---
## Attribution
Adapted from [Aaronontheweb/dotnet-skills](https://github.com/Aaronontheweb/dotnet-skills) (MIT license).
Code Navigation (Serena MCP)
Primary approach: Use Serena symbol operations for efficient code navigation:
- Find definitions:
serena_find_symbol instead of text search
- Understand structure:
serena_get_symbols_overview for file organization
- Track references:
serena_find_referencing_symbols for impact analysis
- Precise edits:
serena_replace_symbol_body for clean modifications
When to use Serena vs traditional tools:
- Use Serena: Navigation, refactoring, dependency analysis, precise edits
- Use Read/Grep: Reading full files, pattern matching, simple text operations
- Fallback: If Serena unavailable, traditional tools work fine
Example workflow:
# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"
1---2name: dotnet-system-commandline3category: developer-experience4subcategory: cli5description: Builds .NET CLI apps with System.CommandLine 2.0. Commands, options, SetAction, parsing, testing.6license: MIT7targets: ['*']8tags: [foundation, dotnet, skill]9version: '0.0.1'10author: 'dotnet-agent-harness'11invocable: true12claudecode:13 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']14codexcli:15 short-description: '.NET skill guidance for foundation tasks'16opencode:17 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']18copilot: {}19geminicli: {}20antigravity: {}21---2223# dotnet-system-commandline2425System.CommandLine 2.0 stable API for building .NET CLI applications. Covers RootCommand, Command, Option\<T\>,26Argument\<T\>, SetAction for handler binding, ParseResult-based value access, custom type parsing, validation, tab27completion, and testing with TextWriter capture.2829**Version assumptions:** .NET 8.0+ baseline. System.CommandLine 2.0.0+ (stable NuGet package, GA since November 2025).30All examples target the 2.0.0 GA API surface.3132**Breaking change note:** System.CommandLine 2.0.0 GA differs significantly from the pre-release beta4 API. Key changes:33`SetHandler` replaced by `SetAction`, `ICommandHandler` removed in favor of34`SynchronousCommandLineAction`/`AsynchronousCommandLineAction`, `InvocationContext` removed (ParseResult passed35directly), `CommandLineBuilder` and `AddMiddleware` removed, `IConsole` removed in favor of TextWriter properties, and36the `System.CommandLine.Hosting`/`System.CommandLine.NamingConventionBinder` packages discontinued. Do not use beta-era37patterns.3839## Scope4041- RootCommand, Command, Option<T>, Argument<T> hierarchy42- SetAction handler binding (sync and async)43- ParseResult-based value access44- Custom type parsing and validation45- Tab completion and directives46- Testing with InvocationConfiguration and TextWriter capture47- Migration from beta4 to 2.0.0 GA48- Dependency injection integration without System.CommandLine.Hosting4950## Out of scope5152- CLI application architecture patterns (layered design, exit codes, stdin/stdout/stderr) -- see53 [skill:dotnet-cli-architecture]54- Native AOT compilation -- see [skill:dotnet-native-aot]55- CLI distribution strategy -- see [skill:dotnet-cli-distribution]56- General CI/CD patterns -- see [skill:dotnet-gha-patterns] and [skill:dotnet-ado-patterns]57- DI container mechanics -- see [skill:dotnet-csharp-dependency-injection]58- General coding standards -- see [skill:dotnet-csharp-coding-standards]59- CLI packaging for Homebrew, apt, winget -- see [skill:dotnet-cli-packaging]6061Cross-references: [skill:dotnet-cli-architecture] for CLI design patterns, [skill:dotnet-native-aot] for AOT publishing62CLI tools, [skill:dotnet-csharp-dependency-injection] for DI fundamentals, [skill:dotnet-csharp-configuration] for63configuration integration, [skill:dotnet-csharp-coding-standards] for naming and style conventions.6465---6667## Package Reference6869````xml7071<ItemGroup>72 <PackageReference Include="System.CommandLine" Version="2.0.*" />73</ItemGroup>7475```bash7677System.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.7879---8081## RootCommand and Command Hierarchy8283### Basic Command Structure8485```csharp8687using System.CommandLine;8889// Root command -- the entry point90var rootCommand = new RootCommand("My CLI tool description");9192// Add a subcommand via mutable collection93var listCommand = new Command("list", "List all items");94rootCommand.Subcommands.Add(listCommand);9596// Nested subcommands: mycli migrate up97var migrateCommand = new Command("migrate", "Database migrations");98var upCommand = new Command("up", "Apply pending migrations");99var downCommand = new Command("down", "Revert last migration");100migrateCommand.Subcommands.Add(upCommand);101migrateCommand.Subcommands.Add(downCommand);102rootCommand.Subcommands.Add(migrateCommand);103104```bash105106### Collection Initializer Syntax107108```csharp109110// Fluent collection initializer (commands, options, arguments)111RootCommand rootCommand = new("My CLI tool")112{113 new Option<string>("--output", "-o") { Description = "Output file path" },114 new Argument<FileInfo>("file") { Description = "Input file" },115 new Command("list", "List all items")116 {117 new Option<int>("--limit") { Description = "Max items to return" }118 }119};120121```text122123---124125## Options and Arguments126127### Option\<T\> -- Named Parameters128129```csharp130131// Option<T> -- named parameter (--output, -o)132// name is the first parameter; additional params are aliases133var outputOption = new Option<FileInfo>("--output", "-o")134{135 Description = "Output file path",136 Required = true // was IsRequired in beta4137};138139// Option with default value via DefaultValueFactory140var verbosityOption = new Option<int>("--verbosity")141{142 Description = "Verbosity level (0-3)",143 DefaultValueFactory = _ => 1144};145146```text147148### Argument\<T\> -- Positional Parameters149150```csharp151152// Argument<T> -- positional parameter153// name is mandatory in 2.0 (used for help text)154var fileArgument = new Argument<FileInfo>("file")155{156 Description = "Input file to process"157};158159rootCommand.Arguments.Add(fileArgument);160161```bash162163### Constrained Values164165```csharp166167var formatOption = new Option<string>("--format")168{169 Description = "Output format"170};171formatOption.AcceptOnlyFromAmong("json", "csv", "table");172173rootCommand.Options.Add(formatOption);174175```bash176177### Aliases178179```csharp180181// Aliases are separate from the name in 2.0182// First constructor param is the name; rest are aliases183var verboseOption = new Option<bool>("--verbose", "-v")184{185 Description = "Enable verbose output"186};187188// Or add aliases after construction189verboseOption.Aliases.Add("-V");190191```text192193### Global Options194195```csharp196197// Global options are inherited by all subcommands198var debugOption = new Option<bool>("--debug")199{200 Description = "Enable debug mode",201 Recursive = true // makes it global (inherited by subcommands)202};203rootCommand.Options.Add(debugOption);204205```bash206207---208209## Setting Actions (Command Handlers)210211In 2.0.0 GA, `SetHandler` is replaced by `SetAction`. Actions receive a `ParseResult` directly (no `InvocationContext`).212213### Synchronous Action214215```csharp216217var outputOption = new Option<FileInfo>("--output", "-o")218{219 Description = "Output file path",220 Required = true221};222var verbosityOption = new Option<int>("--verbosity")223{224 DefaultValueFactory = _ => 1225};226227rootCommand.Options.Add(outputOption);228rootCommand.Options.Add(verbosityOption);229230rootCommand.SetAction(parseResult =>231{232 var output = parseResult.GetValue(outputOption)!;233 var verbosity = parseResult.GetValue(verbosityOption);234 Console.WriteLine($"Output: {output.FullName}, Verbosity: {verbosity}");235 return 0; // exit code236});237238```text239240### Asynchronous Action with CancellationToken241242```csharp243244// Async actions receive ParseResult AND CancellationToken245rootCommand.SetAction(async (ParseResult parseResult, CancellationToken ct) =>246{247 var output = parseResult.GetValue(outputOption)!;248 var verbosity = parseResult.GetValue(verbosityOption);249 await ProcessAsync(output, verbosity, ct);250 return 0;251});252253```text254255### Getting Values by Name256257```csharp258259// Values can also be retrieved by symbol name (requires type parameter)260rootCommand.SetAction(parseResult =>261{262 int delay = parseResult.GetValue<int>("--delay");263 string? message = parseResult.GetValue<string>("--message");264 Console.WriteLine($"Delay: {delay}, Message: {message}");265});266267```text268269### Parsing and Invoking270271```csharp272273// Program.cs entry point -- parse then invoke274static int Main(string[] args)275{276 var rootCommand = BuildCommand();277 ParseResult parseResult = rootCommand.Parse(args);278 return parseResult.Invoke();279}280281// Async entry point282static async Task<int> Main(string[] args)283{284 var rootCommand = BuildCommand();285 ParseResult parseResult = rootCommand.Parse(args);286 return await parseResult.InvokeAsync();287}288289```bash290291### Parse Without Invoking292293```csharp294295// Parse-only mode: inspect results without running actions296ParseResult parseResult = rootCommand.Parse(args);297if (parseResult.Errors.Count > 0)298{299 foreach (var error in parseResult.Errors)300 {301 Console.Error.WriteLine(error.Message);302 }303 return 1;304}305306FileInfo? file = parseResult.GetValue(fileOption);307// Process directly without SetAction308309```text310311---312313314For detailed examples (custom parsing, validation, configuration, tab completion, DI, testing, migration), see `examples.md` in this skill directory.315316## Agent Gotchas3173181. **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`.3192. **Do not reference discontinued packages.** `System.CommandLine.Hosting`, `System.CommandLine.NamingConventionBinder`, and `System.CommandLine.Rendering` are discontinued. Use the single `System.CommandLine` package.3203. **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.3214. **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.3225. **Do not use `IConsole` or `TestConsole` for testing.** These interfaces were removed. Use `InvocationConfiguration` with `StringWriter` for `Output`/`Error` to capture test output.3236. **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.3247. **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.3258. **Do not set default values via constructors.** Use the `DefaultValueFactory` property instead. The old `getDefaultValue` constructor parameter does not exist in 2.0.0.326327---328329## References330331- [System.CommandLine overview](https://learn.microsoft.com/en-us/dotnet/standard/commandline/)332- [System.CommandLine migration guide (beta5+)](https://learn.microsoft.com/en-us/dotnet/standard/commandline/migration-guide-2.0.0-beta5)333- [How to parse and invoke](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-parse-and-invoke)334- [How to customize parsing and validation](https://learn.microsoft.com/en-us/dotnet/standard/commandline/how-to-customize-parsing-and-validation)335- [System.CommandLine GitHub](https://github.com/dotnet/command-line-api)336337---338339## Attribution340341Adapted from [Aaronontheweb/dotnet-skills](https://github.com/Aaronontheweb/dotnet-skills) (MIT license).342````343344## Code Navigation (Serena MCP)345346**Primary approach:** Use Serena symbol operations for efficient code navigation:3473481. **Find definitions**: `serena_find_symbol` instead of text search3492. **Understand structure**: `serena_get_symbols_overview` for file organization3503. **Track references**: `serena_find_referencing_symbols` for impact analysis3514. **Precise edits**: `serena_replace_symbol_body` for clean modifications352353**When to use Serena vs traditional tools:**354355- **Use Serena**: Navigation, refactoring, dependency analysis, precise edits356- **Use Read/Grep**: Reading full files, pattern matching, simple text operations357- **Fallback**: If Serena unavailable, traditional tools work fine358359**Example workflow:**360361```text362# Instead of:363Read: src/Services/OrderService.cs364Grep: "public void ProcessOrder"365366# Use:367serena_find_symbol: "OrderService/ProcessOrder"368serena_get_symbols_overview: "src/Services/OrderService.cs"369```