| name | system-commandline |
| description | 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. |
System.CommandLine 2.0.0-beta5+ Migration Guide
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.
Key Breaking Changes Summary
| 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) |
Renaming
| 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) |
Mutable Collections (No More Add Methods)
Old Add* methods are replaced with mutable collection properties:
command.AddOption(myOption);
command.AddArgument(myArgument);
command.AddCommand(subcommand);
command.AddValidator(validator);
command.AddAlias("alias");
option.AddCompletions("a", "b", "c");
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()
Names and Aliases
Name is now mandatory for all symbol constructors (Argument<T>, Option<T>, Command).
Option<bool> option = new("--verbose", "-v");
Option<bool> option = new("--verbose", "-v")
{
Description = "Enable verbose output"
};
⚠️ BREAKING: Description parameter removed from constructor
Option<bool> beta4 = new("--help", "An option with description.");
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");
Default Values and Custom Parsing
Old approach (not type-safe):
option.SetDefaultValue("text");
New approach (type-safe):
Option<int> number = new("--number")
{
DefaultValueFactory = _ => 42
};
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 and Invocation
Parsing:
ParseResult result = CommandExtensions.Parse(command, args);
ParseResult result = command.Parse(args);
var config = new ParserConfiguration { EnablePosixBundling = false };
ParseResult result = command.Parse(args, config);
Invocation:
command.SetHandler((FileInfo file) => { }, fileOption);
await command.InvokeAsync(args);
command.SetAction(parseResult =>
{
FileInfo? file = parseResult.GetValue(fileOption);
});
ParseResult result = command.Parse(args);
return result.Invoke();
Async actions require CancellationToken:
command.SetAction(async (ParseResult parseResult, CancellationToken token) =>
{
string? url = parseResult.GetValue(urlOption);
return await DoWorkAsync(url, token);
});
Configuration
Old CommandLineBuilder pattern removed. Use mutable configuration classes:
var parserConfig = new ParserConfiguration
{
EnablePosixBundling = true,
ResponseFileTokenReplacer = null
};
var invocationConfig = new InvocationConfiguration
{
ProcessTerminationTimeout = TimeSpan.FromSeconds(2),
EnableDefaultExceptionHandler = true,
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
var root = new RootCommand();
root.Directives.Add(new DiagramDirective());
root.Directives.Add(new EnvironmentVariablesDirective());
Console Abstraction Removed
void Handler(InvocationContext context)
{
context.Console.WriteLine("Hello");
}
var config = new InvocationConfiguration
{
Output = new StringWriter(),
Error = Console.Error
};
InvocationContext Removed
command.SetHandler(async (InvocationContext context) =>
{
var value = context.ParseResult.GetValueForOption(option);
var token = context.GetCancellationToken();
});
command.SetAction(async (ParseResult parseResult, CancellationToken token) =>
{
var value = parseResult.GetValue(option);
});
Complete Example
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}");
return 0;
});
var result = rootCommand.Parse(args);
return result.Invoke();
Quick Reference
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 => { });
cmd.SetAction(async (parseResult, token) => { });
Parsing and invoking:
var result = command.Parse(args);
return result.Invoke();