Desenvolvimento de Ferramentas CLI
Lição 25: Desenvolvimento de Ferramentas CLI
Analogia do Mundo Real
Imagine que você está em frente a uma máquina de venda automática. Você pressiona um botão para selecionar uma bebida (passando argumentos), a máquina dispensa com base na sua escolha (executando a função correspondente) e, se você pressionar o botão errado, exibe "Seleção inválida" (validação de argumentos). Uma ferramenta CLI é como essa máquina de venda automática — os usuários inserem comandos e argumentos via linha de comando, o programa executa as operações correspondentes e exibe os resultados. Uma boa ferramenta CLI é como uma máquina de venda automática bem projetada: layout de botões claros, prompts de operação explícitos e feedback de erro amigável.
Conceitos Fundamentais
Go é naturalmente adequado para construir ferramentas CLI — ele compila para um único binário, suporta desenvolvimento multiplataforma e inicia rapidamente. Aqui estão os conceitos fundamentais:
| Conceito | Descrição |
|---|---|
os.Args |
Slice bruto de argumentos da linha de comando, [0] é o nome do programa |
Pacote flag |
Ferramenta de análise de argumentos fornecida pela biblioteca padrão |
Biblioteca cobra |
Framework CLI de terceiros que suporta subcomandos, auto-ajuda e completamento de shell |
| Subcomandos | Como git commit, docker run — o programa executa diferentes lógicas baseado no subcomando |
| Validação de argumentos | Verificar se a entrada do usuário é válida e fornecer mensagens de erro amigáveis |
| Entrada interativa | Ler a entrada do usuário em tempo de construção para experiências CLI interativas |
os.Args e o Pacote flag
os.Args é a maneira mais primitiva de obter argumentos, adequada para cenários simples; o pacote flag fornece análise de flags tipadas:
Estrutura da linha de comando:
programa [flags] [args]
↑ ↑ ↑
nome do programa flags argumentos posicionais
Exemplo:
go build -o myapp ./cmd
↑ ↑ ↑
go -o myapp ./cmd
A Biblioteca cobra
cobra é o framework CLI mais popular na comunidade Go, usado por projetos como kubectl, docker e hugo:
Componentes principais do cobra:
- RootCmd: comando raiz, ponto de entrada do programa
- SubCmd: subcomandos, como "add", "list", "delete"
- Run/RunE: funções de execução de comandos
- Flags: argumentos de flag vinculados a comandos
Sintaxe Básica e Uso
1. Conceitos Básicos de os.Args
package main
import (
"fmt"
"os"
)
func main() {
// os.Args é um slice de strings, o primeiro elemento é o nome do programa
args := os.Args
fmt.Printf("Número de argumentos: %d\n", len(args))
fmt.Printf("Nome do programa: %s\n", args[0])
// Iterar sobre todos os argumentos
if len(args) > 1 {
fmt.Println("Argumentos passados:")
for i, arg := range args[1:] {
fmt.Printf(" [%d] %s\n", i, arg)
}
}
}
# Executar teste
$ go run main.go hello world
Número de argumentos: 3
Nome do programa: /tmp/go-build.../main
Argumentos passados:
[0] hello
[1] world
os.Args[0] não é necessariamente o nome do programa que você digitou — é o caminho do executável passado pelo sistema operacional. Esteja ciente dessa diferença ao trabalhar multiplataforma.
2. Analisando Flags com o Pacote flag
package main
import (
"flag"
"fmt"
)
func main() {
// Definir argumentos de flag
name := flag.String("name", "World", "Seu nome")
age := flag.Int("age", 18, "Sua idade")
verbose := flag.Bool("v", false, "Saída detalhada")
// Analisar argumentos da linha de comando
flag.Parse()
// Usar valores analisados (nota: são ponteiros, precisam ser desreferenciados)
if *verbose {
fmt.Printf("[Debug] name=%s, age=%d\n", *name, *age)
}
fmt.Printf("Olá, %s! Você tem %d anos.\n", *name, *age)
// Obter argumentos não-flag (argumentos posicionais)
fmt.Printf("Argumentos restantes: %v\n", flag.Args())
}
$ go run main.go -name=Alice -age=25 -v
[Debug] name=Alice, age=25
Olá, Alice! Você tem 25 anos.
Argumentos restantes: []
$ go run main.go --help
Uso de main:
-age int
Sua idade (padrão 18)
-name string
Seu nome (padrão "World")
-v Saída detalhada
flag suporta dois estilos de atribuição: -flag=value e -flag value. No entanto, com o estilo -flag value, -flag deve ser imediatamente seguido pelo valor e não pode ser combinado com outras flags.
3. Usando Vinculação de Variáveis
package main
import (
"flag"
"fmt"
)
func main() {
// Usar vinculação de variáveis para operar diretamente em variáveis em vez de ponteiros
var host string
var port int
var debug bool
flag.StringVar(&host, "host", "localhost", "Endereço do servidor")
flag.IntVar(&port, "port", 8080, "Número da porta")
flag.BoolVar(&debug, "debug", false, "Habilitar modo debug")
flag.Parse()
fmt.Printf("Conectando a %s:%d (debug: %v)\n", host, port, debug)
}
StringVar/IntVar/BoolVar vinculam a variáveis existentes, adequadas para cenários onde a configuração precisa ser compartilhada em múltiplos lugares. Os correspondentes String/Int/Bool retornam ponteiros, adequados para uso local.
4. Estrutura Básica do cobra
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
func main() {
// Comando raiz
rootCmd := &cobra.Command{
Use: "myapp",
Short: "Uma aplicação CLI de exemplo",
Long: "Esta é uma aplicação CLI de exemplo construída com cobra.",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Bem-vindo ao myapp!")
},
}
// Subcomando
helloCmd := &cobra.Command{
Use: "hello [nome]",
Short: "Dizer olá",
Args: cobra.MinimumNArgs(1), // Pelo menos 1 argumento obrigatório
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Olá, %s!\n", args[0])
},
}
// Adicionar subcomando ao comando raiz
rootCmd.AddCommand(helloCmd)
// Executar
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
$ go run main.go
Bem-vindo ao myapp!
$ go run main.go hello Go
Olá, Go!
$ go run main.go --help
Uma aplicação CLI de exemplo
Uso:
myapp [comando]
Comandos Disponíveis:
hello Dizer olá
help Ajuda sobre qualquer comando
Flags:
-h, --help ajuda para myapp
--help e -h, não é necessário implementar manualmente. Ele também gera automaticamente dicas de uso quando os argumentos estão incorretos.
5. Vinculação de Flags do cobra
package main
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
)
func main() {
var name string
var count int
rootCmd := &cobra.Command{
Use: "greeter",
Short: "Ferramenta de saudação repetida",
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < count; i++ {
fmt.Printf("Olá, %s! (%d/%d)\n", name, i+1, count)
}
},
}
// Flags persistentes (aplicam-se a todos os subcomandos)
rootCmd.PersistentFlags().StringVarP(&name, "name", "n", "World", "Alvo da saudação")
// Flags locais (aplicam-se apenas ao comando atual)
rootCmd.Flags().IntVarP(&count, "count", "c", 1, "Contagem de repetição")
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
$ go run main.go -n Alice -c 3
Olá, Alice! (1/3)
Olá, Alice! (2/3)
Olá, Alice! (3/3)
6. Entrada Interativa
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Ler entrada de linha única
fmt.Print("Digite seu nome: ")
name, _ := reader.ReadString('\n')
name = strings.TrimSpace(name) // Remover caractere de nova linha
// Ler entrada com valor padrão
fmt.Print("Digite sua idade (padrão 18): ")
ageInput, _ := reader.ReadString('\n')
ageInput = strings.TrimSpace(ageInput)
if ageInput == "" {
ageInput = "18"
}
fmt.Printf("Olá, %s! Você tem %s anos.\n", name, ageInput)
}
bufio.NewReader é mais confiável que fmt.Scanln — ele lida corretamente com entradas contendo espaços e não vai truncar precocemente devido a caracteres de nova linha.
7. Entrada de Senha (Caracteres Ocultos)
package main
import (
"fmt"
"os"
"syscall"
"golang.org/x/term"
)
func main() {
fmt.Print("Digite a senha: ")
// Ler senha (caracteres não são exibidos)
password, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
fmt.Fprintf(os.Stderr, "Falha ao ler senha: %v\n", err)
return
}
fmt.Println() // Nova linha após a leitura
fmt.Printf("Tamanho da senha: %d\n", len(password))
}
golang.org/x/term é a biblioteca de extensão oficial do Go, fornecendo operações multiplataforma de terminal incluindo leitura de senha, controle de cursor e mais.
Código de Exemplo
Exemplo: Calculadora Básica de Linha de Comando (Dificuldade ⭐)
package main
import (
"flag"
"fmt"
"os"
"strconv"
)
func main() {
// Definir flags
op := flag.String("op", "add", "Tipo de operação: add, sub, mul, div")
verbose := flag.Bool("v", false, "Mostrar informações detalhadas")
flag.Parse()
// Verificar contagem de argumentos posicionais
args := flag.Args()
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "Erro: Pelo menos dois números são necessários como argumentos")
fmt.Fprintln(os.Stderr, "Uso: calc -op=add 10 20")
flag.Usage()
os.Exit(1)
}
// Analisar números
a, err := strconv.ParseFloat(args[0], 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Erro: Não foi possível analisar o número %q: %v\n", args[0], err)
os.Exit(1)
}
b, err := strconv.ParseFloat(args[1], 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Erro: Não foi possível analisar o número %q: %v\n", args[1], err)
os.Exit(1)
}
// Executar operação
var result float64
var opName string
switch *op {
case "add":
result = a + b
opName = "Adição"
case "sub":
result = a - b
opName = "Subtração"
case "mul":
result = a * b
opName = "Multiplicação"
case "div":
if b == 0 {
fmt.Fprintln(os.Stderr, "Erro: Divisor não pode ser zero")
os.Exit(1)
}
result = a / b
opName = "Divisão"
default:
fmt.Fprintf(os.Stderr, "Erro: Tipo de operação não suportado %q\n", *op)
os.Exit(1)
}
// Exibir resultado
if *verbose {
fmt.Printf("Operação: %s\n", opName)
fmt.Printf("Expressão: %g %s %g\n", a, map[string]string{
"add": "+", "sub": "-", "mul": "*", "div": "/",
}[*op], b)
}
fmt.Printf("Resultado: %g\n", result)
}
$ go run main.go -op=add 10 20
Resultado: 30
$ go run main.go -op=mul -v 3.5 4
Operação: Multiplicação
Expressão: 3.5 * 4
Resultado: 14
$ go run main.go -op=div 10 0
Erro: Divisor não pode ser zero
$ go run main.go 10
Erro: Pelo menos dois números são necessários como argumentos
Exemplo: Ferramenta Multi-Subcomando cobra — Gerenciador de Arquivos (Dificuldade ⭐⭐)
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
)
var verbose bool
func main() {
rootCmd := &cobra.Command{
Use: "filetool",
Short: "Ferramenta simples de gerenciamento de arquivos",
Long: "filetool é uma ferramenta CLI para visualizar e gerenciar arquivos.",
}
// Flags persistentes
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Saída detalhada")
// Adicionar subcomandos
rootCmd.AddCommand(infoCmd())
rootCmd.AddCommand(listCmd())
rootCmd.AddCommand(searchCmd())
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
// subcomando info: exibir detalhes do arquivo
func infoCmd() *cobra.Command {
return &cobra.Command{
Use: "info <caminho do arquivo>",
Short: "Exibir detalhes do arquivo",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
path := args[0]
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("não foi possível acessar %q: %w", path, err)
}
fmt.Printf("Nome do arquivo: %s\n", info.Name())
fmt.Printf("Tamanho: %d bytes\n", info.Size())
fmt.Printf("Modificado: %s\n", info.ModTime().Format("2006-01-02 15:04:05"))
fmt.Printf("É diretório: %v\n", info.IsDir())
fmt.Printf("Permissões: %s\n", info.Mode())
if verbose {
fmt.Printf("Caminho absoluto: ")
abs, err := filepath.Abs(path)
if err == nil {
fmt.Println(abs)
}
fmt.Printf("Extensão: %s\n", filepath.Ext(path))
}
return nil
},
}
}
// subcomando list: listar conteúdo do diretório
func listCmd() *cobra.Command {
var showHidden bool
cmd := &cobra.Command{
Use: "list [caminho do diretório]",
Short: "Listar conteúdo do diretório",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
dir := "."
if len(args) > 0 {
dir = args[0]
}
entries, err := os.ReadDir(dir)
if err != nil {
return fmt.Errorf("não foi possível ler o diretório %q: %w", dir, err)
}
fmt.Printf("Diretório: %s\n", dir)
fmt.Println(strings.Repeat("─", 50))
count := 0
for _, entry := range entries {
// Pular arquivos ocultos (a menos que -a seja especificado)
if !showHidden && strings.HasPrefix(entry.Name(), ".") {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
// Adicionar sufixo / aos diretórios
name := entry.Name()
if entry.IsDir() {
name += "/"
}
fmt.Printf(" %-30s %8d %s\n",
name,
info.Size(),
info.ModTime().Format("2006-01-02 15:04"))
count++
}
fmt.Printf("\nTotal %d itens\n", count)
return nil
},
}
cmd.Flags().BoolVarP(&showHidden, "all", "a", false, "Mostrar arquivos ocultos")
return cmd
}
// subcomando search: buscar arquivos por extensão
func searchCmd() *cobra.Command {
var maxDepth int
cmd := &cobra.Command{
Use: "search <extensão>",
Short: "Buscar arquivos por extensão",
Long: "Buscar arquivos com a extensão especificada no diretório atual e subdiretórios. Exemplo: filetool search .go",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ext := args[0]
if !strings.HasPrefix(ext, ".") {
ext = "." + ext
}
root := "."
found := 0
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil // Pular arquivos inacessíveis
}
// Verificar profundidade
if maxDepth > 0 {
depth := strings.Count(filepath.Clean(path), string(os.PathSeparator)) -
strings.Count(filepath.Clean(root), string(os.PathSeparator))
if depth > maxDepth {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
}
if !info.IsDir() && filepath.Ext(path) == ext {
fmt.Printf(" %s (%d bytes, %s)\n",
path,
info.Size(),
info.ModTime().Format("2006-01-02"))
found++
}
return nil
})
if err != nil {
return fmt.Errorf("erro na busca: %w", err)
}
fmt.Printf("\nEncontrados %d arquivos %s\n", found, ext)
return nil
},
}
cmd.Flags().IntVarP(&maxDepth, "depth", "d", 0, "Profundidade máxima de busca (0 significa ilimitado)")
return cmd
}
$ go run main.go info main.go
Nome do arquivo: main.go
Tamanho: 2048 bytes
Modificado: 2025-06-27 14:30:00
É diretório: false
Permissões: -rw-r--r--
$ go run main.go list -a
Diretório: .
──────────────────────────────────────────────────
.git/ 4096 2025-06-27 14:00
main.go 2048 2025-06-27 14:30
go.mod 128 2025-06-27 14:00
Total 3 itens
$ go run main.go search .go -d 2
./main.go (2048 bytes, 2025-06-27)
./internal/handler.go (1024 bytes, 2025-06-26)
Encontrados 2 arquivos .go
Exemplo: Aplicação CLI Todo Completa (Dificuldade ⭐⭐⭐)
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/spf13/cobra"
)
// Estrutura do item Todo
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
CreatedAt time.Time `json:"created_at"`
DoneAt *time.Time `json:"done_at,omitempty"`
}
// TodoList suporta persistência
type TodoList struct {
Todos []Todo `json:"todos"`
NextID int `json:"next_id"`
filePath string
}
// NewTodoList cria ou carrega uma lista de tarefas
func NewTodoList(filePath string) (*TodoList, error) {
list := &TodoList{
Todos: []Todo{},
NextID: 1,
filePath: filePath,
}
// Se o arquivo existir, carregar dados
if _, err := os.Stat(filePath); err == nil {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("falha ao ler arquivo de dados: %w", err)
}
if len(data) > 0 {
if err := json.Unmarshal(data, list); err != nil {
return nil, fmt.Errorf("falha ao analisar arquivo de dados: %w", err)
}
}
}
return list, nil
}
// Save salva no arquivo
func (tl *TodoList) Save() error {
// Garantir que o diretório existe
dir := filepath.Dir(tl.filePath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("falha ao criar diretório: %w", err)
}
data, err := json.MarshalIndent(tl, "", " ")
if err != nil {
return fmt.Errorf("falha ao serializar dados: %w", err)
}
return os.WriteFile(tl.filePath, data, 0644)
}
// Add adiciona uma nova tarefa
func (tl *TodoList) Add(title string) Todo {
todo := Todo{
ID: tl.NextID,
Title: title,
Done: false,
CreatedAt: time.Now(),
}
tl.Todos = append(tl.Todos, todo)
tl.NextID++
return todo
}
// Complete marca como concluída
func (tl *TodoList) Complete(id int) error {
for i := range tl.Todos {
if tl.Todos[i].ID == id {
if tl.Todos[i].Done {
return fmt.Errorf("tarefa #%d já está concluída", id)
}
tl.Todos[i].Done = true
now := time.Now()
tl.Todos[i].DoneAt = &now
return nil
}
}
return fmt.Errorf("tarefa #%d não encontrada", id)
}
// Delete exclui uma tarefa
func (tl *TodoList) Delete(id int) error {
for i, todo := range tl.Todos {
if todo.ID == id {
tl.Todos = append(tl.Todos[:i], tl.Todos[i+1:]...)
return nil
}
}
return fmt.Errorf("tarefa #%d não encontrada", id)
}
// Filter retorna uma lista filtrada
func (tl *TodoList) Filter(showDone bool) []Todo {
var result []Todo
for _, todo := range tl.Todos {
if showDone || !todo.Done {
result = append(result, todo)
}
}
return result
}
// dataFilePath retorna o caminho padrão do arquivo de dados
func dataFilePath() string {
home, err := os.UserHomeDir()
if err != nil {
return ".todo.json"
}
return filepath.Join(home, ".todo.json")
}
func main() {
dataFile := dataFilePath()
rootCmd := &cobra.Command{
Use: "todo",
Short: "Gerenciador de tarefas de linha de comando",
Long: `todo é um gerenciador de tarefas simples de linha de comando.
Os dados são armazenados em ~/.todo.json.`,
}
// subcomando add
var addCmd = &cobra.Command{
Use: "add <título da tarefa>",
Short: "Adicionar uma nova tarefa",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
title := strings.Join(args, " ")
list, err := NewTodoList(dataFile)
if err != nil {
return err
}
todo := list.Add(title)
if err := list.Save(); err != nil {
return err
}
fmt.Printf("✓ Tarefa #%d adicionada: %s\n", todo.ID, todo.Title)
return nil
},
}
// subcomando done
var doneCmd = &cobra.Command{
Use: "done <ID da tarefa>",
Short: "Marcar tarefa como concluída",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("ID de tarefa inválido: %q", args[0])
}
list, err := NewTodoList(dataFile)
if err != nil {
return err
}
if err := list.Complete(id); err != nil {
return err
}
if err := list.Save(); err != nil {
return err
}
fmt.Printf("✓ Tarefa #%d concluída!\n", id)
return nil
},
}
// subcomando list
var showAll bool
var listCmd = &cobra.Command{
Use: "list",
Short: "Visualizar lista de tarefas",
RunE: func(cmd *cobra.Command, args []string) error {
list, err := NewTodoList(dataFile)
if err != nil {
return err
}
todos := list.Filter(showAll)
if len(todos) == 0 {
if showAll {
fmt.Println("Nenhuma tarefa ainda.")
} else {
fmt.Println("Nenhuma tarefa pendente! Use -a para ver todas as tarefas.")
}
return nil
}
fmt.Printf("%-4s %-8s %-30s %s\n", "ID", "Status", "Título", "Criado")
fmt.Println(strings.Repeat("─", 60))
for _, todo := range todos {
status := "○"
if todo.Done {
status = "✓"
}
fmt.Printf("%-4d %-8s %-30s %s\n",
todo.ID,
status,
truncate(todo.Title, 28),
todo.CreatedAt.Format("01-02 15:04"))
}
// Estatísticas
total := len(list.Todos)
done := 0
for _, t := range list.Todos {
if t.Done {
done++
}
}
fmt.Printf("\nTotal %d tarefas, %d concluídas, %d pendentes\n", total, done, total-done)
return nil
},
}
listCmd.Flags().BoolVarP(&showAll, "all", "a", false, "Mostrar todas as tarefas (incluindo concluídas)")
// subcomando delete
var deleteCmd = &cobra.Command{
Use: "delete <ID da tarefa>",
Short: "Excluir uma tarefa",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("ID de tarefa inválido: %q", args[0])
}
list, err := NewTodoList(dataFile)
if err != nil {
return err
}
if err := list.Delete(id); err != nil {
return err
}
if err := list.Save(); err != nil {
return err
}
fmt.Printf("✓ Tarefa #%d excluída\n", id)
return nil
},
}
// subcomando clear
var clearCmd = &cobra.Command{
Use: "clear",
Short: "Limpar todas as tarefas concluídas",
RunE: func(cmd *cobra.Command, args []string) error {
list, err := NewTodoList(dataFile)
if err != nil {
return err
}
before := len(list.Todos)
var remaining []Todo
for _, todo := range list.Todos {
if !todo.Done {
remaining = append(remaining, todo)
}
}
list.Todos = remaining
removed := before - len(list.Todos)
if removed == 0 {
fmt.Println("Nenhuma tarefa concluída para limpar.")
return nil
}
if err := list.Save(); err != nil {
return err
}
fmt.Printf("✓ %d tarefas concluídas removidas\n", removed)
return nil
},
}
rootCmd.AddCommand(addCmd, doneCmd, listCmd, deleteCmd, clearCmd)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
// truncate trunca uma string
func truncate(s string, maxLen int) string {
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
return string(runes[:maxLen-2]) + ".."
}
$ todo add Aprender desenvolvimento CLI Go
✓ Tarefa #1 adicionada: Aprender desenvolvimento CLI Go
$ todo add Completar exercícios
✓ Tarefa #2 adicionada: Completar exercícios
$ todo add Escrever blog técnico
✓ Tarefa #3 adicionada: Escrever blog técnico
$ todo list
ID Status Título Criado
────────────────────────────────────────────────────────────
1 ○ Aprender desenvolvimento CLI Go 06-27 14:30
2 ○ Completar exercícios 06-27 14:31
3 ○ Escrever blog técnico 06-27 14:32
Total 3 tarefas, 0 concluídas, 3 pendentes
$ todo done 1
✓ Tarefa #1 concluída!
$ todo list -a
ID Status Título Criado
────────────────────────────────────────────────────────────
1 ✓ Aprender desenvolvimento CLI Go 06-27 14:30
2 ○ Completar exercícios 06-27 14:31
3 ○ Escrever blog técnico 06-27 14:32
Total 3 tarefas, 1 concluída, 2 pendentes
$ todo clear
✓ 1 tarefas concluídas removidas
Cenários do Mundo Real
Cenário 1: Validação de Argumentos e Validação Personalizada
package main
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"github.com/spf13/cobra"
)
// PortValidator valida números de porta
func PortValidator(s string) error {
port, err := strconv.Atoi(s)
if err != nil {
return fmt.Errorf("porta deve ser um número, recebido: %q", s)
}
if port < 1 || port > 65535 {
return fmt.Errorf("porta deve estar entre 1-65535, recebido: %d", port)
}
return nil
}
// EmailValidator validação simples de formato de email
func EmailValidator(email string) error {
if !strings.Contains(email, "@") {
return fmt.Errorf("formato de email inválido, símbolo @ ausente: %q", email)
}
parts := strings.SplitN(email, "@", 2)
if len(parts[0]) == 0 || len(parts[1]) == 0 {
return fmt.Errorf("formato de email inválido: %q", email)
}
if !strings.Contains(parts[1], ".") {
return fmt.Errorf("formato de domínio de email inválido: %q", email)
}
return nil
}
// HostPortValidator valida formato host:porta
func HostPortValidator(addr string) error {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return fmt.Errorf("formato deve ser host:porta, recebido: %q, erro: %v", addr, err)
}
if host == "" {
return fmt.Errorf("nome do host não pode ser vazio: %q", addr)
}
return PortValidator(port)
}
func main() {
var email string
var port int
var serverAddr string
rootCmd := &cobra.Command{
Use: "server",
Short: "Iniciar o servidor",
PreRunE: func(cmd *cobra.Command, args []string) error {
// Validar todos os argumentos antes da execução
if err := EmailValidator(email); err != nil {
return fmt.Errorf("email do admin: %w", err)
}
if err := HostPortValidator(serverAddr); err != nil {
return fmt.Errorf("endereço do servidor: %w", err)
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Iniciando servidor...\n")
fmt.Printf(" Endereço: %s\n", serverAddr)
fmt.Printf(" Admin: %s\n", email)
fmt.Printf(" Porta: %d\n", port)
},
}
rootCmd.Flags().StringVarP(&email, "email", "e", "admin@example.com", "Email do admin")
rootCmd.Flags().IntVarP(&port, "port", "p", 8080, "Número da porta")
rootCmd.Flags().StringVarP(&serverAddr, "addr", "a", "localhost:8080", "Endereço do servidor (host:porta)")
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Erro: %v\n", err)
os.Exit(1)
}
}
$ go run main.go --addr "invalid"
Erro: endereço do servidor: formato deve ser host:porta, recebido: "invalid", erro: address invalid: missing port in address
$ go run main.go --email "not-email"
Erro: email do admin: formato de email inválido, símbolo @ ausente: "not-email"
$ go run main.go -a "0.0.0.0:9090" -e "admin@mysite.com"
Iniciando servidor...
Endereço: 0.0.0.0:9090
Admin: admin@mysite.com
Porta: 8080
Cenário 2: Barra de Progresso e Saída Colorida
package main
import (
"fmt"
"math"
"os"
"strings"
"time"
"github.com/spf13/cobra"
)
// Constantes de cor ANSI
const (
ColorReset = "\033[0m"
ColorRed = "\033[31m"
ColorGreen = "\033[32m"
ColorYellow = "\033[33m"
ColorBlue = "\033[34m"
ColorCyan = "\033[36m"
ColorBold = "\033[1m"
)
// ProgressBar barra de progresso
type ProgressBar struct {
total int
current int
width int
label string
}
// NewProgressBar cria uma barra de progresso
func NewProgressBar(total int, label string) *ProgressBar {
return &ProgressBar{
total: total,
width: 40,
label: label,
}
}
// Update atualiza o progresso e exibe
func (p *ProgressBar) Update(current int) {
p.current = current
percent := float64(p.current) / float64(p.total)
filled := int(math.Round(percent * float64(p.width)))
bar := strings.Repeat("█", filled) + strings.Repeat("░", p.width-filled)
// Mudança de cor com o progresso
color := ColorYellow
if percent >= 0.8 {
color = ColorGreen
} else if percent >= 0.5 {
color = ColorCyan
}
fmt.Fprintf(os.Stderr, "\r%s %s[%s%s%s] %s%d/%d%s (%.0f%%)",
p.label,
ColorBold,
color, bar, ColorReset,
ColorBold, p.current, p.total, ColorReset,
percent*100)
if p.current >= p.total {
fmt.Fprintf(os.Stderr, "\n")
}
}
func main() {
rootCmd := &cobra.Command{
Use: "downloader",
Short: "Downloader de arquivos simulado (demo de barra de progresso)",
Run: func(cmd *cobra.Command, args []string) {
files := []string{
"go1.21.0.linux-amd64.tar.gz",
"docs.tar.gz",
"examples.zip",
}
totalSteps := 100
for _, file := range files {
fmt.Printf("\n%sBaixando: %s%s\n", ColorBlue, file, ColorReset)
bar := NewProgressBar(totalSteps, " Progresso")
for i := 0; i <= totalSteps; i++ {
bar.Update(i)
// Simular atraso de download
time.Sleep(20 * time.Millisecond)
}
fmt.Printf(" %s✓ Download completo%s\n", ColorGreen, ColorReset)
}
fmt.Printf("\n%s%sTodos os downloads concluídos!%s\n", ColorBold, ColorGreen, ColorReset)
},
}
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
$ go run main.go
Baixando: go1.21.0.linux-amd64.tar.gz
Progresso [████████████████████████████████████████] 100/100 (100%)
✓ Download completo
Baixando: docs.tar.gz
Progresso [████████████████████████████████████████] 100/100 (100%)
✓ Download completo
Todos os downloads concluídos!
❓ Perguntas Frequentes
P1: Como o pacote flag lida com nomes de flags duplicados?
// O pacote `flag` não permite definições de nomes de flags duplicados, ele vai dar panic
// Mas você pode definir flags com o mesmo nome em diferentes subcomandos (se estiver implementando a lógica de subcomandos você mesmo)
// Ao usar cobra, as Flags de cada comando são independentes e não conflitam
cmd1.Flags().StringVar(&name, "name", "", "nome do comando1")
cmd2.Flags().StringVar(&name, "name", "", "nome do comando2") // Completamente OK
P2: Como fazer o cobra suportar arquivos de configuração persistentes (como ~/.config/myapp.yaml)?
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func initConfig() {
viper.SetConfigName("config") // Nome do arquivo de configuração (sem extensão)
viper.SetConfigType("yaml")
viper.AddConfigPath("$HOME/.config/myapp")
viper.AddConfigPath(".")
// Substituição por variáveis de ambiente
viper.AutomaticEnv()
// Ler arquivo de configuração (permitir inexistência)
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Usando arquivo de configuração:", viper.ConfigFileUsed())
}
}
func main() {
rootCmd := &cobra.Command{
Use: "myapp",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
initConfig()
},
}
// ...
}
viper é o melhor companheiro do cobra, fornecendo leitura unificada de arquivos de configuração, variáveis de ambiente e flags da linha de comando. Prioridade: flags da linha de comando > variáveis de ambiente > arquivo de configuração > padrões.
P3: Qual é a diferença entre os.Args e flag.Args()?
// os.Args contém todos os argumentos brutos, incluindo o nome do programa
os.Args // ["myapp", "-v", "hello", "world"]
// Após flag.Parse():
// flag.Args() contém apenas argumentos não-flag (argumentos posicionais)
flag.Args() // ["hello", "world"]
// flag consumiu -v, ele não aparecerá em Args()
P4: Como lidar com completamento Tab de subcomandos?
# cobra tem geração de completamento de shell embutida
$ todo completion bash > /etc/bash_completion.d/todo
$ todo completion zsh > "${fpath[1]}/_todo"
$ todo completion fish > ~/.config/fish/completions/todo.fish
# Para flags personalizadas, você pode registrar funções de completamento
cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"json", "yaml", "table"}, cobra.ShellCompDirectiveNoFileComp
})
📖 Resumo
Nesta lição aprendemos o conteúdo principal do desenvolvimento de ferramentas CLI em Go:
| Tópico | Ferramenta | Pontos-Chave |
|---|---|---|
| Argumentos brutos | os.Args |
Slice de strings, [0] é o nome do programa |
| Análise de flags | Pacote flag |
Flags tipadas, auto-ajuda, argumentos posicionais |
| Framework CLI | cobra |
Subcomandos, flags persistentes, auto-completamento |
| Validação de argumentos | Funções personalizadas | Validadores PreRunE/Args |
| Entrada interativa | bufio/term |
Leitura de entrada de linha, entrada oculta de senha |
Pontos-Chave:
- O pacote
flagé suficiente para ferramentas simples, usecobrapara ferramentas complexas - As
PersistentFlagsdocobraaplicam-se a todos os subcomandos,Flagsaplicam-se apenas ao comando atual - A validação de argumentos deve ser tratada uniformemente em
PreRunE, mantendo a funçãoRunlimpa - Use arquivos JSON para persistência de dados, armazenados no diretório home do usuário
- Barras de progresso e saída colorida podem melhorar significativamente a experiência do usuário em ferramentas CLI
📝 Exercícios
Exercício 1: Visualizador de Variáveis de Ambiente
Escreva uma ferramenta CLI envtool que suporte os seguintes recursos:
# Listar todas as variáveis de ambiente
$ envtool list
# Buscar variáveis de ambiente contendo uma palavra-chave
$ envtool search PATH
# Obter o valor de uma variável de ambiente específica
$ envtool get GOPATH
# Definir uma variável de ambiente (apenas processo atual)
$ envtool set MY_VAR=hello
Requisito: implementar usando cobra, suportar flag --format=json|table para controlar o formato de saída.
Exercício 2: Gerador de Senhas
Escreva um gerador de senhas CLI passgen:
# Gerar senha padrão (16 caracteres, incluindo maiúsculas, minúsculas e números)
$ passgen
# Especificar comprimento e conjunto de caracteres
$ passgen -l 32 -s "abc123!@#"
# Gerar em lote
$ passgen -n 5
# Excluir caracteres ambíguos (0/O, 1/l/I)
$ passgen --no-ambiguous
Requisitos:
- Implementar usando o pacote
flag - A força da senha deve ser avaliada e exibida (fraca/média/forte)
- Os resultados gerados podem ser copiados para a área de transferência (opcional)
Exercício 3: Ferramenta de Processamento de Arquivos em Lote
Escreva uma ferramenta batch que suporte operações em lote de arquivos:
# Renomear em lote: adicionar prefixo
$ batch rename --prefix "2025_" *.jpg
# Conversão de caixa em lote
$ batch case --to lower *.TXT
# Estatísticas em lote
$ batch stats ./docs
# Busca e substituição em lote (simulado)
$ batch replace --old "foo" --new "bar" *.go
Requisito: implementar usando subcomandos cobra, cada operação como um subcomando independente.
Próxima Lição
Na próxima lição aprenderemos sobre Desenvolvimento de REST API, cobrindo como construir serviços RESTful API com Go, incluindo design de rotas, middleware, tratamento JSON, integração com banco de dados e outras habilidades práticas.



