Building CLIs with Cobra
Reviewed & published by Brayan K
Cobra is the library behind many famous Go command-line tools — kubectl, hugo, and the GitHub CLI. You'll build a root command, add subcommands, wire up flags (persistent and local) with pflag, validate arguments, and run it all with Execute() — plus a look at Viper for configuration.
Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Define a root cobra.Command with Use, Short, and Run
- Add subcommands with AddCommand to form a tree
- Choose between Run and RunE for error handling
- Declare persistent vs local flags with pflag
- Validate positional args with cobra.ExactArgs and friends
- Start the app with Execute(); pair Cobra with Viper for config
1️⃣ The root command and Execute()
Every Cobra app has a root command: a &cobra.Command with a Use name, help text, and a Run function. In main you call rootCmd.Execute(), which parses the arguments and dispatches to the right command.
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
// Every Cobra app starts with a ROOT command. Use is the command name
// shown in usage; Short/Long are help text; Run is what it does.
var rootCmd = &cobra.Command{
Use: "app",
Short: "app is a demo CLI built with Cobra",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello from app! Try: app --help")
},
}
func main() {
// Execute parses os.Args, picks the matching command, and runs it.
// It returns an error, so we exit non-zero on failure.
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}2️⃣ Subcommands and AddCommand
Real tools have subcommands like app version or app greet. Each is its own *cobra.Command attached with rootCmd.AddCommand. Use RunE to return errors and Args validators like cobra.ExactArgs(1) to check positional arguments.
package main
import (
"fmt"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{Use: "app", Short: "a demo CLI"}
// A SUBCOMMAND is just another *cobra.Command attached to the root.
var versionCmd = &cobra.Command{
Use: "version",
Short: "print the version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("app v1.0.0")
},
}
// RunE returns an error so Cobra can report it and set the exit code.
var greetCmd = &cobra.Command{
Use: "greet [name]",
Short: "greet someone",
Args: cobra.ExactArgs(1), // require exactly one positional argument
RunE: func(cmd *cobra.Command, args []string) error {
if args[0] == "" {
return fmt.Errorf("name must not be empty")
}
fmt.Println("Hello,", args[0])
return nil
},
}
func init() {
// Build the command tree: app version, app greet
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(greetCmd)
}3️⃣ Flags: persistent vs local (pflag)
Flags come from the spf13/pflag library (POSIX-style --long and -s short forms). A persistent flag on the root is inherited by every subcommand; a local flag belongs to one command only.
package main
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
var (
verbose bool // bound to a PERSISTENT flag (all commands)
upper bool // bound to a LOCAL flag (only this command)
)
var rootCmd = &cobra.Command{Use: "app"}
var echoCmd = &cobra.Command{
Use: "echo [text]",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
msg := strings.Join(args, " ")
if upper {
msg = strings.ToUpper(msg)
}
if verbose {
fmt.Println("[verbose] echoing:", msg)
}
fmt.Println(msg)
},
}
func init() {
// PERSISTENT flag: inherited by every subcommand. -v / --verbose.
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
// LOCAL flag: only valid on echo. -u / --upper.
echoCmd.Flags().BoolVarP(&upper, "upper", "u", false, "uppercase the text")
rootCmd.AddCommand(echoCmd)
}4️⃣ Configuration with Viper (a quick note)
Cobra's sibling spf13/viper handles configuration: it merges flags, environment variables, and config files into one set of values. Bind a flag with viper.BindPFlag and the same setting can come from --port, an env var, or a file.
package main
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var rootCmd = &cobra.Command{
Use: "app",
Run: func(cmd *cobra.Command, args []string) {
// Viper is Cobra's sibling for CONFIG. It merges values from
// flags, environment variables, and config files (YAML/JSON/...).
fmt.Println("port:", viper.GetInt("port"))
},
}
func init() {
// Bind a flag to viper so --port, APP_PORT, and a config file all
// feed the same setting (flags usually take precedence).
rootCmd.Flags().Int("port", 8080, "server port")
viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
viper.SetEnvPrefix("app") // env var APP_PORT
viper.AutomaticEnv() // read matching env vars automatically
}
// Big CLIs like kubectl, hugo, and the GitHub CLI (gh) use this exact
// Cobra + Viper pairing: Cobra for the command tree, Viper for config.🎯 Your Turn
Add a hello subcommand. Fill in the two blanks marked ___ to require one argument and attach the command.
package main
import (
"fmt"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{Use: "app"}
// 🎯 YOUR TURN — fill in the blanks marked ___
var helloCmd = &cobra.Command{
Use: "hello [name]",
// 1) Require exactly one positional argument
Args: cobra.___(1), // 👉 ExactArgs
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hi,", args[0])
},
}
func init() {
// 2) Attach helloCmd to the root command
rootCmd.___(helloCmd) // 👉 AddCommand
}
// ✅ Expected:
// app hello Ada -> Hi, Ada
// app hello -> Error: accepts 1 arg(s), received 0Common Errors (and the fix)
❌ Forgetting rootCmd.AddCommand(child) — the subcommand never appears.
✅ Attach every subcommand to its parent (often in init()).
❌ Never calling Execute() — the program does nothing.
✅ Call rootCmd.Execute() in main and check its error.
❌ Using Flags() for a global option — subcommands can't see it.
✅ Use PersistentFlags() on the root for inherited flags.
❌ Reading args[0] without validating — panics when no arg is given.
✅ Set Args: cobra.ExactArgs(1) (or MinimumNArgs) so Cobra checks first.
🧠 Quick Recall
flag on root, seen by all subcommands = ?
A persistent flag via rootCmd.PersistentFlags(). Local flags from cmd.Flags() stay on one command.
what starts the whole CLI from main?
rootCmd.Execute(). It parses args, finds the command, runs it, and returns an error to check.
🎯 Mini-Challenge: A repeat Command
Build a repeat subcommand with a --times flag that prints the given text N times, validating that at least one argument is provided.
package main
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
// 🎯 MINI-CHALLENGE: build a "repeat" subcommand.
// 1. Create var repeatCmd = &cobra.Command{Use: "repeat [text]", ...}.
// 2. Require at least one arg with cobra.MinimumNArgs(1).
// 3. Add a LOCAL int flag --times (default 1) bound to a count var,
// using cmd.Flags().IntVar.
// 4. In Run, print the joined args that many times.
// 5. Attach it with rootCmd.AddCommand(repeatCmd) in init().
//
// ✅ Expected:
// app repeat --times 3 hi -> hi (printed three times)
var rootCmd = &cobra.Command{Use: "app"}
func main() {
_ = strings.Join
rootCmd.Execute()
}🎉 Lesson Complete!
- ✅ Every CLI starts with a root &cobra.Command with Use and Run
- ✅ AddCommand builds a tree of subcommands like app version
- ✅ Prefer RunE so Cobra reports errors and sets the exit code
- ✅ PersistentFlags are inherited; Flags are local (both via pflag)
- ✅ Execute() runs everything; pair with Viper for config (as kubectl and hugo do)
- ✅ You've finished the Go course — you can now build, test, and ship production Go services and tools.
Practice quiz
In Cobra, what does a single command map to?
- A &cobra.Command value with Use, Short, and a Run/RunE function
- A plain func main
- A JSON config file
- An HTTP route
Answer: A &cobra.Command value with Use, Short, and a Run/RunE function. Each command is a &cobra.Command; you set Use (its name and usage), Short/Long help, and a Run or RunE function.
How do you attach a subcommand to the root command?
- rootCmd.Add(cmd)
- rootCmd.Child(cmd)
- rootCmd.AddCommand(cmd)
- rootCmd.Sub(cmd)
Answer: rootCmd.AddCommand(cmd). rootCmd.AddCommand(childCmd) builds the command tree, e.g. app serve or app version.
What is the difference between Run and RunE?
- RunE runs faster
- RunE returns an error so Cobra can report it and set the exit code
- Run is only for the root command
- There is no difference
Answer: RunE returns an error so Cobra can report it and set the exit code. RunE has signature func(cmd, args) error; returning an error lets Cobra print it and exit non-zero, which is preferred.
Which function actually starts a Cobra program from main?
- rootCmd.Main()
- rootCmd.Run()
- rootCmd.Start()
- rootCmd.Execute()
Answer: rootCmd.Execute(). You call rootCmd.Execute() in main; it parses os.Args, finds the right command, and runs it.
A PERSISTENT flag defined on the root command is...
- available to the root command and all of its subcommands
- ignored unless re-declared
- the same as a positional argument
- available only on the root command
Answer: available to the root command and all of its subcommands. PersistentFlags are inherited by every child command; local Flags apply only to the command that defines them.
Which underlying library provides Cobra's flag parsing?
- viper
- spf13/pflag (POSIX-style flags)
- the standard flag package
- urfave/cli
Answer: spf13/pflag (POSIX-style flags). Cobra uses spf13/pflag, which supports POSIX double-dash long flags and single-dash shorthands.
How do you require exactly one positional argument?
- MinArgs: 1
- Args: "one"
- set Run to nil
- Args: cobra.ExactArgs(1)
Answer: Args: cobra.ExactArgs(1). Cobra ships validators like cobra.ExactArgs(n), cobra.MinimumNArgs(n), and cobra.NoArgs to check args before Run.
Tools like kubectl, hugo, and the GitHub CLI are well known for...
- avoiding any CLI framework
- being written in Python
- being built with Cobra's command/subcommand model
- using only environment variables
Answer: being built with Cobra's command/subcommand model. Cobra powers many large CLIs (kubectl, hugo, gh), which is why its subcommand structure feels familiar.
What is Viper commonly used for alongside Cobra?
- Rendering HTML
- Configuration: reading flags, env vars, and config files together
- Running tests
- Database migrations
Answer: Configuration: reading flags, env vars, and config files together. Viper is Cobra's sibling library for configuration, merging flags, environment variables, and files like YAML or JSON.
How do you define a local string flag named output with default "text"?
- cmd.Flags().StringVar / StringP for the flag
- cmd.Output("text")
- cmd.AddFlag("output")
- cmd.Set("output", "text")
Answer: cmd.Flags().StringVar / StringP for the flag. cmd.Flags().StringVar(&out, "output", "text", "...") (or StringP for a shorthand) declares a local string flag.
Continue this course
- Previous: Web APIs with the Gin Framework
- Next: Profiling & Benchmarking with pprof — Find bottlenecks with CPU, heap and goroutine profiles and go test -bench
Frequently asked questions
What is a cobra.Command and what fields matter most?
A cobra.Command represents one command in your CLI. The fields you set most often are Use (the command's name and usage string, e.g. "greet [name]"), Short and Long (one-line and detailed help), Args (a validator for positional arguments), and the action: Run or RunE. You build an app by creating a root command and attaching child commands to form a tree.
How do I add subcommands like app serve or app version?
Create a separate *cobra.Command for each, then call rootCmd.AddCommand(childCmd). That builds the command tree, so the user runs app version, app serve, and so on. Subcommands can themselves have children, which is how deep CLIs like kubectl get pods are organized. Cobra generates help and usage for the whole tree automatically.
What is the difference between Run and RunE?
Run has the signature func(cmd *cobra.Command, args []string) and returns nothing. RunE is the same but returns an error: func(...) error. Prefer RunE — when you return a non-nil error, Cobra prints it and Execute returns it so you can exit with a non-zero status. That is the idiomatic way to signal failure from a command instead of calling os.Exit yourself deep in the logic.
Persistent flags versus local flags — what is the difference?
A local flag, defined with cmd.Flags(), applies only to the command that declares it. A persistent flag, defined with cmd.PersistentFlags(), is inherited by that command and all of its subcommands. Put global options like --verbose or --config as persistent flags on the root command so every subcommand sees them; keep command-specific options local. Under the hood Cobra uses the spf13/pflag library for POSIX-style flags.
How does a Cobra program actually start?
In main you call rootCmd.Execute(). It parses os.Args, validates flags and arguments, finds the matching command in the tree, and runs its Run or RunE function. Execute returns an error, so the common pattern is: if err := rootCmd.Execute(); err != nil { os.Exit(1) }. You typically register subcommands and flags in init() functions so the tree is assembled before Execute runs.
How do Cobra and Viper work together, and who uses them?
Cobra handles the command tree, flags, and help; Viper handles configuration by merging values from flags, environment variables, and config files (YAML, JSON, TOML, and more). You bind a flag to Viper with viper.BindPFlag so a setting can come from --port, an env var like APP_PORT, or a file, with a clear precedence. Major CLIs built this way include kubectl, hugo, and the GitHub CLI (gh) — both libraries come from the same author, spf13.