Summary
CliHost/ArgParser only treat a token as a command alias when it is the first non-option token. If a registered subcommand appears after a positional argument, it is never recognized as a command — instead the host falls back to DefaultCommand and the real subcommand is swallowed as just another positional argument.
This makes argument order rigid in a way that surprises users: tool sub file works, but tool file sub silently does the wrong thing.
Version
Terminal.Gui.Cli 0.3.0.
Repro
Given a host with a default command (tui) and a registered gui command that accepts positional args:
var host = new CliHost(o => { o.DefaultCommand = "tui"; });
host.Registry.Register(new TuiCommand()); // default
host.Registry.Register(new GuiCommand()); // PrimaryAlias "gui", AcceptsPositionalArgs = true
| Command line |
Resolved command |
Positional args |
tool gui ./file.cpp |
gui ✅ |
["./file.cpp"] |
tool ./file.cpp gui |
tui (default) ❌ |
["./file.cpp", "gui"] |
In the second case the user clearly intended the gui subcommand, but it is treated as a filename and the default command runs instead.
Expected
A registered subcommand should be recognized regardless of whether a positional argument precedes it (or at least there should be a documented/opt-in way to allow it), so tool file sub and tool sub file behave the same.
Root cause
In ArgParser.Parse, the first non-- token is unconditionally captured as the command alias, and every subsequent non-- token is appended to the positional list:
if (text == null && !text2.StartsWith('-')) // first bare token -> command alias
{
text = text2;
index++;
continue;
}
if (text != null && !text2.StartsWith('-')) // any later bare token -> positional arg
{
list.Add(text2);
index++;
continue;
}
Then in CliHost.RunAsync, when Registry.TryResolve(alias) fails (because alias was actually a filename), it calls RunWithDefaultCommandAsync, which prepends the default command and re-parses with all original tokens — including the genuine subcommand — as positional args. There is no point at which a non-first token is matched against the registry.
Impact / workaround
Downstream (winprint's wp CLI) we currently just document "subcommand must come first." Filing this upstream because the asymmetry is a parser-level concern rather than something each consumer should special-case.
Summary
CliHost/ArgParseronly treat a token as a command alias when it is the first non-option token. If a registered subcommand appears after a positional argument, it is never recognized as a command — instead the host falls back toDefaultCommandand the real subcommand is swallowed as just another positional argument.This makes argument order rigid in a way that surprises users:
tool sub fileworks, buttool file subsilently does the wrong thing.Version
Terminal.Gui.Cli
0.3.0.Repro
Given a host with a default command (
tui) and a registeredguicommand that accepts positional args:tool gui ./file.cppgui✅["./file.cpp"]tool ./file.cpp guitui(default) ❌["./file.cpp", "gui"]In the second case the user clearly intended the
guisubcommand, but it is treated as a filename and the default command runs instead.Expected
A registered subcommand should be recognized regardless of whether a positional argument precedes it (or at least there should be a documented/opt-in way to allow it), so
tool file subandtool sub filebehave the same.Root cause
In
ArgParser.Parse, the first non--token is unconditionally captured as the command alias, and every subsequent non--token is appended to the positional list:Then in
CliHost.RunAsync, whenRegistry.TryResolve(alias)fails (becausealiaswas actually a filename), it callsRunWithDefaultCommandAsync, which prepends the default command and re-parses with all original tokens — including the genuine subcommand — as positional args. There is no point at which a non-first token is matched against the registry.Impact / workaround
Downstream (winprint's
wpCLI) we currently just document "subcommand must come first." Filing this upstream because the asymmetry is a parser-level concern rather than something each consumer should special-case.