| name | system-commandline-options-and-arguments |
| version | 2.0.0 |
| description | Use when declaring or configuring System.CommandLine inputs — Option<T> and Argument<T>: names vs aliases, Description, Required, DefaultValueFactory, Arity, restricting a value to a fixed set with AcceptOnlyFromAmong, requiring that a file or directory path already exists, rejecting bad or out-of-range input, and checking one option against another. This is where CustomParser, Validators.Add and result.AddError belong. Covers the constructor-alias gotcha in depth. |
System.CommandLine: options & arguments
Option<T> = a named input (--name, -n). Argument<T> = a positional input. Both are declared as
objects you keep and later read by identity via parseResult.GetValue(instance).
Do NOT web_search / web_fetch — web samples use the removed pre-GA beta shapes
(getDefaultValue:, IsRequired, ExistingOnly, AddOption).
Required setup
System.CommandLine is not in the shared framework — add the package
(dotnet package add <proj> System.CommandLine), then using System.CommandLine;.
Declaring an option is only half of it: you must add the instance to a command and read it back
by that same instance. There is no delegate-parameter binding, so an option you declare but never
add is silently never parsed:
using System.CommandLine;
var name = new Option<string>("--name") { Description = "Who to greet" };
var root = new RootCommand("Greeter");
root.Options.Add(name);
root.SetAction(parseResult =>
{
string n = parseResult.GetValue(name)!;
Console.WriteLine($"Hello, {n}!");
return 0;
});
return await root.Parse(args).InvokeAsync();
The declarations below all attach the same way.
Declaring options
var name = new Option<string>("--name")
{
Description = "Who to greet",
Required = true,
};
name.Aliases.Add("-n");
var count = new Option<int>("--count", "-c")
{
DefaultValueFactory = _ => 1,
Arity = ArgumentArity.ExactlyOne,
};
- Ctor-alias gotcha:
new Option<T>("--name", "description") compiles but the 2nd string is an
alias, silently dropping your help text. Put text in { Description = ... }; extra ctor strings
are always aliases.
Required makes an option mandatory. Optional options should set a DefaultValueFactory.
Arity (ArgumentArity.Zero/ZeroOrOne/ExactlyOne/ZeroOrMore/OneOrMore) controls token counts.
Collecting multiple values
A collection-typed option accumulates repeated occurrences on its own — no extra setting:
var tag = new Option<string[]>("--tag");
Accepting several values after one token (--tag a b) is a different behavior and is off by
default; that form is rejected until you opt in:
var tag = new Option<string[]>("--tag") { AllowMultipleArgumentsPerToken = true };
Decide which command lines you mean to accept: repeating the option needs nothing, and only the
one-token-many-values form needs the flag.
Declaring arguments (positional)
var path = new Argument<FileInfo>("path")
{
Description = "Input file",
Arity = ArgumentArity.ExactlyOne,
};
path.AcceptExistingOnly();
Constrained values
var level = new Option<string>("--level");
level.AcceptOnlyFromAmong("debug", "info", "warn");
level.AcceptOnlyFromAmong(StringComparer.OrdinalIgnoreCase, "debug", "info", "warn");
Custom parsing & validation
var port = new Option<int>("--port")
{
CustomParser = result =>
{
if (int.TryParse(result.Tokens[0].Value, out var p) && p is > 0 and < 65536) return p;
result.AddError("--port must be 1..65535");
return 0;
},
};
port.Validators.Add(result =>
{
if (result.GetValue(port) == 0) result.AddError("--port is required and must be valid");
});
var min = new Option<int>("--min");
var max = new Option<int>("--max");
var root = new RootCommand("range") { min, max };
root.Validators.Add(result =>
{
if (result.GetValue(max) <= result.GetValue(min))
result.AddError("max must be greater than min");
});
- Pick the level by how many inputs the rule touches. One input →
option.Validators. Two or more,
or a rule about the command as a whole → command.Validators. Reaching for an option-level validator
for a cross-input rule is the common mistake; it cannot see the other value.
- Do the check in a validator, not inside the action. A validator runs before invocation, so the
action never has to cope with a combination the parser should have refused.
- Report bad input with
result.AddError(...) — do not throw for user-input errors; errors surface
through ParseResult.Errors and set a non-zero exit code automatically.
Reading values
Always by identity: string n = parseResult.GetValue(name)!;. There is no positional binding — keep
the instance you added to the command.