Golang

Criando ferramentas de linha de comando com flag e Cobra Já leu

10 min de leitura

Criando ferramentas de linha de comando com flag e Cobra
Go é a linguagem de fato para ferramentas de linha de comando. Docker, Kubernetes, Terraform, GitHub CLI, Hugo e centenas de outras ferramentas amplamente usadas foram escritas em Go. As razões são concretas: compilação

Go é a linguagem de fato para ferramentas de linha de comando. Docker, Kubernetes, Terraform, GitHub CLI, Hugo e centenas de outras ferramentas amplamente usadas foram escritas em Go. As razões são concretas: compilação para binário estático sem dependências, distribuição trivial, startup instantâneo, cross-compilation nativa e performance próxima ao C.

Este artigo cobre duas abordagens: o pacote flag da biblioteca padrão para ferramentas simples, e o Cobra para CLIs com subcomandos, flags complexas e help automático — o padrão da indústria para ferramentas profissionais.


O pacote flag: para CLIs simples

package main

import (
    "flag"
    "fmt"
    "os"
)

func main() {
    // Definindo flags
    host := flag.String("host", "localhost", "endereço do servidor")
    porta := flag.Int("port", 8080, "porta do servidor")
    verbose := flag.Bool("verbose", false, "ativar saída detalhada")
    timeout := flag.Duration("timeout", 30*time.Second, "timeout das requisições")

    // Personalizar o texto de uso
    flag.Usage = func() {
        fmt.Fprintf(os.Stderr, "Uso: %s [opções]\n\n", os.Args[0])
        fmt.Fprintln(os.Stderr, "Opções:")
        flag.PrintDefaults()
    }

    flag.Parse()

    // Argumentos posicionais (após as flags)
    args := flag.Args()

    if *verbose {
        fmt.Printf("Conectando em %s:%d (timeout: %v)\n", *host, *porta, *timeout)
        fmt.Printf("Argumentos extras: %v\n", args)
    }

    fmt.Printf("Servidor: %s:%d\n", *host, *porta)
}
# Uso
./app -host=api.exemplo.com -port=443 -verbose arquivo.txt
./app --host api.exemplo.com --port 443
./app -help

O flag funciona bem para ferramentas com um único conjunto de opções. Quando a CLI cresce e precisa de subcomandos como git commit, git push, docker run, o Cobra é a escolha certa.


Cobra: o framework profissional para CLIs

go get github.com/spf13/cobra@latest
go get github.com/spf13/viper@latest  # para configuração

A estrutura de um projeto CLI com Cobra:

minha-cli/
├── go.mod
├── main.go
├── cmd/
│   ├── root.go       ← comando raiz
│   ├── serve.go      ← subcomando: minha-cli serve
│   ├── migrate.go    ← subcomando: minha-cli migrate
│   └── usuario/
│       ├── usuario.go    ← grupo: minha-cli usuario
│       ├── criar.go      ← minha-cli usuario criar
│       └── listar.go     ← minha-cli usuario listar
└── internal/
    └── config/
        └── config.go

Comando raiz

// cmd/root.go
package cmd

import (
    "fmt"
    "os"

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

var (
    cfgFile string
    verbose bool
    formato string
)

var rootCmd = &cobra.Command{
    Use:   "minha-cli",
    Short: "Ferramenta de gerenciamento da aplicação",
    Long: `minha-cli é uma ferramenta completa para gerenciar
a aplicação, incluindo servidor, banco de dados e usuários.

Documentação completa em: https://docs.exemplo.com`,
    // PersistentPreRunE executa antes de QUALQUER subcomando
    PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
        return inicializarConfig()
    },
}

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

func init() {
    cobra.OnInitialize(inicializarViper)

    // Flags persistentes — disponíveis em todos os subcomandos
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
        "arquivo de configuração (padrão: $HOME/.minha-cli.yaml)")
    rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v",
        false, "saída detalhada")
    rootCmd.PersistentFlags().StringVar(&formato, "format", "text",
        "formato de saída: text, json, yaml")
}

func inicializarViper() {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        home, err := os.UserHomeDir()
        cobra.CheckErr(err)

        viper.AddConfigPath(home)
        viper.AddConfigPath(".")
        viper.SetConfigType("yaml")
        viper.SetConfigName(".minha-cli")
    }

    viper.AutomaticEnv() // variáveis de ambiente sobrescrevem config

    if err := viper.ReadInConfig(); err == nil && verbose {
        fmt.Fprintln(os.Stderr, "Configuração:", viper.ConfigFileUsed())
    }
}

func inicializarConfig() error {
    if verbose {
        fmt.Printf("Usando banco: %s\n", viper.GetString("database.url"))
    }
    return nil
}

Subcomando: serve

// cmd/serve.go
package cmd

import (
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

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

var (
    serveHost string
    servePort int
)

var serveCmd = &cobra.Command{
    Use:   "serve",
    Short: "Inicia o servidor HTTP",
    Long:  `Inicia o servidor HTTP da aplicação na porta especificada.`,
    Example: `  minha-cli serve
  minha-cli serve --port 9090
  minha-cli serve --host 0.0.0.0 --port 443`,
    RunE: func(cmd *cobra.Command, args []string) error {
        return executarServidor(serveHost, servePort)
    },
}

func init() {
    rootCmd.AddCommand(serveCmd)

    serveCmd.Flags().StringVar(&serveHost, "host", "0.0.0.0",
        "endereço de escuta")
    serveCmd.Flags().IntVarP(&servePort, "port", "p", 8080,
        "porta de escuta")

    // Vincula flags com viper — permite sobrescrever via config ou env
    viper.BindPFlag("server.port", serveCmd.Flags().Lookup("port"))
    viper.BindPFlag("server.host", serveCmd.Flags().Lookup("host"))
}

func executarServidor(host string, porta int) error {
    addr := fmt.Sprintf("%s:%d", host, porta)

    mux := http.NewServeMux()
    mux.HandleFunc("/saude", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`{"status":"ok"}`))
    })

    servidor := &http.Server{
        Addr:         addr,
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        fmt.Printf("Servidor iniciado em http://%s\n", addr)
        if err := servidor.ListenAndServe(); err != http.ErrServerClosed {
            fmt.Fprintf(os.Stderr, "Erro: %v\n", err)
            os.Exit(1)
        }
    }()

    <-quit
    fmt.Println("\nEncerrando servidor...")

    ctx, cancelar := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancelar()

    return servidor.Shutdown(ctx)
}

Grupo de subcomandos: usuario

// cmd/usuario/usuario.go
package usuario

import (
    "github.com/spf13/cobra"
    "minha-cli/cmd"
)

var UsuarioCmd = &cobra.Command{
    Use:   "usuario",
    Short: "Gerencia usuários da aplicação",
    Long:  `Comandos para criar, listar, atualizar e remover usuários.`,
}

func init() {
    cmd.RootCmd.AddCommand(UsuarioCmd) // expondo RootCmd como variável exportada

    UsuarioCmd.AddCommand(criarCmd)
    UsuarioCmd.AddCommand(listarCmd)
    UsuarioCmd.AddCommand(deletarCmd)
}
// cmd/usuario/criar.go
package usuario

import (
    "fmt"
    "strings"

    "github.com/spf13/cobra"
)

var (
    criarNome  string
    criarEmail string
    criarPapel string
)

var criarCmd = &cobra.Command{
    Use:   "criar",
    Short: "Cria um novo usuário",
    Example: `  minha-cli usuario criar --nome "João Silva" --email joao@exemplo.com
  minha-cli usuario criar --nome "Admin" --email admin@exemplo.com --papel admin`,
    RunE: func(cmd *cobra.Command, args []string) error {
        return executarCriarUsuario(criarNome, criarEmail, criarPapel)
    },
}

func init() {
    criarCmd.Flags().StringVarP(&criarNome, "nome", "n", "", "nome completo (obrigatório)")
    criarCmd.Flags().StringVarP(&criarEmail, "email", "e", "", "email (obrigatório)")
    criarCmd.Flags().StringVar(&criarPapel, "papel", "usuario",
        "papel do usuário: usuario, admin, moderador")

    // Marca flags como obrigatórias
    criarCmd.MarkFlagRequired("nome")
    criarCmd.MarkFlagRequired("email")
}

func executarCriarUsuario(nome, email, papel string) error {
    papeis := map[string]bool{"usuario": true, "admin": true, "moderador": true}
    if !papeis[strings.ToLower(papel)] {
        return fmt.Errorf("papel inválido: %s (aceitos: usuario, admin, moderador)", papel)
    }

    fmt.Printf("Criando usuário...\n")
    fmt.Printf("  Nome:  %s\n", nome)
    fmt.Printf("  Email: %s\n", email)
    fmt.Printf("  Papel: %s\n", papel)

    // Aqui viria a lógica real de criação
    fmt.Println("✓ Usuário criado com sucesso")
    return nil
}
// cmd/usuario/listar.go
package usuario

import (
    "encoding/json"
    "fmt"
    "os"
    "text/tabwriter"

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

var (
    listarLimite int
    listarPapel  string
)

var listarCmd = &cobra.Command{
    Use:   "listar",
    Short: "Lista usuários cadastrados",
    RunE: func(cmd *cobra.Command, args []string) error {
        return executarListarUsuarios(listarLimite, listarPapel)
    },
}

func init() {
    listarCmd.Flags().IntVar(&listarLimite, "limite", 20, "máximo de registros")
    listarCmd.Flags().StringVar(&listarPapel, "papel", "", "filtrar por papel")
}

type UsuarioListado struct {
    ID    int    `json:"id"`
    Nome  string `json:"nome"`
    Email string `json:"email"`
    Papel string `json:"papel"`
    Ativo bool   `json:"ativo"`
}

func executarListarUsuarios(limite int, papel string) error {
    // Simulação — em produção buscaria do banco
    usuarios := []UsuarioListado{
        {1, "Ricardo Matos", "ricardo@exemplo.com", "admin", true},
        {2, "Ana Silva", "ana@exemplo.com", "usuario", true},
        {3, "Carlos Lima", "carlos@exemplo.com", "moderador", false},
    }

    if papel != "" {
        filtrados := usuarios[:0]
        for _, u := range usuarios {
            if u.Papel == papel {
                filtrados = append(filtrados, u)
            }
        }
        usuarios = filtrados
    }

    if len(usuarios) > limite {
        usuarios = usuarios[:limite]
    }

    // Formato de saída configurável
    formato := viper.GetString("format")
    switch formato {
    case "json":
        return json.NewEncoder(os.Stdout).Encode(usuarios)
    default:
        w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
        fmt.Fprintln(w, "ID\tNOME\tEMAIL\tPAPEL\tATIVO")
        fmt.Fprintln(w, "--\t----\t-----\t-----\t-----")
        for _, u := range usuarios {
            ativo := "sim"
            if !u.Ativo {
                ativo = "não"
            }
            fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n",
                u.ID, u.Nome, u.Email, u.Papel, ativo)
        }
        return w.Flush()
    }
}

Subcomando: migrate

// cmd/migrate.go
package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
)

var migrateCmd = &cobra.Command{
    Use:   "migrate",
    Short: "Gerencia migrações do banco de dados",
}

var migrateUpCmd = &cobra.Command{
    Use:   "up",
    Short: "Executa todas as migrações pendentes",
    RunE: func(cmd *cobra.Command, args []string) error {
        fmt.Println("Executando migrações...")
        // golang-migrate aqui
        fmt.Println("✓ Migrações concluídas")
        return nil
    },
}

var migrateDownCmd = &cobra.Command{
    Use:   "down [n]",
    Short: "Reverte a última migração (ou N migrações)",
    Args:  cobra.MaximumNArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        n := 1
        if len(args) > 0 {
            fmt.Sscan(args[0], &n)
        }
        fmt.Printf("Revertendo %d migração(ões)...\n", n)
        fmt.Println("✓ Concluído")
        return nil
    },
}

var migrateStatusCmd = &cobra.Command{
    Use:   "status",
    Short: "Exibe o status das migrações",
    RunE: func(cmd *cobra.Command, args []string) error {
        fmt.Println("Versão atual: 5")
        fmt.Println("Pendentes: 0")
        return nil
    },
}

func init() {
    rootCmd.AddCommand(migrateCmd)
    migrateCmd.AddCommand(migrateUpCmd)
    migrateCmd.AddCommand(migrateDownCmd)
    migrateCmd.AddCommand(migrateStatusCmd)
}

main.go: ponto de entrada enxuto

// main.go
package main

import "minha-cli/cmd"

func main() {
    cmd.Execute()
}

Completions: autocompletar no shell

O Cobra gera completions para bash, zsh, fish e PowerShell automaticamente:

var completionCmd = &cobra.Command{
    Use:   "completion [bash|zsh|fish|powershell]",
    Short: "Gera script de autocompletar para o shell",
    Args:  cobra.ExactArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        switch args[0] {
        case "bash":
            return rootCmd.GenBashCompletion(os.Stdout)
        case "zsh":
            return rootCmd.GenZshCompletion(os.Stdout)
        case "fish":
            return rootCmd.GenFishCompletion(os.Stdout, true)
        case "powershell":
            return rootCmd.GenPowerShellCompletion(os.Stdout)
        default:
            return fmt.Errorf("shell não suportado: %s", args[0])
        }
    },
}
# Instalando o autocompletar no bash
minha-cli completion bash > /etc/bash_completion.d/minha-cli

# Ou temporariamente na sessão atual
source <(minha-cli completion bash)

Saída colorida e progress

import "github.com/fatih/color"

var (
    verde    = color.New(color.FgGreen).SprintFunc()
    vermelho = color.New(color.FgRed).SprintFunc()
    amarelo  = color.New(color.FgYellow).SprintFunc()
    azul     = color.New(color.FgCyan).SprintFunc()
)

func imprimirStatus(ok bool, mensagem string) {
    if ok {
        fmt.Printf("%s %s\n", verde("✓"), mensagem)
    } else {
        fmt.Printf("%s %s\n", vermelho("✗"), mensagem)
    }
}

func imprimirAviso(mensagem string) {
    fmt.Printf("%s %s\n", amarelo("⚠"), mensagem)
}

func imprimirInfo(mensagem string) {
    fmt.Printf("%s %s\n", azul("ℹ"), mensagem)
}

Compilando e distribuindo

# Build padrão
go build -o minha-cli .

# Build com versão embutida
go build -ldflags="-X main.versao=1.2.0 -X main.build=$(date -u +%Y%m%d)" -o minha-cli .

# Cross-compilation para múltiplos sistemas
GOOS=linux   GOARCH=amd64 go build -o dist/minha-cli-linux-amd64 .
GOOS=darwin  GOARCH=arm64 go build -o dist/minha-cli-darwin-arm64 .
GOOS=windows GOARCH=amd64 go build -o dist/minha-cli-windows-amd64.exe .
// Versão embutida via ldflags
var (
    versao = "desenvolvimento"
    build  = "desconhecido"
)

var versaoCmd = &cobra.Command{
    Use:   "version",
    Short: "Exibe a versão da ferramenta",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("minha-cli v%s (build: %s)\n", versao, build)
    },
}

Resumo do que foi coberto

Este artigo apresentou CLIs em Go de forma completa: o pacote flag para ferramentas simples, o Cobra para CLIs com subcomandos e estrutura profissional, comando raiz com flags persistentes, grupos de subcomandos para organização hierárquica, flags obrigatórias, formato de saída configurável, completions para shells, validação de argumentos, saída colorida e compilação cruzada com versão embutida. O próximo artigo explora logging estruturado com slog e Zerolog.


Referências e leituras complementares

  • Documentação do Cobra — Guia oficial com todos os recursos. https://github.com/spf13/cobra

  • Cobra Generator — Ferramenta para gerar estrutura de projeto Cobra. https://github.com/spf13/cobra-cli

  • Viper — Biblioteca de configuração que integra com Cobra. https://github.com/spf13/viper

  • fatih/color — Saída colorida para terminais. https://github.com/fatih/color

  • olekukonko/tablewriter — Tabelas formatadas para saída de CLIs. https://github.com/olekukonko/tablewriter

  • Documentação flag — biblioteca padrão — Referência do pacote flag. https://pkg.go.dev/flag

Comentários

Mais em Golang

Composição vs herança: o jeito Go de reutilizar código
Composição vs herança: o jeito Go de reutilizar código

A herança é um dos pilares da orientação a objetos clássica. Em Java, C++ e C...

Operadores, expressões e conversão de tipo
Operadores, expressões e conversão de tipo

Se vari&aacute;veis s&atilde;o os substantivos de um programa, operadores s&a...

Artigo 46 — Injeção de dependência sem frameworks
Artigo 46 — Injeção de dependência sem frameworks

Artigo 46 — Injeção de dependência sem frameworks Dependências explícitas: a...