Cobra y Viper en Go: construir CLIs con subcomandos y gestión de configuración

Cobra es la librería estándar de facto para construir CLIs en Go: la usan Docker, Kubernetes, GitHub CLI y cientos de herramientas más. Viper complementa Cobra con gestión de configuración desde ficheros YAML/TOML/JSON, variables de entorno y flags de línea de comandos, resolviendo prioridades entre todas las fuentes automáticamente.

Instalación y estructura básica

go get github.com/spf13/cobra@latest
go get github.com/spf13/viper@latest
// cmd/root.go
package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
    Use:   "miapp",
    Short: "Herramienta CLI de ejemplo",
    Long:  `Una CLI construida con Cobra y Viper para demostrar las funcionalidades principales.`,
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

// main.go
package main

import "github.com/acme/miapp/cmd"

func main() {
    cmd.Execute()
}

Subcomandos con AddCommand

// cmd/usuarios.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
)

var (
    limite int
    filtro string
)

var usuariosCmd = &cobra.Command{
    Use:   "usuarios",
    Short: "Gestión de usuarios",
}

var listarCmd = &cobra.Command{
    Use:   "listar",
    Short: "Listar todos los usuarios",
    RunE: func(cmd *cobra.Command, args []string) error {
        fmt.Printf("Listando %d usuarios con filtro: %qn", limite, filtro)
        return nil
    },
}

var crearCmd = &cobra.Command{
    Use:   "crear [nombre] [email]",
    Short: "Crear un usuario nuevo",
    Args:  cobra.ExactArgs(2),
    RunE: func(cmd *cobra.Command, args []string) error {
        fmt.Printf("Creando usuario: %s (%s)n", args[0], args[1])
        return nil
    },
}

func init() {
    // Flags locales al subcomando listar
    listarCmd.Flags().IntVarP(&limite, "limite", "l", 10, "número máximo de resultados")
    listarCmd.Flags().StringVarP(&filtro, "filtro", "f", "", "filtrar por nombre")

    // Construir árbol: root ? usuarios ? listar, crear
    usuariosCmd.AddCommand(listarCmd)
    usuariosCmd.AddCommand(crearCmd)
    rootCmd.AddCommand(usuariosCmd)
}

Flags persistentes

Los flags persistentes se heredan en todos los subcomandos. Perfectos para opciones globales como el nivel de log o la URL de la API:

func init() {
    // Flag disponible en rootCmd y todos sus subcomandos
    rootCmd.PersistentFlags().String("config", "", "fichero de configuración")
    rootCmd.PersistentFlags().String("api-url", "https://api.example.com", "URL de la API")
    rootCmd.PersistentFlags().BoolP("verbose", "v", false, "salida detallada")
}

Viper: configuración desde YAML y variables de entorno

// cmd/root.go (ampliado con Viper)
package cmd

import (
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var cfgFile string

func init() {
    cobra.OnInitialize(inicializarConfig)
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "fichero de configuración (por defecto: $HOME/.miapp.yaml)")
}

func inicializarConfig() {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        viper.AddConfigPath("$HOME")
        viper.SetConfigName(".miapp")
        viper.SetConfigType("yaml")
    }

    // Variables de entorno: MIAPP_API_URL, MIAPP_TOKEN, etc.
    viper.SetEnvPrefix("MIAPP")
    viper.AutomaticEnv()

    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Config cargada desde:", viper.ConfigFileUsed())
    }
}

// config.yaml:
// api_url: https://api.example.com
// token: mi-token-secreto
// limite: 50

Vincular flags de Cobra con Viper

func init() {
    rootCmd.PersistentFlags().String("api-url", "https://api.example.com", "URL de la API")
    rootCmd.PersistentFlags().String("token", "", "token de autenticación")

    // Vincular: Cobra flag ? clave Viper
    viper.BindPFlag("api_url", rootCmd.PersistentFlags().Lookup("api-url"))
    viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token"))
}

// Ahora en cualquier subcomando:
func runComando(cmd *cobra.Command, args []string) error {
    apiURL := viper.GetString("api_url")    // lee de: flag CLI > var entorno > fichero YAML
    token := viper.GetString("token")
    limite := viper.GetInt("limite")
    // ...
}

El antipatrón MarkFlagRequired con Viper

// MAL: MarkFlagRequired ignora si el valor viene de Viper/env
listarCmd.Flags().StringVar(&token, "token", "", "token de auth")
listarCmd.MarkFlagRequired("token") // falla aunque MIAPP_TOKEN esté definido

// BIEN: validar manualmente en RunE para que Viper funcione
listarCmd.RunE = func(cmd *cobra.Command, args []string) error {
    token := viper.GetString("token")
    if token == "" {
        return fmt.Errorf("falta token: usa --token, la var MIAPP_TOKEN o el fichero de config")
    }
    // continuar...
    return nil
}

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP