Skip to content

Subcommand is only recognized as the first positional token (trailing subcommand falls through to DefaultCommand) #37

Description

@tig

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions