cobra & urfave/cli Command Trees
kubectl-style tools need nested subcommands, shared global flags, and consistent help text.
Search across all documentation pages
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).
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.
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:
get, describe, delete).--kubeconfig, --namespace).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:
PersistentFlags on root apply to init and future siblings.initCmd.RunE returns errors; Execute handles printing and exit status.cmd.Flags().GetString reads parsed values inside the handler.*cobra.Command nodes at init time.Execute parses global flags, finds the deepest matching command, parses local flags, validates Args, then calls RunE.cmd.Help() is callable from code.cli.App with []*cli.Command and Before/After hooks instead of persistent flags on a struct.| 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 |
# bash example after generating completion script
mytool completion bash > /etc/bash_completion.d/mytoolRegisterFlagCompletionFunc.cmd/ package; put business logic in internal/.RunE over Run so errors propagate to Execute.SilenceUsage: true on commands when parse errors should not dump full help.Execute from multiple tests without reset - Global state in pflags. Fix: root.SetArgs([]string{"init", "--dir", "/tmp"}) per test./v2. Fix: pin github.com/urfave/cli/v2.Args validators - Operators pass wrong arity and get confusing errors. Fix: cobra.MinimumNArgs, custom ValidArgsFunction.| 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 |
kubebuilder and controller-runtime scaffolds generate cobra commands.
Persistent kubeconfig flags and nested verbs match how kubectl models resources.
Persistent flags register on ancestors and apply to descendants.
Local flags exist only on the command that defines them.
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.
Set Hidden: true on maintenance or alpha commands.
They still run when invoked directly but disappear from default help lists.
viper.BindPFlags(cmd.Flags()) maps parsed flags into viper keys.
Read config after Execute begins or in PersistentPreRunE.
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.
Add a version subcommand or use cobra.Command.Version with SetVersionTemplate.
Stamp version with -ldflags at build time.
Use cmd.Context() inside RunE (cobra sets it on the command).
Pass that context to HTTP and database calls for cancellation.
Mark Deprecated string on the command; cobra prints a warning when used.
Keep the command functional until the documented removal release.
PowerShell completion exists for cobra with generated scripts.
Test on target shells your operators actually use.
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).
Reviewed by Chris St. John·Last updated Jul 16, 2026