3 min read
On this page

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. RunE lets cobra handle errors uniformly. Run forces you to handle errors with os.Exit in each command.
  • Not marking required flags. Users will forget flags. MarkFlagRequired gives 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, flag package 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 (not Run) 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 flag package is sufficient.