Building Modern CLI Tools with Cobra in Go

Cobra is a powerful library for building modern command-line interfaces in Go. While the standard flag package handles basic CLI requirements, Cobra provides additional convenience features that make complex CLI development significantly easier. Created by spf13—a Go project member and the author of Hugo—Cobra has been adopted by many prominent Go projects including GitHub CLI and Docker CLI.

Source repository: https://github.com/spf13/cobra, as of February 2024: 35.3K stars

Feature Overview

Cobra offers numerous built-in capabilities:

  • Generate subcommands quickly using cobra add cmdname
  • Support for global, local, and cascading flags
  • Automatic help message generation for commands and flags
  • Automatic recognition of -h and --help flags
  • Customizable help text and usage information
  • Seamless integration with Viper for configuration management

Core Concepts

A well-designed Cobra-based application follows a natural sentence-like pattern. The structure consists of three components:

# Without subcommands
`app cmd --param=value`:
# With subcommands
`app cmd subCmd --param=value`

Where app represents the compiled binary name, cmd is the command, subCmd is a subcommand, and --param is a flag.

Installation

Install Cobra using Go modules:

$ go get -u github.com/spf13/cobra/cobra

Quick Start

This section demonstrates creating a CLI application with a root command and a server subcommand that launches an HTTP service.

Creating the Root Command

package cmd

import "github.com/spf13/cobra"

var rootCmd = &cobra.Command{
	Use:   "app",
	Short: "A brief description of the application",
	Long: `Building CLI applications with Cobra,
app: represents the compiled binary name`,
}

func init() {
	rootCmd.PersistentFlags().String("version", "", "Application version")
}

func Execute() {
	cobra.CheckErr(rootCmd.Execute())
}

Creating a Subcommand

package cmd

import (
	"log"
	"net/http"
	"github.com/spf13/cobra"
)

var (
	// Port number to listen on
	port string

	serverCmd = &cobra.Command{
		Use:   "server",
		Short: "Start HTTP server, usage: app server --port=?",
		Run: func(cmd *cobra.Command, args []string) {
			if port == "" {
				log.Fatal("port flag is required")
			}
			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				w.Write([]byte("Server running"))
			})
			if err := http.ListenAndServe(":"+port, handler); err != nil {
				log.Fatalf("Server failed to start: %s", err)
			}
		},
	}
)

func init() {
	// Register server command under root
	rootCmd.AddCommand(serverCmd)
	// Bind port flag to server command
	serverCmd.Flags().StringVar(&port, "port", "", "Server port number")
}

Building and Running

(1) Compilation

# Compile (output filename: app)
$ go build -o app .

(2) Running without parameters

$ ./app
Building CLI applications with Cobra,
app: represents the compiled binary name

Usage:
app [command]

Available Commands:
  server      Start HTTP server, usage: app server --port=?
  completion  Generate shell completion script
  help        Help about any command

Flags:
  -h, --help           help for app
      --version string  Application version

Use "app [command] --help" for more information.

(3) Viewing subcommand help

$ ./app server -h
Start HTTP server, usage: app server --port=?


Usage:
  app server [flags]


Flags:
  -h, --help          help for server
      --port string    Server port number

Global Flags:
      --version string  Application version

(4) Executing the subcommand

# Without required parameter
$ ./app server
2024/03/31 20:47:50 port flag is required

# With parameter
$ ./app server --port=8080
Server running on :8080

Nested Subcommands

Cobra supports hierarchical command structures with parent and child commands.

Creating Nested Commands

package cmd

import (
	"fmt"
	"github.com/spf13/cobra"
)

var (
	name string

	// userCmd is the parent command
	userCmd = &cobra.Command{
		Use:   "user",
		Short: "User management operations",
	}

	// addUserCmd adds a new user
	addUserCmd = &cobra.Command{
		Use:   "add",
		Short: "Add user: user add --name=?",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("Added user:", name)
		},
	}

	// deleteUserCmd removes a user
	deleteUserCmd = &cobra.Command{
		Use:   "del",
		Short: "Delete user: user del --name=?",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("Deleted user:", name)
		},
	}
)

func init() {
	rootCmd.AddCommand(userCmd)
	userCmd.AddCommand(addUserCmd)
	userCmd.AddCommand(deleteUserCmd)

	// Bind name flag to parent command (available to all subcommands)
	userCmd.PersistentFlags().StringVarP(&name, "name", "n", "", "Username")
}

Testing Nested Commands

$ ./app user -h
User management operations

Usage:
app user [command]

Available Commands:
  add    Add user: user add --name=?
  del    Delete user: user del --name=?

Flags:
-h, --help          help for user
-n, --name string         Username

Global Flags:
    --version string  Application version
$ ./app user add -n john
Added user: john
$ ./app user del --name john
Deleted user: john

Flags

Cobra provides two types of flags:

  • Local Flags: Available only to the command they are attached to
  • Persistent Flags: Available to the command they're attached to and all its subcommands

Flag Example

package cmd

import (
	"fmt"
	"github.com/spf13/cobra"
)

var (
	name string
	userList []string

	userCmd = &cobra.Command{
		Use:   "user",
		Short: "User management",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("User list:", userList)
		},
	}

	addUserCmd = &cobra.Command{
		Use:   "add",
		Short: "Add user: user add --name=?",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("Adding user:", name)
		},
	}
)

func init() {
	rootCmd.AddCommand(userCmd)
	userCmd.AddCommand(addUserCmd)

	// Persistent flag - available to user and all subcommands
	userCmd.PersistentFlags().StringVarP(&name, "name", "n", "", "Username")

	// Local flag - available only to user command
	userCmd.Flags().StringSliceVarP(&userList, "list", "l", []string{}, "User list")
}

Testing Flags

$ ./app user --list "alice,bob"
User list: [alice bob]
$ ./app user add --list "alice,bob"
Error: unknown flag: --list

This demonstrates that local flags work only on the command they're defined for, while persistent flags cascade to subcommands.

Argument Validation

Cobra provides built-in validators for positional arguments:

  • NoArgs: Reports error if any positional arguments are provided
  • MinimumNArgs(n): Requires atleast n positional arguments
  • MaximumNArgs(n): Allows at most n positional arguments
  • ExactArgs(n): Requires exactly n positional arguments
  • ArbitraryArgs: Accepts any number of positional arguments
  • RangeArgs(min, max): Accepts betwean min and max positional arguments

Built-in Validator Example

var (
	addUserCmd = &cobra.Command{
		Use:   "add",
		Short: "Add user",
		Args:  cobra.RangeArgs(1, 3),
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("Positional arguments:", args)
		},
	}
)
$ ./app user add 1 2 3
Positional arguments: [1 2 3]
$ ./app user add 1 2 3 4
Error: accepts between 1 and 3 arg(s), received 4

Custom Validator Example

var (
	deleteUserCmd = &cobra.Command{
		Use:   "del",
		Short: "Delete user",
		Args: func(cmd *cobra.Command, args []string) error {
			if len(args) != 1 {
				return errors.New("exactly one argument required")
			}
			count := utf8.RuneCountInString(args[0])
			if count > 10 {
				return errors.New("username too long (max 10 characters)")
			}
			return nil
		},
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("Deleting user:", args[0])
		},
	}
)

Flag Validation

To make a flag required, use MarkFlagRequired:

func init() {
	userCmd.AddCommand(addUserCmd)
	rootCmd.AddCommand(userCmd)


	addUserCmd.Flags().StringVar(&name, "name", "", "Username")

	
	err := addUserCmd.MarkFlagRequired("name")
	if err != nil {
		fmt.Println("name flag is required")
	}
}
$ ./app user add
Error: required flag(s) "name" not set
$ ./app user add --name=john
Adding user: john

Integration with Viper

Viper provides complete configuration management for Cobra applications. For detailed Viper usage, refer to the Viper documentation.

Directory Structure

├── app
│   └── config
│       ├── app.go
│       └── app.yaml
├── cmd
│   ├── root.go
│   └── server.go
├── go.mod
├── go.sum
├── local.yaml
└── main.go

Configuration Initialization

package cmd

import (
	"fmt"
	"os"
	"github.com/lgc202/go-example/cobra/demo05/app/config"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

var (
	cfgFile   string
	appConfig *config.AppConfig

	rootCmd = &cobra.Command{
		Use:   "",
		Short: "CLI application description",
		Long:  `Building CLI with Cobra, app is the compiled binary name`,
	}
)

func initConfig() {
	if cfgFile != "" {
		viper.SetConfigFile(cfgFile)
	} else {
		// Add config search paths
		viper.AddConfigPath(".")
		viper.AddConfigPath("./config")
		viper.AddConfigPath("./app/config")
		viper.SetConfigType("yaml")
		viper.SetConfigName("app")
	}

	viper.AutomaticEnv()


	if err := viper.ReadInConfig(); err != nil {
		fmt.Printf("Failed to read config: %v\n", err)
	}

	err := viper.Unmarshal(&appConfig)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fmt.Printf("%+v\n", appConfig)
}

func init() {
	cobra.OnInitialize(initConfig)
	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default: ./app.yaml)")
}

func Execute() {
	cobra.CheckErr(rootCmd.Execute())
}

Using Configuration

package cmd

import (
	"log"
	"net/http"
	"github.com/spf13/cobra"
)

var (
	serverCmd = &cobra.Command{
		Use:   "server",
		Short: "Start HTTP server",
		Run: func(cmd *cobra.Command, args []string) {
			if appConfig.App.Port == "" {
				log.Fatal("port configuration is missing")
			}
			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				w.Write([]byte("Server running"))
			})
			if err := http.ListenAndServe(":"+appConfig.App.Port, handler); err != nil {
				log.Fatalf("Server error: %s", err)
			}
		},
	}
)

func init() {
	rootCmd.AddCommand(serverCmd)
}

Configuration Struct

package config

type AppConfig struct {
	App   App   `yaml:"app"`
	MySql MySQL `yaml:"mysql"`
}


type App struct {
	Version string `yaml:"version"`
	Author  string `yaml:"author"`
	Port    string `yaml:"port"`
}

type MySQL struct {
	Host     string `yaml:"host"`
	Database string `yaml:"database"`
	User     string `yaml:"user"`
	Password string `yaml:"password"`
}

Default Configuration (app.yaml)

app:
  version: v1.0.0
  author: developer
  port: 8080
mysql:
  host: 127.0.0.1
  database: test
  user: root
  password: root

Local Configuration Override (local.yaml)

app:
  version: v1.0.2
  author: developer
  port: 8081
mysql:
  host: 192.168.0.10
  database: test
  user: root
  password: root

Building and Testing

# Compile
$ go build -o cli .


# Run with default config
$ ./cli server
&{App:{Version:v1.0.0 Author:developer Port:8080} MySql:{Host:127.0.0.1 Database:test User:root Password:root}}
Server running on :8080

# Run with custom config
$ ./cli server --config=./local.yaml
&{App:{Version:v1.0.2 Author:developer Port:8081} MySql:{Host:192.168.0.10 Database:test User:root Password:root}}
Server running on :8081

Tags: Go Cobra CLI Command Line Viper

Posted on Wed, 07 Oct 2026 16:09:55 +0000 by RedRasper