Cobra and Viper are the industry-standard libraries for building production-quality CLIs in Go. Cobra provides command structure and argument parsing, while Viper manages configuration from multiple sources with clear precedence rules.
Building DevOps automation tools or deployment scripts
Cobra Framework
Command Structure Pattern
Cobra follows the APPNAME VERB NOUN --FLAG pattern popularized by git and kubectl.
// cmd/root.gopackage cmd
import (
"fmt""os""github.com/spf13/cobra""github.com/spf13/viper"
)
var cfgFile stringvar rootCmd = &cobra.Command{
Use: "myapp",
Short: "A powerful CLI tool for developers",
Long: `MyApp is a CLI tool that demonstrates best practices
for building production-quality command-line applications.
Complete documentation is available at https://myapp.example.com`,
}
func {
err := rootCmd.Execute(); err != {
fmt.Fprintln(os.Stderr, err)
os.Exit()
}
}
{
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, , , )
rootCmd.PersistentFlags().Bool(, , )
viper.BindPFlag(, rootCmd.PersistentFlags().Lookup())
viper.BindPFlag(, rootCmd.PersistentFlags().Lookup())
}
{
cfgFile != {
viper.SetConfigFile(cfgFile)
} {
home, err := os.UserHomeDir()
err != {
fmt.Fprintln(os.Stderr, err)
os.Exit()
}
viper.AddConfigPath(home)
viper.AddConfigPath()
viper.SetConfigType()
viper.SetConfigName()
}
viper.SetEnvPrefix()
viper.AutomaticEnv()
err := viper.ReadInConfig(); err == {
viper.GetBool() {
fmt.Println(, viper.ConfigFileUsed())
}
}
}
Execute
()
if
nil
1
funcinit()
// Persistent flags (available to all subcommands)
"config"
""
"config file (default is $HOME/.myapp.yaml)"
"verbose"
false
"verbose output"
// Bind persistent flags to viper
"config"
"config"
"verbose"
"verbose"
funcinitConfig()
if
""
else
if
nil
1
"."
"yaml"
".myapp"
"MYAPP"
if
nil
if
"verbose"
"Using config file:"
Subcommands with Arguments
// cmd/deploy.gopackage cmd
import (
"fmt""github.com/spf13/cobra""github.com/spf13/viper"
)
var deployCmd = &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy application to specified environment",
Long: `Deploy the application to the specified environment.
Supports: dev, staging, production`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{"dev", "staging", "production"},
PreRunE: func(cmd *cobra.Command, args []string)error {
// Validation logic runs before RunE
env := args[0]
if env == "production" && !viper.GetBool("force") {
return fmt.Errorf("production deploys require --force flag")
}
returnnil
},
RunE: func(cmd *cobra.Command, args []string)error {
env := args[0]
region := viper.GetString("region")
force := viper.GetBool("force")
fmt.Printf("Deploying to %s in region %s (force=%v)\n", env, region, force)
// Actual deployment logicreturn deploy(env, region, force)
},
PostRunE: func(cmd *cobra.Command, args []string)error {
// Cleanup or notifications
fmt.Println("Deployment complete")
returnnil
},
}
funcinit() {
rootCmd.AddCommand(deployCmd)
// Local flags (only for this command)
deployCmd.Flags().StringP("region", "r", "us-east-1", "AWS region")
deployCmd.Flags().BoolP("force", "f", false, "Force deployment without confirmation")
// Bind flags to viper
viper.BindPFlag("region", deployCmd.Flags().Lookup("region"))
viper.BindPFlag("force", deployCmd.Flags().Lookup("force"))
}
funcdeploy(env, region string, force bool)error {
// Implementationreturnnil
}
Persistent vs. Local Flags
// Persistent flags: Available to command and all subcommands
rootCmd.PersistentFlags().String("config", "", "config file path")
rootCmd.PersistentFlags().Bool("verbose", false, "verbose output")
// Local flags: Only available to this specific command
deployCmd.Flags().String("region", "us-east-1", "deployment region")
deployCmd.Flags().Bool("force", false, "force deployment")
// Required flags
deployCmd.MarkFlagRequired("region")
// Flag dependencies
deployCmd.MarkFlagsRequiredTogether("username", "password")
deployCmd.MarkFlagsMutuallyExclusive("json", "yaml")
PreRun/PostRun Hooks
Cobra provides execution hooks for setup and cleanup:
var serverCmd = &cobra.Command{
Use: "server",
Short: "Start API server",
// Execution order (all optional):
PersistentPreRunE: func(cmd *cobra.Command, args []string)error {
// Runs before PreRunE, inherited by subcommandsreturn setupLogging()
},
PreRunE: func(cmd *cobra.Command, args []string)error {
// Validation and setup before RunEreturn validateConfig()
},
RunE: func(cmd *cobra.Command, args []string)error {
// Main command logicreturn startServer()
},
PostRunE: func(cmd *cobra.Command, args []string)error {
// Cleanup after RunEreturn cleanup()
},
PersistentPostRunE: func(cmd *cobra.Command, args []string)error {
// Runs after PostRunE, inherited by subcommandsreturn flushLogs()
},
}
Important: Use RunE, PreRunE, PostRunE (error-returning versions) instead of Run, PreRun, PostRun.
Viper Configuration Management
Configuration Priority
Viper follows a strict precedence order (highest to lowest):
Explicit Set (viper.Set("key", value))
Command-line Flags (bound with viper.BindPFlag)
Environment Variables (MYAPP_KEY=value)
Config File (~/.myapp.yaml, ./config.yaml)
Key/Value Store (etcd, Consul - optional)
Defaults (viper.SetDefault("key", value))
funcinitConfig() {
// 1. Set defaults (lowest priority)
viper.SetDefault("port", 8080)
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", 5432)
// 2. Config file locations (checked in order)
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("/etc/myapp/")
viper.AddConfigPath("$HOME/.myapp")
viper.AddConfigPath(".")
// 3. Environment variables (prefix + automatic mapping)
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv() // MYAPP_PORT, MYAPP_DATABASE_HOST, etc.// 4. Read config file (optional)if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found - use defaults and env vars
} else {
// Config file found but error reading itreturn err
}
}
// 5. Flags will be bound in init() functions (highest priority)
}
Environment Variable Mapping
Viper automatically maps environment variables with prefix and dot notation:
rootCmd.Flags().String("region", "us-east-1", "AWS region")
// Flag not bound to Viper - won't respect precedence!funcdeploy() {
region := viper.GetString("region") // Always returns config file value
}
package main
import"myapp/cmd"funcmain() {
cmd.Execute()
}
cmd/root.go: See "Command Structure Pattern" section above
Building and installing:
# Development
go run main.go deploy staging --region us-west-2
# Production build
go build -o myapp
# Install globally
go install
# Enable shell completion
myapp completion bash > /etc/bash_completion.d/myapp