diff --git a/docs/standard/commandline/conceptual-overview.md b/docs/standard/commandline/conceptual-overview.md new file mode 100644 index 0000000000000..e145d04a01e9d --- /dev/null +++ b/docs/standard/commandline/conceptual-overview.md @@ -0,0 +1,175 @@ +--- +title: Conceptual overview of System.CommandLine +ms.date: 01/22/2026 +description: "Understand the design philosophy and conceptual model of System.CommandLine and how it compares to other .NET command-line libraries." +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" +ms.topic: conceptual +--- + +# Conceptual overview of System.CommandLine + +This article explains the design philosophy and conceptual model behind `System.CommandLine` and how it differs from other popular .NET command-line parsing libraries. + +## Design philosophy + +`System.CommandLine` is built around several core principles: + +### API-first design + +The library provides a strongly-typed, object-oriented API for defining your command-line interface. Instead of using attributes on data classes, you explicitly construct `Command`, `Option`, and `Argument` objects that represent your CLI structure. This approach: + +- Makes the command structure explicit and discoverable through code +- Enables dynamic command construction based on runtime conditions +- Separates CLI definition from your application's data model +- Provides better control over parsing behavior and validation + +### Type safety and parsing + +`System.CommandLine` uses generic types like `Option` and `Argument` to provide compile-time type safety. The library automatically handles parsing command-line strings into strongly-typed values. Built-in support includes: + +- All primitive .NET types +- Common types like `DateTime`, `Guid`, `FileInfo`, and `DirectoryInfo` +- Enums (with case-insensitive matching) +- Arrays and `List` for multi-valued options and arguments +- Custom types through user-defined parsers + +### POSIX and Windows conventions + +The parser natively supports both POSIX-style (`--option value`, `-o value`) and Windows-style (`/option value`) command-line syntax. This ensures your CLI works naturally across platforms without extra configuration. + +### Rich user experience + +`System.CommandLine` automatically provides features that improve the end-user experience: + +- **Help generation** - Automatic `--help` text based on your command definitions and descriptions +- **Tab completion** - Shell integration for suggesting options, subcommands, and argument values +- **Error messages** - Clear, actionable error messages when parsing fails +- **Response files** - Support for reading arguments from files using `@response.txt` syntax + +### Extensibility + +The library is designed to be extended: + +- Custom validators for complex validation rules +- Custom parsers for specialized types or input formats +- Custom completions for dynamic suggestion lists +- Middleware pattern for cross-cutting concerns + +## Conceptual model + +`System.CommandLine` uses a hierarchical model of symbols to represent your CLI: + +### Commands + +A `Command` represents an action your application can perform. Commands can have: + +- Subcommands (creating a tree of nested commands) +- Options (named parameters that modify the command's behavior) +- Arguments (positional parameters required to execute the command) +- A handler (the code that executes when the command is invoked) + +For example, `git commit -m "message"` has: +- Root command: `git` +- Subcommand: `commit` +- Option: `-m` (with value `"message"`) + +### Options + +An `Option` is a named parameter specified with a prefix (`--`, `-`, or `/`). Options: + +- Are usually optional (but can be marked as required) +- Can have one or more values +- Can have aliases (for example, `-v` and `--verbose`) +- Can have default values + +### Arguments + +An `Argument` is a positional parameter that doesn't use a name prefix. Arguments: + +- Are specified by their position in the command line +- Can be required or optional +- Can accept multiple values +- Are often used for the primary input to a command (like a file path) + +### Parsing and invocation + +When you invoke `Parse()` or `Invoke()`, the library: + +1. Tokenizes the command-line input into discrete parts +2. Matches tokens to commands, options, and arguments +3. Validates the parsed values against defined rules +4. Converts string values to the appropriate .NET types +5. (For `Invoke()`) Executes the appropriate command handler + +## Comparison with other libraries + +### System.CommandLine vs. CommandLineParser + +**CommandLineParser** uses an attribute-based approach where you decorate properties on a POCO: + +```csharp +// CommandLineParser style +class Options +{ + [Option('v', "verbose", Required = false)] + public bool Verbose { get; set; } +} +``` + +**System.CommandLine** uses explicit API construction: + +```csharp +// System.CommandLine style +var verboseOption = new Option("--verbose", "Enable verbose output"); +``` + +**Key differences:** + +- **CommandLineParser** is simpler for basic scenarios but less flexible for complex command hierarchies +- **System.CommandLine** provides better support for subcommands and deeply nested command structures +- **System.CommandLine** offers built-in tab completion, which CommandLineParser doesn't support +- **System.CommandLine** is trim-friendly and optimized for AOT compilation +- **System.CommandLine** has a steeper learning curve but greater extensibility + +### System.CommandLine vs. McMaster.Extensions.CommandLineUtils + +**McMaster.Extensions.CommandLineUtils** supports both attribute-based and fluent builder patterns: + +```csharp +// McMaster style +var app = new CommandLineApplication(); +app.Option("-v|--verbose", "Enable verbose output", CommandOptionType.NoValue); +``` + +**Key differences:** + +- **McMaster.Extensions.CommandLineUtils** is lightweight and pragmatic for simple tools +- **System.CommandLine** provides more advanced features like validators, custom parsers, and structured completions +- **System.CommandLine** has stronger type safety through generics +- **System.CommandLine** is officially supported by Microsoft and used in .NET CLI tools +- **System.CommandLine** has better documentation and is the strategic choice for new .NET projects + +## When to use System.CommandLine + +`System.CommandLine` is ideal when you need: + +- **Complex command structures** with multiple levels of subcommands +- **Tab completion** support across different shells +- **Type-safe parsing** with compile-time guarantees +- **Trim-friendly** or AOT-compiled applications +- **Future-proof** CLI tools aligned with .NET's direction +- **Rich validation** and custom parsing logic +- **Consistent behavior** with other .NET CLI tools + +For simple, single-command tools where you just need to parse a few options, lighter-weight alternatives might be sufficient. But for any CLI that will grow in complexity or requires modern features, `System.CommandLine` provides the best foundation. + +## See also + +- [System.CommandLine overview](index.md) +- [Tutorial: Get started with System.CommandLine](get-started-tutorial.md) +- [Command-line syntax overview](syntax.md) +- [How to customize parsing and validation](how-to-customize-parsing-and-validation.md) diff --git a/docs/standard/commandline/custom-types.md b/docs/standard/commandline/custom-types.md new file mode 100644 index 0000000000000..e684dc7bc8e40 --- /dev/null +++ b/docs/standard/commandline/custom-types.md @@ -0,0 +1,539 @@ +--- +title: Custom type parsing in System.CommandLine +ms.date: 01/22/2026 +description: "Learn how to define options and arguments that accept custom data types, including complex types, specialized parsing, and collection scenarios." +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" + - "custom types" + - "custom parsing" +ms.topic: how-to +--- + +# Custom type parsing in System.CommandLine + +While `System.CommandLine` supports many common .NET types out of the box, you often need to parse custom types specific to your domain. This article provides detailed guidance and scenarios for parsing custom types. + +## When to use custom parsers + +Use custom parsers when you need to: + +- Parse domain-specific types (for example, `EmailAddress`, `IpAddress`, `Coordinate`) +- Parse composite types from single string values (for example, `"lat,lon"` to `Coordinate`) +- Parse data in non-standard formats (for example, durations as `"5m30s"`) +- Provide complex validation logic during parsing +- Transform input before creating the final value + +## Basic custom parser + +Define a custom parser using the `CustomParser` property on `Option` or `Argument`: + +```csharp +record Coordinate(double Latitude, double Longitude); + +var locationOption = new Option("--location", "Geographic location") +{ + CustomParser = result => + { + var value = result.Tokens.Single().Value; + var parts = value.Split(','); + + if (parts.Length != 2) + { + result.AddError("Location must be in format: latitude,longitude"); + return null; + } + + if (!double.TryParse(parts[0], out var lat) || + !double.TryParse(parts[1], out var lon)) + { + result.AddError("Latitude and longitude must be valid numbers"); + return null; + } + + if (lat < -90 || lat > 90) + { + result.AddError("Latitude must be between -90 and 90"); + return null; + } + + if (lon < -180 || lon > 180) + { + result.AddError("Longitude must be between -180 and 180"); + return null; + } + + return new Coordinate(lat, lon); + } +}; + +// Command line: --location 37.7749,-122.4194 +``` + +The `CustomParser` delegate receives an `ArgumentResult` and returns the parsed value (or `null` if parsing fails). + +## Parsing from multiple tokens + +Some custom types might need to consume multiple command-line tokens: + +```csharp +record DateRange(DateTime Start, DateTime End); + +var rangeArg = new Argument("date-range", "Start and end dates") +{ + Arity = new ArgumentArity(2, 2), + CustomParser = result => + { + var tokens = result.Tokens; + + if (!DateTime.TryParse(tokens[0].Value, out var start)) + { + result.AddError($"Invalid start date: {tokens[0].Value}"); + return null; + } + + if (!DateTime.TryParse(tokens[1].Value, out var end)) + { + result.AddError($"Invalid end date: {tokens[1].Value}"); + return null; + } + + if (end < start) + { + result.AddError("End date must be after start date"); + return null; + } + + return new DateRange(start, end); + } +}; + +// Command line: myapp 2026-01-01 2026-12-31 +``` + +## Parsing enumerations with aliases + +Extend enum parsing to support aliases or alternative names: + +```csharp +enum Priority +{ + Low, + Medium, + High, + Critical +} + +var priorityOption = new Option("--priority", "Task priority") +{ + CustomParser = result => + { + var value = result.Tokens.Single().Value.ToLowerInvariant(); + + return value switch + { + "low" or "l" or "1" => Priority.Low, + "medium" or "med" or "m" or "2" => Priority.Medium, + "high" or "h" or "3" => Priority.High, + "critical" or "crit" or "c" or "4" => Priority.Critical, + _ => throw new InvalidOperationException( + $"Invalid priority: {value}. Valid values: low, medium, high, critical") + }; + } +}; + +// Command line: --priority high +// Command line: --priority h +// Command line: --priority 3 +``` + +## Parsing collections of custom types + +Parse arrays or lists of custom types: + +```csharp +record Server(string Name, int Port); + +var serversOption = new Option("--servers", "Server configurations") +{ + AllowMultipleArgumentsPerToken = true, + CustomParser = result => + { + var servers = new List(); + + foreach (var token in result.Tokens) + { + var parts = token.Value.Split(':'); + if (parts.Length != 2) + { + result.AddError($"Invalid server format: {token.Value}. Use name:port"); + return null; + } + + if (!int.TryParse(parts[1], out var port)) + { + result.AddError($"Invalid port number: {parts[1]}"); + return null; + } + + servers.Add(new Server(parts[0], port)); + } + + return servers.ToArray(); + } +}; + +// Command line: --servers web:8080 api:8081 db:5432 +``` + +## Parsing from external sources + +Custom parsers can load data from files or other sources: + +```csharp +record Configuration +{ + public string Environment { get; init; } + public Dictionary Settings { get; init; } +} + +var configOption = new Option("--config", "Configuration file path") +{ + CustomParser = result => + { + var filePath = result.Tokens.Single().Value; + + if (!File.Exists(filePath)) + { + result.AddError($"Configuration file not found: {filePath}"); + return null; + } + + try + { + var json = File.ReadAllText(filePath); + return JsonSerializer.Deserialize(json); + } + catch (JsonException ex) + { + result.AddError($"Invalid JSON in configuration file: {ex.Message}"); + return null; + } + } +}; + +// Command line: --config settings.json +``` + +## Parsing URLs and URIs + +Parse and validate URL strings: + +```csharp +var apiUrlOption = new Option("--api-url", "API endpoint URL") +{ + CustomParser = result => + { + var value = result.Tokens.Single().Value; + + if (!Uri.TryCreate(value, UriKind.Absolute, out var uri)) + { + result.AddError($"Invalid URL: {value}"); + return null; + } + + if (uri.Scheme != "http" && uri.Scheme != "https") + { + result.AddError("URL must use HTTP or HTTPS scheme"); + return null; + } + + return uri; + } +}; + +// Command line: --api-url https://api.example.com/v1 +``` + +## Parsing time durations with units + +Parse human-friendly duration strings: + +```csharp +var timeoutOption = new Option("--timeout", "Operation timeout") +{ + CustomParser = result => + { + var value = result.Tokens.Single().Value; + var pattern = @"^(\d+)([smhd])$"; + var match = Regex.Match(value, pattern); + + if (!match.Success) + { + result.AddError("Timeout must be in format: (e.g., 30s, 5m, 2h, 1d)"); + return null; + } + + var amount = int.Parse(match.Groups[1].Value); + var unit = match.Groups[2].Value; + + return unit switch + { + "s" => TimeSpan.FromSeconds(amount), + "m" => TimeSpan.FromMinutes(amount), + "h" => TimeSpan.FromHours(amount), + "d" => TimeSpan.FromDays(amount), + _ => throw new InvalidOperationException() + }; + } +}; + +// Command line: --timeout 30s +// Command line: --timeout 5m +``` + +## Combining parsing with validation + +Custom parsers can include complex validation logic: + +```csharp +record EmailAddress +{ + public string Value { get; init; } + + public EmailAddress(string email) + { + if (!IsValidEmail(email)) + throw new ArgumentException($"Invalid email address: {email}"); + Value = email; + } + + private static bool IsValidEmail(string email) + { + try + { + var addr = new System.Net.Mail.MailAddress(email); + return addr.Address == email; + } + catch + { + return false; + } + } +} + +var emailOption = new Option("--email", "Email address") +{ + CustomParser = result => + { + var value = result.Tokens.Single().Value; + + try + { + return new EmailAddress(value); + } + catch (ArgumentException ex) + { + result.AddError(ex.Message); + return null; + } + } +}; + +// Command line: --email user@example.com +``` + +## Reusable custom parsers + +Create reusable parser functions for types used across multiple options: + +```csharp +static class CustomParsers +{ + public static Coordinate? ParseCoordinate(ArgumentResult result) + { + var value = result.Tokens.Single().Value; + var parts = value.Split(','); + + if (parts.Length != 2 || + !double.TryParse(parts[0], out var lat) || + !double.TryParse(parts[1], out var lon)) + { + result.AddError("Coordinate must be in format: latitude,longitude"); + return null; + } + + return new Coordinate(lat, lon); + } +} + +// Use in multiple options +var startOption = new Option("--start", "Start location") +{ + CustomParser = CustomParsers.ParseCoordinate +}; + +var endOption = new Option("--end", "End location") +{ + CustomParser = CustomParsers.ParseCoordinate +}; +``` + +## Generic parsing with ISpanParseable + +.NET 7 and later provide the interface, which allows types to be parsed from a `ReadOnlySpan`. You can create a generic parser that works with any type implementing this interface: + +```csharp +#if NET7_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; + +static class CustomParsers +{ + public static T? ParseSpanParseable(ArgumentResult result) + where T : ISpanParseable + { + var value = result.Tokens.Single().Value; + + if (T.TryParse(value.AsSpan(), provider: null, out var parsed)) + { + return parsed; + } + + result.AddError($"Cannot parse '{value}' as {typeof(T).Name}"); + return default; + } +} + +// This works with any type that implements ISpanParseable +// Examples: Int128, Half, DateOnly, TimeOnly, and your own custom types + +var int128Option = new Option("--large-number", "A very large integer") +{ + CustomParser = CustomParsers.ParseSpanParseable +}; + +var halfOption = new Option("--precision", "Half-precision floating point") +{ + CustomParser = CustomParsers.ParseSpanParseable +}; +#endif +``` + +You can also implement `ISpanParseable` on your own types to enable this generic parsing: + +```csharp +#if NET7_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; + +readonly record struct Temperature : ISpanParseable +{ + public double Value { get; init; } + public TemperatureUnit Unit { get; init; } + + public static Temperature Parse(ReadOnlySpan s, IFormatProvider? provider) + { + if (!TryParse(s, provider, out var result)) + throw new FormatException($"Invalid temperature format: {s}"); + return result; + } + + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, + [MaybeNullWhen(false)] out Temperature result) + { + result = default; + + if (s.IsEmpty) + return false; + + var unit = s[^1] switch + { + 'C' or 'c' => TemperatureUnit.Celsius, + 'F' or 'f' => TemperatureUnit.Fahrenheit, + 'K' or 'k' => TemperatureUnit.Kelvin, + _ => TemperatureUnit.None + }; + + var valueSpan = unit != TemperatureUnit.None ? s[..^1] : s; + + if (!double.TryParse(valueSpan, out var value)) + return false; + + result = new Temperature { Value = value, Unit = unit }; + return true; + } + + public static Temperature Parse(string s, IFormatProvider? provider) + => Parse(s.AsSpan(), provider); + + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, + [MaybeNullWhen(false)] out Temperature result) + { + if (s is null) + { + result = default; + return false; + } + return TryParse(s.AsSpan(), provider, out result); + } +} + +enum TemperatureUnit { None, Celsius, Fahrenheit, Kelvin } + +// Use the generic parser +var tempOption = new Option("--temperature", "Temperature value") +{ + CustomParser = CustomParsers.ParseSpanParseable +}; + +// Command line: --temperature 23.5C +// Command line: --temperature 75F +// Command line: --temperature 298K +#endif +``` + +This approach provides: + +- **Type safety** - Works with any `ISpanParseable` type +- **Performance** - Avoids string allocations using `ReadOnlySpan` +- **Reusability** - Single generic parser for multiple types +- **Consistency** - Uses the same parsing logic as the type's built-in parser + +## Error handling best practices + +When creating custom parsers: + +1. **Use specific error messages** - Tell users exactly what went wrong and what format is expected +2. **Use `result.AddError()`** - Add errors to the `ArgumentResult` rather than throwing exceptions +3. **Return `null` on failure** - Signal parsing failure by returning `null` (or `default`) +4. **Validate early** - Check format and constraints as soon as possible +5. **Provide examples** - Include format examples in error messages + +```csharp +// Good error handling +CustomParser = result => +{ + var value = result.Tokens.Single().Value; + + if (string.IsNullOrWhiteSpace(value)) + { + result.AddError("Value cannot be empty"); + return null; + } + + if (!value.Contains('@')) + { + result.AddError($"Invalid email format: {value}. Example: user@example.com"); + return null; + } + + // ... rest of parsing logic +} +``` + +## See also + +- [How to customize parsing and validation](how-to-customize-parsing-and-validation.md) +- [Supported types in System.CommandLine](supported-types.md) +- [System.CommandLine overview](index.md) diff --git a/docs/standard/commandline/defaultvalue-vs-customparser.md b/docs/standard/commandline/defaultvalue-vs-customparser.md new file mode 100644 index 0000000000000..ab5be783ba293 --- /dev/null +++ b/docs/standard/commandline/defaultvalue-vs-customparser.md @@ -0,0 +1,560 @@ +--- +title: DefaultValueFactory vs CustomParser in System.CommandLine +ms.date: 01/23/2026 +description: "Understand the difference between DefaultValueFactory and CustomParser, when to use each, and how they work together in System.CommandLine." +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" + - "DefaultValueFactory" + - "CustomParser" + - "default values" + - "parsing" +ms.topic: conceptual +--- + +# DefaultValueFactory vs CustomParser in System.CommandLine + +`DefaultValueFactory` and `CustomParser` are two powerful features in System.CommandLine, but they serve very different purposes. Understanding when to use each is crucial for building robust command-line applications. + +## The fundamental difference + +The key distinction is **when** each executes and **what** it controls: + +| Feature | Purpose | When it executes | Input | Output | +|---------|---------|------------------|-------|--------| +| `DefaultValueFactory` | Provide a value when nothing is specified | When option/argument is **not present** | `ArgumentResult` | Default value | +| `CustomParser` | Control how input is parsed | When option/argument **is present** | Command-line tokens | Parsed value | + +## DefaultValueFactory: Providing defaults + +`DefaultValueFactory` is called when the user **does not** provide a value on the command line. + +### Basic usage + +```csharp +var portOption = new Option("--port", "-p") +{ + Description = "Server port", + DefaultValueFactory = _ => 8080 +}; + +// Command line: myapp → port = 8080 (from DefaultValueFactory) +// Command line: myapp --port 3000 → port = 3000 (from command line) +``` + +### When DefaultValueFactory executes + +```csharp +var debugOption = new Option("--debug") +{ + DefaultValueFactory = result => + { + Console.WriteLine("DefaultValueFactory called!"); + return false; + } +}; + +// Command line: myapp --debug +// Output: (nothing - DefaultValueFactory NOT called) + +// Command line: myapp +// Output: DefaultValueFactory called! +``` + +**Important:** `DefaultValueFactory` is **never** called if the user provides the option on the command line, even if they don't provide a value. + +### Common DefaultValueFactory patterns + +#### Environment variable fallback + +```csharp +var apiKeyOption = new Option("--api-key") +{ + DefaultValueFactory = _ => + Environment.GetEnvironmentVariable("API_KEY") ?? "default-key" +}; + +// Priority: +// 1. Command line value (if provided) +// 2. API_KEY environment variable +// 3. "default-key" +``` + +#### Dynamic defaults + +```csharp +var outputOption = new Option("--output") +{ + DefaultValueFactory = _ => + new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "bin")) +}; + +// Default changes based on current directory +``` + +#### Expensive computation + +```csharp +var cacheOption = new Option("--cache-dir") +{ + DefaultValueFactory = _ => + { + // Only computed if --cache-dir not provided + return FindUserCacheDirectory(); // Expensive operation + } +}; +``` + +## CustomParser: Controlling parsing + +`CustomParser` is called when the user **does** provide a value, to convert command-line tokens into the target type. + +### Basic usage + +```csharp +var coordinateOption = new Option<(double lat, double lon)>("--location") +{ + CustomParser = result => + { + Console.WriteLine("CustomParser called!"); + var value = result.Tokens.Single().Value; + var parts = value.Split(','); + return (double.Parse(parts[0]), double.Parse(parts[1])); + } +}; + +// Command line: myapp --location 37.7,-122.4 +// Output: CustomParser called! +// Result: (37.7, -122.4) + +// Command line: myapp +// Output: (nothing - CustomParser NOT called) +// Result: default value for tuple type +``` + +### When CustomParser executes + +```csharp +var valueOption = new Option("--value") +{ + CustomParser = result => + { + Console.WriteLine($"Parsing: {result.Tokens.Single().Value}"); + return int.Parse(result.Tokens.Single().Value); + } +}; + +// Command line: myapp --value 42 +// Output: Parsing: 42 + +// Command line: myapp +// Output: (nothing - CustomParser NOT called) +``` + +**Important:** `CustomParser` is **only** called when tokens are present for the option/argument. + +### Common CustomParser patterns + +#### Complex type parsing + +```csharp +record DateRange(DateTime Start, DateTime End); + +var rangeOption = new Option("--range") +{ + Arity = new ArgumentArity(2, 2), + CustomParser = result => + { + var start = DateTime.Parse(result.Tokens[0].Value); + var end = DateTime.Parse(result.Tokens[1].Value); + + if (end < start) + { + result.AddError("End date must be after start date"); + return null; + } + + return new DateRange(start, end); + } +}; + +// Command line: myapp --range 2026-01-01 2026-12-31 +``` + +#### Alternative formats + +```csharp +var durationOption = new Option("--timeout") +{ + CustomParser = result => + { + var value = result.Tokens.Single().Value; + + // Support multiple formats + if (value.EndsWith("ms")) + return TimeSpan.FromMilliseconds(double.Parse(value[..^2])); + if (value.EndsWith("s")) + return TimeSpan.FromSeconds(double.Parse(value[..^1])); + if (value.EndsWith("m")) + return TimeSpan.FromMinutes(double.Parse(value[..^1])); + + // Fall back to TimeSpan.Parse + return TimeSpan.Parse(value); + } +}; + +// Command line: myapp --timeout 500ms → TimeSpan.FromMilliseconds(500) +// Command line: myapp --timeout 30s → TimeSpan.FromSeconds(30) +// Command line: myapp --timeout 00:05:00 → TimeSpan.Parse("00:05:00") +``` + +## Using both together + +`DefaultValueFactory` and `CustomParser` work together to provide complete control: + +```csharp +var portOption = new Option("--port") +{ + Description = "Server port (or set PORT environment variable)", + + // Called when --port is NOT provided + DefaultValueFactory = _ => + { + var envPort = Environment.GetEnvironmentVariable("PORT"); + return envPort != null ? int.Parse(envPort) : 8080; + }, + + // Called when --port IS provided + CustomParser = result => + { + var value = result.Tokens.Single().Value; + + if (!int.TryParse(value, out var port)) + { + result.AddError($"Invalid port: {value}"); + return 0; + } + + if (port < 1 || port > 65535) + { + result.AddError($"Port must be 1-65535, got {port}"); + return 0; + } + + return port; + } +}; + +// Command line: myapp +// → DefaultValueFactory called → checks PORT env var → defaults to 8080 + +// Command line: myapp --port 3000 +// → CustomParser called → validates 3000 → returns 3000 + +// Command line: myapp --port 99999 +// → CustomParser called → validates 99999 → error! +``` + +## Execution flow diagram + +``` +User input: myapp --port 3000 + | + v + Option present? + / \ + No Yes + | | + v v + DefaultValueFactory CustomParser + | | + v v + Default Parsed + Value Value + | | + +----+----+ + | + v + Final Value +``` + +## Common pitfalls + +### Pitfall 1: Using CustomParser for defaults + +❌ **Don't use CustomParser to provide defaults:** + +```csharp +// WRONG: CustomParser won't be called if option is missing +var option = new Option("--value") +{ + CustomParser = result => + { + if (result.Tokens.Count == 0) + return 42; // This never executes! + return int.Parse(result.Tokens.Single().Value); + } +}; +``` + +✅ **Use DefaultValueFactory for defaults:** + +```csharp +// CORRECT +var option = new Option("--value") +{ + DefaultValueFactory = _ => 42 +}; +``` + +### Pitfall 2: Using DefaultValueFactory for parsing + +❌ **Don't use DefaultValueFactory to parse values:** + +```csharp +// WRONG: DefaultValueFactory not called when option is provided +var option = new Option("--value") +{ + DefaultValueFactory = result => + { + // Trying to parse from tokens - wrong place! + if (result.Tokens.Count > 0) + return int.Parse(result.Tokens.Single().Value); + return 0; + } +}; +``` + +✅ **Use CustomParser for parsing:** + +```csharp +// CORRECT +var option = new Option("--value") +{ + CustomParser = result => + { + var value = result.Tokens.Single().Value; + return int.Parse(value); + }, + DefaultValueFactory = _ => 0 +}; +``` + +### Pitfall 3: Duplicate logic + +❌ **Avoid duplicating validation in both:** + +```csharp +// WRONG: Validation duplicated +var portOption = new Option("--port") +{ + DefaultValueFactory = result => + { + var port = 8080; + if (port < 1 || port > 65535) // Unnecessary validation + return 0; + return port; + }, + CustomParser = result => + { + var port = int.Parse(result.Tokens.Single().Value); + if (port < 1 || port > 65535) // Duplicated validation + { + result.AddError("Invalid port"); + return 0; + } + return port; + } +}; +``` + +✅ **Use validators for validation:** + +```csharp +// CORRECT: Validation in one place +var portOption = new Option("--port") +{ + DefaultValueFactory = _ => 8080, + CustomParser = result => int.Parse(result.Tokens.Single().Value) +}; + +portOption.Validators.Add(result => +{ + var port = result.GetValue(); + if (port < 1 || port > 65535) + { + result.AddError($"Port must be 1-65535, got {port}"); + } +}); +``` + +## Decision tree + +Use this decision tree to choose the right approach: + +``` +Do you need custom behavior? + | + +-- No → Use built-in type support + | + +-- Yes → When does it apply? + | + +-- When option is NOT provided + | → Use DefaultValueFactory + | Examples: + | - Environment variable fallback + | - Computed default values + | - Context-aware defaults + | + +-- When option IS provided + → Use CustomParser + Examples: + - Parsing complex types + - Alternative input formats + - Multi-token parsing +``` + +## Real-world example: Configuration file path + +A complete example showing both features: + +```csharp +var configOption = new Option("--config") +{ + Description = "Configuration file path", + + // Default: look in standard locations + DefaultValueFactory = _ => + { + // Check standard locations in order + var candidates = new[] + { + "app.config.json", + Path.Combine(Environment.GetFolderPath( + Environment.SpecialFolder.ApplicationData), + "myapp", "config.json"), + "/etc/myapp/config.json" + }; + + foreach (var path in candidates) + { + if (File.Exists(path)) + { + return new FileInfo(path); + } + } + + return null; // No default config found + }, + + // Custom parsing: expand ~ and environment variables + CustomParser = result => + { + var value = result.Tokens.Single().Value; + + // Expand ~ to home directory + if (value.StartsWith("~/") || value == "~") + { + var home = Environment.GetFolderPath( + Environment.SpecialFolder.UserProfile); + value = value == "~" + ? home + : Path.Combine(home, value[2..]); + } + + // Expand environment variables + value = Environment.ExpandEnvironmentVariables(value); + + // Create FileInfo + var fileInfo = new FileInfo(value); + + if (!fileInfo.Exists) + { + result.AddError($"Config file not found: {fileInfo.FullName}"); + return null; + } + + return fileInfo; + } +}; + +// Usage examples: +// myapp +// → DefaultValueFactory searches standard locations + +// myapp --config ~/my-config.json +// → CustomParser expands ~ and validates file exists + +// myapp --config %APPDATA%\myapp\config.json +// → CustomParser expands %APPDATA% and validates +``` + +## Performance considerations + +### DefaultValueFactory + +- Only called when option is not provided +- Good for expensive computations (lazy evaluation) +- Can safely do I/O, network calls, etc. + +```csharp +var cacheOption = new Option("--cache-dir") +{ + DefaultValueFactory = _ => + { + // This expensive search only runs if --cache-dir not provided + return SearchFileSystem("/", "cache"); + } +}; +``` + +### CustomParser + +- Called for every token when option is provided +- Should be relatively fast +- Avoid expensive operations in the hot path + +```csharp +var urlOption = new Option("--url") +{ + CustomParser = result => + { + var value = result.Tokens.Single().Value; + + // GOOD: Fast parsing + if (Uri.TryCreate(value, UriKind.Absolute, out var uri)) + return uri; + + // BAD: Don't do network I/O in CustomParser + // return CheckUrlIsReachable(value); // Don't do this! + + result.AddError($"Invalid URL: {value}"); + return null; + } +}; +``` + +## Summary + +| Scenario | Use | +|----------|-----| +| Provide a default when option is missing | `DefaultValueFactory` | +| Read from environment variable as fallback | `DefaultValueFactory` | +| Compute expensive default value | `DefaultValueFactory` | +| Parse custom type from tokens | `CustomParser` | +| Support alternative input formats | `CustomParser` | +| Parse multiple tokens into one value | `CustomParser` | +| Validate parsed values | `Validators` (not CustomParser) | + +**Remember:** +- `DefaultValueFactory` = What happens when option is **absent** +- `CustomParser` = What happens when option is **present** + +## See also + +- [How to customize parsing and validation](how-to-customize-parsing-and-validation.md) +- [Custom type parsing in System.CommandLine](custom-types.md) +- [Using environment variables with options](environment-variables.md) +- [Supported types in System.CommandLine](supported-types.md) diff --git a/docs/standard/commandline/environment-variables.md b/docs/standard/commandline/environment-variables.md new file mode 100644 index 0000000000000..5836abfbc44d2 --- /dev/null +++ b/docs/standard/commandline/environment-variables.md @@ -0,0 +1,491 @@ +--- +title: Using environment variables with System.CommandLine +ms.date: 01/23/2026 +description: "Learn how to integrate environment variables into command-line options using DefaultValueFactory and CustomParser." +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" + - "environment variables" + - "default values" +ms.topic: how-to +--- + +# Using environment variables with System.CommandLine + +Command-line applications often need to read configuration from environment variables. This article explains how to integrate environment variables into your CLI using `DefaultValueFactory` and `CustomParser`. + +## Why environment variables? + +Environment variables are useful for: +- **CI/CD pipelines** - Set configuration without changing command lines +- **User preferences** - Allow users to set defaults in their shell profile +- **Sensitive data** - Avoid passing secrets via command-line arguments (which appear in process lists) +- **Container orchestration** - Configure applications via environment in Docker/Kubernetes +- **Fallback values** - Provide defaults when command-line options aren't specified + +## Using DefaultValueFactory + +The `DefaultValueFactory` property lets you compute default values at parse time, including reading from environment variables. + +### Basic environment variable fallback + +```csharp +var apiKeyOption = new Option("--api-key") +{ + Description = "API key for authentication (or set API_KEY environment variable)", + DefaultValueFactory = _ => Environment.GetEnvironmentVariable("API_KEY") +}; + +// Command line: myapp → reads from API_KEY env var +// Command line: myapp --api-key abc123 → uses "abc123" +``` + +The environment variable is only used when the option isn't specified on the command line. + +### Multiple fallback levels + +```csharp +var configOption = new Option("--configuration", "-c") +{ + Description = "Build configuration", + DefaultValueFactory = _ => + Environment.GetEnvironmentVariable("BUILD_CONFIGURATION") ?? + Environment.GetEnvironmentVariable("CONFIGURATION") ?? + "Debug" // Final fallback +}; + +// Priority: +// 1. Command line: --configuration Release +// 2. BUILD_CONFIGURATION environment variable +// 3. CONFIGURATION environment variable +// 4. Hard-coded default: "Debug" +``` + +### Environment-aware defaults + +Adjust defaults based on the environment: + +```csharp +private static bool IsCIEnvironment() => + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) || + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TF_BUILD")) || + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")); + +var interactiveOption = new Option("--interactive") +{ + Description = "Enable interactive mode", + DefaultValueFactory = _ => !IsCIEnvironment() && !Console.IsOutputRedirected +}; + +// Automatically defaults to: +// - false in CI environments +// - false when output is redirected +// - true in normal interactive terminal sessions +``` + +### Parsing environment variable values + +Some environment variables need type conversion: + +```csharp +var parallelOption = new Option("--parallel", "-p") +{ + Description = "Number of parallel operations (or set MAX_PARALLELISM)", + DefaultValueFactory = _ => + { + var envValue = Environment.GetEnvironmentVariable("MAX_PARALLELISM"); + if (int.TryParse(envValue, out var parsed)) + { + return parsed; + } + return Environment.ProcessorCount; // Default to CPU count + } +}; +``` + +### Boolean environment variables + +Parse truthy/falsy environment variable values: + +```csharp +public static bool ParseBooleanEnvVar(string? value, bool defaultValue = false) +{ + if (string.IsNullOrWhiteSpace(value)) + { + return defaultValue; + } + + var normalized = value.Trim().ToLowerInvariant(); + return normalized switch + { + "true" or "1" or "yes" or "on" or "enabled" => true, + "false" or "0" or "no" or "off" or "disabled" => false, + _ => defaultValue + }; +} + +var verboseOption = new Option("--verbose", "-v") +{ + Description = "Enable verbose output (or set VERBOSE=true)", + DefaultValueFactory = _ => + ParseBooleanEnvVar(Environment.GetEnvironmentVariable("VERBOSE")) +}; +``` + +### File path expansion + +Expand environment variables in file paths: + +```csharp +var outputOption = new Option("--output", "-o") +{ + Description = "Output directory", + DefaultValueFactory = _ => + { + var path = Environment.GetEnvironmentVariable("OUTPUT_DIR") ?? "./bin"; + var expanded = Environment.ExpandEnvironmentVariables(path); + return new DirectoryInfo(expanded); + } +}; + +// Supports: OUTPUT_DIR=%USERPROFILE%\builds +// Expands to: C:\Users\username\builds +``` + +## Using CustomParser with environment variables + +For more complex scenarios, use `CustomParser` to combine command-line and environment variable input. + +### Override or augment with environment variables + +```csharp +var sourcesOption = new Option>("--source", "-s") +{ + Description = "Package sources (or set NUGET_SOURCES)", + Arity = ArgumentArity.ZeroOrMore, + CustomParser = result => + { + var sources = new List(); + + // Add sources from command line + sources.AddRange(result.Tokens.Select(t => t.Value)); + + // Add sources from environment variable + var envSources = Environment.GetEnvironmentVariable("NUGET_SOURCES"); + if (!string.IsNullOrEmpty(envSources)) + { + sources.AddRange(envSources.Split(';', StringSplitOptions.RemoveEmptyEntries)); + } + + // Add default if nothing provided + if (sources.Count == 0) + { + sources.Add("https://api.nuget.org/v3/index.json"); + } + + return sources; + } +}; + +// Command line: --source https://myget.org +// NUGET_SOURCES: https://private.feed;https://backup.feed +// Result: All three sources are used +``` + +### Environment variable as required fallback + +```csharp +var connectionStringOption = new Option("--connection-string") +{ + Description = "Database connection string (or set DB_CONNECTION_STRING)", + CustomParser = result => + { + // Check command line first + if (result.Tokens.Count > 0) + { + return result.Tokens.Single().Value; + } + + // Fall back to environment variable + var envValue = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); + if (!string.IsNullOrEmpty(envValue)) + { + return envValue; + } + + // Neither provided - error + result.AddError("Connection string must be provided via --connection-string or DB_CONNECTION_STRING environment variable"); + return null; + } +}; +``` + +### Validate environment variable values + +```csharp +var timeoutOption = new Option("--timeout") +{ + Description = "Request timeout (or set REQUEST_TIMEOUT in seconds)", + CustomParser = result => + { + TimeSpan parsed; + + if (result.Tokens.Count > 0) + { + // Parse from command line + if (!TimeSpan.TryParse(result.Tokens.Single().Value, out parsed)) + { + result.AddError($"Invalid timeout format: {result.Tokens.Single().Value}"); + return default; + } + } + else + { + // Parse from environment variable + var envValue = Environment.GetEnvironmentVariable("REQUEST_TIMEOUT"); + if (envValue != null && int.TryParse(envValue, out var seconds)) + { + parsed = TimeSpan.FromSeconds(seconds); + } + else + { + parsed = TimeSpan.FromSeconds(30); // Default + } + } + + // Validate the value + if (parsed > TimeSpan.FromMinutes(10)) + { + result.AddError("Timeout cannot exceed 10 minutes"); + return default; + } + + return parsed; + } +}; +``` + +## Patterns and best practices + +### Document environment variables in help text + +Make environment variables discoverable: + +```csharp +var option = new Option("--api-url") +{ + Description = "API endpoint URL (default: API_URL environment variable or https://api.example.com)" +}; +``` + +### Prefix environment variables consistently + +Use a consistent prefix for your application: + +```csharp +public static class AppEnvironment +{ + private const string Prefix = "MYAPP_"; + + public static string? GetValue(string name) => + Environment.GetEnvironmentVariable($"{Prefix}{name}"); + + public static T GetValue(string name, T defaultValue, Func parser) + { + var value = GetValue(name); + return value != null ? parser(value) : defaultValue; + } +} + +var verboseOption = new Option("--verbose") +{ + DefaultValueFactory = _ => + AppEnvironment.GetValue("VERBOSE", false, bool.Parse) +}; +``` + +### Lazy evaluation for expensive operations + +Use `DefaultValueFactory` for expensive operations that should only run when needed: + +```csharp +private static readonly Lazy CachedUserDirectory = new(() => +{ + // Expensive: might hit network, read multiple files, etc. + return FindUserConfigurationDirectory(); +}); + +var configOption = new Option("--config-dir") +{ + Description = "Configuration directory", + DefaultValueFactory = _ => + Environment.GetEnvironmentVariable("CONFIG_DIR") ?? + CachedUserDirectory.Value +}; +``` + +### Combine with validation + +```csharp +var portOption = new Option("--port", "-p") +{ + Description = "Server port (or set PORT environment variable)", + DefaultValueFactory = _ => + { + var envValue = Environment.GetEnvironmentVariable("PORT"); + return int.TryParse(envValue, out var port) ? port : 8080; + } +}; + +portOption.Validators.Add(result => +{ + var port = result.GetValue(); + if (port < 1 || port > 65535) + { + result.AddError($"Port must be between 1 and 65535, got {port}"); + } +}); +``` + +### Provide dotenv file support + +Read environment variables from a `.env` file: + +```csharp +public static void LoadDotEnvFile(string filePath = ".env") +{ + if (!File.Exists(filePath)) + { + return; + } + + foreach (var line in File.ReadAllLines(filePath)) + { + var trimmed = line.Trim(); + if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith('#')) + { + continue; + } + + var parts = trimmed.Split('=', 2); + if (parts.Length == 2) + { + Environment.SetEnvironmentVariable(parts[0].Trim(), parts[1].Trim()); + } + } +} + +// Load before parsing +LoadDotEnvFile(); +var parseResult = rootCommand.Parse(args); +``` + +## Security considerations + +### Avoid logging environment variable values + +Be careful not to log sensitive environment variables: + +```csharp +var apiKeyOption = new Option("--api-key") +{ + DefaultValueFactory = _ => + { + var key = Environment.GetEnvironmentVariable("API_KEY"); + + // DON'T: Log the actual key + // Console.WriteLine($"Using API key: {key}"); + + // DO: Log that it was found without revealing the value + if (key != null) + { + Console.WriteLine("Using API key from API_KEY environment variable"); + } + + return key; + } +}; +``` + +### Clear sensitive environment variables + +Remove sensitive values from the environment after reading: + +```csharp +var passwordOption = new Option("--password") +{ + DefaultValueFactory = _ => + { + var password = Environment.GetEnvironmentVariable("DB_PASSWORD"); + if (password != null) + { + // Clear it from environment + Environment.SetEnvironmentVariable("DB_PASSWORD", null); + } + return password; + } +}; +``` + +### Prefer command-line for secrets in containers + +In containerized environments, use secrets management instead of environment variables: + +```csharp +var tokenOption = new Option("--token") +{ + Description = "Authentication token (or set TOKEN_FILE for container secret path)", + CustomParser = result => + { + if (result.Tokens.Count > 0) + { + return result.Tokens.Single().Value; + } + + // Read from Docker/Kubernetes secret file + var tokenFile = Environment.GetEnvironmentVariable("TOKEN_FILE"); + if (tokenFile != null && File.Exists(tokenFile)) + { + return File.ReadAllText(tokenFile).Trim(); + } + + result.AddError("Token required via --token or TOKEN_FILE"); + return null; + } +}; +``` + +## Real-world example: .NET CLI + +The .NET CLI uses environment variables extensively: + +```csharp +// Simplified from dotnet/sdk +public static Option CreateNoLogoOption() +{ + return new Option("--no-logo", "--nologo") + { + Description = "Do not display the startup banner", + DefaultValueFactory = _ => + { + var envValue = Environment.GetEnvironmentVariable("DOTNET_NOLOGO"); + return ParseBooleanEnvVar(envValue, defaultValue: false); + }, + Arity = ArgumentArity.Zero + }; +} + +// Environment variables used: +// - DOTNET_NOLOGO: Suppress startup banner +// - DOTNET_CLI_TELEMETRY_OPTOUT: Disable telemetry +// - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: Skip first-run experience +// - CI: Detect continuous integration environment +``` + +## See also + +- [System.CommandLine overview](index.md) +- [How to customize parsing and validation](how-to-customize-parsing-and-validation.md) +- [Sharing options across commands](sharing-options.md) diff --git a/docs/standard/commandline/index.md b/docs/standard/commandline/index.md index df74bdb3b0c25..59a7de6db29ab 100644 --- a/docs/standard/commandline/index.md +++ b/docs/standard/commandline/index.md @@ -41,11 +41,30 @@ To get started with System.CommandLine, see the following resources: To learn more, see the following resources: +### Conceptual topics + +- [Conceptual overview of System.CommandLine](conceptual-overview.md) +- [Supported types in System.CommandLine](supported-types.md) +- [DefaultValueFactory vs CustomParser](defaultvalue-vs-customparser.md) + +### How-to guides + - [How to parse and invoke the result](how-to-parse-and-invoke.md) - [How to customize parsing and validation](how-to-customize-parsing-and-validation.md) +- [Custom type parsing in System.CommandLine](custom-types.md) - [How to configure the parser](how-to-configure-the-parser.md) - [How to customize help](how-to-customize-help.md) - [How to enable and customize tab completion](how-to-enable-tab-completion.md) +- [How to use System.CommandLine with NativeAOT](nativeaot.md) + +### Advanced patterns + +- [Sharing options across commands](sharing-options.md) +- [Using environment variables with options](environment-variables.md) +- [Option actions for cross-cutting behaviors](option-actions.md) + +### Additional resources + - [Command-line design guidance](design-guidance.md) - [Migration guide to 2.0.0-beta5](migration-guide-2.0.0-beta5.md) - [System.CommandLine API reference](xref:System.CommandLine) diff --git a/docs/standard/commandline/nativeaot.md b/docs/standard/commandline/nativeaot.md new file mode 100644 index 0000000000000..6908bb0ae419f --- /dev/null +++ b/docs/standard/commandline/nativeaot.md @@ -0,0 +1,279 @@ +--- +title: How to use System.CommandLine with NativeAOT +ms.date: 01/22/2026 +description: "Learn how to build trim-friendly and NativeAOT-compatible command-line applications using System.CommandLine." +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" + - "NativeAOT" + - "AOT compilation" + - "trimming" +ms.topic: how-to +--- + +# How to use System.CommandLine with NativeAOT + +`System.CommandLine` is designed to be trim-friendly and compatible with Native AOT compilation, making it an excellent choice for building fast, lightweight command-line applications. This article explains how to use `System.CommandLine` in NativeAOT applications and what to consider. + +## What is NativeAOT? + +Native AOT (Ahead-Of-Time) compilation compiles your .NET application directly to native code, eliminating the need for the .NET runtime. Benefits include: + +- **Faster startup** - No JIT compilation at runtime +- **Smaller deployment** - Self-contained native executable without runtime dependencies +- **Lower memory usage** - No JIT compiler overhead +- **Better performance** - Optimized native code + +For more information, see [Native AOT deployment](../../core/deploying/native-aot/index.md). + +## System.CommandLine and NativeAOT + +`System.CommandLine` is specifically designed to work well with trimming and NativeAOT: + +- Minimal use of reflection +- Trim-friendly API design +- Explicit type information through generics +- No dynamic code generation + +This makes it a better choice for AOT scenarios compared to attribute-based libraries that rely heavily on reflection. + +## Enable NativeAOT in your project + +To enable NativeAOT compilation, add the following to your project file (`.csproj`): + +```xml + + + Exe + net8.0 + true + + + + + + +``` + +The `true` property enables Native AOT compilation when you publish your application. + +## Build and publish + +Build and publish your NativeAOT application using: + +```dotnetcli +dotnet publish -c Release +``` + +The output is a self-contained native executable with no external dependencies. + +## Best practices for NativeAOT compatibility + +### Use explicit types + +Always use strongly-typed `Option` and `Argument` rather than non-generic versions: + +```csharp +// Good - explicit type information +var countOption = new Option("--count", "Number of items"); + +// Avoid - requires runtime type inspection +var option = new Option("--count"); +``` + +### Avoid dynamic parsing + +Stick to built-in type converters or provide explicit custom parsers: + +```csharp +// Good - built-in type support +var inputOption = new Option("--input", "Input file"); + +// Good - explicit custom parser +var personOption = new Option("--person", "Person data") +{ + CustomParser = result => + { + var value = result.Tokens.Single().Value; + var parts = value.Split(','); + return new Person(parts[0], int.Parse(parts[1])); + } +}; +``` + +### Use simple validators + +Custom validators work well with NativeAOT as long as they don't use reflection: + +```csharp +var delayOption = new Option("--delay", "Delay in milliseconds"); +delayOption.Validators.Add(result => +{ + if (result.GetValue() < 0) + { + result.AddError("Delay must be non-negative."); + } +}); +``` + +### Avoid reflection-based binding + +Don't use reflection to bind parsed values to properties. Instead, use the parsed values directly: + +```csharp +// Good - direct value access +var rootCommand = new RootCommand("My application"); +var nameOption = new Option("--name", "User name"); +rootCommand.Options.Add(nameOption); + +rootCommand.SetAction((string name) => +{ + Console.WriteLine($"Hello, {name}!"); +}, nameOption); + +// Avoid - reflection-based approaches +``` + +### Test trimming warnings + +Before enabling `PublishAot`, test your application with trimming enabled: + +```xml + + true + link + +``` + +Publish the application and check for trim warnings: + +```dotnetcli +dotnet publish -c Release +``` + +Any warnings indicate potential runtime issues with NativeAOT. + +## Example: NativeAOT-compatible CLI application + +Here's a complete example of a NativeAOT-compatible application using `System.CommandLine`: + +```csharp +using System; +using System.CommandLine; +using System.IO; + +var rootCommand = new RootCommand("File processor"); + +var inputOption = new Option("--input", "Input file path") +{ + Required = true +}.AcceptExistingOnly(); + +var outputOption = new Option("--output", "Output file path") +{ + Required = true +}; + +var verboseOption = new Option("--verbose", "Enable verbose logging"); + +rootCommand.Options.Add(inputOption); +rootCommand.Options.Add(outputOption); +rootCommand.Options.Add(verboseOption); + +rootCommand.SetAction((FileInfo input, FileInfo output, bool verbose) => +{ + if (verbose) + { + Console.WriteLine($"Processing {input.FullName}..."); + } + + var content = File.ReadAllText(input.FullName); + var processed = content.ToUpper(); // Example processing + File.WriteAllText(output.FullName, processed); + + if (verbose) + { + Console.WriteLine($"Output written to {output.FullName}"); + } + + Console.WriteLine("Done."); +}, inputOption, outputOption, verboseOption); + +return rootCommand.Invoke(args); +``` + +Project file: + +```xml + + + Exe + net8.0 + true + true + + + + + + +``` + +> [!NOTE] +> `InvariantGlobalization` is set to `true` to reduce the application size by excluding culture-specific data. This is optional but common for NativeAOT applications. + +## Performance characteristics + +NativeAOT applications using `System.CommandLine` typically show: + +- **Startup time:** 2-5x faster than JIT-compiled equivalents +- **Memory usage:** 50-70% lower runtime memory footprint +- **Binary size:** Comparable or larger than trimmed self-contained deployments, but with no runtime dependencies + +## Limitations and considerations + +### Reflection limitations + +NativeAOT has limited reflection support. Avoid: + +- Dynamic type discovery +- Attribute-based configuration +- Runtime code generation + +### Startup size + +NativeAOT binaries include all required code, which can make them larger than framework-dependent deployments. Use trimming optimizations to minimize size. + +### Platform support + +NativeAOT requires platform-specific toolchains: + +- Windows: Visual Studio 2022+ or Build Tools +- Linux: clang and developer packages +- macOS: Xcode command-line tools + +See [Native AOT deployment prerequisites](../../core/deploying/native-aot/index.md#prerequisites) for details. + +## Troubleshooting + +### Trim warnings + +If you see trim warnings related to `System.CommandLine`, file an issue on the [command-line-api GitHub repository](https://github.com/dotnet/command-line-api/issues). The library is designed to be trim-safe, and warnings may indicate a bug. + +### Runtime errors + +If your application builds but fails at runtime: + +1. Test with `PublishTrimmed=true` first to identify trim-related issues +2. Check that you're not using reflection-based patterns +3. Verify all custom parsers use explicit types +4. Review the [Native AOT compatibility docs](../../core/deploying/native-aot/index.md) + +## See also + +- [Native AOT deployment](../../core/deploying/native-aot/index.md) +- [Trim self-contained deployments](../../core/deploying/trimming/trim-self-contained.md) +- [System.CommandLine overview](index.md) +- [Supported types in System.CommandLine](supported-types.md) diff --git a/docs/standard/commandline/option-actions.md b/docs/standard/commandline/option-actions.md new file mode 100644 index 0000000000000..18632909318db --- /dev/null +++ b/docs/standard/commandline/option-actions.md @@ -0,0 +1,485 @@ +--- +title: Option actions for cross-cutting behaviors in System.CommandLine +ms.date: 01/23/2026 +description: "Learn how to use the Action property on options to implement middleware-like behaviors such as debugging, logging, and early termination." +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" + - "option actions" + - "middleware" + - "cross-cutting concerns" +ms.topic: how-to +--- + +# Option actions for cross-cutting behaviors in System.CommandLine + +The `Action` property on options allows you to implement middleware-like behaviors that execute before your main command logic. This article explains how to use option actions for cross-cutting concerns like debugging, early termination, and global state configuration. + +## What are option actions? + +An option action is code that executes when an option is present, before the command handler runs. Actions are useful for: +- **Debugging** - Waiting for debugger attachment +- **Early termination** - Exiting before running the command (like `--version` or `--help`) +- **Global configuration** - Setting up logging, telemetry, or other global state +- **Side effects** - Performing actions that don't affect the command's return value + +## Basic option action + +The simplest action is a synchronous operation: + +```csharp +using System.CommandLine; +using System.CommandLine.Invocation; + +var debugOption = new Option("--debug") +{ + Description = "Wait for debugger to attach", + Arity = ArgumentArity.Zero, + Action = new SynchronousCommandLineAction + { + Name = "WaitForDebugger" + } +}; + +// Implement the action +class WaitForDebuggerAction : SynchronousCommandLineAction +{ + public override int Invoke(ParseResult parseResult) + { + if (parseResult.GetValue(debugOption)) + { + Console.WriteLine("Waiting for debugger to attach..."); + Console.WriteLine($"Process ID: {Environment.ProcessId}"); + + while (!System.Diagnostics.Debugger.IsAttached) + { + System.Threading.Thread.Sleep(100); + } + + Console.WriteLine("Debugger attached!"); + System.Diagnostics.Debugger.Break(); + } + + return 0; // Continue to command handler + } +} + +debugOption.Action = new WaitForDebuggerAction(); +``` + +## Terminating actions + +Some actions should prevent the command from running. Use the `Terminating` property: + +```csharp +class VersionAction : SynchronousCommandLineAction +{ + public override bool Terminating => true; // Don't run command handler + + public override int Invoke(ParseResult parseResult) + { + var version = typeof(Program).Assembly.GetName().Version; + Console.WriteLine($"MyApp version {version}"); + return 0; + } +} + +var versionOption = new Option("--version") +{ + Description = "Display version information", + Arity = ArgumentArity.Zero, + Action = new VersionAction() +}; + +// Command line: myapp --version +// Output: MyApp version 1.2.3.0 +// (command handler does not run) +``` + +## Real-world example: Debug option + +A complete implementation of a `--debug` option with conditional compilation: + +```csharp +class DebugAction : SynchronousCommandLineAction +{ + public override int Invoke(ParseResult parseResult) + { + #if DEBUG + Console.WriteLine($"Process ID: {Environment.ProcessId}"); + Console.WriteLine("Waiting for debugger..."); + Console.WriteLine("Attach a debugger and press Enter to continue."); + Console.ReadLine(); + + if (System.Diagnostics.Debugger.IsAttached) + { + System.Diagnostics.Debugger.Break(); + } + #else + Console.WriteLine("Debug option is only available in Debug builds."); + #endif + + return 0; + } +} + +var debugOption = new Option("--debug") +{ + Description = "Wait for debugger to attach (Debug builds only)", + Arity = ArgumentArity.Zero, + Action = new DebugAction() +}; +``` + +Usage: +```bash +# Start the app +myapp --debug my-command --arg value + +# Output: +# Process ID: 12345 +# Waiting for debugger... +# Attach a debugger and press Enter to continue. + +# After attaching debugger and pressing Enter: +# (Debugger breaks at current line) +# (Then continues to execute my-command) +``` + +## Logging and telemetry configuration + +Use actions to set up global logging before the command runs: + +```csharp +class VerboseAction : SynchronousCommandLineAction +{ + public override int Invoke(ParseResult parseResult) + { + if (parseResult.GetValue(verboseOption)) + { + // Enable verbose logging globally + LogLevel.Minimum = LogLevel.Debug; + Console.WriteLine("Verbose logging enabled"); + } + + return 0; + } +} + +var verboseOption = new Option("--verbose", "-v") +{ + Description = "Enable verbose output", + Arity = ArgumentArity.Zero, + Recursive = true, // Available to all subcommands + Action = new VerboseAction() +}; + +// The action runs before any command handler +// All subsequent logging respects the verbose setting +``` + +## Diagnostic schema output + +The .NET CLI uses this pattern to output schema information: + +```csharp +class PrintSchemaAction : SynchronousCommandLineAction +{ + public override bool Terminating => true; + + public override int Invoke(ParseResult parseResult) + { + // Generate and print CLI schema + var schema = GenerateSchema(parseResult.CommandResult.Command); + Console.WriteLine(schema); + return 0; + } + + private string GenerateSchema(Command command) + { + // Serialize command structure to JSON + return JsonSerializer.Serialize(new + { + command.Name, + command.Description, + Options = command.Options.Select(o => new { o.Name, o.Description }), + Subcommands = command.Subcommands.Select(c => c.Name) + }, new JsonSerializerOptions { WriteIndented = true }); + } +} + +var schemaOption = new Option("--cli-schema") +{ + Description = "Output CLI schema as JSON", + Hidden = true, // Not shown in normal help + Arity = ArgumentArity.Zero, + Recursive = true, + Action = new PrintSchemaAction() +}; +``` + +## Asynchronous actions + +For async operations, use `AsynchronousCommandLineAction`: + +```csharp +class TelemetryAction : AsynchronousCommandLineAction +{ + public override async Task InvokeAsync(ParseResult parseResult, + CancellationToken cancellationToken = default) + { + var optOut = Environment.GetEnvironmentVariable("TELEMETRY_OPTOUT"); + if (optOut == "1") + { + return 0; // Skip telemetry + } + + try + { + // Send anonymous usage data + await TelemetryClient.TrackCommandAsync( + parseResult.CommandResult.Command.Name, + cancellationToken); + } + catch + { + // Never fail on telemetry errors + } + + return 0; + } +} + +var rootCommand = new RootCommand(); +// Add action directly to the command +rootCommand.Action = new TelemetryAction(); +``` + +## Combining multiple actions + +When multiple options have actions, they execute in the order the options are added: + +```csharp +var rootCommand = new RootCommand("My app"); + +var diagnosticsOption = new Option("--diagnostics", "-d") +{ + Arity = ArgumentArity.Zero, + Recursive = true, + Action = new DiagnosticsAction() // Runs first +}; + +var profileOption = new Option("--profile") +{ + Arity = ArgumentArity.Zero, + Action = new ProfilingAction() // Runs second +}; + +rootCommand.Options.Add(diagnosticsOption); +rootCommand.Options.Add(profileOption); + +// Command line: myapp --diagnostics --profile build +// Execution order: +// 1. DiagnosticsAction +// 2. ProfilingAction +// 3. build command handler +``` + +## Action execution flow + +Understanding when actions execute: + +```csharp +class LoggingAction : SynchronousCommandLineAction +{ + public override int Invoke(ParseResult parseResult) + { + Console.WriteLine("Action: Setting up logging"); + return 0; + } +} + +var rootCommand = new RootCommand(); +rootCommand.Options.Add(new Option("--log") +{ + Arity = ArgumentArity.Zero, + Action = new LoggingAction() +}); + +var buildCommand = new Command("build", "Build project"); +buildCommand.SetAction((bool log) => +{ + Console.WriteLine("Handler: Building project"); + return 0; +}); + +rootCommand.Subcommands.Add(buildCommand); + +// Command line: myapp --log build +// Output: +// Action: Setting up logging +// Handler: Building project +``` + +The flow is: +1. Parse command line +2. Execute option actions (in order added) +3. If any action is terminating and returns non-zero, stop +4. Execute command handler +5. Return exit code + +## Conditional actions + +Actions can check the parse result to run conditionally: + +```csharp +class ConditionalAction : SynchronousCommandLineAction +{ + public override int Invoke(ParseResult parseResult) + { + // Only run for specific commands + if (parseResult.CommandResult.Command.Name == "deploy") + { + Console.WriteLine("WARNING: This will deploy to production!"); + Console.Write("Are you sure? (yes/no): "); + var response = Console.ReadLine(); + + if (response?.ToLowerInvariant() != "yes") + { + Console.WriteLine("Deployment cancelled."); + return 1; // Non-zero exit code stops execution + } + } + + return 0; + } +} + +var confirmOption = new Option("--confirm") +{ + Description = "Confirm dangerous operations", + Arity = ArgumentArity.Zero, + Recursive = true, + Action = new ConditionalAction() +}; +``` + +## Error handling in actions + +Actions should handle errors gracefully: + +```csharp +class SafeAction : SynchronousCommandLineAction +{ + public override int Invoke(ParseResult parseResult) + { + try + { + // Potentially failing operation + InitializeExternalDependency(); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine($"Initialization failed: {ex.Message}"); + Console.Error.WriteLine("Run with --help for usage information."); + return 1; // Return error code to stop execution + } + } +} +``` + +## Best practices + +### Keep actions focused + +Actions should do one thing: + +```csharp +// GOOD: Focused action +class DebugAction : SynchronousCommandLineAction +{ + public override int Invoke(ParseResult parseResult) + { + WaitForDebugger(); + return 0; + } +} + +// BAD: Kitchen sink action +class MegaAction : SynchronousCommandLineAction +{ + public override int Invoke(ParseResult parseResult) + { + SetupLogging(); + InitializeTelemetry(); + CheckForUpdates(); + WaitForDebugger(); + ValidateLicense(); + // Too many responsibilities! + return 0; + } +} +``` + +### Use terminating actions for early exits + +```csharp +// Options that should prevent command execution +var versionOption = new Option("--version") +{ + Arity = ArgumentArity.Zero, + Action = new VersionAction() { Terminating = true } +}; + +var licenseOption = new Option("--license") +{ + Arity = ArgumentArity.Zero, + Action = new LicenseAction() { Terminating = true } +}; +``` + +### Document action behavior + +Make it clear in help text when actions have side effects: + +```csharp +var debugOption = new Option("--debug") +{ + Description = "Wait for debugger to attach before executing command", + Arity = ArgumentArity.Zero, + Action = new DebugAction() +}; +``` + +### Be cautious with recursive actions + +Recursive options with actions run for every subcommand: + +```csharp +var diagnosticsOption = new Option("--diagnostics") +{ + Description = "Enable diagnostic output", + Arity = ArgumentArity.Zero, + Recursive = true, // Action runs for root AND subcommands + Action = new DiagnosticsAction() +}; + +// Can lead to the action running multiple times! +// Be idempotent or check if already executed +``` + +## Limitations + +- Actions cannot access the command handler's return value +- Actions cannot directly modify the ParseResult +- Terminating actions prevent the command handler from running +- Action execution order depends on option order, which may not be obvious + +## See also + +- [How to parse and invoke the result](how-to-parse-and-invoke.md) +- [How to customize parsing and validation](how-to-customize-parsing-and-validation.md) +- [Sharing options across commands](sharing-options.md) diff --git a/docs/standard/commandline/sharing-options.md b/docs/standard/commandline/sharing-options.md new file mode 100644 index 0000000000000..50beabea06b04 --- /dev/null +++ b/docs/standard/commandline/sharing-options.md @@ -0,0 +1,317 @@ +--- +title: Sharing options across commands in System.CommandLine +ms.date: 01/23/2026 +description: "Learn different approaches for sharing common options across multiple commands, including the Recursive property and factory pattern." +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" + - "recursive options" + - "shared options" +ms.topic: how-to +--- + +# Sharing options across commands in System.CommandLine + +Many CLI applications have options that apply to multiple commands, such as `--verbose`, `--configuration`, or `--output`. This article explains different approaches for sharing options across commands and the trade-offs of each approach. + +## The challenge of shared options + +Consider a CLI with multiple commands that all need a `--verbose` option: + +```csharp +var buildCommand = new Command("build", "Build the project"); +buildCommand.Options.Add(new Option("--verbose", "Enable verbose output")); + +var testCommand = new Command("test", "Run tests"); +testCommand.Options.Add(new Option("--verbose", "Enable verbose output")); + +var publishCommand = new Command("publish", "Publish the project"); +publishCommand.Options.Add(new Option("--verbose", "Enable verbose output")); +``` + +This approach has several problems: +- Code duplication +- Inconsistent aliases or descriptions +- Difficult to maintain (changes must be made in multiple places) +- Can't share option instances (each command needs its own instance) + +## Approach 1: Recursive options + +The `Recursive` property makes an option available to a command and all its subcommands automatically. + +### Basic usage + +```csharp +var rootCommand = new RootCommand("My application"); + +var verboseOption = new Option("--verbose", "-v") +{ + Description = "Enable verbose output", + Recursive = true // Available to the command it is added to, and all subcommands of that command +}; + +rootCommand.Options.Add(verboseOption); + +var buildCommand = new Command("build", "Build the project"); +var testCommand = new Command("test", "Run tests"); +var publishCommand = new Command("publish", "Publish the project"); + +rootCommand.Subcommands.Add(buildCommand); +rootCommand.Subcommands.Add(testCommand); +rootCommand.Subcommands.Add(publishCommand); + +// verboseOption is now available on all commands +// myapp --verbose build +// myapp build --verbose +// myapp test --verbose +``` + +### Advantages of recursive options + +- **Simple to implement** - Add the option once at the root +- **Automatic propagation** - Available everywhere without manual wiring +- **Consistent behavior** - Same aliases and parsing everywhere +- **Single source of truth** - One option definition for the entire CLI + +### Disadvantages of recursive options + +- **No per-command customization** - Can't change description or help text for specific commands +- **All-or-nothing** - If added to root, applies to every subcommand (even if not needed) +- **Harder to document** - Can't provide command-specific usage examples in help +- **Less explicit** - Not obvious from command definition which options are available + +### Example: When customization is needed + +```csharp +var rootCommand = new RootCommand(); + +var configOption = new Option("--configuration", "-c") +{ + Description = "Build configuration", // Generic description + Recursive = true +}; +rootCommand.Options.Add(configOption); + +var buildCommand = new Command("build", "Build the project"); +var cleanCommand = new Command("clean", "Clean output"); + +rootCommand.Subcommands.Add(buildCommand); +rootCommand.Subcommands.Add(cleanCommand); + +// Problem: Both commands show "Build configuration" +// but clean doesn't build anything! +// Can't customize the description per command +``` + +## Approach 2: Factory pattern + +Create static factory methods that return new option instances for each command. + +### Basic implementation + +```csharp +public static class CommonOptions +{ + public static Option CreateVerboseOption() => + new("--verbose", "-v") + { + Description = "Enable verbose output", + Arity = ArgumentArity.Zero + }; + + public static Option CreateConfigurationOption(string description) => + new("--configuration", "-c") + { + Description = description, // Customizable per command + HelpName = "CONFIGURATION" + }; + + public static Option CreateOutputOption(string description) => + new("--output", "-o") + { + Description = description + }; +} +``` + +### Using factory methods + +```csharp +var buildCommand = new Command("build", "Build the project"); +buildCommand.Options.Add(CommonOptions.CreateVerboseOption()); +buildCommand.Options.Add(CommonOptions.CreateConfigurationOption( + "The build configuration (Debug or Release)")); +buildCommand.Options.Add(CommonOptions.CreateOutputOption( + "Directory for build output")); + +var publishCommand = new Command("publish", "Publish the project"); +publishCommand.Options.Add(CommonOptions.CreateVerboseOption()); +publishCommand.Options.Add(CommonOptions.CreateConfigurationOption( + "The publish configuration")); +publishCommand.Options.Add(CommonOptions.CreateOutputOption( + "Directory for published output")); +``` + +### Advantages of factory pattern + +- **Customizable per command** - Each command can provide its own description +- **Explicit opt-in** - Commands explicitly declare which options they support +- **Type-safe and consistent** - Factory ensures consistent aliases and behavior +- **Command-specific help** - Each command can explain the option in its own context +- **Selective application** - Only add options where they make sense + +### Disadvantages of factory pattern + +- **More verbose** - Must explicitly add options to each command +- **Not DRY** - Repetitive option addition code +- **Risk of inconsistency** - Developers might forget to add an option to a command +- **More code to maintain** - Factory class needs to be kept up-to-date + +### Advanced factory pattern + +You can enhance factories with additional features: + +```csharp +public static class CommonOptions +{ + // Support customization while providing defaults + public static Option CreateVerboseOption( + string description = "Enable verbose output", + bool includeDebugAlias = false) + { + var aliases = new List { "--verbose", "-v" }; + if (includeDebugAlias) + { + aliases.Add("--debug"); + } + + return new Option([.. aliases]) + { + Description = description, + Arity = ArgumentArity.Zero + }; + } + + // Factory with common default value logic + public static Option CreateConfigurationOption( + string description, + string? defaultValue = null) + { + return new Option("--configuration", "-c") + { + Description = description, + DefaultValueFactory = _ => defaultValue ?? + Environment.GetEnvironmentVariable("BUILD_CONFIGURATION") ?? + "Debug" + }; + } +} +``` + +## Approach 3: Hybrid approach + +Combine both patterns for maximum flexibility. + +```csharp +public static class CommonOptions +{ + // Factory for when customization is needed + public static Option CreateVerboseOption(string? description = null) => + new("--verbose", "-v") + { + Description = description ?? "Enable verbose output", + Arity = ArgumentArity.Zero + }; + + // Singleton for truly global options + public static readonly Option DiagnosticsOption = new("--diagnostics", "-d") + { + Description = "Enable diagnostics output", + Recursive = true, + Arity = ArgumentArity.Zero + }; +} + +var rootCommand = new RootCommand(); + +// Add recursive option once at root +rootCommand.Options.Add(CommonOptions.DiagnosticsOption); + +// Use factory for command-specific customization +var buildCommand = new Command("build", "Build the project"); +buildCommand.Options.Add(CommonOptions.CreateVerboseOption( + "Show detailed build output")); + +var testCommand = new Command("test", "Run tests"); +testCommand.Options.Add(CommonOptions.CreateVerboseOption( + "Show detailed test output")); + +rootCommand.Subcommands.Add(buildCommand); +rootCommand.Subcommands.Add(testCommand); +``` + +## Choosing an approach + +Use **recursive options** when: +- The option has identical meaning across all commands +- You don't need per-command customization of help text +- The option truly applies to every subcommand +- Simplicity is more important than flexibility + +Use **factory pattern** when: +- Different commands need different descriptions or help text +- Not all commands need the option +- You want explicit control over which commands have which options +- You need command-specific default values or validation + +Use **hybrid approach** when: +- You have both truly global options and customizable options +- Some options need `Recursive` for simplicity +- Other options need per-command descriptions + +## Real-world example: .NET CLI + +The .NET CLI uses the factory pattern extensively: + +```csharp +// From dotnet/sdk repository +public static class CommonOptions +{ + public static Option CreateFrameworkOption(string description) => + new("--framework", "-f") + { + Description = description, // Different for each command + HelpName = "FRAMEWORK" + }; + + public static Option CreateVerbosityOption() => + new("--verbosity", "-v") + { + Description = "Set the MSBuild verbosity level", + HelpName = "LEVEL" + }; +} + +// Used in multiple commands with custom descriptions: +// dotnet build --framework: "Target framework for the build" +// dotnet publish --framework: "Target framework to publish for" +// dotnet test --framework: "Target framework to run tests against" +``` + +This allows each command to provide context-appropriate help text while maintaining consistency in aliases and behavior. + +## Best practices + +1. **Document your approach** - Make it clear whether you use recursive options or factories +2. **Be consistent** - Don't mix patterns for similar options +3. **Centralize definitions** - Keep all shared option logic in one place +4. **Use meaningful descriptions** - Especially with factories, tailor descriptions to each command +5. **Consider maintenance** - Factor pattern is more code but easier to customize later + +## See also + +- [System.CommandLine overview](index.md) +- [How to customize help](how-to-customize-help.md) +- [Environment variables in options](environment-variables.md) diff --git a/docs/standard/commandline/supported-types.md b/docs/standard/commandline/supported-types.md new file mode 100644 index 0000000000000..6933f06a36ee9 --- /dev/null +++ b/docs/standard/commandline/supported-types.md @@ -0,0 +1,259 @@ +--- +title: Supported types in System.CommandLine +ms.date: 01/22/2026 +description: "Learn about the types that System.CommandLine can parse automatically, including primitives, common .NET types, enums, and collections." +no-loc: [System.CommandLine] +helpviewer_keywords: + - "command line interface" + - "command line" + - "System.CommandLine" + - "type parsing" + - "argument types" +ms.topic: conceptual +--- + +# Supported types in System.CommandLine + +`System.CommandLine` provides built-in parsing support for a wide range of .NET types. This article describes which types are supported out of the box and how the library handles type conversion from command-line strings. + +## Primitive types + +The following primitive types are supported automatically: + +### Integer types + +- `byte` and `sbyte` +- `short` and `ushort` +- `int` and `uint` +- `long` and `ulong` + +Integer values are parsed using the standard .NET `TryParse` methods. Parsing respects the current culture and accepts standard integer formats. + +```csharp +var countOption = new Option("--count", "Number of items"); +// Command line: --count 42 +``` + +### Floating-point types + +- `float` +- `double` +- `decimal` + +Floating-point values support standard decimal notation and scientific notation where applicable. + +```csharp +var priceOption = new Option("--price", "Item price"); +// Command line: --price 19.99 +``` + +### Boolean type + +- `bool` + +Boolean options can be specified with or without a value: + +```csharp +var verboseOption = new Option("--verbose", "Enable verbose output"); +// Command line: --verbose (value is true) +// Command line: --verbose true (value is true) +// Command line: --verbose false (value is false) +``` + +For flag-style boolean options (where the option's presence means `true` and its absence means `false`), set the arity to zero: + +```csharp +var debugOption = new Option("--debug", "Enable debug mode") +{ + Arity = ArgumentArity.Zero +}; + +// Command line: --debug (value is true, flag is present) +// Command line: (value is false, flag is absent) +// This option cannot accept an explicit true/false value +``` + +Flag-style options are common for switches that enable features or modes. The default arity for `Option` is `ZeroOrOne`, which accepts both flag-style (`--verbose`) and explicit value (`--verbose true`) usage. Setting `Arity = ArgumentArity.Zero` restricts the option to flag-style only. + +### String type + +- `string` + +String arguments accept any text value. Quotes can be used to include spaces: + +```csharp +var nameOption = new Option("--name", "User name"); +// Command line: --name "John Doe" +``` + +## Date and time types + +`System.CommandLine` supports parsing various date and time types: + +### DateTime types + +- `DateTime` - Date and time values +- `DateTimeOffset` - Date and time with time zone offset + +```csharp +var startOption = new Option("--start", "Start date and time"); +// Command line: --start "2026-01-22" +// Command line: --start "2026-01-22 14:30" +``` + +### .NET 6+ date and time types + +Available when targeting .NET 6 or later: + +- `DateOnly` - Date without time component +- `TimeOnly` - Time of day without date component + +```csharp +var birthdayOption = new Option("--birthday", "Date of birth"); +// Command line: --birthday 1990-05-15 +``` + +### Duration + +- `TimeSpan` - Duration or time interval + +```csharp +var timeoutOption = new Option("--timeout", "Request timeout"); +// Command line: --timeout 00:01:30 +// Command line: --timeout 1.5:30:00 (1.5 days) +``` + +## Other common types + +### Unique identifier + +- `Guid` - Globally unique identifier + +```csharp +var idOption = new Option("--id", "Entity identifier"); +// Command line: --id a1b2c3d4-e5f6-7890-abcd-ef1234567890 +``` + +### File system types + +- - Represents a file +- - Represents a directory +- - Base class for files and directories + +These types automatically create `FileInfo` or `DirectoryInfo` objects from path strings. The file or directory doesn't need to exist unless you add validation. + +```csharp +var inputOption = new Option("--input", "Input file path"); +var outputDirOption = new Option("--output-dir", "Output directory"); + +// Command line: --input data.txt --output-dir ./results +``` + +To require that files or directories exist, use the `AcceptExistingOnly()` method: + +```csharp +var inputOption = new Option("--input", "Input file path") + .AcceptExistingOnly(); +``` + +## Enum types + +All enum types are supported with case-insensitive parsing: + +```csharp +enum LogLevel +{ + Debug, + Info, + Warning, + Error +} + +var logLevelOption = new Option("--log-level", "Logging level"); +// Command line: --log-level info (case-insensitive) +// Command line: --log-level INFO +// Command line: --log-level Info +``` + +Enum parsing uses the enum member names. If parsing fails, the error message suggests valid values. + +## Nullable types + +All value types listed above can be used as nullable types (`T?`). When no value is provided, the result is `null` instead of the type's default value: + +```csharp +var ageOption = new Option("--age", "User age"); +// If --age is not specified, the value is null (not 0) +``` + +## Collection types + +`System.CommandLine` automatically supports collections of any supported type: + +### Arrays + +- `T[]` - Arrays of any supported type + +```csharp +var namesOption = new Option("--names", "List of names"); +// Command line: --names Alice Bob Charlie +``` + +### Lists + +- `List` - Generic lists of any supported type +- `IEnumerable` - Enumerable sequences (parsed as arrays) +- `IList` - List interface (parsed as arrays) +- `ICollection` - Collection interface (parsed as arrays) + +```csharp +var portsOption = new Option>("--ports", "Port numbers to listen on"); +// Command line: --ports 8080 8081 8082 +``` + +Collections support both space-separated and repeated option syntax: + +```csharp +// Space-separated +// Command line: --tags alpha beta gamma + +// Repeated option +// Command line: --tag alpha --tag beta --tag gamma +``` + +## How type conversion works + +When `System.CommandLine` parses command-line input, it: + +1. **Identifies the target type** from the generic parameter of `Option` or `Argument` +2. **Extracts string tokens** from the command line +3. **Calls the appropriate converter** based on the target type +4. **Validates the conversion** and reports errors if parsing fails +5. **Returns the typed value** or a collection of typed values + +For example, with `Option`: + +```csharp +var countOption = new Option("--count", "Number of items"); +``` + +The command line `--count 42` is parsed as: +1. Token "42" is extracted +2. `int.TryParse("42", out var value)` is called +3. The result `42` is returned as an `int` + +If parsing fails (for example, `--count abc`), an error message is automatically generated: + +``` +Cannot parse argument 'abc' for option '--count' as expected type 'System.Int32'. +``` + +## Custom types + +For types not listed above, you need to provide a custom parser. See [How to customize parsing and validation](how-to-customize-parsing-and-validation.md) for details on parsing custom types. + +## See also + +- [How to customize parsing and validation](how-to-customize-parsing-and-validation.md) +- [Command-line syntax overview](syntax.md) +- [System.CommandLine overview](index.md)