Construindo um CLI com Go e Cobra
Se você já usou kubectl, hugo, gh (GitHub CLI) ou docker, usou um tool construído com Cobra. É a biblioteca padrão da indústria para criar CLIs em Go. Nesse artigo vamos construir um CLI completo do zero.
O que é Cobra?
Cobra é uma biblioteca Go para criar aplicações de linha de comando modernas. Ela fornece:
- Subcomandos (
app serve,app create,app delete) - Flags globais e por comando
- Help automático
- Shell completion para bash, zsh, fish, powershell
Instalação
mkdir meu-cli && cd meu-cli
go mod init meu-cli
go get github.com/spf13/cobra@latest
Estrutura do projeto
meu-cli/
├── go.mod
├── main.go
└── cmd/
├── root.go
├── add.go
├── list.go
└── delete.go
O comando root
Todo CLI Cobra começa com um comando raiz:
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "taskman",
Short: "Gerenciador de tarefas no terminal",
Long: "Taskman é uma CLI para gerenciar tarefas do dia a dia direto do terminal.",
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// main.go
package main
import "meu-cli/cmd"
func main() {
cmd.Execute()
}
Subcomando: add
// cmd/add.go
package cmd
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
)
type Tarefa struct {
ID int `json:"id"`
Titulo string `json:"titulo"`
Concluida bool `json:"concluida"`
CriadaEm time.Time `json:"criada_em"`
}
var addCmd = &cobra.Command{
Use: "add [título]",
Short: "Adiciona uma nova tarefa",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
titulo := args[0]
prioridade, _ := cmd.Flags().GetString("prioridade")
tarefas := carregarTarefas()
nova := Tarefa{
ID: len(tarefas) + 1,
Titulo: titulo,
CriadaEm: time.Now(),
}
tarefas = append(tarefas, nova)
salvarTarefas(tarefas)
fmt.Printf("Tarefa #%d adicionada: %s", nova.ID, titulo)
if prioridade != "" {
fmt.Printf(" [prioridade: %s]", prioridade)
}
fmt.Println()
},
}
func init() {
addCmd.Flags().StringP("prioridade", "p", "", "Prioridade (alta, media, baixa)")
rootCmd.AddCommand(addCmd)
}
Subcomando: list
// cmd/list.go
package cmd
import (
"fmt"
"os"
"text/tabwriter"
"github.com/spf13/cobra"
)
var listCmd = &cobra.Command{
Use: "list",
Short: "Lista todas as tarefas",
Aliases: []string{"ls"},
Run: func(cmd *cobra.Command, args []string) {
tarefas := carregarTarefas()
if len(tarefas) == 0 {
fmt.Println("Nenhuma tarefa encontrada.")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tTITULO\tSTATUS\tCRIADA EM")
for _, t := range tarefas {
status := "pendente"
if t.Concluida {
status = "concluída"
}
fmt.Fprintf(w, "%d\t%s\t%s\t%s\n",
t.ID, t.Titulo, status,
t.CriadaEm.Format("02/01/2006"),
)
}
w.Flush()
},
}
func init() {
rootCmd.AddCommand(listCmd)
}
Subcomando: delete
// cmd/delete.go
package cmd
import (
"fmt"
"strconv"
"github.com/spf13/cobra"
)
var deleteCmd = &cobra.Command{
Use: "delete [id]",
Short: "Remove uma tarefa pelo ID",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
id, err := strconv.Atoi(args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "ID inválido: %s\n", args[0])
return
}
tarefas := carregarTarefas()
for i, t := range tarefas {
if t.ID == id {
tarefas = append(tarefas[:i], tarefas[i+1:]...)
salvarTarefas(tarefas)
fmt.Printf("Tarefa #%d removida.\n", id)
return
}
}
fmt.Fprintf(os.Stderr, "Tarefa #%d não encontrada.\n", id)
},
}
func init() {
rootCmd.AddCommand(deleteCmd)
}
Funções auxiliares
// cmd/storage.go
package cmd
import (
"encoding/json"
"os"
)
const arquivoTarefas = "tarefas.json"
func carregarTarefas() []Tarefa {
dados, err := os.ReadFile(arquivoTarefas)
if err != nil {
return []Tarefa{}
}
var tarefas []Tarefa
json.Unmarshal(dados, &tarefas)
return tarefas
}
func salvarTarefas(tarefas []Tarefa) {
dados, _ := json.MarshalIndent(tarefas, "", " ")
os.WriteFile(arquivoTarefas, dados, 0644)
}
Testando
go build -o taskman .
# Adicionar tarefas
./taskman add "Estudar Go"
./taskman add "Escrever artigo" -p alta
# Listar
./taskman list
# ID TITULO STATUS CRIADA EM
# 1 Estudar Go pendente 20/12/2020
# 2 Escrever artigo pendente 20/12/2020
# Deletar
./taskman delete 1
# Ajuda
./taskman --help
./taskman add --help
Shell completion
Cobra gera completions automaticamente:
# Bash
taskman completion bash > /etc/bash_completion.d/taskman
# Zsh
taskman completion zsh > "${fpath[1]}/_taskman"
# Fish
taskman completion fish > ~/.config/fish/completions/taskman.fish
# PowerShell
taskman completion powershell > taskman.ps1
Global flags
Flags que se aplicam a todos os comandos:
// cmd/root.go
var verbose bool
func init() {
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Modo verboso")
}
Conclusão
Cobra é a escolha padrão para CLIs em Go por um motivo: a API é limpa, a documentação é excelente, e o ecossistema é vasto. Ferramentas como kubectl, Hugo, GitHub CLI, e CockroachDB usam Cobra.
Comece com um comando root, adicione subcomandos conforme necessário, e use flags para opções. O Cobra cuida do help, parsing e completions automaticamente.
Caso eu tenha falado alguma besteira, por favor, agradecerei correções e sugestões.