Skip to content

Optionally list available commands in the unknown-command error #2414

Description

@vlsi

What

When Cobra rejects an unknown command, the error names the bad token and, when the edit distance is small enough, a "Did you mean this?" suggestion. It never lists the commands the caller could actually run. I would like an opt-in that appends the available command set to that error.

Current behaviour

legacyArgs produces:

Error: unknown command "lst" for "app"

findSuggestions adds a "Did you mean this?" block only when SuggestionsFor finds a near match (Levenshtein distance <= SuggestionsMinimumDistance, default 2, or a name prefix). A token with no near match gets nothing beyond Run 'app --help' for usage.

So app lst (a typo of list) may get a suggestion, but app frobnicate gets none. In neither case does the caller see the full menu without a second --help round-trip.

Why this helps

Two audiences benefit:

  • Humans see the valid options inline, without re-running with --help.
  • LLM agents driving the CLI recover in one step. A fuzzy "did you mean" only rescues a near-miss typo. An agent that guesses a plausible but wrong verb hits a dead end today. The available-command list gives it the menu to pick the right call on the next turn. That is the difference between a one-shot recovery and a stalled retry loop.

Proposed change

An opt-in that appends the available commands to the unknown-command error. It would use IsAvailableCommand, so hidden and deprecated commands stay out:

Error: unknown command "frobnicate" for "app"; available commands: list, get, add, edit, delete

A cap (say, the first 10) keeps the line readable for large trees. The exact shape is open: a Command field, a setting on the suggestion machinery, or a hook. It composes with, rather than replaces, the "did you mean" block.

Reference implementation

I ship this in a CLI today by building the error by hand:

func unknownSubcommandError(cmd *cobra.Command, name string) error {
	msg := fmt.Sprintf("unknown command %q for %q", name, cmd.CommandPath())
	if s := cmd.SuggestionsFor(name); len(s) > 0 {
		msg += fmt.Sprintf(" (did you mean %q?)", s[0])
	}
	var avail []string
	for _, sub := range cmd.Commands() {
		if sub.IsAvailableCommand() {
			avail = append(avail, sub.Name())
		}
	}
	if len(avail) > 0 {
		msg += fmt.Sprintf("; available commands: %s", strings.Join(avail, ", "))
	}
	return fmt.Errorf("%s", msg)
}

It works, but every user who wants this re-implements it, and wiring it onto a command group also means giving the group its own RunE to dodge a separate problem (a non-runnable parent swallows an unknown subcommand and exits 0, tracked in #2130 / #2167). Built-in support would cover the common case without the boilerplate.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions