Como criar CLIs simples com cobra para automações internas em Go

1. Introdução ao Cobra e seu papel em automações internas

Cobra é um framework CLI para Go, criado por Steve Francia (spf13) e usado por projetos como Kubernetes, Hugo e Docker. Ele fornece uma estrutura robusta para criar interfaces de linha de comando com suporte a subcomandos, flags e argumentos, sendo ideal para automações internas.

Por que usar Cobra para automações internas? A simplicidade de criação de comandos aninhados, o suporte nativo a flags persistentes e locais, e a facilidade de gerar documentação automática fazem dele uma escolha natural para scripts que precisam ser usados por equipes.

Pré-requisitos: Go instalado (versão 1.16+), familiaridade com go mod e conhecimento básico de linha de comando.

2. Configuração inicial e criação do projeto

Primeiro, instale a ferramenta cobra-cli:

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

Crie um novo módulo Go e inicialize o projeto:

mkdir automacao-interna
cd automacao-interna
go mod init github.com/seu-usuario/automacao-interna
cobra-cli init

A estrutura gerada será:

automacao-interna/
├── cmd/
│   └── root.go
├── main.go
├── go.mod
└── go.sum

O arquivo main.go já vem configurado para executar o comando raiz:

package main

import "github.com/seu-usuario/automacao-interna/cmd"

func main() {
    cmd.Execute()
}

3. Estrutura de comandos e subcomandos

Vamos criar um comando backup com subcomando database. Primeiro, crie o comando raiz customizado em cmd/root.go:

package cmd

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

var rootCmd = &cobra.Command{
    Use:   "automacao",
    Short: "Ferramenta de automação interna",
    Long:  `CLI para automações internas da equipe de infraestrutura`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Use 'automacao --help' para ver os comandos disponíveis")
    },
}

func Execute() {
    cobra.CheckErr(rootCmd.Execute())
}

Agora, crie cmd/backup.go:

package cmd

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

var backupCmd = &cobra.Command{
    Use:   "backup",
    Short: "Gerencia backups do sistema",
    Long:  `Comandos para criar e gerenciar backups de diferentes recursos`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Use 'automacao backup --help' para ver os subcomandos")
    },
}

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

E cmd/backup_database.go:

package cmd

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

var backupDatabaseCmd = &cobra.Command{
    Use:   "database",
    Short: "Faz backup do banco de dados",
    Long:  `Executa dump do banco de dados PostgreSQL e compacta o resultado`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Executando backup do banco de dados...")
    },
}

func init() {
    backupCmd.AddCommand(backupDatabaseCmd)
}

Para testar:

go run main.go backup database

4. Trabalhando com flags e argumentos

Vamos enriquecer o comando backup database com flags e argumentos. Modifique cmd/backup_database.go:

package cmd

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

var (
    outputDir string
    compress  bool
)

var backupDatabaseCmd = &cobra.Command{
    Use:   "database [filepath]",
    Short: "Faz backup do banco de dados",
    Long:  `Executa dump do banco de dados PostgreSQL e salva no diretório especificado`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        filepath := args[0]
        fmt.Printf("Backup do banco de dados será salvo em: %s/%s\n", outputDir, filepath)
        if compress {
            fmt.Println("Compactação habilitada")
        }
    },
}

func init() {
    backupDatabaseCmd.Flags().StringVarP(&outputDir, "output", "o", "./backups", "Diretório de saída")
    backupDatabaseCmd.Flags().BoolVarP(&compress, "compress", "c", false, "Compactar o backup")
    backupCmd.AddCommand(backupDatabaseCmd)
}

Flags persistentes (disponíveis em todos os subcomandos) vs flags locais (apenas no comando atual):

// Flag persistente no comando backup
backupCmd.PersistentFlags().String("log-level", "info", "Nível de log")

// Flag local apenas no comando database
backupDatabaseCmd.Flags().Bool("dry-run", false, "Simular execução sem alterar dados")

5. Implementando lógica de automação prática

Vamos implementar um backup real usando os/exec. Modifique o Run do comando database:

package cmd

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

var (
    dbName   string
    dbUser   string
    outputDir string
    compress  bool
)

var backupDatabaseCmd = &cobra.Command{
    Use:   "database",
    Short: "Faz backup do banco de dados",
    Long:  `Executa dump do banco PostgreSQL usando pg_dump`,
    Run: func(cmd *cobra.Command, args []string) {
        timestamp := time.Now().Format("20060102_150405")
        filename := fmt.Sprintf("%s/backup_%s.sql", outputDir, timestamp)

        fmt.Printf("Iniciando backup do banco %s...\n", dbName)

        dumpCmd := exec.Command("pg_dump", "-U", dbUser, "-h", "localhost", dbName)
        output, err := os.Create(filename)
        if err != nil {
            fmt.Printf("Erro ao criar arquivo: %v\n", err)
            return
        }
        defer output.Close()

        dumpCmd.Stdout = output
        dumpCmd.Stderr = os.Stderr

        if err := dumpCmd.Run(); err != nil {
            fmt.Printf("Erro no backup: %v\n", err)
            return
        }

        fmt.Printf("Backup concluído: %s\n", filename)

        if compress {
            gzipCmd := exec.Command("gzip", filename)
            if err := gzipCmd.Run(); err != nil {
                fmt.Printf("Erro na compressão: %v\n", err)
                return
            }
            fmt.Printf("Arquivo compactado: %s.gz\n", filename)
        }
    },
}

func init() {
    backupDatabaseCmd.Flags().StringVarP(&dbName, "database", "d", "", "Nome do banco de dados (obrigatório)")
    backupDatabaseCmd.Flags().StringVarP(&dbUser, "user", "u", "postgres", "Usuário do banco")
    backupDatabaseCmd.Flags().StringVarP(&outputDir, "output", "o", "./backups", "Diretório de saída")
    backupDatabaseCmd.Flags().BoolVarP(&compress, "compress", "c", false, "Compactar com gzip")
    backupDatabaseCmd.MarkFlagRequired("database")
    backupCmd.AddCommand(backupDatabaseCmd)
}

6. Boas práticas para automações internas

Tratamento de erros com mensagens amigáveis:

if err := dumpCmd.Run(); err != nil {
    cobra.CheckErr(fmt.Errorf("falha no backup: %w", err))
}

Logs estruturados com fmt para rastreamento:

fmt.Fprintf(os.Stderr, "[INFO] Iniciando backup às %s\n", time.Now().Format(time.RFC3339))

Testes unitários com testing:

package cmd

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

func TestBackupDatabaseCommand(t *testing.T) {
    cmd := &cobra.Command{}
    cmd.SetArgs([]string{"database", "--database=testdb"})
    err := cmd.Execute()
    if err != nil {
        t.Errorf("Comando falhou: %v", err)
    }
}

7. Distribuição e uso em equipe

Compile para binário único:

go build -o automacao main.go

Adicione ao PATH:

sudo mv automacao /usr/local/bin/

Ou crie um alias no shell:

alias auto='~/projetos/automacao-interna/automacao'

Documente os comandos gerando help automático:

automacao --help
automacao backup --help
automacao backup database --help

8. Exemplo completo: CLI de automação de deploys

Crie cmd/deploy.go:

package cmd

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

var (
    branch  string
    dryRun  bool
    timeout int
)

var deployCmd = &cobra.Command{
    Use:   "deploy [environment]",
    Short: "Faz deploy da aplicação",
    Long:  `Automação de deploy para ambientes staging e production`,
    Args: cobra.ExactValidArgs(1),
    ValidArgs: []string{"staging", "production"},
    Run: func(cmd *cobra.Command, args []string) {
        env := args[0]

        fmt.Printf("Iniciando deploy para %s\n", env)
        fmt.Printf("Branch: %s\n", branch)

        if dryRun {
            fmt.Println("[DRY-RUN] Simulação concluída")
            return
        }

        // Simula execução de deploy
        timeoutDur := time.Duration(timeout) * time.Second

        deployCmd := exec.Command("sh", "-c", fmt.Sprintf(
            "echo 'Deployando %s em %s com timeout de %ds'",
            branch, env, timeout,
        ))

        deployCmd.Stdout = os.Stdout
        deployCmd.Stderr = os.Stderr

        if err := deployCmd.Run(); err != nil {
            fmt.Printf("Erro no deploy: %v\n", err)
            os.Exit(1)
        }

        fmt.Printf("Deploy para %s concluído com sucesso!\n", env)
        _ = timeoutDur // Usado em implementação real
    },
}

var deployStagingCmd = &cobra.Command{
    Use:   "staging",
    Short: "Faz deploy no ambiente de staging",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Executando deploy no staging...")
        // Reaproveita lógica do comando pai
    },
}

var deployProductionCmd = &cobra.Command{
    Use:   "production",
    Short: "Faz deploy no ambiente de produção",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Executando deploy na produção...")
        // Implementar validações extras
    },
}

func init() {
    deployCmd.Flags().StringVarP(&branch, "branch", "b", "main", "Branch para deploy")
    deployCmd.Flags().BoolVarP(&dryRun, "dry-run", "n", false, "Simular sem executar")
    deployCmd.Flags().IntVarP(&timeout, "timeout", "t", 300, "Timeout em segundos")

    deployCmd.AddCommand(deployStagingCmd)
    deployCmd.AddCommand(deployProductionCmd)
    rootCmd.AddCommand(deployCmd)
}

Uso completo:

# Deploy normal
automacao deploy staging --branch feature-x

# Simulação
automacao deploy production --branch main --dry-run

# Com timeout customizado
automacao deploy staging --branch hotfix --timeout 600

# Help completo
automacao deploy --help

O Cobra oferece uma base sólida para construir CLIs poderosos e bem estruturados. Com esses padrões, você pode criar automações internas que são fáceis de manter, testar e compartilhar com sua equipe.

Referências