cli-config

CLI Configuration with Cobra & Viper

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "cli-config" with this command: npx skills add yurifrl/cly/yurifrl-cly-cli-config

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") }

  1. Use Environment Variables

viper.AutomaticEnv() viper.SetEnvPrefix("MYAPP")

// Now MYAPP_SERVER_PORT overrides config

  1. Validate Config

type Config struct { Port int validate:"required,min=1,max=65535" }

if err := validate.Struct(cfg); err != nil { return err }

  1. Provide Config Commands

myapp config init # Create default config myapp config show # Show current config myapp config validate # Validate config

  1. 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 } }

  1. 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")

  1. 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/

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

charm-stack

No summary provided by upstream source.

Repository SourceNeeds Review
General

cobra-modularity

No summary provided by upstream source.

Repository SourceNeeds Review
General

add-module

No summary provided by upstream source.

Repository SourceNeeds Review