Building with Cobra
Cobra is the standard CLI framework in Go. Docker, Kubernetes, Hugo, GitHub CLI, and Helm all use it. It handles commands, subcommands, flags, argument validation, help text, and shell completions. For simple tools, the standard flag package is enough. For anything with subcommands, cobra is the right choice.
Why Cobra
The Go standard library's flag package handles simple flag parsing:
port := flag.Int("port", 8080, "server port")
verbose := flag.Bool("verbose", false, "enable verbose output")
flag.Parse()
But flag does not support subcommands, POSIX-style flags (--long and -s), or auto-generated help. Once your tool has more than one operation, you need cobra.
Commands & Subcommands
Cobra models your CLI as a tree of commands:
myapp
serve -- start the HTTP server
migrate
up -- run pending migrations
down -- roll back last migration
user
create -- create a new user
list -- list all users
Each command is a cobra.Command:
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "My application",
Long: "A longer description of my application and what it does.",
}
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the HTTP server",
RunE: func(cmd *cobra.Command, args []string) error {
port, _ := cmd.Flags().GetInt("port")
fmt.Printf("Starting server on port %d\n", port)
return startServer(port)
},
}
var migrateUpCmd = &cobra.Command{
Use: "up",
Short: "Run pending migrations",
RunE: func(cmd *cobra.Command, args []string) error {
return runMigrationsUp()
},
}
func init() {
serveCmd.Flags().IntP("port", "p", 8080, "port to listen on")
migrateCmd := &cobra.Command{Use: "migrate", Short: "Database migrations"}
migrateCmd.AddCommand(migrateUpCmd)
rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(migrateCmd)
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
Flags
Persistent Flags vs Local Flags
Persistent flags are inherited by all subcommands. Local flags apply only to the command they are defined on:
// Persistent: available to rootCmd and ALL subcommands
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file path")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
// Local: only available on serveCmd
serveCmd.Flags().IntP("port", "p", 8080, "port to listen on")
serveCmd.Flags().StringP("host", "H", "0.0.0.0", "host to bind to")
Required Flags
serveCmd.Flags().String("db-url", "", "database connection URL")
serveCmd.MarkFlagRequired("db-url")
If the user omits a required flag, cobra prints an error and the help text.
Flag Types
Cobra supports all common types through pflag:
cmd.Flags().String("name", "", "user name")
cmd.Flags().Int("count", 10, "number of items")
cmd.Flags().Bool("force", false, "skip confirmation")
cmd.Flags().StringSlice("tags", nil, "comma-separated tags")
cmd.Flags().Duration("timeout", 30*time.Second, "request timeout")
Argument Validation
Cobra can validate positional arguments:
var userCreateCmd = &cobra.Command{
Use: "create [name] [email]",
Short: "Create a new user",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
name, email := args[0], args[1]
return createUser(name, email)
},
}
Built-in validators:
Validator Description
-------------------------------------------
cobra.NoArgs no positional args allowed
cobra.ExactArgs(n) exactly n args required
cobra.MinimumNArgs(n) at least n args
cobra.MaximumNArgs(n) at most n args
cobra.RangeArgs(m, n) between m and n args
Custom validation:
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("requires exactly one argument: user ID")
}
if _, err := strconv.Atoi(args[0]); err != nil {
return fmt.Errorf("user ID must be a number, got %q", args[0])
}
return nil
},
Auto-Generated Help & Completions
Cobra generates help text automatically:
$ myapp --help
A longer description of my application and what it does.
Usage:
myapp [command]
Available Commands:
help Help about any command
migrate Database migrations
serve Start the HTTP server
Flags:
--config string config file path
-h, --help help for myapp
-v, --verbose verbose output
Use "myapp [command] --help" for more information about a command.
Generate shell completions for bash, zsh, fish, and PowerShell:
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion script",
Args: cobra.ExactValidArgs(1),
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return rootCmd.GenBashCompletion(os.Stdout)
case "zsh":
return rootCmd.GenZshCompletion(os.Stdout)
case "fish":
return rootCmd.GenFishCompletion(os.Stdout, true)
case "powershell":
return rootCmd.GenPowerShellCompletionWithDesc(os.Stdout)
}
return nil
},
}
Project Structure with cobra-cli
The cobra-cli scaffolding tool generates project structure:
go install github.com/spf13/cobra-cli@latest
cobra-cli init myapp
cobra-cli add serve
cobra-cli add migrate
This creates:
myapp/
cmd/
root.go
serve.go
migrate.go
main.go
The generated structure is a fine starting point. For larger projects, move business logic out of cmd/ into internal/:
myapp/
cmd/
root.go -- cobra setup only
serve.go -- wires dependencies, starts server
internal/
server/ -- actual server logic
store/ -- database logic
main.go
RunE vs Run
Use RunE (returns error) instead of Run (returns nothing):
// Prefer this
RunE: func(cmd *cobra.Command, args []string) error {
return doSomething()
},
// Not this
Run: func(cmd *cobra.Command, args []string) {
if err := doSomething(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
},
RunE lets cobra handle errors consistently. When any command returns an error, cobra prints it and Execute() returns it.
When Cobra Is Overkill
For simple tools with no subcommands, the standard flag package works fine:
func main() {
input := flag.String("input", "", "input file path")
output := flag.String("output", "stdout", "output file path")
verbose := flag.Bool("v", false, "verbose output")
flag.Parse()
if *input == "" {
fmt.Fprintln(os.Stderr, "error: -input is required")
flag.Usage()
os.Exit(1)
}
if err := process(*input, *output, *verbose); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Tool complexity Use
------------------------------------------
Simple flags, no subcommands flag package
One level of subcommands cobra
Complex CLI with nesting cobra
Common Pitfalls
- Putting business logic in cobra RunE functions. The command function should parse flags, wire dependencies, and call into your domain code. Keep the
cmd/package thin. - Using Run instead of RunE.
RunElets cobra handle errors uniformly.Runforces you to handle errors withos.Exitin each command. - Not marking required flags. Users will forget flags.
MarkFlagRequiredgives clear error messages instead of confusing zero-value behavior. - Defining all commands in one file. Split commands into separate files:
root.go,serve.go,migrate.go. One command per file. - Forgetting to add shell completions. Completions make your CLI feel professional. Cobra generates them for free.
- Using cobra for a script-like tool. If your tool takes two flags and does one thing,
flagpackage is simpler.
Key Takeaways
- Cobra is the standard Go CLI framework, used by Docker, Kubernetes, and GitHub CLI.
- Commands form a tree: root command, subcommands, sub-subcommands.
- Persistent flags propagate to all subcommands. Local flags apply only to their command.
- Use
RunE(notRun) so cobra handles errors consistently. - Argument validators (
ExactArgs,MinimumNArgs, custom) catch usage errors early. - Cobra generates help text and shell completions automatically.
- For simple tools without subcommands, the standard
flagpackage is sufficient.