Nesta aula, vamos explorar a criação de aplicações de linha de comando (CLI) em Go, uma das áreas mais comuns de uso da linguagem. CLIs são ferramentas essenciais para desenvolvedores e administradores de sistemas, e Go é uma excelente escolha para construí-las devido à sua simplicidade, desempenho e facilidade de distribuição como binários estáticos.

Começaremos com a biblioteca padrão flag, que fornece funcionalidades básicas de parsing de argumentos. Em seguida, avançaremos para a criação de subcomandos, padrão utilizado por ferramentas como git e docker. Depois, apresentaremos a biblioteca cobra, que é um padrão de facto para CLIs mais complexas, e finalizaremos com recomendações de estrutura de projeto para que sua aplicação seja organizada e escalável.

flag

A biblioteca padrão flag é a forma mais simples de processar argumentos de linha de comando em Go. Ela permite definir flags (opções) que podem ser do tipo string, inteiro, booleano, entre outros, e também aceita argumentos posicionais.

O pacote flag funciona registrando flags por meio de variáveis ou ponteiros, e então chamando flag.Parse() para analisar os argumentos fornecidos. Vamos ver um exemplo básico:

package main

import (
    "flag"
    "fmt"
)

func main() {
    // Definindo flags
    name := flag.String("name", "mundo", "um nome para cumprimentar")
    age := flag.Int("age", 0, "sua idade")
    verbose := flag.Bool("verbose", false, "modo verboso")

    // Parse dos argumentos
    flag.Parse()

    // Acessando os valores
    fmt.Printf("Olá, %s!\n", *name)
    if *age != 0 {
        fmt.Printf("Você tem %d anos.\n", *age)
    }
    if *verbose {
        fmt.Println("Modo verboso ativado.")
    }
}

Para executar: go run main.go -name=Maria -age=30 -verbose. O pacote flag também fornece a função flag.Args() para acessar argumentos posicionais após o parsing, e suporta flags com um ou dois traços (ex.: -name ou --name).

Uma limitação do flag é que ele não suporta subcomandos nativamente, e o tratamento de erros é básico. Para aplicações mais complexas, é recomendável usar bibliotecas como cobra.

Subcomandos

Subcomandos são uma forma de organizar uma CLI em comandos menores, cada um com suas próprias flags e argumentos. O exemplo clássico é o git, que tem comandos como git commit, git push, etc. Em Go, é possível implementar subcomandos manualmente, verificando o primeiro argumento posicional.

Vamos criar uma CLI simples com dois subcomandos: hello e bye. Cada um terá sua própria flag -name.

package main

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

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Uso: app <comando> [flags]")
        fmt.Println("Comandos disponíveis: hello, bye")
        return
    }

    // O primeiro argumento é o subcomando
    subcommand := os.Args[1]

    // Criar um novo FlagSet para cada subcomando
    fs := flag.NewFlagSet(subcommand, flag.ExitOnError)
    name := fs.String("name", "mundo", "um nome")

    // Parse dos argumentos após o subcomando
    fs.Parse(os.Args[2:])

    switch subcommand {
    case "hello":
        fmt.Printf("Olá, %s!\n", *name)
    case "bye":
        fmt.Printf("Adeus, %s!\n", *name)
    default:
        fmt.Printf("Comando desconhecido: %s\n", subcommand)
        os.Exit(1)
    }
}

Nesse exemplo, usamos flag.NewFlagSet para criar um conjunto de flags independente para cada subcomando. Isso permite que cada subcomando tenha suas próprias opções. O parsing é feito com fs.Parse(os.Args[2:]), ignorando o nome do comando.

Essa abordagem funciona, mas se torna trabalhosa quando há muitos comandos, flags compartilhadas ou ajuda automática. Para esses casos, bibliotecas como cobra oferecem uma solução mais robusta.

Bibliotecas (cobra, visão geral)

O cobra é uma biblioteca poderosa para criar CLIs modernas, com suporte a subcomandos, flags, help automático, autocompletar e muito mais. É usada por projetos famosos como Kubernetes, Hugo e GitHub CLI.

Para instalar: go get github.com/spf13/cobra. A estrutura básica de um comando cobra envolve a criação de um comando raiz e comandos filhos. Vamos ver um exemplo simples:

package main

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

func main() {
    var rootCmd = &cobra.Command{
        Use:   "app",
        Short: "Aplicação de exemplo",
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Println("Bem-vindo ao app!")
        },
    }

    var name string
    var helloCmd = &cobra.Command{
        Use:   "hello",
        Short: "Diz olá",
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Printf("Olá, %s!\n", name)
        },
    }
    helloCmd.Flags().StringVarP(&name, "name", "n", "mundo", "nome para cumprimentar")

    rootCmd.AddCommand(helloCmd)

    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
    }
}

O comando raiz pode ter um Run que é executado quando nenhum subcomando é fornecido. Os comandos filhos são adicionados com AddCommand. O cobra gera automaticamente a ajuda (--help) e o autocompletar para shells.

Além do cobra, existem outras bibliotecas como urfave/cli e kong, mas o cobra é o mais popular e completo.

Estrutura

Para projetos de CLI mais complexos, é importante organizar o código de forma modular. Uma estrutura comum é separar o comando raiz em um arquivo root.go e cada subcomando em um arquivo próprio, todos dentro de um pacote cmd.

Um layout típico para um projeto com cobra:

myapp/
├── main.go
├── cmd/
│   ├── root.go
│   ├── hello.go
│   └── bye.go
└── internal/
    └── ... (lógica de negócio)

O main.go apenas chama a função Execute do comando raiz. Cada arquivo no pacote cmd inicializa um comando e o adiciona ao comando pai. Isso facilita a adição de novos comandos sem alterar o código existente.

Além disso, é recomendável separar a lógica de negócio em pacotes internos (internal) para manter os comandos enxutos e testáveis. Também é importante usar variáveis de ambiente para configurações, e fornecer uma saída de ajuda clara.

Boas práticas e observações finais

Ao desenvolver CLIs, considere:

  • Use nomes de flags consistentes e forneça versões curtas quando fizer sentido.
  • Implemente o comando version para informar a versão do aplicativo.
  • Forneça mensagens de erro claras e códigos de saída adequados (0 para sucesso, diferente de 0 para erro).
  • Teste sua CLI usando o pacote testing e a função Execute com argumentos simulados.
  • Documente os comandos e flags usando os campos Long e Example no cobra.

Com essas ferramentas e práticas, você estará pronto para criar CLIs profissionais em Go.

Exercícios

  1. Crie uma CLI que aceite uma flag -name e imprima uma saudação. Use o pacote flag.
  2. ✓ Resposta:
    package main
    
    import (
        "flag"
        "fmt"
    )
    
    func main() {
        name := flag.String("name", "mundo", "um nome")
        flag.Parse()
        fmt.Printf("Olá, %s!\n", *name)
    }
    
  3. Modifique a CLI do exercício 1 para aceitar também uma flag -age e imprimir a idade.
  4. ✓ Resposta:
    package main
    
    import (
        "flag"
        "fmt"
    )
    
    func main() {
        name := flag.String("name", "mundo", "um nome")
        age := flag.Int("age", 0, "idade")
        flag.Parse()
        fmt.Printf("Olá, %s!\n", *name)
        if *age != 0 {
            fmt.Printf("Você tem %d anos.\n", *age)
        }
    }
    
  5. Implemente uma CLI com subcomandos add e sub que recebam dois inteiros como argumentos e imprimam o resultado.
  6. ✓ Resposta:
    package main
    
    import (
        "flag"
        "fmt"
        "os"
        "strconv"
    )
    
    func main() {
        if len(os.Args) < 2 {
            fmt.Println("Uso: calc <add|sub> <n1> <n2>")
            return
        }
        cmd := os.Args[1]
        if len(os.Args) < 4 {
            fmt.Println("Forneça dois números.")
            return
        }
        n1, err1 := strconv.Atoi(os.Args[2])
        n2, err2 := strconv.Atoi(os.Args[3])
        if err1 != nil || err2 != nil {
            fmt.Println("Números inválidos.")
            return
        }
        switch cmd {
        case "add":
            fmt.Println(n1 + n2)
        case "sub":
            fmt.Println(n1 - n2)
        default:
            fmt.Println("Comando desconhecido.")
        }
    }
    
  7. Crie uma CLI usando cobra com um comando greet que tenha uma flag --name e imprima uma saudação.
  8. ✓ Resposta:
    package main
    
    import (
        "fmt"
        "github.com/spf13/cobra"
    )
    
    func main() {
        var rootCmd = &cobra.Command{Use: "app"}
        var name string
        var greetCmd = &cobra.Command{
            Use: "greet",
            Run: func(cmd *cobra.Command, args []string) {
                fmt.Printf("Olá, %s!\n", name)
            },
        }
        greetCmd.Flags().StringVarP(&name, "name", "n", "mundo", "nome para cumprimentar")
        rootCmd.AddCommand(greetCmd)
        rootCmd.Execute()
    }
    
  9. Estruture um projeto CLI com cobra, separando o comando raiz e subcomandos em arquivos diferentes.
  10. ✓ Resposta:

    Crie a estrutura:

    mycli/
    ├── main.go
    └── cmd/
        ├── root.go
        └── hello.go
    

    main.go:

    package main
    
    import "mycli/cmd"
    
    func main() {
        cmd.Execute()
    }
    

    cmd/root.go:

    package cmd
    
    import (
        "fmt"
        "github.com/spf13/cobra"
    )
    
    var rootCmd = &cobra.Command{
        Use:   "mycli",
        Short: "CLI de exemplo",
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Println("Bem-vindo ao mycli!")
        },
    }
    
    func Execute() {
        rootCmd.Execute()
    }
    

    cmd/hello.go:

    package cmd
    
    import (
        "fmt"
        "github.com/spf13/cobra"
    )
    
    func init() {
        rootCmd.AddCommand(helloCmd)
    }
    
    var helloCmd = &cobra.Command{
        Use:   "hello",
        Short: "Diz olá",
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Println("Olá!")
        },
    }
    

Referências