XenoAtom.CommandLine supports commands and sub-commands, allowing you to build rich multi-command CLIs similar to git, docker, or dotnet.
CommandApp is the entry point for your application. It inherits from Command and is the root of the command tree:
var app = new CommandApp("myexe")
{
{ "v|verbose", "Enable verbose output", v => {} },
new HelpOption(),
(ctx, _) =>
{
ctx.Out.WriteLine("Root command executed");
return ValueTask.FromResult(0);
}
};
await app.RunAsync(args);
If you omit the name, it defaults to the current executable name.
Add sub-commands using Command:
using XenoAtom.CommandLine;
const string _ = "";
string? name = null;
var messages = new List<string>();
var files = new List<string>();
var app = new CommandApp("myexe")
{
new CommandUsage(),
_,
{ "n|name=", "Your {NAME}", v => name = v },
new HelpOption(),
_,
"Available commands:",
new Command("commit", "Commit changes")
{
_,
"Options:",
{ "m|message=", "Commit {MESSAGE}", messages },
new HelpOption(),
_,
"Arguments:",
{ "<files>*", "Files to commit", files },
(ctx, _) =>
{
ctx.Out.WriteLine($"Committing as {name}");
foreach (var msg in messages)
ctx.Out.WriteLine($" Message: {msg}");
foreach (var file in files)
ctx.Out.WriteLine($" File: {file}");
return ValueTask.FromResult(0);
}
},
(ctx, _) =>
{
ctx.Out.WriteLine($"Hello, {name}! Use 'myexe commit' to commit.");
return ValueTask.FromResult(0);
}
};
await app.RunAsync(args);
Running myexe --help:
Usage: myexe [options] <command>
-n, --name=NAME Your NAME
-h, -?, --help Show this message and exit
Available commands:
commit Commit changes
Running myexe commit --help:
Usage: myexe commit [options] <files>*
Options:
-m, --message=MESSAGE Commit MESSAGE
-h, -?, --help Show this message and exit
Arguments:
<files>* Files to commit
--name) parsed before the sub-command are available to the sub-command's action.Commands can be nested to any depth:
var app = new CommandApp("myexe")
{
new Command("remote")
{
new Command("add", "Add a remote")
{
{ "n|name=", "Remote {NAME}", v => {} },
(ctx, _) => ValueTask.FromResult(0)
},
new Command("remove", "Remove a remote")
{
{ "n|name=", "Remote {NAME}", v => {} },
(ctx, _) => ValueTask.FromResult(0)
},
},
};
This creates myexe remote add --name origin and myexe remote remove --name origin.
Commands can be hidden from help output:
var cmd = new Command("internal-debug", "Debug command") { Hidden = true };
Hidden commands are still executable — they just don't appear in --help.
Every Command (including CommandApp) can have a single action that runs after parsing. There are several callback signatures:
(ctx, _) =>
{
ctx.Out.WriteLine("Hello!");
return ValueTask.FromResult(0);
}
The ctx parameter is a CommandRunContext providing access to Out, Error, and RunConfig. The second parameter is the array of remaining positional arguments (empty when using CommandArgument declarations).
(arguments) =>
{
foreach (var arg in arguments)
Console.WriteLine(arg);
return ValueTask.FromResult(0);
}
All action signatures return ValueTask<int> for native async support:
async (ctx, _) =>
{
await SomeAsyncWork();
return 0;
}
The return value is the process exit code. Return 0 for success and any non-zero value for failure. When an error is thrown during parsing, RunAsync returns 1.
CommandGroup lets you group related nodes (options, commands, text) together. More importantly, groups can be conditional — their contents are only active when a condition is met:
bool advanced = false;
var app = new CommandApp("myexe")
{
"Options:",
{ "advanced", "Activate advanced options", v => advanced = v is not null },
new HelpOption(),
new CommandGroup(() => advanced)
{
"Advanced Options:",
{ "special1", "Special option 1", v => {} },
{ "special2", "Special option 2", v => {} },
},
(ctx, _) => ValueTask.FromResult(0)
};
Running myexe --help:
Usage: myexe [options]
Options:
--advanced Activate advanced options
-h, -?, --help Show this message and exit
Running myexe --advanced --help:
Usage: myexe [options]
Options:
--advanced Activate advanced options
-h, -?, --help Show this message and exit
Advanced Options:
--special1 Special option 1
--special2 Special option 2
The conditional group affects both visibility (help output) and availability (parsing). When the group is inactive, its options and commands are not recognized — the parser reports them as unknown.
You can also put commands inside conditional groups:
new CommandGroup(() => advanced)
{
"Advanced Commands:",
new Command("debug", "Debug the application")
{
new HelpOption(),
(ctx, _) => ValueTask.FromResult(0)
},
}
The debug command only appears and is executable when --advanced is passed.
Any string added to a command becomes descriptive text in the help output:
var app = new CommandApp("myexe")
{
"Options:",
{ "v|verbose", "Verbose", v => {} },
new HelpOption(),
"",
"Available commands:",
new Command("build", "Build the project")
{
(ctx, _) => ValueTask.FromResult(0)
},
};
The empty string "" (or const string _ = "") adds a blank line in the help output. All items — options, commands, text — are rendered in declaration order.
XenoAtom.CommandLine provides two built-in option types:
Adds -h, -?, and --help:
new HelpOption()
// Equivalent to:
// { "h|?|help", "Show this message and exit", v => { /* triggers help */ } }
You can customize the prototype and description:
new HelpOption("h|help", "Display help information")
Adds --version (and optionally -v):
new VersionOption("1.2.3")
// Equivalent to:
// { "v|version", "Show the version of this command", v => { /* prints version */ } }
If no version string is provided, it extracts the version from the assembly's informational version attribute.
Understanding the parsing flow helps debug complex scenarios:
-, --, or /), applying option callbacks as values are consumed.CommandArgument) for the resolved command.Error and RunAsync returns 1.If the parser is currently expecting a value for an option, the next token is always consumed as that value — even if it looks like -- or matches a sub-command name.