byob-go-cli

Cobra flag-group helpers over hand-rolled validation

byob-command-shape.6 command-shape

Problem: validating flag combinations (mutual exclusion, required-together, at-least-one-required) inside runFunc means validation runs after side effects like opening files or logging in have already happened, error messages are hand-written and inconsistent across commands, and shell completion has no idea which flags conflict — so tab-complete happily offers combinations that will fail validation.

Idea: use cobra's declarative flag-group helpers. They run in cobra's validation phase before RunE, emit consistent error messages, and integrate with shell completion so conflicting flags are hidden from tab-complete:

  • cmd.MarkFlagsMutuallyExclusive("json", "yaml", "template") — at most one.
  • cmd.MarkFlagsRequiredTogether("key", "secret") — all or none.
  • cmd.MarkFlagsOneRequired("file", "stdin", "url") — at least one.

For value validation (e.g., --port must be 1-65535), there's no declarative helper — a runFunc check is appropriate. Wrap the error with cmdutil.FlagErrorf so the top-level runner maps it to exit code 2 (usage error) instead of 1 (generic error).

Tradeoffs: the helpers cover the three most common flag-relationship shapes. Rarer ones ("A and B require C, but not D") still need runFunc validation — and that's fine; don't contort a simple check into a helper combination.

Design

func NewCmdExport(f *Factory, runF func(*Options) error) *cobra.Command {
    opts := &Options{IO: f.IOStreams}
    cmd := &cobra.Command{
        Use: "export",
        RunE: func(c *cobra.Command, args []string) error {
            if opts.Port < 1 || opts.Port > 65535 {
                return cmdutil.FlagErrorf("--port must be 1-65535, got %d", opts.Port)
            }
            if runF != nil {
                return runF(opts)
            }
            return exportRun(opts)
        },
    }
    cmd.Flags().BoolVar(&opts.JSON, "json", false, "emit JSON")
    cmd.Flags().BoolVar(&opts.YAML, "yaml", false, "emit YAML")
    cmd.Flags().StringVar(&opts.Template, "template", "", "custom template")
    cmd.Flags().IntVar(&opts.Port, "port", 8080, "listen port")

    // declarative flag relationships — validated before RunE
    cmd.MarkFlagsMutuallyExclusive("json", "yaml", "template")
    cmd.MarkFlagsOneRequired("json", "yaml", "template")

    return cmd
}