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
}
