| name | cli-config |
| description | Manage CLI application configuration with Cobra and Viper. Use when implementing config files, environment variables, flags binding, or when user mentions Viper, configuration management, config files, or CLI settings. |
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
1. Always Set Defaults
func init() {
viper.SetDefault("server.port", 8080)
viper.SetDefault("log.level", "info")
}
2. Use Environment Variables
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// Now MYAPP_SERVER_PORT overrides config
3. Validate Config
type Config struct {
Port int `validate:"required,min=1,max=65535"`
}
if err := validate.Struct(cfg); err != nil {
return err
}
4. Provide Config Commands
myapp config init # Create default config
myapp config show # Show current config
myapp config validate # Validate config
5. 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
}
}
6. 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")
7. 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/