2 Punkte von GN⁺ 2024-02-07 | 1 Kommentare | Auf WhatsApp teilen
  • Eine als Open-Source-Dokument veröffentlichte Referenz, die Prinzipien für das Design von CLI-Programmen und konkrete Richtlinien als moderne Neuinterpretation der traditionellen UNIX-Philosophie zusammenfasst und sich vor allem an Entwickler richtet, die Kommandozeilen-Tools bauen
  • CLI hat sich von einer reinen Skripting-Plattform zu einer menschenzentrierten textbasierten UI weiterentwickelt, und entsprechend müssen auch die Designprinzipien aktualisiert werden
  • Komponierbarkeit (composability) und Benutzerfreundlichkeit stehen nicht im Widerspruch; wenn man UNIX-Konventionen wie Standard-Ein/Ausgabe, Pipes und Exit-Codes einhält, lassen sich beide zugleich erreichen
  • Bietet konkrete Empfehlungen auch für in der Praxis oft übersehene Details wie Hilfetexte, Fehlermeldungen, Ausgabeformate, Interaktivität und Konfigurationssysteme
  • Die Zukunftskompatibilität und das Vertrauen der Nutzer von CLI-Tools hängen von der Stabilität der Schnittstelle und von Transparenz bei Analytics-Daten ab; dieser Leitfaden definiert dafür eine Basislinie

Philosophie (Philosophy)

Menschenzentriertes Design

  • Traditionelle UNIX-Befehle wurden meist mit der Annahme entworfen, dass sie vor allem von anderen Programmen verwendet werden; heute werden CLIs jedoch überwiegend direkt von Menschen benutzt, daher ist Human-First-Design nötig
  • Früher war CLI „machine-first“, heute ist es zu einer „human-first“ textbasierten UI geworden

Kombinierbare kleine Bausteine

  • Der Kern der UNIX-Philosophie ist, kleine, einfache Programme zu kombinieren, um größere Systeme zu bauen, und das gilt bis heute
  • Standard-stdin/stdout/stderr, Signale und Exit-Codes sichern die Verbindung zwischen Programmen; JSON unterstützt zusätzlich strukturierteren Datenaustausch
  • Software wird zwangsläufig Teil eines größeren Systems, und ob sie darin ein gut funktionierender Baustein ist, entscheidet sich bereits im Design

Konsistenz

  • Terminal-Nutzer sind mit bestehenden Konventionen vertraut, daher wird empfohlen, dass CLI-Tools bestehenden Mustern folgen
  • Wenn Konsistenz jedoch die Benutzerfreundlichkeit verschlechtert, kann man Konventionen mit Bedacht brechen

Angemessene Informationsmenge

  • Wenn ein Befehl mehrere Minuten ohne jede Ausgabe wartet, ist das „zu wenig“ Information; wenn er massenhaft Debug-Logs ausgibt, ist das „zu viel“ Information
  • Ein gutes Gleichgewicht bei der Informationsmenge ist entscheidend dafür, dass Software Nutzer wirksam unterstützt

Auffindbarkeit (Ease of Discovery)

  • GUI zeigt alle Funktionen sichtbar auf dem Bildschirm, während CLI oft fälschlich als rein gedächtnisabhängig angesehen wird
  • Mit umfassenden Hilfetexten, guten Beispielen und Vorschlägen für den nächsten Befehl kann auch eine CLI leicht erlernbar gestaltet werden, indem man Techniken aus GUIs übernimmt

CLI als Gespräch

  • Die Nutzung einer CLI hat durch wiederholtes Ausprobieren und Scheitern eine dialogartige Struktur; Vorschläge zur Fehlerkorrektur, Anzeigen von Zwischenständen und Bestätigungen vor riskanten Aktionen sind Designtechniken, die das nutzen
  • Die schlechteste Interaktion ist ein feindseliger Dialog, der Nutzer hilflos macht; die beste ist ein angenehmer Austausch, der ein Gefühl von Erfolg vermittelt

Robustheit (Robustness)

  • Software muss tatsächlich robust sein und sich auch robust anfühlen
  • Zentrale Punkte sind der elegante Umgang mit unerwarteten Eingaben, die Wahrung von Idempotenz, Hinweise zum Fortschritt und Zurückhaltung beim Anzeigen von Stack-Traces
  • Wer komplexe Sonderfälle reduziert und Dinge einfach hält, erhöht die Robustheit

Empathie (Empathy)

  • CLI-Tools sind kreative Werkzeuge für Entwickler und sollten Freude bei der Nutzung machen
  • Das Design sollte so durchdacht sein, dass Nutzer spüren, dass das Tool auf ihrer Seite ist

Chaos

  • Die Terminal-Welt ist voller Inkonsistenzen, aber genau dieses Chaos ist auch eine Quelle freier Kreativität
  • „Wenn ein Standard der Produktivität oder der Benutzerzufriedenheit eindeutig schadet, wirf ihn weg.“ — Jef Raskin

Richtlinien — Die Grundlagen (The Basics)

  • Eine Library zum Parsen von Argumenten verwenden: empfohlene Libraries je Sprache sind unter anderem Go(Cobra, cli), Python(Click, Typer, Argparse), Rust(clap) und Node(oclif)
  • Bei Erfolg Exit-Code 0, bei Fehlschlag ein von 0 verschiedener Code zurückgeben — daran erkennen Skripte Erfolg oder Fehler
  • Standardausgabe an stdout, Meldungen wie Logs und Fehler an stderr senden

Richtlinien — Hilfe (Help)

  • Bei den Flags -h oder --help ausführlichen Hilfetext anzeigen; das gilt ebenso für Subcommands
  • Bei Ausführung ohne Argumente kompakte Hilfe anzeigen (mit Beschreibung, 1–2 Beispielen, Flag-Erklärungen und Hinweis auf --help)
    • jq wird als Beispiel für eine gute Umsetzung genannt
  • Verschiedene Formen von Hilfeanfragen wie --help, -h oder help subcommand allesamt unterstützen
  • Oben im Hilfetext Link zur Web-Dokumentation und Feedback-Kanal bereitstellen
  • Zuerst Beispiele zeigen — empfohlen wird ein Aufbau als Geschichte, die schrittweise zu komplexeren Anwendungsfällen führt
  • Häufig verwendete Flags und Befehle oben im Hilfetext platzieren (vgl. die Struktur von git)
  • Mit Formatierung wie fetten Überschriften die Scanbarkeit verbessern, dabei aber terminalunabhängige Methoden verwenden
  • Wenn Nutzer etwas falsch eingeben, kann das Tool die Absicht erraten und Korrekturen vorschlagen — automatische Ausführung sollte aber mit Vorsicht entschieden werden
    • Falsche Eingaben können logische Fehler und nicht bloß Tippfehler sein; außerdem entsteht bei automatischer Korrektur die Last, diese Syntax dauerhaft zu unterstützen

Richtlinien — Dokumentation (Documentation)

  • Webbasierte Dokumentation bereitstellen — unverzichtbar für Durchsuchbarkeit und das Teilen von Links
  • Terminalbasierte Dokumentation bereitstellen — bleibt mit der installierten Version synchron und ist auch offline zugänglich
  • man-Seiten in Betracht ziehen — sie können etwa mit Tools wie ronn erzeugt werden; empfohlen wird, den Zugriff auch per Subcommand zu ermöglichen, wie bei npm help ls

Richtlinien — Ausgabe (Output)

  • Menschliche Lesbarkeit zuerst — ob an Menschen ausgegeben wird, lässt sich über den TTY-Status erkennen
  • Text-Streams sind die universelle Schnittstelle von UNIX; daher sollte auch maschinenlesbare Ausgabe unterstützt werden
  • Wenn menschenfreundliche Ausgabe die Pipe-Kompatibilität beeinträchtigt, mit dem Flag --plain schlichte Textausgabe anbieten
  • Bei Übergabe des Flags --json Ausgabe im JSON-Format unterstützen
  • Ausgabe bei Erfolg knapp halten, wenn nicht nötig auch ganz weglassen — für Skripte Unterstützung zur Unterdrückung per Option -q anbieten
  • Nutzer über Statusänderungen informieren — ein gutes Beispiel ist, dass git push den Status des Remote-Branches ausgibt
  • Die Ausgabe so gestalten, dass man wie bei git status den aktuellen Systemzustand leicht prüfen und zugleich die nächsten Schritte erkennen kann
  • Farben gezielt einsetzen und sie bei Pipes, NO_COLOR, TERM=dumb, --no-color und ähnlichen Bedingungen zwingend deaktivieren
  • In Nicht-TTY-Umgebungen keine Animationen oder Spinner anzeigen (um CI-Logs nicht zu verschmutzen)
  • Emojis und Symbole nur verwenden, wenn sie die Verständlichkeit erhöhen (yubikey-agent wird als Beispiel genannt)
  • Informationen, die nur Entwickler verstehen, aus der Standardausgabe heraushalten und nur im Verbose-Modus zeigen
  • stderr nicht wie eine Log-Datei verwenden — standardmäßig möglichst keine Log-Level-Labels wie ERR oder WARN ausgeben
  • Bei sehr großen Ausgaben den Einsatz eines Pagers wie less erwägen — nur in TTY-Umgebungen aktivieren, empfohlen sind die Optionen less -FIRX

Richtlinien — Fehler (Errors)

  • Erwartbare Fehler in für Menschen verständliche Meldungen umschreiben (z. B. „chmod +w file.txt ausführen erforderlich“)
  • Ein gutes Signal-Rausch-Verhältnis wahren — Fehler desselben Typs unter einer gemeinsamen Überschrift bündeln
  • Wichtige Informationen ans Ende der Ausgabe setzen — roten Text gezielt und sparsam einsetzen
  • Bei unerwarteten Fehlern Debug-Informationen und Anleitung zum Einreichen eines Bug-Reports mit ausgeben
  • Die URL für Bug-Reports so gestalten, dass Informationen automatisch vorausgefüllt werden und das Einreichen leichter fällt

Richtlinien — Argumente und Flags (Arguments and Flags)

  • Argumente (args) sind positionsbasiert, Flags sind namensbasiert — Flags gegenüber Argumenten bevorzugen
  • Für alle Flags auch eine Langform anbieten (z. B. -h und --help gleichzeitig unterstützen)
  • Einzelzeichen-Flags nur für häufig genutzte Flags verwenden
  • Wenn es Standards gibt, standardisierte Flag-Namen verwenden (-f/--force, -q/--quiet, -v, --json usw.)
  • Standardwerte so setzen, dass sie für die meisten Nutzer geeignet sind
  • Wenn Argumente oder Flags fehlen, Eingabe per Prompt anfordern, jedoch in nicht-interaktiven Umgebungen niemals erzwingen
  • Vor riskanten Aktionen Bestätigung anfordern — je nach Risikostufe per y/n, per Dry-Run oder durch direkte Texteingabe
    • Unterschieden wird zwischen mild (Datei löschen), moderate (Verzeichnis löschen, Remote-Ressourcen ändern) und severe (gesamten Server löschen)
  • Bei Datei-Ein/Ausgabe mit - das Lesen/Schreiben über stdin/stdout unterstützen (z. B. curl ... | tar xvf -)
  • Über Flags keine Secrets direkt entgegennehmen — empfohlen werden etwa ein Flag wie --password-file oder die Nutzung von stdin (wegen Sichtbarkeit in ps-Ausgaben und Shell-Historie)

Richtlinien — Interaktivität (Interactivity)

  • Prompts und interaktive Elemente nur anzeigen, wenn stdin ein TTY ist
  • Wenn --no-input übergeben wird, alle Prompts deaktivieren
  • Bei Passworteingaben Echo deaktivieren (eingegebener Inhalt wird nicht auf dem Bildschirm angezeigt)
  • Klar darauf hinweisen, dass Nutzer jederzeit abbrechen könnenCtrl-C muss immer funktionieren

Richtlinien — Subcommands (Subcommands)

  • Zwischen Subcommands Konsistenz bei Flag-Namen und Ausgabeformaten wahren
  • Komplexe Tools sollten eine zweistufige Subcommand-Struktur im Format noun verb oder verb noun verwenden (z. B. docker container create)
  • Mehrdeutige oder sehr ähnliche Subcommand-Namen vermeiden (z. B. update und upgrade nicht parallel verwenden)

Richtlinien — Robustheit (Robustness Guidelines)

  • Eingaben früh validieren und bei fehlerhaften Daten mit einer leicht verständlichen Fehlermeldung frühzeitig beenden
  • Reaktionsfähigkeit ist wichtiger als Geschwindigkeit — innerhalb von 100 ms irgendetwas ausgeben
  • Für lange laufende Aufgaben eine Fortschrittsanzeige (progress bar) bereitstellen — mögliche Libraries sind Python(tqdm), Go(schollz/progressbar) und Node(node-progress)
  • Bei Parallelverarbeitung darauf achten, dass Ausgaben nicht durcheinandergeraten
  • Netzwerk-Timeouts konfigurieren — inklusive Standardwerten, um endloses Warten zu verhindern
  • Nach temporären Fehlern ein Fortsetzen vom vorherigen Zustand bei Wiederholung ermöglichen
  • Crash-only-Design — eine Struktur wählen, die ohne Aufräumarbeiten sofort beendet werden kann, um Idempotenz zu sichern

Richtlinien — Zukunftskompatibilität (Future-proofing)

  • Änderungen abwärtskompatibel additiv halten
  • Vor Breaking Changes vorab im Programm warnen
  • Änderungen an menschenlesbarer Ausgabe sind im Allgemeinen zulässig — für Skripte sollte zur Nutzung von --plain oder --json geraten werden
  • Keine Catch-all-Subcommands — sonst kann später kein gleichnamiges echtes Subcommand mehr hinzugefügt werden
  • Keine automatische Zulassung von Subcommand-Abkürzungen — nur explizite Aliase erlauben und diese stabil halten
  • Keine „Zeitbomben“ — externe Abhängigkeiten minimieren, damit das Tool auch in 20 Jahren noch funktionieren kann

Richtlinien — Signale und Steuerzeichen (Signals)

  • Bei Empfang von Ctrl-C (INT-Signal) sofort beenden, für Aufräumarbeiten aber ein Timeout setzen
  • Während der Aufräumphase darauf hinweisen, dass ein erneutes Ctrl-C ein erzwungenes Beenden auslösen kann (siehe Beispiel Docker Compose)
  • Programme so entwerfen, dass sie auch starten können, wenn Aufräumarbeiten noch nicht abgeschlossen sind

Richtlinien — Konfiguration (Configuration)

Priorität bei der Anwendung von Konfiguration (hoch → niedrig):

  • Flags → aktuelle Shell-Umgebungsvariablen → Konfiguration auf Projektebene (.env) → Konfiguration auf Benutzerebene → systemweite Konfiguration

Empfehlungen nach Konfigurationstyp:

  • Einstellungen, die sich bei jedem Aufruf ändern (Debug-Level, Dry-Run): Flags verwenden

  • Einstellungen, die je nach Projekt oder Rechner variieren (Pfade, Farben, HTTP-Proxy): Kombination aus Flags und Umgebungsvariablen

  • Projektweit geteilte Einstellungen (Typ Makefile, package.json): versionierte Dateien verwenden

  • Die XDG Base Directory Spec einhalten — Konfigurationspfade auf Basis von ~/.config werden empfohlen (unterstützt etwa von yarn, fish, neovim, tmux)

  • Beim automatischen Ändern von Konfigurationsdateien anderer Programme unbedingt die Zustimmung des Nutzers einholen


Richtlinien — Umgebungsvariablen (Environment Variables)

  • Umgebungsvariablen eignen sich für Verhalten, das sich je nach Ausführungskontext ändert
  • Für Namen nur Großbuchstaben, Ziffern und Unterstriche verwenden; sie dürfen nicht mit einer Ziffer beginnen
  • Einzeilige Werte werden empfohlen — mehrzeilige Werte führen zu Kompatibilitätsproblemen mit dem Befehl env
  • Zuerst gängige Umgebungsvariablen wie NO_COLOR, DEBUG, EDITOR, HTTP_PROXY, SHELL, TMPDIR, HOME, PAGER prüfen
  • Unterstützung zum Einlesen projektspezifischer .env-Dateien wird empfohlen — .env ist jedoch kein Ersatz für eine formale Konfigurationsdatei
    • Grenzen von .env: nicht versioniert, keine Historie, nur der Typ String, anfällig für Encoding-Probleme
  • Keine Secrets aus Umgebungsvariablen lesen — sie werden an alle Prozesse vererbt, können in Logs auslaufen und über docker inspect oder systemctl show offengelegt werden
    • Secrets sollten nur über Credential-Dateien, Pipes, AF_UNIX-Sockets oder Secret-Management-Services entgegengenommen werden

Richtlinien — Benennung (Naming)

  • Einfache und gut merkbare Wörter verwenden — sind sie zu allgemein, drohen Konflikte mit anderen Befehlen
  • Nur Kleinbuchstaben und bei Bedarf Bindestriche verwenden (curl ist ein gutes Beispiel, DownloadURL ein schlechtes)
  • Kurz halten, aber extrem kurze Namen wie cd, ls, ps für allgemeine Utilities reservieren
  • Das Umbenennungsbeispiel des Vorläufers von Docker Compose von plum zu fig zu docker compose zeigt konkret, dass Tippkomfort ein wichtiges Kriterium bei der Benennung ist

Richtlinien — Distribution (Distribution)

  • Wenn möglich als einzelne Binärdatei verteilen — z. B. mit PyInstaller
  • Falls eine einzelne Binärdatei nicht möglich ist, plattformnative Paketinstaller verwenden
  • Deinstallationsmethode am Ende der Installationsanleitung angeben

Richtlinien — Analytics

  • Keine Nutzungs- oder Crash-Daten ohne Zustimmung des Nutzers übertragen
  • Falls Daten erhoben werden, klar offenlegen, was gesammelt wird, warum, wie anonymisiert wird und wie lange die Daten gespeichert werden
  • Standardmäßig wird Opt-in empfohlen — bei einem Opt-out-Modell muss dies beim ersten Start oder auf der Website klar mitgeteilt werden
    • Drei Beispiele werden vorgestellt: Angular.js (explizites Opt-in), Homebrew (Google Analytics, FAQ offengelegt), Next.js (anonyme Telemetrie standardmäßig aktiviert)
  • Als Alternativen zu Analytics kommen Messung der Web-Dokumentation, Zählung von Downloads und direkte Nutzerinterviews in Frage

1 Kommentare

 
GN⁺ 2024-02-07
Hacker-News-Kommentare
  • Viele Menschen wissen heute nicht, was die Kommandozeile ist, und interessieren sich auch nicht dafür, warum sie sie verwenden sollten.

    • In den 1980er Jahren war die Situation ähnlich, aber heute gibt es mehr Menschen als je zuvor, die sich mit der Kommandozeile auskennen. Man könnte von einem goldenen Zeitalter der CLI (Command Line Interface) sprechen.
  • In Skripten sollten keine beliebigen Abkürzungen von Subcommands erlaubt sein. Wenn man zum Beispiel statt mycmd install auch mycmd ins oder mycmd i zulässt, kann später kein neuer Befehl mehr hinzugefügt werden, der mit i beginnt.

    • In Skripten sollte man kurze Argumente vermeiden. Kurze Argumente sind eine Bequemlichkeit für Menschen, um Tipparbeit zu sparen, aber in Skripten ist explizites Ausschreiben kostengünstiger und angesichts des Verhältnisses von Lesen zu Schreiben vorzuziehen.
  • Man sollte die Option --dry-run in Betracht ziehen. Eine Funktion, die vorab zeigt, welche Aktionen ausgeführt würden, ohne tatsächlich Änderungen vorzunehmen, ist sehr nützlich, um ein Tool zu erlernen und zu prüfen, ob komplexe Optionen korrekt gesetzt wurden.

  • Wenn stdout kein interaktives Terminal ist, sollten keine Animationen angezeigt werden. Das verhindert, dass Fortschrittsbalken in CI-Logs wie ein Weihnachtsbaum aussehen.

    • Auf stdout sollten grundsätzlich keine Animationen angezeigt werden. stderr ist für Logging, Hinweise usw. gedacht, und stdout sollte unabhängig davon, ob es sich um ein TTY handelt, nützliche Ausgaben liefern.
  • Symbole und Emojis sollten nur verwendet werden, wenn sie die Klarheit erhöhen.

    • Symbole und Emojis können je nach Terminal unterschiedlich gerendert werden und je nach Geschmack der Nutzer unterschiedlich aufgenommen werden, daher sollten sie mit großer Vorsicht eingesetzt werden.
  • Die heutige Unix-Kommandozeile ist einerseits „überraschend nützlich“ und andererseits von „Designfehlern“ geprägt.

    • Warum die Unix-Kommandozeile nützlich ist, wird klar, wenn man bedenkt, wie viel Zeit dieselbe Aufgabe in C oder Rust kosten würde.
    • Die Designfehler rühren daher, dass eine Kommandozeilenschnittstelle gleichzeitig für Menschen und Maschinen lesbar sein soll. Für dieses Problem gibt es keine Standardlösung.
  • Außer wenn eine CLI sehr groß ist und Verschachtelung braucht (z. B. aws), bevorzugen die meisten Apps, alle Optionen in der Hilfe auszugeben und die Nutzer mit less das Gesuchte finden zu lassen.

  • Traditionell wurden UNIX-Befehle unter der Annahme geschrieben, dass sie hauptsächlich von anderen Programmen verwendet werden.

    • Tatsächlich waren sie für die interaktive Nutzung innerhalb einer Login-Shell gedacht. Es gab eine Trennung zwischen Programmen, die Ausgabe erzeugen, und „stillen“ Textfiltern, während komplexe Programme in C geschrieben wurden.
  • Passwörter sollten nicht aus Umgebungsvariablen gelesen werden.

    • Passwörter sollten nur über Credential-Dateien, Pipes, AF_UNIX-Sockets, Secret-Management-Dienste oder andere IPC-Mechanismen entgegengenommen werden.
  • Das umfassendste Buch zu CLI-Richtlinien stammt von Eric Raymond.

    • Es ist zwar schon lange her, aber beim Überfliegen von clig.dev wird deutlich, dass sich die Ansichten im Laufe der Zeit stark verändert haben.