Construyendo aplicaciones CLI en Go con Cobra y Viper

Desarrollo de CLI en Go con los marcos Cobra y Viper

Índice

Las aplicaciones de interfaz de línea de comandos (CLI) son herramientas esenciales para desarrolladores, administradores de sistemas y profesionales de DevOps.

Dos bibliotecas de Go han se convertido en el estándar de facto para el desarrollo de CLI en Go: Cobra para la estructura de comandos y Viper para la gestión de configuración.

Go ha emergido como un excelente lenguaje para construir herramientas de CLI debido a su rendimiento, despliegue sencillo y soporte multiplataforma.

Tetris

¿Por qué elegir Go para aplicaciones CLI?

Go ofrece ventajas atractivas para el desarrollo de CLI:

  • Distribución de un solo binario: No se requieren dependencias de tiempo de ejecución ni gestores de paquetes
  • Ejecución rápida: La compilación nativa proporciona un excelente rendimiento
  • Soporte multiplataforma: Compilación fácil para Linux, macOS, Windows y más
  • Biblioteca estándar robusta: Herramientas ricas para E/S de archivos, redes y procesamiento de texto
  • Concurrencia: Goroutines integradas para operaciones paralelas
  • Tipado estático: Captura de errores en tiempo de compilación

Herramientas CLI populares construidas con Go incluyen Docker, Kubernetes (kubectl), Hugo, Terraform y GitHub CLI. Si eres nuevo en Go o necesitas una referencia rápida, consulta nuestra Hoja de trucos de Go para sintaxis y patrones esenciales de Go.

Introducción a Cobra

Cobra es una biblioteca que proporciona una interfaz sencilla para crear poderosas aplicaciones CLI modernas. Creada por Steve Francia (spf13), el mismo autor detrás de Hugo y Viper, Cobra se utiliza en muchos de los proyectos más populares de Go.

Características clave de Cobra

Estructura de comandos: Cobra implementa el patrón de comandos, permitiéndote crear aplicaciones con comandos y subcomandos (como git commit o docker run).

Manejo de banderas: Banderas locales y persistentes con análisis automático y conversión de tipos.

Ayuda automática: Genera texto de ayuda e información de uso automáticamente.

Sugerencias inteligentes: Proporciona sugerencias cuando los usuarios cometen errores tipográficos ("¿Quiso decir ‘status’?").

Completación de shell: Genera scripts de completación para bash, zsh, fish y PowerShell.

Salida flexible: Trabaja de forma fluida con formateadores personalizados y estilos de salida.

Introducción a Viper

Viper es una solución completa de configuración para aplicaciones de Go, diseñada para trabajar de forma fluida con Cobra. Maneja la configuración de múltiples fuentes con un orden claro de precedencia.

Características clave de Viper

Múltiples fuentes de configuración:

  • Archivos de configuración (JSON, YAML, TOML, HCL, INI, envfile, propiedades de Java)
  • Variables de entorno
  • Banderas de línea de comandos
  • Sistemas de configuración remota (etcd, Consul)
  • Valores predeterminados

Orden de prioridad de configuración:

  1. Llamadas explícitas a Set
  2. Banderas de línea de comandos
  3. Variables de entorno
  4. Archivo de configuración
  5. Almacén de clave/valor
  6. Valores predeterminados

Vigilancia en vivo: Monitorea archivos de configuración y los recarga automáticamente cuando ocurren cambios.

Conversión de tipos: Conversión automática a varios tipos de Go (cadena, entero, booleano, duración, etc.).

Comenzando: Instalación

Primero, inicializa un nuevo módulo de Go e instala ambas bibliotecas:

go mod init myapp
go get -u github.com/spf13/cobra@latest
go get -u github.com/spf13/viper@latest

Opcionalmente, instala el generador de CLI de Cobra para scaffolding:

go install github.com/spf13/cobra-cli@latest

Construyendo tu primera aplicación CLI

Vamos a construir un ejemplo práctico: una herramienta CLI de gestión de tareas con soporte de configuración.

Estructura del proyecto

mytasks/
├── cmd/
│   ├── root.go
│   ├── add.go
│   ├── list.go
│   └── complete.go
├── config/
│   └── config.go
├── main.go
└── config.yaml

Punto de entrada principal

// main.go
package main

import "mytasks/cmd"

func main() {
    cmd.Execute()
}

Comando principal con integración de Viper

// cmd/root.go
package cmd

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

var cfgFile string

var rootCmd = &cobra.Command{
    Use:   "mytasks",
    Short: "Una herramienta CLI simple para gestión de tareas",
    Long: `MyTasks es una herramienta CLI para gestión de tareas que te ayuda a organizar 
tus tareas diarias con facilidad. Construida con Cobra y Viper.`,
}

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

func init() {
    cobra.OnInitialize(initConfig)
    
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", 
        "archivo de configuración (por defecto es $HOME/.mytasks.yaml)")
    rootCmd.PersistentFlags().String("db", "", 
        "ubicación del archivo de base de datos")
    
    viper.BindPFlag("database", rootCmd.PersistentFlags().Lookup("db"))
}

func initConfig() {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        home, err := os.UserHomeDir()
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
        
        viper.AddConfigPath(home)
        viper.AddConfigPath(".")
        viper.SetConfigType("yaml")
        viper.SetConfigName(".mytasks")
    }
    
    viper.SetEnvPrefix("MYTASKS")
    viper.AutomaticEnv()
    
    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Usando archivo de configuración:", viper.ConfigFileUsed())
    }
}

Añadiendo subcomandos

// cmd/add.go
package cmd

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

var priority string

var addCmd = &cobra.Command{
    Use:   "add [descripción de la tarea]",
    Short: "Añadir una nueva tarea",
    Args:  cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        task := args[0]
        db := viper.GetString("database")
        
        fmt.Printf("Añadiendo tarea: %s\n", task)
        fmt.Printf("Prioridad: %s\n", priority)
        fmt.Printf("Base de datos: %s\n", db)
        
        // Aquí implementarías el almacenamiento real de tareas
    },
}

func init() {
    rootCmd.AddCommand(addCmd)
    
    addCmd.Flags().StringVarP(&priority, "priority", "p", "medium", 
        "Prioridad de la tarea (baja, media, alta)")
}

Comando de lista

// cmd/list.go
package cmd

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

var showCompleted bool

var listCmd = &cobra.Command{
    Use:   "list",
    Short: "Listar todas las tareas",
    Run: func(cmd *cobra.Command, args []string) {
        db := viper.GetString("database")
        
        fmt.Printf("Listando tareas desde: %s\n", db)
        fmt.Printf("Mostrar completadas: %v\n", showCompleted)
        
        // Aquí implementarías la lista real de tareas
    },
}

func init() {
    rootCmd.AddCommand(listCmd)
    
    listCmd.Flags().BoolVarP(&showCompleted, "completed", "c", false, 
        "Mostrar tareas completadas")
}

Implementando persistencia de datos

Para una aplicación CLI de gestión de tareas en producción, necesitarás implementar un almacenamiento real de datos. Mientras estamos usando una ruta de base de datos simple aquí, tienes varias opciones para persistir datos:

  • SQLite: Base de datos ligera, sin servidor perfecta para herramientas CLI
  • PostgreSQL/MySQL: Bases de datos completas para aplicaciones más complejas
  • Archivos JSON/YAML: Almacenamiento basado en archivos simple para necesidades ligeras
  • Bases de datos embebidas: BoltDB, BadgerDB para almacenamiento de clave-valor

Si estás trabajando con bases de datos relacionales como PostgreSQL, querrás usar un ORM o constructor de consultas. Para una comparación completa de bibliotecas de bases de datos en Go, consulta nuestra guía sobre Comparando ORMs de Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc.

Configuración avanzada con Viper

Ejemplo de archivo de configuración

# .mytasks.yaml
database: ~/.mytasks.db
format: table
colors: true

notifications:
  enabled: true
  sound: true

priorities:
  high: red
  medium: yellow
  low: green

Leyendo configuración anidada

func getNotificationSettings() {
    enabled := viper.GetBool("notifications.enabled")
    sound := viper.GetBool("notifications.sound")
    
    fmt.Printf("Notificaciones habilitadas: %v\n", enabled)
    fmt.Printf("Sonido habilitado: %v\n", sound)
}

func getPriorityColors() map[string]string {
    return viper.GetStringMapString("priorities")
}

Variables de entorno

Viper lee automáticamente las variables de entorno con el prefijo configurado:

export MYTASKS_DATABASE=/tmp/tasks.db
export MYTASKS_NOTIFICATIONS_ENABLED=false
mytasks list

Recarga de configuración en vivo

func watchConfig() {
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        fmt.Println("Archivo de configuración cambiado:", e.Name)
        // Recargar componentes dependientes de la configuración
    })
}

Buenas prácticas

1. Usa el generador de Cobra para consistencia

cobra-cli init
cobra-cli add serve
cobra-cli add create

2. Organiza los comandos en archivos separados

Mantén cada comando en su propio archivo bajo el directorio cmd/ para mantener la legibilidad.

3. Aprovecha las banderas persistentes

Usa banderas persistentes para opciones que se aplican a todos los subcomandos:

rootCmd.PersistentFlags().StringP("output", "o", "text", 
    "Formato de salida (texto, json, yaml)")

4. Implementa un manejo adecuado de errores

var rootCmd = &cobra.Command{
    Use:   "myapp",
    Short: "Mi aplicación",
    RunE: func(cmd *cobra.Command, args []string) error {
        if err := doSomething(); err != nil {
            return fmt.Errorf("falló al hacer algo: %w", err)
        }
        return nil
    },
}

5. Proporciona texto de ayuda significativo

var cmd = &cobra.Command{
    Use:   "deploy [entorno]",
    Short: "Despliega la aplicación en el entorno especificado",
    Long: `Despliega construcciones y despliega tu aplicación en el 
entorno especificado. Entornos admitidos son:
  - desarrollo (dev)
  - staging
  - producción (prod)`,
    Example: `  myapp deploy staging
  myapp deploy production --version=1.2.3`,
}

6. Establece valores predeterminados sensatos

func init() {
    viper.SetDefault("port", 8080)
    viper.SetDefault("timeout", "30s")
    viper.SetDefault("retries", 3)
}

7. Valida la configuración

func validateConfig() error {
    port := viper.GetInt("port")
    if port < 1024 || port > 65535 {
        return fmt.Errorf("puerto inválido: %d", port)
    }
    return nil
}

Pruebas de aplicaciones CLI

Pruebas de comandos

// cmd/root_test.go
package cmd

import (
    "bytes"
    "testing"
)

func TestRootCommand(t *testing.T) {
    cmd := rootCmd
    b := bytes.NewBufferString("")
    cmd.SetOut(b)
    cmd.SetArgs([]string{"--help"})
    
    if err := cmd.Execute(); err != nil {
        t.Fatalf("ejecución del comando fallida: %v", err)
    }
}

Pruebas con diferentes configuraciones

func TestWithConfig(t *testing.T) {
    viper.Set("database", "/tmp/test.db")
    viper.Set("debug", true)
    
    // Ejecuta tus pruebas
    
    viper.Reset() // Limpieza
}

Generando scripts de completación de shell

Cobra puede generar scripts de completación para varios shells:

// cmd/completion.go
package cmd

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

var completionCmd = &cobra.Command{
    Use:   "completion [bash|zsh|fish|powershell]",
    Short: "Generar script de completación",
    Long: `Para cargar completaciones:

Bash:
  $ source <(mytasks completion bash)

Zsh:
  $ source <(mytasks completion zsh)

Fish:
  $ mytasks completion fish | source

PowerShell:
  PS> mytasks completion powershell | Out-String | Invoke-Expression
`,
    DisableFlagsInUseLine: true,
    ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
    Args: cobra.ExactValidArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        switch args[0] {
        case "bash":
            cmd.Root().GenBashCompletion(os.Stdout)
        case "zsh":
            cmd.Root().GenZshCompletion(os.Stdout)
        case "fish":
            cmd.Root().GenFishCompletion(os.Stdout, true)
        case "powershell":
            cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
        }
    },
}

func init() {
    rootCmd.AddCommand(completionCmd)
}

Construcción y distribución

Construir para múltiples plataformas

# Linux
GOOS=linux GOARCH=amd64 go build -o mytasks-linux-amd64

# macOS
GOOS=darwin GOARCH=amd64 go build -o mytasks-darwin-amd64
GOOS=darwin GOARCH=arm64 go build -o mytasks-darwin-arm64

# Windows
GOOS=windows GOARCH=amd64 go build -o mytasks-windows-amd64.exe

Reducir el tamaño del binario

go build -ldflags="-s -w" -o mytasks

Añadir información de versión

// cmd/version.go
package cmd

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

var (
    Version   = "dev"
    Commit    = "none"
    BuildTime = "unknown"
)

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "Imprimir información de versión",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Versión: %s\n", Version)
        fmt.Printf("Commit: %s\n", Commit)
        fmt.Printf("Compilado: %s\n", BuildTime)
    },
}

func init() {
    rootCmd.AddCommand(versionCmd)
}

Compilar con información de versión:

go build -ldflags="-X 'mytasks/cmd.Version=1.0.0' \
    -X 'mytasks/cmd.Commit=$(git rev-parse HEAD)' \
    -X 'mytasks/cmd.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)'" \
    -o mytasks

Ejemplos del mundo real

Herramientas de código abierto populares construidas con Cobra y Viper:

  • kubectl: Herramienta de línea de comandos de Kubernetes
  • Hugo: Generador de sitios estáticos
  • GitHub CLI (gh): CLI oficial de GitHub
  • Docker CLI: Gestión de contenedores
  • Helm: Gestor de paquetes de Kubernetes
  • Skaffold: Herramienta de flujo de trabajo de desarrollo de Kubernetes
  • Cobra CLI: Autohospedado - Cobra se usa a sí mismo!

Más allá de las herramientas tradicionales de DevOps, las capacidades de CLI de Go se extienden a aplicaciones de inteligencia artificial y aprendizaje automático. Si estás interesado en construir herramientas CLI que interactúen con modelos de lenguaje grandes, consulta nuestra guía sobre Restringir LLMs con salida estructurada usando Ollama y Go, que muestra cómo construir aplicaciones Go que trabajen con modelos de IA.

Recursos útiles

Conclusión

Cobra y Viper juntos proporcionan una base poderosa para construir aplicaciones CLI profesionales en Go. Cobra maneja la estructura de comandos, el análisis de banderas y la generación de ayuda, mientras que Viper gestiona la configuración de múltiples fuentes con precedencia inteligente.

Esta combinación te permite crear herramientas CLI que sean:

  • Fáciles de usar con comandos intuitivos y ayuda automática
  • Flexibles con múltiples fuentes de configuración
  • Profesionales con completación de shell y manejo adecuado de errores
  • Mantenibles con organización de código limpia
  • Portables entre diferentes plataformas

Ya estés construyendo herramientas para desarrolladores, utilidades del sistema o automatización de DevOps, Cobra y Viper proporcionan la base sólida que necesitas para crear aplicaciones CLI que los usuarios amarán.