La semaine dernière, l'équipe d'ingénierie d'OpenSauced a publié Pizza CLI, un outil de ligne de commande puissant et composable pour générer des fichiers CODEOWNER et s'intégrer à la plateforme OpenSauced. Construire des outils de ligne de commande robustes peut sembler simple, mais sans une planification minutieuse et des paradigmes réfléchis, les CLI peuvent rapidement se transformer en un fouillis de code difficile à maintenir et criblé de bogues. Dans cet article de blog, nous examinerons en profondeur comment nous avons construit cette CLI à l'aide de Go, comment nous organisons nos commandes à l'aide de Cobra et comment notre équipe d'ingénierie Lean itère rapidement pour créer des fonctionnalités puissantes.
Pizza CLI est un outil de ligne de commande Go qui exploite plusieurs bibliothèques standard. La simplicité, la vitesse et l'accent mis sur la programmation système de Go en font un choix idéal pour créer des CLI. À la base, Pizza-CLI utilise spf13/cobra, une bibliothèque d'amorçage CLI dans Go, pour organiser et gérer l'intégralité de l'arborescence des commandes.
Vous pouvez considérer Cobra comme l'échafaudage qui fait fonctionner une interface de ligne de commande elle-même, permet à tous les indicateurs de fonctionner de manière cohérente et gère la communication avec les utilisateurs via des messages d'aide et une documentation automatisée.
L'un des premiers (et plus grands) défis lors de la création d'une Go CLI basée sur Cobra est de savoir comment structurer tout votre code et vos fichiers. Contrairement à la croyance populaire, il n’existe aucune méthode prescrite pour procéder ainsi dans Go. Ni la commande go build ni l'utilitaire gofmt ne se plaindront de la façon dont vous nommez vos packages ou organisez vos répertoires. C'est l'une des meilleures parties de Go : sa simplicité et sa puissance permettent de définir facilement des structures qui fonctionnent pour vous et votre équipe d'ingénierie !
En fin de compte, à mon avis, il est préférable de penser et de structurer une base de code Go basée sur Cobra comme un arbre de commandes :
├── Root command │ ├── Child command │ ├── Child command │ │ └── Grandchild command
À la base de l'arborescence se trouve la commande racine : c'est l'ancre de l'ensemble de votre application CLI et obtiendra le nom de votre CLI. Attaché en tant que commandes enfants, vous disposerez d’un arbre de logique de branchement qui informe la structure du fonctionnement de l’ensemble de votre flux CLI.
L'une des choses qu'il est incroyablement facile de manquer lors de la création de CLI est l'expérience utilisateur. Je recommande généralement aux gens de suivre un paradigme de « nom verbe racine » lors de la création de commandes et de structures de commandes enfants, car il se déroule logiquement et conduit à d'excellentes expériences utilisateur.
Par exemple, dans Kubectl, vous verrez ce paradigme partout : « kubectl get pods », « kubectl apply… », ou « kubectl label pods… » Cela garantit un flux sensé dans la façon dont les utilisateurs interagiront avec votre ligne de commande. application et aide beaucoup lorsque l'on parle de commandes avec d'autres personnes.
En fin de compte, cette structure et cette suggestion peuvent vous éclairer sur la façon dont vous organisez vos fichiers et répertoires, mais encore une fois, c'est à vous de déterminer comment vous structurez votre CLI et de présenter le flux aux utilisateurs finaux.
Dans Pizza CLI, nous avons une structure bien définie où vivent les commandes enfants (et les petits-enfants ultérieurs de ces commandes enfants). Sous le répertoire cmd de leurs propres packages, chaque commande obtient sa propre implémentation. L'échafaudage de commande racine existe dans un répertoire pkg/utils car il est utile de considérer la commande racine comme un utilitaire de niveau supérieur utilisé par main.go, plutôt que comme une commande qui pourrait nécessiter beaucoup de maintenance. En règle générale, dans l'implémentation de votre commande racine Go, vous aurez beaucoup de paramètres de configuration standard auxquels vous ne toucherez pas beaucoup, donc c'est bien de supprimer ces éléments.
Voici une vue simplifiée de notre structure de répertoires :
├── main.go ├── pkg/ │ ├── utils/ │ │ └── root.go ├── cmd/ │ ├── Child command dir │ ├── Child command dir │ │ └── Grandchild command dir
Cette structure permet une séparation claire des préoccupations et facilite la maintenance et l'extension de la CLI à mesure qu'elle se développe et que nous ajoutons plus de commandes.
L'une des principales bibliothèques que nous utilisons dans Pizza-CLI est la bibliothèque go-git, une pure implémentation de git dans Go qui est hautement extensible. Lors de la génération CODEOWNERS, cette bibliothèque nous permet de parcourir le journal des références git, d'examiner les différences de code et de déterminer quels auteurs git sont associés aux attributions configurées définies par un utilisateur.
Itérer le journal git ref d'un dépôt git local est en fait assez simple :
// 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") }
Si vous créez une application basée sur Git, je vous recommande vivement d'utiliser go-git : il est rapide, s'intègre bien dans l'écosystème Go et peut être utilisé pour faire toutes sortes de choses !
Our engineering and product team is deeply invested in bringing the best possible command line experience to our end users: this means we’ve taken steps to integrate anonymized telemetry that can report to Posthog on usage and errors out in the wild. This has allowed us to fix the most important bugs first, iterate quickly on popular feature requests, and understand how our users are using the CLI.
Posthog has a first party library in Go that supports this exact functionality. First, we define a Posthog client:
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 }
Then, after initializing a new client, we can use it through the various struct methods we’ve defined. For example, when logging into the OpenSauced platform, we capture specific information on a successful login:
// 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 }
During command execution, the various “capture” functions get called to capture error paths, happy paths, etc.
For the anonymized IDs, we use Google’s excellent UUID Go library:
newUUID := uuid.New().String()
These UUIDs get stored locally on end users machines as JSON under their home directory: ~/.pizza-cli/telemtry.json. This gives the end user complete authority and autonomy to delete this telemetry data if they want (or disable telemetry altogether through configuration options!) to ensure they’re staying anonymous when using the CLI.
Our lean engineering team follows an iterative development process, focusing on delivering small, testable features rapidly. Typically, we do this through GitHub issues, pull requests, milestones, and projects. We use Go's built-in testing framework extensively, writing unit tests for individual functions and integration tests for entire commands.
Unfortunately, Go’s standard testing library doesn’t have great assertion functionality out of the box. It’s easy enough to use “==” or other operands, but most of the time, when going back and reading through tests, it’s nice to be able to eyeball what’s going on with assertions like “assert.Equal” or “assert.Nil”.
We’ve integrated the excellent testify library with its “assert” functionality to allow for smoother test implementation:
config, _, err := LoadConfig(nonExistentPath) require.Error(t, err) assert.Nil(t, config)
We heavily use Just at OpenSauced, a command runner utility, much like GNU’s “make”, for easily executing small scripts. This has enabled us to quickly onramp new team members or community members to our Go ecosystem since building and testing is as simple as “just build” or “just test”!
For example, to create a simple build utility in Just, within a justfile, we can have:
build: go build main.go -o build/pizza
Which will build a Go binary into the build/ directory. Now, building locally is as simple as executing a “just” command.
But we’ve been able to integrate more functionality into using Just and have made it a cornerstone of how our entire build, test, and development framework is executed. For example, to build a binary for the local architecture with injected build time variables (like the sha the binary was built against, the version, the date time, etc.), we can use the local environment and run extra steps in the script before executing the “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
We’ve even extended this to enable cross architecture and OS build: Go uses the GOARCH and GOOS env vars to know which CPU architecture and operating system to build against. To build other variants, we can create specific Just commands for that:
# 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}
Building the Pizza CLI using Go and Cobra has been an exciting journey and we’re thrilled to share it with you. The combination of Go's performance and simplicity with Cobra's powerful command structuring has allowed us to create a tool that's not only robust and powerful, but also user-friendly and maintainable.
We invite you to explore the Pizza CLI GitHub repository, try out the tool, and let us know your thoughts. Your feedback and contributions are invaluable as we work to make code ownership management easier for development teams everywhere!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!