Loading...
Loading...
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.
npx skill4agent add yurifrl/cly cli-configviper.Set()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 setpackage 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
}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())
}
}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")
}cmd.Flags().IntP("port", "p", 8080, "Port to run on")
viper.BindPFlag("server.port", cmd.Flags().Lookup("port"))cmd.Flags().Int("port", 8080, "Port")
cmd.Flags().String("host", "localhost", "Host")
viper.BindPFlags(cmd.Flags())rootCmd.PersistentFlags().String("log-level", "info", "Log level")
viper.BindPFlag("log.level", rootCmd.PersistentFlags().Lookup("log-level"))viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// MYAPP_SERVER_PORT → server.port
// MYAPP_DATABASE_NAME → database.nameimport "strings"
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// MYAPP_SERVER_PORT → server.port (. → _)viper.BindEnv("database.password", "DB_PASSWORD")
// DB_PASSWORD → database.passwordport := viper.GetInt("server.port")
host := viper.GetString("server.host")
enabled := viper.GetBool("feature.enabled")
timeout := viper.GetDuration("server.timeout")
tags := viper.GetStringSlice("tags")if viper.IsSet("server.port") {
port := viper.GetInt("server.port")
}port := viper.GetInt("server.port")
if port == 0 {
port = 8080
}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)
}var serverConfig ServerConfig
if err := viper.UnmarshalKey("server", &serverConfig); err != nil {
return fmt.Errorf("unable to decode server config: %w", err)
}func createDefaultConfig(path string) error {
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.host", "localhost")
return viper.WriteConfigAs(path)
}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()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)
}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)",
)
}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
},
}
}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
}
}
}()
}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)
})// Default instance
viper.SetConfigName("config")
viper.ReadInConfig()
// Custom instance
v := viper.New()
v.SetConfigName("other-config")
v.AddConfigPath(".")
v.ReadInConfig()
port := v.GetInt("port")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
}// 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")
}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{
"server": {
"port": 8080,
"host": "localhost"
},
"database": {
"host": "localhost",
"port": 5432
}
}[server]
port = 8080
host = "localhost"
[database]
host = "localhost"
port = 5432
name = "myapp"func init() {
viper.SetDefault("server.port", 8080)
viper.SetDefault("log.level", "info")
}viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
// Now MYAPP_SERVER_PORT overrides configtype Config struct {
Port int `validate:"required,min=1,max=65535"`
}
if err := validate.Struct(cfg); err != nil {
return err
}myapp config init # Create default config
myapp config show # Show current config
myapp config validate # Validate configif 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
}
}// ❌ BAD
database:
password: "mysecret"
// ✅ GOOD - Use env vars
database:
password: ${DB_PASSWORD}
// Or
viper.BindEnv("database.password", "DB_PASSWORD")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"`
}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
}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()
}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"))
}pkg/config/modules/config/