unknownSubcommandRunE replaces cobra's silent help fallback on group commands with a typed *errs.ValidationError: a flag that belongs to a missing subcommand, a misplaced subcommand-only flag, or an unknown subcommand name each fail structured (exit 2) instead of degrading to help + exit 0.
(cmd *cobra.Command, args []string)
| 366 | // subcommand, a misplaced subcommand-only flag, or an unknown subcommand name |
| 367 | // each fail structured (exit 2) instead of degrading to help + exit 0. |
| 368 | func unknownSubcommandRunE(cmd *cobra.Command, args []string) error { |
| 369 | if len(args) == 0 { |
| 370 | // A bare group (e.g. `sheets`), or one carrying only group-valid flags |
| 371 | // like the global --profile, legitimately prints help. But a flag that |
| 372 | // belongs to a (missing) subcommand is a user error: the guard's |
| 373 | // FParseErrWhitelist swallows such flags and leaves args empty, so without |
| 374 | // the checks below they would silently fall through to help + exit 0 — |
| 375 | // letting an agent mistake a malformed call (`im --format json`, |
| 376 | // `sheets --badflag`) for success. Recover the swallowed tokens from the |
| 377 | // raw invocation and fail structured instead. |
| 378 | flags := flagTokensInArgs(rawInvocationArgs) |
| 379 | if len(flags) == 0 { |
| 380 | return cmd.Help() |
| 381 | } |
| 382 | if unknown := unknownFlagTokens(cmd, rawInvocationArgs); len(unknown) > 0 { |
| 383 | verr := errs.NewValidationError(errs.SubtypeInvalidArgument, |
| 384 | "unknown flag %s before a subcommand for %q", strings.Join(unknown, ", "), cmd.CommandPath()). |
| 385 | WithHint("flags belong to a subcommand; run `%s --help` to list subcommands and their flags", cmd.CommandPath()) |
| 386 | for _, flag := range unknown { |
| 387 | verr.WithParams(errs.InvalidParam{Name: flag, Reason: "unknown flag before a subcommand"}) |
| 388 | } |
| 389 | return verr |
| 390 | } |
| 391 | // The remaining flags are all defined somewhere in the tree. Those valid |
| 392 | // on the group itself or inherited (e.g. the global --profile) do not |
| 393 | // require a subcommand, so a bare group carrying only those still prints |
| 394 | // help. Anything left belongs to a subcommand that was omitted |
| 395 | // (e.g. `im --format json`): distinct from unknown_flag — the flags are |
| 396 | // real, the subcommand is what's missing. |
| 397 | misplaced := subcommandOnlyFlagTokens(cmd, rawInvocationArgs) |
| 398 | if len(misplaced) == 0 { |
| 399 | return cmd.Help() |
| 400 | } |
| 401 | verr := errs.NewValidationError(errs.SubtypeInvalidArgument, |
| 402 | "missing subcommand for %q; flag %s belongs to a subcommand, not the group", cmd.CommandPath(), strings.Join(misplaced, ", ")). |
| 403 | WithHint("run `%s --help` to list subcommands and their flags", cmd.CommandPath()) |
| 404 | for _, flag := range misplaced { |
| 405 | verr.WithParams(errs.InvalidParam{Name: flag, Reason: "flag belongs to a subcommand, not the group"}) |
| 406 | } |
| 407 | return verr |
| 408 | } |
| 409 | unknown := args[0] |
| 410 | available, deprecated := availableSubcommandNames(cmd) |
| 411 | // Rank suggestions across both current and deprecated names so a mistyped |
| 412 | // legacy command (e.g. +raed → +read) still resolves; the alias stays |
| 413 | // runnable and self-flags via the _notice on execution. |
| 414 | suggestions := suggest.Closest(unknown, append(append([]string{}, available...), deprecated...), 6) |
| 415 | msg := fmt.Sprintf("unknown subcommand %q for %q", unknown, cmd.CommandPath()) |
| 416 | hint := fmt.Sprintf("run `%s --help` to see available subcommands", cmd.CommandPath()) |
| 417 | if len(suggestions) > 0 { |
| 418 | hint = fmt.Sprintf("did you mean one of: %s? (run `%s --help` for the full list)", |
| 419 | strings.Join(suggestions, ", "), cmd.CommandPath()) |
| 420 | } |
| 421 | // Record the offending subcommand and its ranked candidates as a param with |
| 422 | // machine-readable Suggestions so an agent can retry without parsing the |
| 423 | // hint; the hint carries the same candidates as prose. |
| 424 | return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", msg). |
| 425 | WithParams(errs.InvalidParam{Name: unknown, Reason: "unknown subcommand", Suggestions: suggestions}). |