Guide for System.CommandLine 2.0.0-beta5+ and GA. Use when writing CLI apps with System.CommandLine, migrating from beta4, or reviewing code using the new APIs. Covers breaking changes, new patterns for options/arguments/commands, actions, parsing, and configuration.
This skill covers breaking changes from System.CommandLine 2.0.0-beta5 and later (including GA). Use this when writing new CLI code or migrating from beta4.
| Area | Old (beta4) | New (beta5+) |
|---|---|---|
| Adding options/args | Command.AddOption() | Command.Options.Add() |
| Handler | Command.SetHandler() | Command.SetAction() |
| Name parameter | Optional (derived from alias) | Required for all symbols |
| Default values | SetDefaultValue(object) | DefaultValueFactory property |
| Custom parsing | ParseArgument<T> delegate |
CustomParser property |
| Parser class | Parser | CommandLineParser (static) |
| Configuration | CommandLineBuilder | ParserConfiguration / InvocationConfiguration |
| Console abstraction | IConsole | TextWriter (Output/Error) |
| Old Name | New Name |
|---|---|
Parser | CommandLineParser |
OptionResult.IsImplicit | OptionResult.Implicit |
Option.IsRequired | Option.Required |
Symbol.IsHidden | Symbol.Hidden |
Option.ArgumentHelpName | Option.HelpName |
OptionResult.Token | OptionResult.IdentifierToken |
ParseResult.FindResultFor | ParseResult.GetResult |
SymbolResult.ErrorMessage | SymbolResult.AddError(string) |
Old Add* methods are replaced with mutable collection properties:
// OLD (beta4)
command.AddOption(myOption);
command.AddArgument(myArgument);
command.AddCommand(subcommand);
command.AddValidator(validator);
command.AddAlias("alias");
option.AddCompletions("a", "b", "c");
// NEW (beta5+)
command.Options.Add(myOption);
command.Arguments.Add(myArgument);
command.Subcommands.Add(subcommand);
command.Validators.Add(validator);
command.Aliases.Add("alias");
option.CompletionSources.Add("a", "b", "c");
Alias methods removed:
RemoveAlias() → Use Aliases.Remove()HasAlias() → Use Aliases.Contains()Name is now mandatory for all symbol constructors (Argument<T>, Option<T>, Command).
// OLD (beta4) - name derived from longest alias
Option<bool> option = new("--verbose", "-v");
// NEW (beta5+) - name is first parameter, aliases are separate
Option<bool> option = new("--verbose", "-v")
{
Description = "Enable verbose output"
};
⚠️ BREAKING: Description parameter removed from constructor
// OLD (beta4) - second param was description
Option<bool> beta4 = new("--help", "An option with description.");
// NEW (beta5+) - second param is alias! Set Description separately
Option<bool> beta5 = new("--help", "-h", "/h")
{
Description = "An option with description."
};
Get parsed values by name:
RootCommand command = new("The description.")
{
new Option<int>("--number")
};
ParseResult parseResult = command.Parse(args);
int number = parseResult.GetValue<int>("--number");
Old approach (not type-safe):
// OLD (beta4)
option.SetDefaultValue("text"); // object, not type-safe!
New approach (type-safe):
// NEW (beta5+) - DefaultValueFactory property
Option<int> number = new("--number")
{
DefaultValueFactory = _ => 42
};
// NEW (beta5+) - CustomParser property
Argument<Uri> uri = new("uri")
{
CustomParser = result =>
{
if (!Uri.TryCreate(result.Tokens.Single().Value, UriKind.RelativeOrAbsolute, out var uriValue))
{
result.AddError("Invalid URI format.");
return null!;
}
return uriValue;
}
};
Parsing:
// OLD (beta4) - extension method
ParseResult result = CommandExtensions.Parse(command, args);
// NEW (beta5+) - instance method on Command
ParseResult result = command.Parse(args);
// With configuration
var config = new ParserConfiguration { EnablePosixBundling = false };
ParseResult result = command.Parse(args, config);
Invocation:
// OLD (beta4)
command.SetHandler((FileInfo file) => { /* ... */ }, fileOption);
await command.InvokeAsync(args);
// NEW (beta5+) - SetAction + ParseResult.Invoke
command.SetAction(parseResult =>
{
FileInfo? file = parseResult.GetValue(fileOption);
// ... handle
});
ParseResult result = command.Parse(args);
return result.Invoke(); // or await result.InvokeAsync();
Async actions require CancellationToken:
// NEW (beta5+) - CancellationToken is mandatory for async
command.SetAction(async (ParseResult parseResult, CancellationToken token) =>
{
string? url = parseResult.GetValue(urlOption);
return await DoWorkAsync(url, token);
});
Old CommandLineBuilder pattern removed. Use mutable configuration classes:
// Parser configuration
var parserConfig = new ParserConfiguration
{
EnablePosixBundling = true, // default: true
ResponseFileTokenReplacer = null // disable response files
};
// Invocation configuration
var invocationConfig = new InvocationConfiguration
{
ProcessTerminationTimeout = TimeSpan.FromSeconds(2), // default, set null to disable
EnableDefaultExceptionHandler = true, // default
Output = Console.Out,
Error = Console.Error
};
CommandLineBuilderExtensions mappings:
| Old Extension | New Approach |
|---|---|
CancelOnProcessTermination() | InvocationConfiguration.ProcessTerminationTimeout |
EnablePosixBundling() | ParserConfiguration.EnablePosixBundling |
UseExceptionHandler() | InvocationConfiguration.EnableDefaultExceptionHandler |
EnableDirectives() | RootCommand.Directives collection |
UseHelp() / UseVersion() | Included by default in RootCommand |
UseTokenReplacer() | ParserConfiguration.ResponseFileTokenReplacer |
AddMiddleware() | Removed (no replacement) |
// Directives are now a collection on RootCommand
var root = new RootCommand();
// SuggestDirective included by default
// Add others as needed:
root.Directives.Add(new DiagramDirective());
root.Directives.Add(new EnvironmentVariablesDirective());
// OLD (beta4) - IConsole interface
void Handler(InvocationContext context)
{
context.Console.WriteLine("Hello");
}
// NEW (beta5+) - Use TextWriter directly
var config = new InvocationConfiguration
{
Output = new StringWriter(), // for testing
Error = Console.Error
};
// Or just use Console.WriteLine in your action
// OLD (beta4)
command.SetHandler(async (InvocationContext context) =>
{
var value = context.ParseResult.GetValueForOption(option);
var token = context.GetCancellationToken();
// ...
});
// NEW (beta5+) - ParseResult and CancellationToken passed directly
command.SetAction(async (ParseResult parseResult, CancellationToken token) =>
{
var value = parseResult.GetValue(option);
// ...
});
using System.CommandLine;
var fileOption = new Option<FileInfo>("--file", "-f")
{
Description = "The file to process",
Required = true
};
var verboseOption = new Option<bool>("--verbose", "-v")
{
Description = "Enable verbose output"
};
var rootCommand = new RootCommand("Process files")
{
fileOption,
verboseOption
};
rootCommand.SetAction(parseResult =>
{
var file = parseResult.GetValue(fileOption);
var verbose = parseResult.GetValue(verboseOption);
if (verbose)
Console.WriteLine($"Processing {file?.FullName}");
// Process file...
return 0;
});
var result = rootCommand.Parse(args);
return result.Invoke();
Creating symbols:
var arg = new Argument<string>("name") { Description = "..." };
var opt = new Option<int>("--count", "-c") { Description = "..." };
var cmd = new Command("sub", "Description");
Building command tree:
cmd.Options.Add(opt);
cmd.Arguments.Add(arg);
rootCommand.Subcommands.Add(cmd);
Setting action:
cmd.SetAction(parseResult => { /* sync */ });
cmd.SetAction(async (parseResult, token) => { /* async */ });
Parsing and invoking:
var result = command.Parse(args);
return result.Invoke(); // or await result.InvokeAsync()