Browse Docs

๐Ÿ Cobra

A Command Builder for Go

Cobra is a Go library for building command-line applications.

It is used by many well-known tools from the Go ecosystem because it gives us a convenient structure for:

  • commands
  • subcommands
  • arguments
  • flags
  • validation
  • help
  • shell completion
  • error handling

A Cobra application usually reads naturally:

1app command argument --flag value

For example:

1git clone repository --bare
2kubectl get pods --namespace production

A useful introduction is also available here:

How to use Cobra in Go


Installation

1go install github.com/spf13/cobra-cli@latest
2# Add it to the `PATH` if necessary:
3export PATH="$(go env GOPATH)/bin:$PATH"
4# Check:
5cobra-cli --help

There are actually two different things involved here. cobra-cli helps create files. The application itself depends on the cobra library.


1. Start with a Go project

Let’s create a small CLI called clock.

Its goal will eventually be to work with time zones:

1clock timezone Europe/Warsaw
2clock timezone America/New_York

Init the project:

1mkdir clock
2cd clock
3go mod init example.com/clock
4cobra-cli init

The generated project will look approximately like:

1clock/
2โ”œโ”€โ”€ cmd/
3โ”‚   โ””โ”€โ”€ root.go
4โ”œโ”€โ”€ go.mod
5โ”œโ”€โ”€ go.sum
6โ””โ”€โ”€ main.go

2. Understand the Cobra model

Before adding code, it is important to understand Cobra’s vocabulary:

1#"root command" "command" "argument" "flag"
2clock timezone Europe/Warsaw --format "15:04"

A command is represented by:

1cmd := &cobra.Command{
2    Use:   "timezone <zone>",
3    Short: "Display the current time in a timezone",
4}

3. The root command

The top-level command is called the root command. So in our example clock is the root.

A simplified root command could look like this:

 1package cmd
 2
 3import "github.com/spf13/cobra"
 4
 5func NewRootCommand() *cobra.Command {
 6    return &cobra.Command{
 7        Use:   "clock",
 8        Short: "A small CLI for working with time",
 9    }
10}

4. Add the first command

The generator can create a command:

1cobra-cli add timezone

We now have something similar to:

1clock/
2โ”œโ”€โ”€ cmd/
3โ”‚   โ”œโ”€โ”€ root.go
4โ”‚   โ””โ”€โ”€ timezone.go
5โ”œโ”€โ”€ main.go
6โ””โ”€โ”€ go.mod

Our first version can simply print something:

1var timezoneCmd = &cobra.Command{
2    Use:   "timezone",
3    Short: "Display timezone information",
4    Run: func(cmd *cobra.Command, args []string) {
5        fmt.Println("timezone command")
6    },
7}

Test it: go run . timezone


5. Arguments

The zone is an argument: clock timezone Europe/Warsaw

 1func newTimezoneCommand() *cobra.Command {
 2    return &cobra.Command{
 3        Use:   "timezone <zone>",
 4        Short: "Display the current time in a timezone",
 5        Args:  cobra.ExactArgs(1),
 6
 7        RunE: func(cmd *cobra.Command, args []string) error {
 8            zone := args[0]
 9
10            location, err := time.LoadLocation(zone)
11            if err != nil {
12                return err
13            }
14
15            now := time.Now().In(location)
16
17            fmt.Fprintln(cmd.OutOrStdout(), now.Format(time.RFC3339))
18
19            return nil
20        },
21    }
22}

test it:

1go run . timezone Europe/Warsaw
2go run . timezone Asia/Tokyo

6. Validate arguments with Cobra

This line means that Cobra checks the number of arguments before executing the command :

1Args: cobra.ExactArgs(1),

Cobra checks the number of arguments before executing the command.

For example:

1# Fails because an argument is missing
2clock timezone
3# Fails because too many arguments
4clock timezone Europe/Warsaw Asia/Tokyo

Other useful validators include:

1cobra.NoArgs
2cobra.ExactArgs(1)
3cobra.MinimumNArgs(1)
4cobra.MaximumNArgs(2)
5cobra.RangeArgs(1, 3)
6cobra.ArbitraryArgs

7. Run versus RunE

Instead of:

1Run: func(cmd *cobra.Command, args []string) {
2    if err != nil {
3        fmt.Println(err)
4        os.Exit(1)
5    }
6}

we can return the error:

 1RunE: func(cmd *cobra.Command, args []string) error {
 2    result, err := doSomething()
 3    if err != nil {
 4        return err
 5    }
 6
 7    fmt.Fprintln(cmd.OutOrStdout(), result)
 8
 9    return nil
10}

The error then travels upward:

 1business code
 2     โ”‚
 3     โ”‚ return error
 4     โ–ผ
 5RunE
 6     โ”‚
 7     โ”‚ return error
 8     โ–ผ
 9Cobra
10     โ”‚
11     โ–ผ
12main()

This gives us one place at the top of the application where exit codes and error printing can be controlled.


8. Add flags

Arguments identify the thing we are operating on. Flags modify how the operation behaves.

 1func newTimezoneCommand() *cobra.Command {
 2    format := time.RFC3339
 3
 4    cmd := &cobra.Command{
 5        Use:   "timezone <zone>",
 6        Short: "Display the current time in a timezone",
 7        Args:  cobra.ExactArgs(1),
 8
 9        RunE: func(cmd *cobra.Command, args []string) error {
10            location, err := time.LoadLocation(args[0])
11            if err != nil {
12                return err
13            }
14
15            now := time.Now().In(location)
16
17            fmt.Fprintln(cmd.OutOrStdout(), now.Format(format))
18
19            return nil
20        },
21    }
22
23    cmd.Flags().StringVarP(
24        &format,
25        "format",
26        "f",
27        time.RFC3339,
28        "Go time format",
29    )
30
31    return cmd
32}

Now both work:

1clock timezone Europe/Warsaw

and:

1clock timezone Europe/Warsaw --format "15:04"

Short flags work too:

1clock timezone Europe/Warsaw -f "15:04"

The CLI grammar is now:

1clock timezone <zone> --format <format>
2       โ”‚          โ”‚          โ”‚
3       command    argument   flag

9. Local flags and persistent flags

There are two particularly important kinds of flags.

Local flags

A normal flag belongs only to a command:

1cmd.Flags().String(...)

Persistent flags

A persistent flag is inherited by child commands:

1root.PersistentFlags().BoolP(
2    "verbose",
3    "v",
4    false,
5    "enable verbose output",
6)

For a larger CLI this is useful for global concerns such as:

1--verbose
2--config
3--debug
4--noninteractive

10. Keep main.go boring

An important design principle for CLI applications is:

main.go should be boring.

A simple version is:

 1package main
 2
 3import (
 4    "fmt"
 5    "os"
 6
 7    "example.com/clock/cmd"
 8)
 9
10func main() {
11    root := cmd.NewRootCommand()
12
13    if err := root.Execute(); err != nil {
14        fmt.Fprintln(os.Stderr, err)
15        os.Exit(1)
16    }
17}

The important code should live somewhere else.

1main.go
2   โ”‚
3   โ””โ”€โ”€ construct application
4           โ”‚
5           โ””โ”€โ”€ construct root command
6                   โ”‚
7                   โ””โ”€โ”€ execute

11. Context and Ctrl+C

CLI commands often perform operations that can take time:

  • HTTP requests
  • Git clones
  • deployments
  • database queries
  • backups
  • API calls

Users expect Ctrl+C to stop them.

Go’s context.Context fits naturally with Cobra.

At startup:

1ctx, stop := signal.NotifyContext(
2    context.Background(),
3    os.Interrupt,
4    syscall.SIGTERM,
5)
6defer stop()

Execute Cobra with the context:

1root.ExecuteContext(ctx)

Inside a command:

1RunE: func(cmd *cobra.Command, args []string) error {
2    return service.DoSomething(cmd.Context())
3}

The context flow becomes OS signal > context cancelled > Cobra command > cmd.Context() >HTTP / Git / DB operation. This is a very useful pattern for production CLI tools.


12. Don’t put the entire application inside RunE

This is where Cobra tutorials often stop too early.

You can technically write:

 1RunE: func(cmd *cobra.Command, args []string) error {
 2    // read config
 3
 4    // authenticate
 5
 6    // call HTTP API
 7
 8    // clone repository
 9
10    // create files
11
12    // update database
13
14    // print output
15
16    return nil
17}

But eventually RunE becomes hundreds of lines long.

A better mental model is:

1Cobra
2  โ”‚
3  โ”‚ parse input
4  โ–ผ
5Application
6  โ”‚
7  โ”‚ perform use case
8  โ–ผ
9Infrastructure

Cobra’s responsibility should mostly be:

  • parse command
  • parse arguments
  • parse flags
  • validate basic CLI input
  • call application code
  • display result

But not implement your entire business logic.


13. Introduce an application service

inside internal/timezone/service.go:

 1package timezone
 2
 3import "time"
 4
 5type Service struct{}
 6
 7func (Service) Current(zone string, format string) (string, error) {
 8    location, err := time.LoadLocation(zone)
 9    if err != nil {
10        return "", err
11    }
12
13    now := time.Now().In(location)
14
15    return now.Format(format), nil
16}

Now Cobra only connects CLI input to our application:

 1func newTimezoneCommand(service timezone.Service) *cobra.Command {
 2    format := time.RFC3339
 3
 4    cmd := &cobra.Command{
 5        Use:   "timezone <zone>",
 6        Short: "Display the current time in a timezone",
 7        Args:  cobra.ExactArgs(1),
 8
 9        RunE: func(cmd *cobra.Command, args []string) error {
10            result, err := service.Current(args[0], format)
11            if err != nil {
12                return err
13            }
14
15            fmt.Fprintln(cmd.OutOrStdout(), result)
16
17            return nil
18        },
19    }
20
21    cmd.Flags().StringVarP(
22        &format,
23        "format",
24        "f",
25        time.RFC3339,
26        "Go time format",
27    )
28
29    return cmd
30}

Now our architecture is:

 1terminal
 2   โ”‚
 3   โ–ผ
 4Cobra
 5   โ”‚
 6   โ”‚ zone + format
 7   โ–ผ
 8timezone.Service
 9   โ”‚
10   โ–ผ
11time package

This distinction becomes extremely valuable when the application grows.


14. Dependency injection without a framework

Go does not need a dependency injection framework for most CLI applications.

We can simply create an application struct:

1type App struct {
2    Timezone timezone.Service
3}

Create its dependencies:

1func New() *App {
2    return &App{
3        Timezone: timezone.Service{},
4    }
5}

And let it build the Cobra tree:

 1func (a *App) Root() *cobra.Command {
 2    root := &cobra.Command{
 3        Use:   "clock",
 4        Short: "A CLI for working with time",
 5    }
 6
 7    root.AddCommand(
 8        newTimezoneCommand(a.Timezone),
 9    )
10
11    return root
12}

Startup becomes:

 1main()
 2  โ”‚
 3  โ–ผ
 4app.New()
 5  โ”‚
 6  โ”œโ”€โ”€ construct services
 7  โ”‚
 8  โ–ผ
 9App.Root()
10  โ”‚
11  โ”œโ”€โ”€ construct Cobra commands
12  โ”‚
13  โ–ผ
14ExecuteContext()

This is very close to the pattern used by Colt.

Sunday, October 4, 2026 Tuesday, August 1, 2023