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:
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:
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.
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
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}
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}
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
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
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
Run versus RunEInstead 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.
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
There are two particularly important kinds of flags.
A normal flag belongs only to a command:
1cmd.Flags().String(...)
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
main.go boringAn important design principle for CLI applications is:
main.goshould 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
CLI commands often perform operations that can take time:
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.
RunEThis 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:
But not implement your entire business logic.
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.
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.