Files
gh-lmorchard-lmorchard-agen…/skills/go-cli-builder/references/cobra-viper-integration.md
2025-11-30 08:37:58 +08:00

3.7 KiB

Cobra + Viper Integration Pattern

This document explains how Cobra (CLI framework) and Viper (configuration management) are integrated in the generated Go CLI projects.

Architecture Overview

The integration follows these principles:

  1. Configuration Priority (highest to lowest):

    • Command-line flags
    • Environment variables
    • Config file values
    • Default values
  2. Lazy Loading: Configuration is loaded once in PersistentPreRun, before any command executes

  3. Centralized Access: The GetConfig() and GetLogger() functions in cmd/root.go provide access to configuration and logging

Key Components

Root Command (cmd/root.go)

The root command sets up the entire configuration system:

var rootCmd = &cobra.Command{
    PersistentPreRun: func(cmd *cobra.Command, args []string) {
        initConfig()
        setupLogging()
    },
}

Configuration Initialization (initConfig())

This function:

  1. Determines config file location (from flag or default)
  2. Sets default values
  3. Enables environment variable reading
  4. Reads the config file (if it exists)

Flag Binding

Flags are bound to Viper keys using viper.BindPFlag():

rootCmd.PersistentFlags().StringP("verbose", "v", false, "verbose output")
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))

This creates the hierarchy: CLI flag → Viper key → Config struct

Adding New Configuration

To add a new configuration option:

  1. Add to config struct (internal/config/config.go):

    type Config struct {
        MyNewOption string
    }
    
  2. Add flag (cmd/root.go or command-specific file):

    rootCmd.PersistentFlags().String("my-option", "default", "description")
    viper.BindPFlag("my_option", rootCmd.PersistentFlags().Lookup("my-option"))
    
  3. Set default (cmd/root.go in initConfig()):

    viper.SetDefault("my_option", "default_value")
    
  4. Add to config example (.yaml.example):

    my_option: "default_value"
    
  5. Access in commands:

    cfg := GetConfig()
    value := cfg.MyNewOption
    // or directly from viper:
    value := viper.GetString("my_option")
    

Command-Specific Configuration

For configuration specific to a single command:

  1. Add the flag to the command's init() function, not the root command

  2. Use a nested structure in the config struct:

    type Config struct {
        Fetch struct {
            Concurrency int
            Timeout     time.Duration
        }
    }
    
  3. Bind with a namespaced key:

    viper.BindPFlag("fetch.concurrency", fetchCmd.Flags().Lookup("concurrency"))
    

Environment Variables

Viper automatically maps environment variables when you call viper.AutomaticEnv().

By default, environment variables are matched by converting the key to uppercase and replacing . with _:

  • Config key: fetch.concurrency
  • Environment variable: FETCH_CONCURRENCY

Best Practices

  1. Use PersistentFlags for global options: Options that apply to all commands should be on rootCmd.PersistentFlags()

  2. Use command-specific Flags for local options: Options specific to one command should be on that command's Flags()

  3. Provide sensible defaults: Always set defaults in initConfig() so the tool works without a config file

  4. Document in .yaml.example: Keep the example config file up to date

  5. Keep flag names kebab-case: Use hyphens in CLI flags (--my-option) and underscores in Viper keys (my_option)

  6. Use GetConfig() for structured access: Prefer accessing configuration through the typed Config struct rather than calling viper.Get* directly in commands