cobra & urfave/cli Command Trees
kubectl-style tools need nested subcommands, shared global flags, and consistent help text.
cobra (github.com/spf13/cobra) is the most common choice in the Go ecosystem.
urfave/cli v2 (github.com/urfave/cli/v2) offers a struct-driven alternative with hooks and bash completion.
Both compile into the same single binary model; pick based on API taste and ecosystem integration (cobra pairs naturally with viper and controller-runtime scaffolds).
Summary
Command trees map argv to a selected Run or RunE function.
Parent commands hold persistent flags (config path, verbosity) while leaf commands declare action-specific flags.
Execute walks the tree, prints help on errors, and sets exit codes for shell scripts.
Shell completion generators reduce typos for long command paths like mytool db migrate up.
Recipe
Quick-reference recipe card - copy-paste ready.
root := &cobra.Command{Use: "mytool"}
root.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
root.AddCommand(&cobra.Command{
Use: "run",
Short: "start worker",
RunE: func(cmd *cobra.Command, args []string) error { return runWorker(cfgFile) },
})
return root.Execute()When to reach for this:
- More than two subcommands or nested resource verbs (
get,describe,delete). - Shared flags across many commands (
--kubeconfig,--namespace). - You want auto-generated help and shell completion.
- Kubernetes operators generated by kubebuilder (cobra by default).
Working Example
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var verbose bool
func main() {
root := &cobra.Command{
Use: "ship",
Short: "shipping utilities",
}
root.PersistentFlags().BoolVar(&verbose, "verbose", false, "verbose logs")
initCmd := &cobra.Command{
Use: "init",
Short: "initialize workspace",
RunE: func(cmd *cobra.Command, args []string) error {
dir, _ := cmd.Flags().GetString("dir")
if verbose {
fmt.Println("init", dir)
}
return nil
},
}
initCmd.Flags().String("dir", ".", "target directory")
root.AddCommand(initCmd)
if err := root.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}What this demonstrates:
PersistentFlagsonrootapply toinitand future siblings.- Local flags attach only to
initCmd. RunEreturns errors;Executehandles printing and exit status.cmd.Flags().GetStringreads parsed values inside the handler.
Deep Dive
How It Works
- cobra builds a tree of
*cobra.Commandnodes at init time. Executeparses global flags, finds the deepest matching command, parses local flags, validatesArgs, then callsRunE.- Help merges parent and child documentation;
cmd.Help()is callable from code. - urfave/cli uses
cli.Appwith[]*cli.CommandandBefore/Afterhooks instead of persistent flags on a struct.
cobra vs urfave/cli
| Feature | cobra | urfave/cli v2 |
|---|---|---|
| Persistent flags | PersistentFlags() | Flags on parent + inheritance via subcommands |
| Args validation | Args: cobra.ExactArgs(1) | Action checks cli.Context |
| Completion | GenBashCompletion / GenZshCompletion | EnableBashCompletion + Complete |
| Config pairing | viper BindPFlags | manual or third-party |
| Ecosystem | kubebuilder, operator-sdk | standalone CLIs |
Shell Completion
# bash example after generating completion script
mytool completion bash > /etc/bash_completion.d/mytool- Register valid flag values with
RegisterFlagCompletionFunc. - Document installation in README for operators using zsh or fish.
- Keep completion fast; avoid network calls during tab press.
Go Notes
- Keep command wiring in
cmd/package; put business logic ininternal/. - Prefer
RunEoverRunso errors propagate toExecute. - Use
SilenceUsage: trueon commands when parse errors should not dump full help.
Gotchas
- Duplicate flag names on parent and child - cobra panics or errors at init. Fix: rename or use persistent only on root.
- Calling
Executefrom multiple tests without reset - Global state in pflags. Fix:root.SetArgs([]string{"init", "--dir", "/tmp"})per test. - Heavy init in every command package - Slow startup for unused subcommands. Fix: lazy imports or split binaries for CI-hot tools.
- urfave/cli v1 API - Old tutorials reference v1; module path changed to
/v2. Fix: pingithub.com/urfave/cli/v2. - Missing
Argsvalidators - Operators pass wrong arity and get confusing errors. Fix:cobra.MinimumNArgs, customValidArgsFunction.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
stdlib flag + switch | One or two subcommands | Completion and help maintenance hurt |
| kong | Struct-tag CLI definitions | Team already standardized on cobra |
| manual argv parsing | 10-line internal tool | Users need discoverable --help |
| separate binaries | Extreme startup sensitivity | Operators expect one mytool entry point |
FAQs
Why is cobra the default for Kubernetes tools?
kubebuilder and controller-runtime scaffolds generate cobra commands.
Persistent kubeconfig flags and nested verbs match how kubectl models resources.
How do persistent flags differ from local flags?
Persistent flags register on ancestors and apply to descendants.
Local flags exist only on the command that defines them.
How do I test a cobra command?
Call cmd.SetArgs([]string{...}) and cmd.Execute() or invoke RunE directly with a *cobra.Command built in the test.
Avoid parsing real os.Args in parallel tests.
Can I hide internal commands?
Set Hidden: true on maintenance or alpha commands.
They still run when invoked directly but disappear from default help lists.
How does viper integrate?
viper.BindPFlags(cmd.Flags()) maps parsed flags into viper keys.
Read config after Execute begins or in PersistentPreRunE.
urfave/cli or cobra for a new greenfield CLI?
Choose cobra when you want kube-style patterns and completion generators.
Choose urfave/cli when you prefer app-centric hooks and a smaller conceptual surface.
How do I add a --version command?
Add a version subcommand or use cobra.Command.Version with SetVersionTemplate.
Stamp version with -ldflags at build time.
What about context.Context in commands?
Use cmd.Context() inside RunE (cobra sets it on the command).
Pass that context to HTTP and database calls for cancellation.
How do I deprecate a subcommand?
Mark Deprecated string on the command; cobra prints a warning when used.
Keep the command functional until the documented removal release.
Does completion work on Windows?
PowerShell completion exists for cobra with generated scripts.
Test on target shells your operators actually use.
Related
- CLI Tools Basics - manual subcommand dispatch
- flag Package & POSIX-Style Flags - stdlib parsing baseline
- Configuration: Viper, Env & Config Files - BindPFlags integration
- kingpin & Advanced CLI UX - typed flag alternative
- CLI Tools Best Practices - distribution and testing
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).