dotnet-system-commandline
Using System.CommandLine 2.0. Commands, options, SetAction, custom parsing, middleware, testing.
What this skill does
# 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.
---
## 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.
---
## 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);
```
### 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" }
}
};
```
---
## 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
};
```
### 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);
```
### Constrained Values
```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");
```
### 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);
```
---
## 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
});
```
### 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;
});
```
### 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}");
});
```
### 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();
}
```
### 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
```
---
## Custom Type Parsing
### CustomParser Property
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");
Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.