Wenn man mit Go schonmal die ersten Gehversuche gemacht hat, weiss man, dass es nicht immer leicht ist die ideale Library zu finden, wenn man das Rad nicht immer neu erfinden möchte. In seiner relativen kurzen Lebensdauer (Golang gibt es seit 2009), hat Go genug Erfolg gehabt, dass sich eine globale Community und ein entsprechendes Library-Ökosystem gebildet hat. Es gibt beispielsweise eine gepflegte Liste aller Go-relevanten Projekten und Libraries unter https://github.com/avelino/awesome-go. Dadurch wurden auch viele Problemstellungen mehrmals gelöst und als Repository (das Dependency Management bei Go beruht immer noch auf Git-Repositories) bereitgestellt.
Wir bei b-nova sehen stets darauf ab die Best Breeds im Einsatz zu haben. In letzter Zeit entwickeln wir zudem vermehrt Go-Projekte und möchten heute aus diesem Grund unsere Bread-&-Butter Go-Library vorstellen: nämlich das CLI-Duo Cobra und Viper.
Wer ist spf13?
Bevor wir uns Cobra und Viper weiter anschauen, möchte ich ein paar Worte zu Steve Francia, auch bekannt unter seinem Kürzel spf13, verlieren. Steve ist eine bekannte Grösse in der Go-Community und ist der amtierende Product Lead der Go-Programmiersprache bei Google. Er ist zudem der Erfinder und Hauptentwickler von Hugo, dem Static-Site-Generator (erfahre mehr SSGs wie Hugo in unserem TechUp über JAMstack), Cobra und Viper.
Cobra – Die Library für die CLI
Cobra ist eine vielfach genutzte Library um flink CLI-fähige Software in Go zu schreiben. Beispielsweise nutzen neben Hugo auch die allseits bekannte Kubernete-CLI oder die Github-CLI die Cobra-Library. Dabei stellt Cobra die Möglichkeit zur Verfügung POSIX-konforme Flags, verschachtelte Command-Strukturen, globale wie lokale Parameter, kustomizierte Hilfeausgaben oder Auto-Completions für die bekannten Shells wie Bash, Zsh oder sogar Powershell zu verwenden.
Cobra verwendet dabei ein geschickt erdachtes Strategy-Pattern, um alle möglichen Command-Strukturen abzubilden. Was es damit auf sich hat schauen wir uns jetzt kurz an.
Command-Line und CLI-Strukturen und Unix-Philosophie
Ein CLI-Tool haben wir alle sicher schonmal genutzt. Dabei gibt es ganz einfache Commands wie etwa ls
, cd
oder cat
, die man zur Navigation und/oder zum Lesen von Dateien nutzt. Es gibt aber auch CLI-Tools, die etwas mehr können und heutzutage oft zur Anwendung kommen. Ein gutes und bekanntes Beispiel wäre die CLI für Kubernetes: kubectl
. Damit kann man sich an einen Kubernetes-Cluster verbinden und den Cluster komplett per Command-Line bedienen. Falls man noch nie darüber nachgedacht hat, folgen so ziemlich alle CLI-Tools einer gewissen Konvention, sodass man intuitiv diese Tools nutzen kann, auch wenn man die spezifische Implementation eines neuen CLI-Tools noch nicht genau kennen sollte.
Diese Konvention ist auch als die Unix-Philosophie bekannt. Kurz zusammengefasst, und ich zitiere Peter H. Salus aus dem Jahre 1994, unterliegt ein jedes Tool folgender Eigenschaften (frei übersetzt):
-
Write programs that do one thing and do it well: Schreibe Programme die genau ein Ding gut machen
-
Write programs to work together: Schreibe Programme die gut miteinander zusammen arbeiten
-
Write programs to handle text streams, because that is a universal interface: Schreibe Programme die Text-Streams nutzen, die als universelle Schnittstelle gelten
Dies werden wir bei der Gestaltung unserer CLI so gut wie möglich im Hinterkopf behalten. Wir werden sehen, warum dies so wichtig ist. Als Nächstes möchten wir uns anschauen wie ein Nutzung (Usage) einer CLI aussieht. Diese unterliegt auch Konventionen, die sich aus den Prinzipien der Unix-Philosophie ableiten lassen.
Anhand des Beispiels der kubectl
werde ich kurz die Struktur erläutern. Zuerst den Top-Level Command. Das ist die Binary die aufgerufen wird. Im Beispiel oben ist dies democtl und kubectl ganz einfach kubectl. Danach kommt der eigentliche Command (wird auch oft als Verb bezeichnet) und kann beschreibt was gemacht werden muss.
|
|
Die Begriffe werden oft auch anders genannt und können alternativ dem unten stehenden Format entsprechen. Dies ist in erster Linie Geschmackssache, aber sicher gut zu wissen, wenn man die beiden Schreibweisen kennt.
|
|
Um Cobra anzuwenden, gilt es die Command-Struktur in den go-Dateien abzubilden. Dies kann für eine klassische CLI-Anwendung wie folgt aussehen:
|
|
Die commands.go stellt dabei die unterste Ebene, die rootCmd, der CLI-Anwendung dar. Diese implementiert dann jeweils dessen Sub-Commands. Hier eine Vorlage wie die Implementation dieser Cobra-Commands aussehen kann:
|
|
Viper – Die Library für die Configs
Viper ist genau wie Cobra eine Go-Library die ganze Konfiguration der Applikation übernimmt. Genau wie Cobra ist auch Viper in vielen Go-Projekten im Einsatz und stellt mittlerweile den Gold-Standard für Applikationskonfigurationen dar.
Unterstützte Formate und Config-Datenquellen:
-
JSON
-
TOML
-
YAML
-
HCL
-
envfile
-
Java properties Config-Dateien
-
Environment-Variablen
-
Remote Config-Systems wie etcd oder Consul (plus Live-Watching)
-
Buffer-Reading
Wichtige Eigenschaften die man bei Viper berücksichtigen muss:
-
Viper agiert wie ein Singleton: Trotzdem kann Viper auch mehrmals instanziiert werden.
-
Viper komplett eigenständig anwendbar: Viper kann ganz ohne Cobra eingesetzt werden.
-
Config-Keys sind Case-Insensitive: Da unterschiedliche Quellen für die Konfiguration angezogen werden, ist es unpraktisch Case-Sensitiveness zu gewährleisten
Die wichtigsten Aufrufe bei Viper in Kürze sind folgende:
|
|
In Eigenregie
Da wir in letzter Zeit ausführlich über den JAMstack berichtet haben, bauen wir jetzt gemeinsam ein einfaches CLI-Tool welches folgende Funktionalitäten bereitstellten soll:
-
eine beliebige Content Git-Repository klonen
-
Markdown-Files innerhalb der Repo in HTML konvertieren
-
der konvertierte Content anzeigen lassen
Mit dieser CLI können wir somit Markdown aus einer Git-Repository lokal per Terminal konvertieren lassen. Diese Funktionalität ist das Grundgerüst eines vollwertigen Static-Page-Generators wie beispielsweise Hugo oder Gatsby.
jamctl – unser CLI mit Cobra und Golang
In diesem Projekt fokussieren wir uns auf drei möglichen Befehle, auch Command genannt:
-
add / create
-
get
-
list
-
update
-
help
Installieren der Libraries
In unserem Go-Projekt installieren wir uns zunächst einmal die beiden Libraries wie folgt:
|
|
Für diese Übung habe ich eine Git-Repository auf Github, https://github.com/b-nova-techhub/jamctl, hochgeladen, welche Sie gerne dafür nutzen könnt. Diese beinhaltet die ganzen Go Source-Dateien, sowie ein Makefile womit man das Projekt durch bauen kann.
|
|
Projektstruktur
Die Projektstruktur lehnt sich an das obige Beispiel an und implementiert unter cmd/
die vier oben definierten Commands.
|
|
add-Command
Der add-Command fügt eine neue Content-Repository hinzu. Dabei wird geprüft ob die Repository bereits im Zielverzeichnis vorhanden ist. Falls nicht, wird diese dort ins Verzeichnis geklont.
|
|
Dies ist das cmd/add.go
das wir dem cmd-Projektverzeichnis hinzufügen müssen:
|
|
get-Command
Der get-Command liest das gewünschte Content-Repository aus und konvertiert alle darin befindlichen Markdown-Dateien nach HTML und gibt diese im StdOut aus.
|
|
Dies ist das cmd/get.go
das wir dem cmd-Projektverzeichnis hinzufügen müssen:
|
|
list-Command
Der list-Command prüft ob und welche Repositories bereits im Zielverzeichnis vorhanden sind und gibt dessen Projektnamen (Ordnernamen) als Liste wieder.
|
|
Dies ist das cmd/list.go
das wir dem cmd-Projektverzeichnis hinzufügen müssen:
|
|
update-Command
Der update-Command prüft ob eine Repository bereits vorhanden ist. Falls ja, wieder das entsprechende Verzeichnis gelöscht und die Repository an gleicher Stelle neu geklont.
|
|
Dies ist das cmd/update.go
das wir dem cmd-Projektverzeichnis hinzufügen müssen:
|
|
help-Command
Der help-Command gibt alle möglichen Commands wieder und zeigt auch welche Flags für welchen Command erlaubt sind. Der help-Command wird durch Cobra automatisch generiert und liest die deklarierten Metadaten dazu aus den entsprechenden cobra.Command
-Definitionen aus. Dieser help-Output wird auch beim einfachen Aufruf der ctl ausgegeben. Dieses Verhalten könnte man anpassen, aber in unserer Implementation ist dies so gelöst.
|
|
completion-Command
Der completion-Command ist auch ein von Cobra automatisch generierter Command. Dieser gibt eine Shell-spezifische Autocompletion-Deklaration heraus welche man für seine Shell der Wahl (bash, fish, powershell oder zsh) generieren lassen kann. Sehr nützlich wie ich finde.
|
|
Es sein angemerkt, dass Cobra auch automatisiert Dokumentation in unterschiedlichen Formaten generieren kann. Da wir dies hier jetzt noch gesondert behandeln können, empfehle ich einen Blick in die offizielle Dokumentation dazu: https://github.com/spf13/cobra/blob/master/doc/README.md
Verwendung der jamctl
Bevor wir die jamctl nutzen können müssen wir uns zuerst noch das Binary bauen. Dafür gibt es in der Git-Repository ein Makefile welches den Kompilierungssvorgang automatisiert. Es gibt wahlweise die Möglichkeit per make build
das Binary nach bin/
bauen lassen oder per make install
direkt nach den festgelegten $GOROOT
, standardmässig unter $GOPATH/bin
oder falls gesetzt $GOBIN
, wobei die jamctl ohne direkten Verweis aufrufbar wird.
Der einfachheitshalber, entscheiden wir uns für den letzteren Fall:
|
|
Ab jetzt können wir jamctl im Terminal aufrufen und unseres Projektes durchtesten. Falls Ihnen keine Content-Repository mit dazugehörigem Markdown-Files zur Hand haben, können Sie auch eine unserer Test-Repos nutzen: https://github.com/b-nova-openhub/jams-vanilla-content. Darin befinden sich unter content/de/
exemplarische Markdown-Dateien mit einem entsprechenden Metadaten-Header.
Dafür nutzen wir unseren add-Command wie folgt:
|
|
Jetzt können wir nachsehen, ob die Repository angelegt wurde und prüfen dass am besten mir der list-Command:
|
|
Falls Sie den --targetPath
nicht per Flag angeben haben, sollte die Repo unter /tmp/jamctl angelegt worden und dabei als Git-Repository geklont sein.
|
|
Sehr gut, scheint zu passen. Jetzt müssen wir erinnert werden, dass der Header durch ein HTML-Tag gekennzeichnet ist.
Als Nächstes wollen wir den Content als HTML ausgegeben bekommen. Dafür haben wir den get-Command welcher die Repository nach Markdown-Dateien parst und diese konvertiert.
|
|
Der Header dabei sieht wie folgt aus:
|
|
Wichtig dabei zu wissen, ist das die jetzige Implementation von jamctl auf den Delimiter hört und das Front-Matter (Metadaten) anhand davon ausliest. Dieser ist Default-mässig <content-header>
, dh. wir müssen unserer CLI per Flag sagen, dass der Delimiter in diesem Fall anders ist, nämlich ---
.
Als nächstes wollen wir den Content als HTML ausgegeben bekommen. Dafür haben wir den get-Command welcher die Repository nach Markdown-Dateien parst und diese konvertiert.
|
|
Globales Config-File
Die Konfigurationen die zur Laufzeit über die Argumente und die Flags eingegeben werden, können auch in einer Config-Datei abgespeichert werden. Die Idee dabei ist, dass die Config-Datei in einem Home-Verzeichnis liegt und somit die Konfiguration der Applikation darüber vorgenommen werden kann, oder zumindest alternativ, dass die Config-Datei per Flag-Parameter beim Aufruf von jamctl angegeben werden kann.
Die Config-Datei wird demnach unter dem Home-Verzeichnis ~/ liegen und sollte folgende Einträge beinhalten. Diese sind alle Konfigurationseinträge die wir während der Laufzeit nutzen:
|
|
Fazit
So, jetzt haben wir zusammen eine CLI mit Cobra in Go geschrieben. Es sei schonmal gesagt, es gibt bei der Implementation von Cobra und Viper noch viel mehr zu erwähnen, denn der Teufel liegt bekanntermassen im Detail. Aber ich denke, hier haben wir bereits das Grundwissen gelegt, um weitere Einarbeitung in das Ökosystem von Go zu fördern. Falls wir ihr Interesse geweckt haben, zögern Sie nicht uns um das nötige Fachwissen bei Ihrem nächsten Go-Projekt fragen.
Nächste Woche wird uns Ricky das Unit-Testing in Go beibringen und die gängigsten Implementation eines Test-Frameworks vorstellen. Stay tuned!
Weiterführende Links und Ressourcen
Quelle der Gopher-Illustration im Artikel ist Takuya Ueda
https://github.com/spf13/cobra
https://github.com/spf13/viper
https://medium.com/@skdomino/writing-better-clis-one-snake-at-a-time-d22e50e60056
https://www.educative.io/edpresso/how-to-use-cobra-in-golang
https://www.linode.com/docs/guides/using-cobra/
https://blog.knoldus.com/create-kubectl-like-cli-with-go-and-cobra/
https://levelup.gitconnected.com/exploring-go-packages-cobra-fce6c4e331d6