CLI Configuration with Cobra & Viper
Build flexible, hierarchical configuration systems for CLI applications using Cobra (commands/flags) and Viper (config management).
Your Role: Configuration Architect
You design configuration systems with proper precedence and flexibility. You:
✅ Implement config hierarchy - Flags > Env > Config > Defaults ✅ Bind flags to Viper - Seamless integration ✅ Support multiple formats - YAML, JSON, TOML ✅ Handle environment variables - With prefixes ✅ Provide config commands - init, show, validate ✅ Follow CLY patterns - Use project structure
❌ Do NOT hardcode paths - Use conventions ❌ Do NOT skip validation - Validate config ❌ Do NOT ignore precedence - Follow hierarchy
Configuration Precedence
Viper uses this precedence order (highest to lowest):
-
Explicit viper.Set() calls
-
Command-line flags
-
Environment variables
-
Config file values
-
Defaults
viper.SetDefault("port", 8080) // 5. Default // config.yaml: port: 8081 // 4. Config file os.Setenv("APP_PORT", "8082") // 3. Environment cobra.Flags().Int("port", 0, "Port") // 2. Flag viper.Set("port", 8083) // 1. Explicit set
Basic Setup
Initialize Viper
package config
import ( "fmt" "os"
"github.com/spf13/viper"
)
func Init() error { // Set config name (no extension) viper.SetConfigName("config")
// Set config type
viper.SetConfigType("yaml")
// Add search paths
viper.AddConfigPath(".")
viper.AddConfigPath("$HOME/.myapp")
viper.AddConfigPath("/etc/myapp")
// Read config
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found; use defaults
return nil
}
return fmt.Errorf("error reading config: %w", err)
}
return nil
}
With Cobra Integration
package cmd
import ( "fmt" "os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
var rootCmd = &cobra.Command{ Use: "myapp", Short: "My application", }
func Execute() { if err := rootCmd.Execute(); err != nil { os.Exit(1) } }
func init() { cobra.OnInitialize(initConfig)
// Global flags
rootCmd.PersistentFlags().StringVar(
&cfgFile,
"config",
"",
"config file (default is $HOME/.myapp/config.yaml)",
)
}
func initConfig() { if cfgFile != "" { // Use explicit config file viper.SetConfigFile(cfgFile) } else { // Find home directory home, err := os.UserHomeDir() if err != nil { fmt.Println(err) os.Exit(1) }
// Search config in home directory and current directory
viper.AddConfigPath(home + "/.myapp")
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
viper.SetConfigName("config")
}
// Read environment variables
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// Read config file
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
Configuration Patterns
Set Defaults
func setDefaults() { // Server viper.SetDefault("server.port", 8080) viper.SetDefault("server.host", "localhost") viper.SetDefault("server.timeout", "30s")
// Database
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", 5432)
viper.SetDefault("database.name", "myapp")
// Logging
viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", "json")
}
Bind Flags
Single flag:
cmd.Flags().IntP("port", "p", 8080, "Port to run on") viper.BindPFlag("server.port", cmd.Flags().Lookup("port"))
All flags:
cmd.Flags().Int("port", 8080, "Port") cmd.Flags().String("host", "localhost", "Host")
viper.BindPFlags(cmd.Flags())
Persistent flags:
rootCmd.PersistentFlags().String("log-level", "info", "Log level") viper.BindPFlag("log.level", rootCmd.PersistentFlags().Lookup("log-level"))
Environment Variables
Auto-map all env vars:
viper.AutomaticEnv() viper.SetEnvPrefix("MYAPP")
// MYAPP_SERVER_PORT → server.port // MYAPP_DATABASE_NAME → database.name
Custom env key replacer:
import "strings"
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() viper.SetEnvPrefix("MYAPP")
// MYAPP_SERVER_PORT → server.port (. → _)
Bind specific env var:
viper.BindEnv("database.password", "DB_PASSWORD")
// DB_PASSWORD → database.password
Read Config Values
Get typed values:
port := viper.GetInt("server.port") host := viper.GetString("server.host") enabled := viper.GetBool("feature.enabled") timeout := viper.GetDuration("server.timeout") tags := viper.GetStringSlice("tags")
Check if set:
if viper.IsSet("server.port") { port := viper.GetInt("server.port") }
Get with default:
port := viper.GetInt("server.port") if port == 0 { port = 8080 }
Unmarshal to Struct
Full config:
type Config struct {
Server ServerConfig mapstructure:"server"
Database DatabaseConfig mapstructure:"database"
Log LogConfig mapstructure:"log"
}
type ServerConfig struct {
Port int mapstructure:"port"
Host string mapstructure:"host"
Timeout string mapstructure:"timeout"
}
var config Config
if err := viper.Unmarshal(&config); err != nil { return fmt.Errorf("unable to decode config: %w", err) }
Subsection:
var serverConfig ServerConfig
if err := viper.UnmarshalKey("server", &serverConfig); err != nil { return fmt.Errorf("unable to decode server config: %w", err) }
Write Config
Create default config:
func createDefaultConfig(path string) error { viper.SetDefault("server.port", 8080) viper.SetDefault("server.host", "localhost")
return viper.WriteConfigAs(path)
}
Save current config:
viper.Set("server.port", 9090)
// Write to current config file viper.WriteConfig()
// Write to specific file viper.WriteConfigAs("/path/to/config.yaml")
// Safe write (won't overwrite) viper.SafeWriteConfig()
CLY Project Pattern
Config Package
pkg/config/config.go:
package config
import ( "fmt" "os" "path/filepath"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig mapstructure:"server"
Log LogConfig mapstructure:"log"
}
type ServerConfig struct {
Port int mapstructure:"port"
Host string mapstructure:"host"
}
type LogConfig struct {
Level string mapstructure:"level"
Format string mapstructure:"format"
}
var cfg *Config
// Init initializes the configuration func Init(cfgFile string) error { if cfgFile != "" { viper.SetConfigFile(cfgFile) } else { home, err := os.UserHomeDir() if err != nil { return err }
viper.AddConfigPath(filepath.Join(home, ".cly"))
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
viper.SetConfigName("config")
}
setDefaults()
viper.AutomaticEnv()
viper.SetEnvPrefix("CLY")
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return err
}
}
cfg = &Config{}
if err := viper.Unmarshal(cfg); err != nil {
return fmt.Errorf("unable to decode config: %w", err)
}
return nil
}
func setDefaults() { viper.SetDefault("server.port", 8080) viper.SetDefault("server.host", "localhost") viper.SetDefault("log.level", "info") viper.SetDefault("log.format", "text") }
// Get returns the current config func Get() *Config { return cfg }
// GetString returns a config value as string func GetString(key string) string { return viper.GetString(key) }
// GetInt returns a config value as int func GetInt(key string) int { return viper.GetInt(key) }
// GetBool returns a config value as bool func GetBool(key string) bool { return viper.GetBool(key) }
Root Command Integration
cmd/root.go:
package cmd
import ( "fmt" "os"
"github.com/spf13/cobra"
"github.com/yurifrl/cly/pkg/config"
)
var cfgFile string
var RootCmd = &cobra.Command{ Use: "cly", Short: "CLY - Command Line Yuri", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return config.Init(cfgFile) }, }
func Execute() { if err := RootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }
func init() { RootCmd.PersistentFlags().StringVar( &cfgFile, "config", "", "config file (default is $HOME/.cly/config.yaml)", ) }
Config Command
modules/config/cmd.go:
package configcmd
import ( "fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "config", Short: "Manage configuration", }
cmd.AddCommand(
initCmd(),
showCmd(),
validateCmd(),
)
parent.AddCommand(cmd)
}
func initCmd() *cobra.Command { return &cobra.Command{ Use: "init", Short: "Initialize config file", RunE: func(cmd *cobra.Command, args []string) error { path, _ := cmd.Flags().GetString("path") if path == "" { path = "$HOME/.cly/config.yaml" }
if err := viper.SafeWriteConfigAs(path); err != nil {
return fmt.Errorf("failed to create config: %w", err)
}
fmt.Printf("Config created at: %s\n", path)
return nil
},
}
}
func showCmd() *cobra.Command { return &cobra.Command{ Use: "show", Short: "Show current configuration", RunE: func(cmd *cobra.Command, args []string) error { fmt.Println("Current configuration:") fmt.Println("Config file:", viper.ConfigFileUsed()) fmt.Println()
for _, key := range viper.AllKeys() {
fmt.Printf("%s: %v\n", key, viper.Get(key))
}
return nil
},
}
}
func validateCmd() *cobra.Command { return &cobra.Command{ Use: "validate", Short: "Validate configuration", RunE: func(cmd *cobra.Command, args []string) error { // Add validation logic fmt.Println("Configuration is valid") return nil }, } }
Advanced Patterns
Remote Config (etcd, Consul)
import _ "github.com/spf13/viper/remote"
func initRemoteConfig() error { viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/myapp.json") viper.SetConfigType("json")
if err := viper.ReadRemoteConfig(); err != nil {
return err
}
return nil
}
// Watch for changes func watchRemoteConfig() { go func() { for { time.Sleep(time.Second * 5) err := viper.WatchRemoteConfig() if err != nil { log.Printf("unable to read remote config: %v", err) continue } } }() }
Watch Config File
viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { fmt.Println("Config file changed:", e.Name)
// Reload config
var newConfig Config
if err := viper.Unmarshal(&newConfig); err != nil {
log.Printf("error reloading config: %v", err)
return
}
// Update application state
updateAppConfig(newConfig)
})
Multiple Config Instances
// Default instance viper.SetConfigName("config") viper.ReadInConfig()
// Custom instance v := viper.New() v.SetConfigName("other-config") v.AddConfigPath(".") v.ReadInConfig()
port := v.GetInt("port")
Config with Validation
type Config struct {
Server ServerConfig mapstructure:"server" validate:"required"
DB DBConfig mapstructure:"database" validate:"required"
}
type ServerConfig struct {
Port int mapstructure:"port" validate:"required,min=1,max=65535"
Host string mapstructure:"host" validate:"required,hostname"
}
func Load() (*Config, error) { var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, err
}
// Validate
validate := validator.New()
if err := validate.Struct(cfg); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
return &cfg, nil
}
Nested Config Keys
// Dot notation viper.Set("server.database.host", "localhost")
// Nested maps viper.Set("server", map[string]interface{}{ "database": map[string]interface{}{ "host": "localhost", "port": 5432, }, })
// Access nested host := viper.GetString("server.database.host")
// Get sub-tree dbConfig := viper.Sub("server.database") if dbConfig != nil { host := dbConfig.GetString("host") }
Config File Formats
YAML
config.yaml:
server: port: 8080 host: localhost timeout: 30s
database: host: localhost port: 5432 name: myapp user: postgres password: secret
log: level: info format: json output: stdout
features: enabled: - feature1 - feature2
JSON
config.json:
{ "server": { "port": 8080, "host": "localhost" }, "database": { "host": "localhost", "port": 5432 } }
TOML
config.toml:
[server] port = 8080 host = "localhost"
[database] host = "localhost" port = 5432 name = "myapp"
Best Practices
- Always Set Defaults
func init() { viper.SetDefault("server.port", 8080) viper.SetDefault("log.level", "info") }
- Use Environment Variables
viper.AutomaticEnv() viper.SetEnvPrefix("MYAPP")
// Now MYAPP_SERVER_PORT overrides config
- Validate Config
type Config struct {
Port int validate:"required,min=1,max=65535"
}
if err := validate.Struct(cfg); err != nil { return err }
- Provide Config Commands
myapp config init # Create default config myapp config show # Show current config myapp config validate # Validate config
- Handle Missing Config Gracefully
if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { // Config not found, use defaults log.Println("No config file found, using defaults") } else { return err } }
- Don't Store Secrets in Config
// ❌ BAD database: password: "mysecret"
// ✅ GOOD - Use env vars database: password: ${DB_PASSWORD}
// Or viper.BindEnv("database.password", "DB_PASSWORD")
- Use Struct Tags
type ServerConfig struct {
Port int mapstructure:"port" json:"port" yaml:"port"
Host string mapstructure:"host" json:"host" yaml:"host"
Timeout string mapstructure:"timeout" json:"timeout" yaml:"timeout"
}
Common Patterns
Config Init Command
func initConfigCmd() *cobra.Command { var force bool
cmd := &cobra.Command{
Use: "init",
Short: "Initialize configuration",
RunE: func(cmd *cobra.Command, args []string) error {
configPath := viper.ConfigFileUsed()
if configPath == "" {
configPath = filepath.Join(os.Getenv("HOME"), ".myapp", "config.yaml")
}
// Check if exists
if _, err := os.Stat(configPath); err == nil && !force {
return fmt.Errorf("config already exists: %s (use --force to overwrite)", configPath)
}
// Create directory
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
return err
}
// Write config
if err := viper.WriteConfigAs(configPath); err != nil {
return err
}
fmt.Printf("Config initialized: %s\n", configPath)
return nil
},
}
cmd.Flags().BoolVar(&force, "force", false, "Overwrite existing config")
return cmd
}
Config Migration
func migrateConfig() error { version := viper.GetInt("version")
switch version {
case 0:
// Migrate from v0 to v1
viper.Set("new_field", "default")
viper.Set("version", 1)
fallthrough
case 1:
// Migrate from v1 to v2
viper.Set("another_field", true)
viper.Set("version", 2)
}
return viper.WriteConfig()
}
Testing
func TestConfig(t *testing.T) { // Use separate viper instance v := viper.New() v.SetConfigType("yaml")
var yamlConfig = []byte(`
server: port: 8080 host: localhost `)
v.ReadConfig(bytes.NewBuffer(yamlConfig))
assert.Equal(t, 8080, v.GetInt("server.port"))
assert.Equal(t, "localhost", v.GetString("server.host"))
}
Checklist
-
Defaults set for all config values
-
Config file search paths defined
-
Environment variable support
-
Flags bound to config
-
Config struct with mapstructure tags
-
Config validation
-
Config commands (init, show, validate)
-
Error handling for missing config
-
Secrets via env vars only
-
Config file format documented
Resources
-
Viper Documentation
-
Cobra User Guide
-
12-Factor Config
-
CLY config: pkg/config/ , modules/config/