Na semana passada, a equipe de engenharia do OpenSauced lançou o Pizza CLI, uma ferramenta de linha de comando poderosa e combinável para gerar arquivos CODEOWNER e integração com a plataforma OpenSauced. Construir ferramentas robustas de linha de comando pode parecer simples, mas sem um planejamento cuidadoso e paradigmas bem pensados, as CLIs podem rapidamente se tornar uma confusão de código difícil de manter e repleta de bugs. Nesta postagem do blog, nos aprofundaremos em como construímos essa CLI usando Go, como organizamos nossos comandos usando Cobra e como nossa equipe de engenharia enxuta itera rapidamente para construir funcionalidades poderosas.
O Pizza CLI é uma ferramenta de linha de comando Go que utiliza várias bibliotecas padrão. A simplicidade, velocidade e foco na programação de sistemas do Go o tornam a escolha ideal para a construção de CLIs. Basicamente, o Pizza-CLI usa spf13/cobra, uma biblioteca de inicialização CLI em Go, para organizar e gerenciar toda a árvore de comandos.
Você pode pensar no Cobra como o andaime que faz uma interface de linha de comando funcionar, permite que todos os sinalizadores funcionem de forma consistente e lida com a comunicação com os usuários por meio de mensagens de ajuda e documentação automatizada.
Um dos primeiros (e maiores) desafios ao construir uma Go CLI baseada em Cobra é como estruturar todo o seu código e arquivos. Ao contrário da crença popular, não existe nenhuma maneira prescrita de fazer isso em Go. Nem o comando go build nem o utilitário gofmt reclamarão sobre como você nomeia seus pacotes ou organiza seus diretórios. Esta é uma das melhores partes do Go: sua simplicidade e poder facilitam a definição de estruturas que funcionam para você e sua equipe de engenharia!
Em última análise, na minha opinião, é melhor pensar e estruturar uma base de código Go baseada em Cobra como uma árvore de comandos:
├── Root command │ ├── Child command │ ├── Child command │ │ └── Grandchild command
Na base da árvore está o comando root: esta é a âncora para todo o seu aplicativo CLI e obterá o nome de sua CLI. Anexados como comandos filhos, você terá uma árvore de lógica de ramificação que informa a estrutura de como funciona todo o seu fluxo CLI.
Uma das coisas que é incrivelmente fácil de perder ao construir CLIs é a experiência do usuário. Normalmente recomendo que as pessoas sigam um paradigma de “verbo raiz substantivo” ao construir comandos e estruturas de comando filho, uma vez que flui logicamente e leva a excelentes experiências do usuário.
Por exemplo, no Kubectl, você verá este paradigma em todos os lugares: “kubectl get pods”, “kubectl apply …“ ou “kubectl label pods …” Isso garante um fluxo sensato de como os usuários irão interagir com sua linha de comando aplicativo e ajuda muito na hora de conversar sobre comandos com outras pessoas.
No final, essa estrutura e sugestão podem informar como você organiza seus arquivos e diretórios, mas, novamente, cabe a você determinar como estruturar sua CLI e apresentar o fluxo aos usuários finais.
No Pizza CLI, temos uma estrutura bem definida onde residem os comandos filhos (e os netos subsequentes desses comandos filhos). No diretório cmd em seus próprios pacotes, cada comando obtém sua própria implementação. A estrutura do comando root existe em um diretório pkg/utils, pois é útil pensar no comando root como um utilitário de nível superior usado por main.go, em vez de um comando que pode precisar de muita manutenção. Normalmente, em sua implementação do comando root Go, você terá muitos padrões de configuração que não mexerá muito, então é bom tirar essas coisas do caminho.
Aqui está uma visão simplificada de nossa estrutura de diretórios:
├── main.go ├── pkg/ │ ├── utils/ │ │ └── root.go ├── cmd/ │ ├── Child command dir │ ├── Child command dir │ │ └── Grandchild command dir
Essa estrutura permite uma separação clara de preocupações e torna mais fácil manter e estender a CLI à medida que ela cresce e adicionamos mais comandos.
Uma das principais bibliotecas que usamos no Pizza-CLI é a biblioteca go-git, uma implementação git pura em Go que é altamente extensível. Durante a geração de CODEOWNERS, esta biblioteca nos permite iterar o log de referência do git, observar as diferenças de código e determinar quais autores do git estão associados às atribuições configuradas definidas por um usuário.
Iterar o git ref log de um repositório git local é realmente muito simples:
// 1. Open the local git repository repo, err := git.PlainOpen("/path/to/your/repo") if err != nil { panic("could not open git repository") } // 2. Get the HEAD reference for the local git repo head, err := repo.Head() if err != nil { panic("could not get repo head") } // 3. Create a git ref log iterator based on some options commitIter, err := repo.Log(&git.LogOptions{ From: head.Hash(), }) if err != nil { panic("could not get repo log iterator") } defer commitIter.Close() // 4. Iterate through the commit history err = commitIter.ForEach(func(commit *object.Commit) error { // process each commit as the iterator iterates them return nil }) if err != nil { panic("could not process commit iterator") }
Se você estiver construindo um aplicativo baseado em Git, eu definitivamente recomendo usar o go-git: é rápido, integra-se bem ao ecossistema Go e pode ser usado para fazer todo tipo de coisa!
Nossa equipe de engenharia e produto está profundamente investida em trazer a melhor experiência de linha de comando possível para nossos usuários finais: isso significa que tomamos medidas para integrar a telemetria anônima que pode relatar ao Posthog sobre uso e erros existentes. Isso nos permitiu corrigir primeiro os bugs mais importantes, iterar rapidamente nas solicitações de recursos populares e entender como nossos usuários estão usando a CLI.
Posthog tem uma biblioteca própria em Go que suporta exatamente essa funcionalidade. Primeiro, definimos um cliente Posthog:
import "github.com/posthog/posthog-go" // PosthogCliClient is a wrapper around the posthog-go client and is used as a // API entrypoint for sending OpenSauced telemetry data for CLI commands type PosthogCliClient struct { // client is the Posthog Go client client posthog.Client // activated denotes if the user has enabled or disabled telemetry activated bool // uniqueID is the user's unique, anonymous identifier uniqueID string }
Então, após inicializar um novo cliente, podemos usá-lo através dos vários métodos struct que definimos. Por exemplo, ao fazer login na plataforma OpenSauced, capturamos informações específicas sobre um login bem-sucedido:
// CaptureLogin gathers telemetry on users who log into OpenSauced via the CLI func (p *PosthogCliClient) CaptureLogin(username string) error { if p.activated { return p.client.Enqueue(posthog.Capture{ DistinctId: username, Event: "pizza_cli_user_logged_in", }) } return nil }
Durante a execução do comando, as várias funções de “captura” são chamadas para capturar caminhos de erro, caminhos felizes, etc.
Para os IDs anonimizados, usamos a excelente biblioteca UUID Go do Google:
newUUID := uuid.New().String()
Esses UUIDs são armazenados localmente nas máquinas dos usuários finais como JSON em seu diretório inicial: ~/.pizza-cli/telemtry.json. Isso dá ao usuário final total autoridade e autonomia para excluir esses dados de telemetria, se desejar (ou desativar completamente a telemetria por meio de opções de configuração!) para garantir que permaneçam anônimos ao usar a CLI.
Nossa equipe de engenharia enxuta segue um processo de desenvolvimento iterativo, com foco no fornecimento rápido de recursos pequenos e testáveis. Normalmente, fazemos isso por meio de problemas do GitHub, pull requests, marcos e projetos. Usamos extensivamente a estrutura de testes integrada do Go, escrevendo testes de unidade para funções individuais e testes de integração para comandos inteiros.
Infelizmente, a biblioteca de testes padrão do Go não possui uma grande funcionalidade de asserção pronta para uso. É bastante fácil usar “==” ou outros operandos, mas na maioria das vezes, ao voltar e ler os testes, é bom poder observar o que está acontecendo com asserções como “assert.Equal” ou “assert.Nil ”.
Integramos a excelente biblioteca testify com sua funcionalidade “assert” para permitir uma implementação de teste mais suave:
config, _, err := LoadConfig(nonExistentPath) require.Error(t, err) assert.Nil(t, config)
Usamos muito o Just at OpenSauced, um utilitário de execução de comandos, muito parecido com o “make” do GNU, para executar facilmente pequenos scripts. Isso nos permitiu atrair rapidamente novos membros da equipe ou da comunidade para nosso ecossistema Go, já que construir e testar é tão simples quanto “apenas construir” ou “apenas testar”!
Por exemplo, para criar um utilitário de compilação simples no Just, dentro de um justfile, podemos ter:
build: go build main.go -o build/pizza
O que criará um binário Go no diretório build/. Agora, construir localmente é tão simples quanto executar um comando “apenas”.
Mas conseguimos integrar mais funcionalidades ao uso do Just e torná-lo a base de como toda a nossa estrutura de construção, teste e desenvolvimento é executada. Por exemplo, para construir um binário para a arquitetura local com variáveis de tempo de construção injetadas (como o sha contra o qual o binário foi construído, a versão, a data e hora, etc.), podemos usar o ambiente local e executar etapas extras no script antes de executar o “go build”:
build: #!/usr/bin/env sh echo "Building for local arch" export VERSION="${RELEASE_TAG_VERSION:-dev}" export DATETIME=$(date -u "%Y-%m-%d-%H:%M:%S") export SHA=$(git rev-parse HEAD) go build \ -ldflags="-s -w \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ -o build/pizza
Nós até estendemos isso para permitir arquitetura cruzada e construção de sistema operacional: Go usa os env vars GOARCH e GOOS para saber em qual arquitetura de CPU e sistema operacional construir. Para construir outras variantes, podemos criar comandos Just específicos para isso:
# Builds for Darwin linux (i.e., MacOS) on arm64 architecture (i.e. Apple silicon) build-darwin-arm64: #!/usr/bin/env sh echo "Building darwin arm64" export VERSION="${RELEASE_TAG_VERSION:-dev}" export DATETIME=$(date -u "%Y-%m-%d-%H:%M:%S") export SHA=$(git rev-parse HEAD) export CGO_ENABLED=0 export GOOS="darwin" export GOARCH="arm64" go build \ -ldflags="-s -w \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.Version=${VERSION}' \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.Sha=${SHA}' \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.Datetime=${DATETIME}' \ -X 'github.com/open-sauced/pizza-cli/pkg/utils.writeOnlyPublicPosthogKey=${POSTHOG_PUBLIC_API_KEY}'" \ -o build/pizza-${GOOS}-${GOARCH}
Construir o Pizza CLI usando Go e Cobra foi uma jornada emocionante e estamos entusiasmados em compartilhá-la com você. A combinação do desempenho e da simplicidade do Go com a poderosa estruturação de comandos do Cobra nos permitiu criar uma ferramenta que não é apenas robusta e poderosa, mas também fácil de usar e de fácil manutenção.
Convidamos você a explorar o repositório Pizza CLI GitHub, experimentar a ferramenta e nos contar sua opinião. Seus comentários e contribuições são inestimáveis enquanto trabalhamos para tornar o gerenciamento de propriedade de código mais fácil para equipes de desenvolvimento em todos os lugares!
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3