Desarrollo de CLI Robustas en Go: Construyendo Herramientas de Línea de Comandos Interactivas
Este tutorial te guiará paso a paso en la creación de aplicaciones de línea de comandos (CLI) robustas y amigables con Go. Explorarás el uso de Cobra para la estructura de comandos, Viper para la gestión de configuración, y otras herramientas esenciales para una experiencia de usuario superior. Desarrollarás una CLI completa desde cero.
🚀 Introducción al Desarrollo de CLIs con Go
Las herramientas de línea de comandos (CLI) son esenciales para desarrolladores y administradores de sistemas, permitiendo automatizar tareas, interactuar con servicios y gestionar proyectos de manera eficiente. Go, con su rendimiento, concurrencia y facilidad de compilación en binarios estáticos, es un lenguaje ideal para construir CLIs potentes y portables.
En este tutorial, exploraremos cómo crear una CLI robusta y bien estructurada en Go, utilizando bibliotecas populares como Cobra para la gestión de comandos y Viper para la configuración. Aprenderás a definir comandos, subcomandos, banderas (flags), gestionar entradas de usuario y crear una experiencia interactiva.
¿Por qué Go para CLIs? 🤔
Go ofrece varias ventajas significativas para el desarrollo de herramientas de línea de comandos:
- Rendimiento: Go es un lenguaje compilado que ofrece un rendimiento excelente, crucial para herramientas que necesitan ejecutarse rápidamente.
- Binarios Estáticos: Compila tus aplicaciones en un único binario que no tiene dependencias externas, facilitando la distribución y el despliegue.
- Concurrencia: Las goroutines y canales permiten manejar operaciones concurrentes de forma sencilla y eficiente, ideal para CLIs que interactúan con APIs o realizan tareas en paralelo.
- Ecosistema Rico: Una gran cantidad de bibliotecas bien mantenidas para parsear argumentos, gestionar configuración, interactuar con sistemas de archivos, etc.
- Portabilidad: El mismo código fuente puede compilarse fácilmente para diferentes sistemas operativos y arquitecturas.
🛠️ Herramientas Esenciales para una CLI en Go
Para construir una CLI de calidad, nos apoyaremos en algunas bibliotecas de Go que simplifican el proceso y añaden funcionalidad avanzada:
- Cobra: Un generador y parser de comandos moderno y poderoso, popularizado por proyectos como
kubectlyHugo. Proporciona una estructura robusta para comandos, subcomandos, banderas y ayudas. - Viper: Una solución completa para la gestión de configuración en aplicaciones Go, que soporta JSON, TOML, YAML, HCL, env vars, y más.
- pterm: Una biblioteca para salida de terminal hermosa, con tablas, barras de progreso, spinners y colores.
- survey: Una biblioteca para crear encuestas interactivas con preguntas de entrada, selecciones múltiples, confirmaciones, etc.
🏗️ Estructura Básica de una CLI con Cobra
Cobra organiza tu aplicación CLI en una jerarquía de comandos. Tendrás un comando raíz, y opcionalmente, subcomandos anidados. Cada comando puede tener sus propias banderas y acciones.
Inicializando el Proyecto Go 📁
Primero, crearemos un nuevo módulo de Go y lo inicializaremos:
mkdir go-cli-tool
cd go-cli-tool
go mod init github.com/tu_usuario/go-cli-tool
go get github.com/spf13/cobra@latest github.com/spf13/viper@latest github.com/pterm/pterm@latest github.com/AlecAivazis/survey/v2@latest
Ahora, crearemos la estructura básica de Cobra. Puedes usar cobra init para generar un boilerplate, pero para entenderlo mejor, lo haremos manualmente.
Crea un archivo main.go:
package main
import (
"go-cli-tool/cmd"
"log"
)
func main() {
if err := cmd.Execute(); err != nil {
log.Fatalf("Error al ejecutar la CLI: %v", err)
}
}
Y crea un directorio cmd con cmd/root.go:
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd representa el comando base cuando se llama a la aplicación sin subcomandos
var rootCmd = &cobra.Command{
Use: "go-cli-tool",
Short: "Una herramienta CLI simple para gestionar tareas",
Long: `go-cli-tool es una poderosa y flexible aplicación CLI
que te ayuda a gestionar tus tareas diarias de manera eficiente.
Completa con subcomandos para añadir, listar y completar tareas.`,
// Uncomment the following line if your bare application has an action
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute agrega todos los comandos hijos al comando raíz y establece las banderas apropiadamente.
// Esto es llamado por main.main(). Solo necesita suceder una vez.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
// Banderas globales
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "archivo de configuración (ej: $HOME/.go-cli-tool.yaml)")
rootCmd.PersistentFlags().BoolP("toggle", "t", false, "Ayuda a la bandera de alternancia para demostración")
// Banderas locales que solo se ejecutan cuando se llama a este comando directamente
// rootCmd.Flags().BoolP("toggle", "t", false, "Ayuda a la bandera de alternancia para demostración")
}
// initConfig lee el archivo de configuración y las variables de entorno si están presentes.
func initConfig() {
if cfgFile != "" {
// Usa el archivo de configuración especificado por la bandera.
vip.SetConfigFile(cfgFile)
} else {
// Busca en el directorio home.
home, err := os.UserHomeDir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Nombre del archivo de configuración (sin extensión).
vip.AddConfigPath(home)
vip.SetConfigName(".go-cli-tool")
vip.SetConfigType("yaml") // o "json", "toml", etc.
}
vip.AutomaticEnv() // Lee variables de entorno que coincidan con los nombres de las claves
if err := vip.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Usando archivo de configuración:", vip.ConfigFileUsed())
} else {
// Manejar el error de configuración, por ejemplo, si no se encuentra el archivo
// fmt.Fprintln(os.Stderr, "Advertencia: No se encontró archivo de configuración. Usando valores por defecto o variables de entorno.")
}
}
var vip = viper.New() // Instancia de Viper para esta CLI
Ahora, puedes probar tu CLI ejecutando go run main.go. Verás el mensaje de ayuda de Cobra.
Añadiendo Comandos y Subcomandos ➕
Vamos a crear una CLI simple para gestionar tareas (todo). Necesitaremos comandos como add, list y complete.
Crea cmd/add.go:
package cmd
import (
"fmt"
"time"
"github.com/spf13/cobra"
)
var taskDescription string
var dueDate string
var addCmd = &cobra.Command{
Use: "add [description]",
Short: "Añade una nueva tarea",
Long: `Añade una nueva tarea a tu lista de pendientes.`,
Args: cobra.ExactArgs(1), // Requiere exactamente un argumento para la descripción
Run: func(cmd *cobra.Command, args []string) {
description := args[0]
fmt.Printf("Añadiendo tarea: '%s'\n", description)
if dueDate != "" {
// Lógica para parsear y usar la fecha de vencimiento
fmt.Printf("Fecha de vencimiento: %s\n", dueDate)
} else {
fmt.Println("Sin fecha de vencimiento especificada.")
}
// Aquí iría la lógica para guardar la tarea en una base de datos o archivo
fmt.Println("Tarea añadida con éxito! ✅")
},
}
func init() {
rootCmd.AddCommand(addCmd)
// Banderas locales para el comando 'add'
addCmd.Flags().StringVarP(&dueDate, "due", "d", "", "Fecha de vencimiento de la tarea (YYYY-MM-DD)")
}
Actualiza cmd/root.go para añadir el comando add al rootCmd (ya lo hicimos en add.go dentro de init(), así que solo asegúrate de que esté ahí si usaste el boilerplate de Cobra).
Ahora puedes ejecutar go run main.go add "Comprar leche" o go run main.go add "Enviar informe" -d "2023-12-31".
Gestión de Banderas (Flags) 🚩
Cobra simplifica la definición y el parseo de banderas. Hemos visto StringVarP para cadenas con una versión corta (-d) y larga (--due). Otros tipos de banderas incluyen BoolVarP, IntVarP, etc.
⚙️ Gestión de Configuración con Viper
Viper es una herramienta poderosa para manejar la configuración de tu aplicación. Puede leer archivos de configuración, variables de entorno, remote key-value stores y más.
Hemos configurado Viper en cmd/root.go para buscar un archivo .go-cli-tool.yaml en el directorio home del usuario o un archivo especificado con --config.
Prioridad de Carga de Configuración 🎯
Viper tiene una prioridad de carga bien definida:
- Set values (establecidos programáticamente)
- Flags (banderas de línea de comandos)
- Variables de entorno
- Archivos de configuración
- Valores por defecto
Ejemplo de Configuración 📝
Imaginemos que nuestra herramienta necesita una API key. Podemos añadirla a la configuración. Modifica cmd/root.go para establecer un valor por defecto y luego leerlo.
// ... dentro de init()
vip.SetDefault("api_key", "default_api_key_here")
vip.SetDefault("database.path", "~/.go-cli-tool.db")
// ...
Para acceder a estos valores, puedes usar vip.GetString("api_key") o vip.Get("database.path").
Ahora, crea un archivo ~/.go-cli-tool.yaml (o en tu directorio actual para pruebas) con el siguiente contenido:
api_key: "mi_clave_secreta_desde_yaml"
database:
path: "/var/lib/go-cli-tool/tasks.db"
Si luego ejecutas go run main.go, verás que se usa el archivo de configuración. Si también defines una variable de entorno GO_CLI_TOOL_API_KEY="env_key", esta tendrá prioridad sobre el YAML.
Uso Práctico de Configuración en un Comando
Vamos a crear un comando config para ver los valores actuales:
Crea cmd/config.go:
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/pterm/pterm"
)
var configCmd = &cobra.Command{
Use: "config",
Short: "Muestra la configuración actual",
Long: `Muestra todos los valores de configuración cargados por Viper.`,
Run: func(cmd *cobra.Command, args []string) {
data := [][]string{{"Clave", "Valor"}}
for _, key := range vip.AllKeys() {
data = append(data, []string{key, fmt.Sprintf("%v", vip.Get(key))})
}
pterm.DefaultTable.WithHas Header().WithData(data).Render()
},
}
func init() {
rootCmd.AddCommand(configCmd)
}
Ahora, al ejecutar go run main.go config, verás una tabla con tus configuraciones.
🎨 Mejorando la Experiencia del Usuario con pterm y survey
Una CLI no solo debe ser funcional, sino también amigable y fácil de usar. Aquí es donde pterm y survey brillan.
Salida Atractiva con pterm ✨
pterm permite añadir colores, tablas, barras de progreso y spinners para una mejor experiencia.
// Ejemplo de uso de pterm para mensajes coloridos
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/pterm/pterm"
)
var greetCmd = &cobra.Command{
Use: "greet [nombre]",
Short: "Saluda a un usuario",
Long: `Este comando saluda a la persona especificada.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
pterm.Info.Printf("Hola, %s! 👋\n", name)
pterm.Success.Println("¡Operación exitosa!")
pterm.Error.Println("¡Ha ocurrido un error inesperado!")
pterm.Warning.Println("¡Atención, algo podría no ir bien!")
pterm.DefaultSection.WithTopPadding(1).WithBottomPadding(1).WithTitle("Detalles Adicionales").Println("Aquí puedes poner información extra relevante.")
},
}
func init() {
rootCmd.AddCommand(greetCmd)
}
Agrega greetCmd a rootCmd en su init() correspondiente (similar a addCmd). Luego, ejecuta go run main.go greet "Mundo".
Interactividad con survey 🗣️
survey te permite hacer preguntas interactivas al usuario, ideal para cuando necesitas entrada dinámica.
Vamos a modificar el comando add para que, si no se proporciona una descripción, pregunte interactivamente.
package cmd
import (
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/AlecAivazis/survey/v2"
"github.com/pterm/pterm"
)
// ... (variables existentes para addCmd)
var addCmd = &cobra.Command{
Use: "add [description]",
Short: "Añade una nueva tarea",
Long: `Añade una nueva tarea a tu lista de pendientes.`,
// Args: cobra.ExactArgs(1), // Quitamos esta línea para permitir entrada interactiva
Run: func(cmd *cobra.Command, args []string) {
description := ""
if len(args) > 0 {
description = args[0]
} else {
// Si no se proporcionó descripción, preguntar interactivamente
prompt := &survey.Input{
Message: "¿Cuál es la descripción de la tarea?",
Help: "Ej: Comprar víveres, enviar email, etc.",
}
err := survey.AskOne(prompt, &description)
if err != nil {
pterm.Error.Printf("Error al obtener la descripción: %v\n", err)
return
}
if description == "" {
pterm.Warning.Println("La descripción de la tarea no puede estar vacía. Abortando.")
return
}
}
pterm.Info.Printf("Añadiendo tarea: '%s'\n", description)
if dueDate != "" {
// Lógica para parsear y usar la fecha de vencimiento
pterm.DefaultSection.WithTitle("Detalles de la Tarea").Println(
pterm.Sprintf("Descripción: %s\n", description) +
pterm.Sprintf("Fecha de Vencimiento: %s", dueDate),
)
} else {
pterm.Println("Sin fecha de vencimiento especificada.")
}
// Aquí iría la lógica para guardar la tarea en una base de datos o archivo
pterm.Success.Println("Tarea añadida con éxito! ✅")
},
}
func init() {
rootCmd.AddCommand(addCmd)
addCmd.Flags().StringVarP(&dueDate, "due", "d", "", "Fecha de vencimiento de la tarea (YYYY-MM-DD)")
}
Ahora, ejecuta go run main.go add. Te pedirá la descripción interactivamente.
Ejemplo de otros tipos de preguntas con `survey`
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/AlecAivazis/survey/v2"
"github.com/pterm/pterm"
)
var surveyCmd = &cobra.Command{
Use: "survey",
Short: "Demuestra preguntas interactivas con survey",
Long: `Este comando demuestra varios tipos de preguntas interactivas usando la biblioteca survey.`,
Run: func(cmd *cobra.Command, args []string) {
var name string
promptName := &survey.Input{
Message: "¿Cuál es tu nombre?",
Default: "Usuario",
}
survey.AskOne(promptName, &name)
pterm.Info.Printf("Hola, %s!\n", name)
var color string
promptColor := &survey.Select{
Message: "Elige tu color favorito:",
Options: []string{"Rojo", "Verde", "Azul", "Amarillo"},
Default: "Azul",
}
survey.AskOne(promptColor, &color)
pterm.Info.Printf("Tu color favorito es: %s\n", color)
var agree bool
promptAgree := &survey.Confirm{
Message: "¿Estás de acuerdo con los términos y condiciones?",
Default: true,
}
survey.AskOne(promptAgree, &agree)
pterm.Info.Printf("Acuerdo: %t\n", agree)
var toppings []string
promptToppings := &survey.MultiSelect{
Message: "Elige tus toppings de pizza favoritos:",
Options: []string{"Pepperoni", "Champiñones", "Cebolla", "Pimientos", "Extra Queso"},
}
survey.AskOne(promptToppings, &toppings)
pterm.Info.Printf("Tus toppings elegidos: %v\n", toppings)
},
}
func init() {
rootCmd.AddCommand(surveyCmd)
}
📂 Persistencia de Datos para la CLI
Una CLI útil a menudo necesita almacenar datos. Aunque Viper es para configuración, para datos transaccionales, podemos usar una base de datos o archivos.
Usando un Archivo JSON Simple 📄
Para una CLI pequeña, un archivo JSON es una opción sencilla. Vamos a crear un comando list y complete para nuestra herramienta todo.
Primero, definamos una estructura para nuestras tareas y funciones auxiliares para leer/escribir.
Crea un nuevo paquete internal/task y dentro un archivo task.go:
package task
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"sync"
"time"
"github.com/spf13/viper"
)
// Task representa una única tarea en la lista de pendientes.
type Task struct {
ID int `json:"id"`
Description string `json:"description"`
Completed bool `json:"completed"`
CreatedAt time.Time `json:"created_at"`
DueDate *time.Time `json:"due_date,omitempty"`
}
// taskFile representa el archivo JSON que almacena las tareas.
type taskFile struct {
Tasks []Task `json:"tasks"`
LastID int `json:"last_id"`
}
var (
mu sync.Mutex // Para sincronizar el acceso al archivo de tareas
currentTasks []Task
nextID int
)
// GetTasksFilePath devuelve la ruta del archivo de tareas.
func GetTasksFilePath() (string, error) {
configPath := viper.GetString("data.path") // Usamos Viper para obtener la ruta del archivo de datos
if configPath == "" {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("no se pudo obtener el directorio home del usuario: %w", err)
}
configPath = filepath.Join(home, ".go-cli-tool", "tasks.json")
} else {
// Asegúrate de que la ruta sea absoluta y resuelve ~ si es necesario
if configPath[0] == '~' {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("no se pudo obtener el directorio home del usuario: %w", err)
}
configPath = filepath.Join(home, configPath[1:])
}
// Asegúrate de que el directorio existe
dir := filepath.Dir(configPath)
if _, err := os.Stat(dir); os.IsNotExist(err) {
err := os.MkdirAll(dir, 0755)
if err != nil {
return "", fmt.Errorf("no se pudo crear el directorio de datos '%s': %w", dir, err)
}
}
}
return configPath, nil
}
// LoadTasks carga las tareas desde el archivo JSON.
func LoadTasks() ([]Task, error) {
mu.Lock()
defer mu.Unlock()
filePath, err := GetTasksFilePath()
if err != nil {
return nil, err
}
data, err := os.ReadFile(filePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// Archivo no existe, devolver lista vacía
currentTasks = []Task{}
nextID = 1
return currentTasks, nil
}
return nil, fmt.Errorf("no se pudo leer el archivo de tareas: %w", err)
}
var tf taskFile
if err := json.Unmarshal(data, &tf); err != nil {
return nil, fmt.Errorf("no se pudo deserializar el archivo de tareas: %w", err)
}
currentTasks = tf.Tasks
nextID = tf.LastID + 1
return currentTasks, nil
}
// SaveTasks guarda las tareas en el archivo JSON.
func SaveTasks(tasks []Task) error {
mu.Lock()
defer mu.Unlock()
filePath, err := GetTasksFilePath()
if err != nil {
return err
}
tf := taskFile{
Tasks: tasks,
LastID: nextID - 1, // El último ID usado
}
data, err := json.MarshalIndent(tf, "", " ")
if err != nil {
return fmt.Errorf("no se pudo serializar las tareas: %w", err)
}
err = os.WriteFile(filePath, data, 0644)
if err != nil {
return fmt.Errorf("no se pudo escribir el archivo de tareas: %w", err)
}
return nil
}
// AddTask añade una nueva tarea a la lista.
func AddTask(description string, dueDate *time.Time) (Task, error) {
mu.Lock()
defer mu.Unlock()
t, err := LoadTasks()
if err != nil {
return Task{}, err
}
newTask := Task{
ID: nextID,
Description: description,
Completed: false,
CreatedAt: time.Now(),
DueDate: dueDate,
}
currentTasks = append(t, newTask)
nextID++
if err := SaveTasks(currentTasks); err != nil {
return Task{}, err
}
return newTask, nil
}
// CompleteTask marca una tarea como completada por su ID.
func CompleteTask(id int) (bool, error) {
mu.Lock()
defer mu.Unlock()
tasks, err := LoadTasks()
if err != nil {
return false, err
}
found := false
for i := range tasks {
if tasks[i].ID == id {
tasks[i].Completed = true
found = true
break
}
}
if !found {
return false, fmt.Errorf("tarea con ID %d no encontrada", id)
}
if err := SaveTasks(tasks); err != nil {
return false, err
}
return true, nil
}
// GetTasks obtiene todas las tareas (opcionalmente filtradas por completadas).
func GetTasks(showCompleted bool) ([]Task, error) {
tasks, err := LoadTasks()
if err != nil {
return nil, err
}
if showCompleted {
return tasks, nil
}
var incompleteTasks []Task
for _, t := range tasks {
if !t.Completed {
incompleteTasks = append(incompleteTasks, t)
}
}
return incompleteTasks, nil
}
Ahora, volvamos a cmd/add.go y modifiquémoslo para usar nuestro nuevo paquete internal/task.
package cmd
import (
"fmt"
"go-cli-tool/internal/task"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
"github.com/pterm/pterm"
)
var dueDateStr string // Cambiamos el nombre para evitar colisiones
var addCmd = &cobra.Command{
Use: "add [description]",
Short: "Añade una nueva tarea",
Long: `Añade una nueva tarea a tu lista de pendientes.`,
Run: func(cmd *cobra.Command, args []string) {
description := ""
if len(args) > 0 {
description = args[0]
} else {
prompt := &survey.Input{
Message: "¿Cuál es la descripción de la tarea?",
Help: "Ej: Comprar víveres, enviar email, etc.",
}
err := survey.AskOne(prompt, &description)
if err != nil {
pterm.Error.Printf("Error al obtener la descripción: %v\n", err)
return
}
if description == "" {
pterm.Warning.Println("La descripción de la tarea no puede estar vacía. Abortando.")
return
}
}
var parsedDueDate *time.Time
if dueDateStr != "" {
d, err := time.Parse("2006-01-02", dueDateStr)
if err != nil {
pterm.Error.Printf("Formato de fecha de vencimiento inválido (esperado YYYY-MM-DD): %v\n", err)
return
}
parsedDueDate = &d
}
addedTask, err := task.AddTask(description, parsedDueDate)
if err != nil {
pterm.Error.Printf("Error al añadir la tarea: %v\n", err)
return
}
pterm.Success.Printf("Tarea añadida con éxito! ID: %d, Descripción: '%s' ✅\n", addedTask.ID, addedTask.Description)
},
}
func init() {
rootCmd.AddCommand(addCmd)
addCmd.Flags().StringVarP(&dueDateStr, "due", "d", "", "Fecha de vencimiento de la tarea (YYYY-MM-DD)")
}
Comando list 📋
Crea cmd/list.go:
package cmd
import (
"fmt"
"go-cli-tool/internal/task"
"time"
"github.com/spf13/cobra"
"github.com/pterm/pterm"
)
var showCompleted bool
var listCmd = &cobra.Command{
Use: "list",
Short: "Lista todas las tareas",
Long: `Muestra todas las tareas pendientes o completadas.`,
Run: func(cmd *cobra.Command, args []string) {
tasks, err := task.GetTasks(showCompleted)
if err != nil {
pterm.Error.Printf("Error al cargar las tareas: %v\n", err)
return
}
if len(tasks) == 0 {
pterm.Info.Println("¡No hay tareas para mostrar! 🎉")
return
}
// Preparar datos para la tabla
data := [][]string{{"ID", "Descripción", "Completada", "Creación", "Vencimiento"}}
for _, t := range tasks {
completedStatus := "❌ No"
if t.Completed {
completedStatus = "✅ Sí"
}
dueDate := "N/A"
if t.DueDate != nil {
dueDate = t.DueDate.Format("2006-01-02")
}
data = append(data, []string{
fmt.Sprintf("%d", t.ID),
t.Description,
completedStatus,
t.CreatedAt.Format("2006-01-02 15:04"),
dueDate,
})
}
pterm.DefaultTable.WithHasHeader().WithData(data).Render()
},
}
func init() {
rootCmd.AddCommand(listCmd)
listCmd.Flags().BoolVarP(&showCompleted, "all", "a", false, "Mostrar también las tareas completadas")
}
Comando complete ✅
Crea cmd/complete.go:
package cmd
import (
"fmt"
"go-cli-tool/internal/task"
"strconv"
"github.com/spf13/cobra"
"github.com/pterm/pterm"
)
var completeCmd = &cobra.Command{
Use: "complete [task ID]",
Short: "Marca una tarea como completada",
Long: `Marca una tarea específica como completada usando su ID.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
taskID, err := strconv.Atoi(args[0])
if err != nil {
pterm.Error.Printf("ID de tarea inválido: %v\n", err)
return
}
completed, err := task.CompleteTask(taskID)
if err != nil {
pterm.Error.Printf("Error al completar la tarea %d: %v\n", taskID, err)
return
}
if completed {
pterm.Success.Printf("Tarea %d marcada como completada! 🎉\n", taskID)
} else {
pterm.Warning.Printf("La tarea %d no se pudo completar (¿ya estaba completada o no existía?).\n", taskID)
}
},
}
func init() {
rootCmd.AddCommand(completeCmd)
}
🧪 Pruebas y Despliegue de tu CLI
Compilación y Prueba 🧪
Una vez que hayas desarrollado tu CLI, el siguiente paso es compilarla y probarla.
Para compilar un binario ejecutable:
go build -o go-cli-tool .
Esto creará un archivo go-cli-tool (o go-cli-tool.exe en Windows) en tu directorio actual. Luego, puedes ejecutarlo directamente:
./go-cli-tool add "Aprender Go CLI"
./go-cli-tool list
./go-cli-tool complete 1
./go-cli-tool list -a
Para compilaciones cross-platform:
GOOS=linux GOARCH=amd64 go build -o go-cli-tool-linux-amd64 .
GOOS=windows GOARCH=amd64 go build -o go-cli-tool-windows-amd64.exe .
GOOS=darwin GOARCH=arm64 go build -o go-cli-tool-darwin-arm64 .
Despliegue de tu CLI 🚀
El despliegue de una CLI en Go es sencillo debido a los binarios estáticos. Simplemente distribuye el binario compilado. Para que sea accesible globalmente, puedes:
- Moverlo a un directorio en el PATH: Por ejemplo,
/usr/local/bin/en Linux/macOS o un directorio en tuPathde Windows.
sudo mv go-cli-tool /usr/local/bin/
- Usar un gestor de paquetes: Para proyectos más grandes, puedes crear paquetes
.deb,.rpmo usar herramientas comoHomebrewpara macOS/Linux oScooppara Windows.
💡 Buenas Prácticas y Consejos Adicionales
Aquí tienes algunas buenas prácticas para llevar tu CLI al siguiente nivel:
- Documentación de Ayuda: Cobra genera automáticamente una ayuda exhaustiva. Asegúrate de que
ShortyLongdescripciones de tus comandos sean claras y útiles. - Validación de Entrada: Implementa validaciones robustas para las entradas del usuario (ej.
cobra.ExactArgs,cobra.MinimumNArgs, o validaciones personalizadas). - Manejo de Errores: Utiliza el paquete
errorsde Go para crear errores significativos y manejarlos apropiadamente, informando al usuario cuando algo va mal. - Contexto: Pasa un
context.Contexta tus funciones para permitir la cancelación o timeouts, especialmente en operaciones de red o larga duración. - Pruebas Unitarias e Integración: Escribe pruebas para tus comandos y lógica de negocio. Go facilita la escritura de pruebas con su paquete
testing. - Estructura del Proyecto: Mantén tu código organizado en paquetes (
cmdpara comandos,internalpara lógica interna,pkgpara librerías reutilizables).
graph TD
A[Usuario Ejecuta CLI] --> B{Comando Raíz (`rootCmd`)}
B --> C{Parsear Banderas Globales y Configuración (`Viper`)}
B --> D{Comando `add`}
B --> E{Comando `list`}
B --> F{Comando `complete`}
B --> G{Otros Comandos}
D --> D1{Validar Argumentos}
D1 --> D2{Preguntar Interactivamente (`survey`)}
D2 --> D3{Llamar a Lógica de Negocio (`internal/task`)}
D3 --> D4{Mostrar Éxito/Error (`pterm`)}
E --> E1{Cargar Tareas (`internal/task`)}
E1 --> E2{Filtrar Tareas}
E2 --> E3{Mostrar en Tabla (`pterm`)}
F --> F1{Parsear ID de Tarea}
F1 --> F2{Llamar a Lógica de Negocio (`internal/task`)}
F2 --> F3{Mostrar Éxito/Error (`pterm`)}
Resumen de Características Implementadas:
| Característica | Biblioteca Usada | Descripción |
|---|---|---|
| --- | --- | --- |
| Estructura de Comandos | Cobra | Define comandos raíz y subcomandos, parsea argumentos y banderas. |
| Gestión de Configuración | Viper | Lee configuración desde archivos, variables de entorno, etc. |
| --- | --- | --- |
| Salida Elegante | pterm | Colores, tablas, mensajes de estado (info, success, error). |
| Entrada Interactiva | survey | Preguntas al usuario, selección múltiple, confirmaciones. |
| --- | --- | --- |
| Persistencia de Datos | JSON (Manual) | Almacenamiento y recuperación de tareas en un archivo JSON. |
| Manejo de Errores | Go errors | Gestión de errores estándar de Go. |
Conclusión 🎉
Has llegado al final de este tutorial. Ahora tienes los conocimientos y la base para construir tus propias herramientas de línea de comandos en Go. Hemos cubierto la estructuración con Cobra, la gestión de configuración con Viper, la mejora de la experiencia de usuario con pterm y survey, y un sistema básico de persistencia de datos. El ecosistema de Go ofrece una gran cantidad de herramientas para hacer tus CLIs aún más potentes y robustas. ¡Anímate a explorar y construir tu próxima gran herramienta!
Tutoriales relacionados
- Desarrollo de APIs RESTful en Go: Creando Servicios Web Eficientes con Gin Gonicintermediate20 min
- Configuración y Despliegue de Microservicios Go con Docker y Kubernetesintermediate20 min
- Interfaces en Go: Abstracción y Polimorfismo para un Código Flexibleintermediate18 min
- Optimización de Rendimiento en Go: Perfilado y Benchmarking para Aplicaciones Velozesintermediate20 min
- Manejo Eficiente de Errores en Go: Estrategias y Buenas Prácticas para Código Robustointermediate10 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!