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
-hand--helpflags - 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 providedMinimumNArgs(n): Requires atleast n positional argumentsMaximumNArgs(n): Allows at most n positional argumentsExactArgs(n): Requires exactly n positional argumentsArbitraryArgs: Accepts any number of positional argumentsRangeArgs(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