cli-config
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.
$ インストール
git clone https://github.com/majiayu000/claude-skill-registry /tmp/claude-skill-registry && cp -r /tmp/claude-skill-registry/skills/development/cli-config ~/.claude/skills/claude-skill-registry// tip: Run this command in your terminal to install the skill
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/
Repository
