When flags turn into a mess
You wrote a Go script to process logs. It worked fine for a while. Then you needed a --verbose flag. Then a --output directory. Then a subcommand to init a config file. Now your main.go is a tangle of flag.StringVar calls and switch statements on os.Args. Adding a new feature feels like pulling a thread that unravels the whole sweater. You need a structure that scales.
Cobra is the standard library for building command-line interfaces in Go. It treats your CLI as a tree of commands. Each command can have its own flags, its own help text, and its own execution logic. Cobra handles the parsing, the help generation, and the error reporting. You focus on what the command does. The name comes from the idea that a cobra command strikes fast and accurately. It's the tool behind kubectl, helm, and docker.
The command tree
Think of your CLI as a tree. The root is the executable name. Branches are subcommands. Leaves are the actions. Cobra traverses this tree based on the arguments. If you run kubectl get pods, Cobra finds kubectl, then get, then pods. Each node can have flags. Flags on kubectl apply to everything. Flags on get apply to get and its children. Flags on pods apply only to pods. This hierarchy matches how users think about tools.
The Use field defines the command signature. You can include flags in brackets and arguments in angle brackets. Cobra uses this to generate the usage line in help output. For example, Use: "add [flags] <name>" tells users that name is required and flags are optional. The Short description appears in the command list. The Long description appears when users ask for help on a specific command. Use Short for a one-line summary. Use Long for examples and detailed explanations. Cobra supports multi-line strings in Long, so you can format the text with newlines.
Minimal example
Here's the smallest Cobra app: a root command that prints a greeting.
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "greet",
Short: "A simple greeting CLI",
Run: func(cmd *cobra.Command, args []string) {
// Execute the core logic of the root command
fmt.Println("Hello from Cobra!")
},
}
func main() {
// Execute runs the command tree and handles errors
if err := rootCmd.Execute(); err != nil {
// Print the error and exit with a non-zero status code
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Cobra handles the parsing. You handle the logic.
How it runs
When you run go run main.go, Cobra builds an internal tree of commands. The rootCmd becomes the entry point. When you type greet, Cobra matches the input to rootCmd, runs the Run function, and prints the output. If you type greet --help, Cobra intercepts the flag, skips the Run function, and prints the Short description along with available flags. If you pass an unknown flag, Cobra stops execution and prints a usage error. The Execute method is the engine: it parses os.Args, finds the matching command, binds flags, and calls the handler.
The cobra-cli tool scaffolds a project structure. It creates a cmd directory with root.go and a main.go that calls Execute. This separation is helpful for larger projects. You can install the generator with go install github.com/spf13/cobra/cobra@latest. Running cobra init myapp creates the boilerplate. Running cobra add subcommand generates a new file in cmd and registers it. The generator is optional. You can write Cobra code by hand, which gives you full control. The generator shines when you have more than three subcommands.
Realistic example
Real CLIs have subcommands and flags. Here's a tool that manages users with an add subcommand and a global --config flag.
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
// rootCmd defines the base behavior and global flags
var rootCmd = &cobra.Command{
Use: "usermgr",
Short: "Manage application users",
}
// configPath stores the path provided by the --config flag
var configPath string
// init sets up flags before the command tree runs
func init() {
// Persistent flags are inherited by all subcommands
rootCmd.PersistentFlags().StringVar(&configPath, "config", "config.yaml", "path to config file")
}
The init function is where you bind flags to variables. PersistentFlags make the flag available to every subcommand. Local flags belong only to a specific command. This separation keeps global configuration distinct from command-specific options. Cobra commands are usually defined as package-level variables. This makes them easy to reference and modify. Don't hide commands inside functions unless you have a dynamic reason.
// addCmd defines the logic for adding a user
var addCmd = &cobra.Command{
Use: "add [name]",
Short: "Add a new user",
Args: cobra.MinimumNArgs(1), // Reject calls without a name argument
Run: func(cmd *cobra.Command, args []string) {
// args contains the positional arguments passed after the command
fmt.Printf("Adding user %s using config %s\n", args[0], configPath)
},
}
func main() {
// Wire the subcommand into the tree
rootCmd.AddCommand(addCmd)
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Persistent flags flow down. Local flags stay put.
Run versus RunE
Cobra offers two ways to define command logic: Run and RunE. The Run function takes no arguments and returns nothing. If you need to report an error, you have to print it and call os.Exit. The RunE function returns an error. Cobra captures that error and prints it to stderr with a non-zero exit code. Using RunE is the preferred pattern because it lets Cobra handle the error formatting consistently. It also makes testing easier since you can inspect the error return value.
var rootCmd = &cobra.Command{
Use: "app",
RunE: func(cmd *cobra.Command, args []string) error {
// Return an error instead of printing and exiting
return fmt.Errorf("something went wrong")
},
}
Return errors from RunE. Cobra prints the rest.
Validation and lifecycle
The Args field on a command lets you validate arguments before the Run function executes. Cobra provides helpers like cobra.ExactArgs, cobra.MinimumNArgs, and cobra.MaximumNArgs. You can also pass a custom function. If validation fails, Cobra prints the error and usage, then stops. This keeps your Run function clean. You don't need to check len(args) manually.
Commands have a lifecycle. PersistentPreRun runs before the command, and it runs for all subcommands. This is where you initialize databases or load configuration. PostRun runs after the command finishes. Use these hooks to manage resources. The order is PersistentPreRun, PreRun, Run, PostRun, PersistentPostRun.
Validate early. Fail fast. Keep Run focused on work.
Pitfalls and errors
Cobra catches mistakes early, but a few patterns trip people up. Forgetting to add a subcommand to the root is a silent failure: the command exists in memory but the parser never sees it. The compiler won't complain; the runtime just won't find the command. You'll get Error: unknown command "sub" for "root" when you try to run it.
Variable shadowing in flags is another trap. If you define a flag with the same name on a subcommand and the root, the subcommand flag takes precedence. The root flag value remains unchanged. This can be confusing when debugging. Use unique names or document the override behavior.
init functions run in an undefined order across packages. If your flag setup depends on another package's state, don't rely on init. Pass configuration explicitly. Cobra code follows standard Go formatting. Run gofmt on your files. The community expects consistent indentation and spacing. Most editors run gofmt on save, so you rarely need to think about it.
Wire commands in main. Bind flags in init. Define commands at package level.
When to use Cobra
Use Cobra when you need a full-featured CLI with subcommands, auto-generated help, and flag parsing. Use the standard flag package when your tool has a single command and fewer than five flags. Use urfave/cli when you prefer a more functional API or need built-in version handling without extra setup. Use plain os.Args parsing when you are writing a tiny script and want zero dependencies.
Pick the tool that matches your complexity. Simple scripts don't need a framework.