- Das wahrgenommene Verhalten eines Terminals entsteht nicht nur aus Betriebssystem, Shell und Terminal-Emulator, sondern auch aus den Konventionen einzelner Programme – und diese Konventionen sind konsistenter, als man denkt
- POSIX behandelt vor allem das Zusammenspiel von Terminal-Emulator, Betriebssystem und Shell sowie einige zentrale Utilities, legt aber nicht das Detailverhalten von Programmen wie
htop fest
- Muster wie
Ctrl-C, q, Ctrl-D, 16 ANSI-Farben, readline-Keybindings, deaktivierte Farbausgabe bei Pipes und die Bedeutung von - als stdin/stdout wiederholen sich
- Verhaltensweisen, die sich wie Naturgesetze anfühlen, müssen tatsächlich von Programmen oder Eingabebibliotheken implementiert werden; Beispiele sind das Beenden von
ipython mit Ctrl-D und das Abbrechen der Suche in prompt-toolkit mit Ctrl-C
- Wenn man neue Terminal-Programme entwickelt oder das Verhalten von Tools verstehen will, ist es praktischer, zuerst auf Grunderwartungen und Ausnahmen zu schauen, statt von „alles ist möglich“ auszugehen
Die vier Akteure, die Terminal-Verhalten erzeugen
- Was im Terminal passiert, lässt sich meist als Ergebnis von vier gemeinsam wirkenden Akteuren verstehen
- Betriebssystem
- Shell
- Terminal-Emulator
- das aktuell laufende Programm, etwa
top, vim oder cat
- In Kombinationen wie bash, GNOME Terminal und Linux sind die ersten drei Elemente vergleichsweise gut bekannt, und einige Verhaltensweisen sind durch POSIX standardisiert
- Einzelne Programme scheinen zwar alles tun zu können, verhalten sich in der tatsächlichen Nutzung aber ziemlich konsistent
Wenige Standards, aber Konventionen
- Es gibt kaum echte Standards, die das Verhalten von Terminal-Programmen umfassend festlegen
- Zwei naheliegende Referenzen sind
- POSIX: behandelt vor allem das Zusammenspiel von Terminal-Emulator, Betriebssystem und Shell und legt auch einige Verhaltensweisen zentraler Utilities wie
cp fest
- command line interface guidelines: ein Leitfaden zu Konventionen für Kommandozeilen-Interfaces
- Diese Regeln sind keine Vorschriften, denen Programmierer zwingend folgen müssen, sondern eher technische Konventionen, die über 20 Jahre Terminal-Nutzung beobachtet wurden
- Es gibt viele Ausnahmen, und für Ausnahmen gibt es oft gute Gründe
Manche Regeln müssen Programme selbst implementieren
- Bei Speicherorten für Konfigurationsdateien,
--help-Ausgaben oder der Praxis, normale Ausgaben nach stdout und Fehler nach stderr zu schreiben, ist die Verantwortung des Programms relativ klar
- Manche Verhaltensweisen wirken dagegen selbstverständlich, müssen in Wirklichkeit aber vom Programm oder von einer Eingabebibliothek direkt behandelt werden
- Ein typisches Beispiel ist das Beenden eines REPL mit
Ctrl-D
- Bei
cat gibt das Betriebssystem im cooked mode ohne separate Implementierung EOF zurück
ipython implementiert die Verarbeitung von Ctrl-D über Code in prompt-toolkit
- Wenn man diese Implementierungsverantwortung versteht, wirken kleine Unterschiede im Detailverhalten zwischen Programmen weniger überraschend
Regel 1: Nicht-interaktive Programme beenden sich mit Ctrl-C
- Nicht-interaktive Programme beenden sich in der Regel, wenn man
Ctrl-C drückt
- Das liegt daran, dass
Ctrl-C standardmäßig beendet, wenn ein Programm keinen Handler für das Signal SIGINT setzt
- Diese Regel gilt nicht unverändert für interaktive Programme wie
python3, bc oder less
- In interaktiven Programmen kann
Ctrl-C statt des gesamten Programms die aktuell laufende Aufgabe abbrechen
- Beispiele sind eine Suche in
less oder gerade ausgeführter Python-Code in python3
- In der von
ipython verwendeten Eingabebibliothek prompt-toolkit gibt es Code, der mit Ctrl-C eine Suche abbricht
Regel 2: TUIs beendet man meist mit q
- TUI-Programme wie
less oder htop beendet man normalerweise durch Drücken von q
- Diese Regel gilt nicht für Programme, bei denen das Beenden mit
q nicht natürlich wäre
Regel 3: REPLs beendet man auf einer leeren Zeile mit Ctrl-D
- REPLs wie
python3 oder ed werden normalerweise beendet, wenn man auf einer leeren Zeile Ctrl-D drückt
- Führt man im cooked mode ein Programm wie
cat aus, gibt das Betriebssystem bei Ctrl-D auf einer leeren Zeile EOF zurück
sqlite3, python3, fish, bash und andere nutzen tatsächlich keinen cooked mode, implementieren diese Tastenkombination aber, um das Standardverhalten nachzuahmen
- Implementierungsbeispiele findet man an zwei Stellen
- Der Erlang-REPL ist eine Ausnahme, die sich nicht mit
Ctrl-D beendet
Regel 4: Nicht über die 16 Grundfarben hinausgehen
- Terminal-Programme verwenden meist kaum Farben jenseits der 16 ANSI-Farben
- Wenn man Hex-Farben direkt angibt, ist die Wahrscheinlichkeit groß, dass sie mit der Hintergrundfarbe des Nutzers kollidieren
- Zum Beispiel ist Text in
#EEEEEE auf weißem Hintergrund fast unsichtbar, kann auf dunklem Hintergrund aber gut aussehen
- Verwendet man die 16 Grundfarben, ist die Wahrscheinlichkeit höher, dass Nutzer sie im Terminal-Emulator passend zu ihrem Hintergrund eingestellt haben
- Diese Vorgehensweise reduziert außerdem Annahmen darüber, welche Farben der Terminal-Emulator unterstützt
- Eine Ausnahme sind Texteditoren
- Helix verwendet standardmäßig einen violetten Hintergrund, der keine ANSI-Grundfarbe ist
- Helix ist kein „zentrales“ Programm, und man geht davon aus, dass Nutzer, denen das Farbschema nicht gefällt, das Theme ändern können
Regel 5: Readline-Keybindings bis zu einem gewissen Grad unterstützen
- Wo es sinnvoll ist, unterstützen fast alle Programme readline-Keybindings bis zu einem gewissen Grad
- Viele Programme verwenden
readline nicht direkt, ahmen aber Keybindings im emacs/readline-Stil nach
- Nicht alle Programme ahmen sie exakt gleich nach
- atuin scheint
Ctrl-A als Prefix zu verwenden; deshalb bewegt Ctrl-A den Cursor nicht an den Zeilenanfang
- Diese Programme implementieren eigene Ausschneiden-/Einfügen-Puffer, sodass man mit
Ctrl-U eine Zeile löschen und sie anschließend mit Ctrl-Y wieder einfügen kann
- Es gibt auch Ausnahmen
- Programme wie
git, cat oder nc bieten abgesehen von Backspace, Ctrl-W und Ctrl-U keine Unterstützung für Zeilenbearbeitung
- Texteditoren haben jeweils ihre eigenen Bearbeitungsweisen
- Mehr zu diesem Thema steht in entering text in the terminal is complicated
Regel 5.1: Ctrl-W löscht das letzte Wort
- Abgesehen von Texteditoren habe ich kein Programm gesehen, bei dem
Ctrl-W nicht das letzte Wort löscht
- Im cooked mode löscht das Betriebssystem standardmäßig bei
Ctrl-W das letzte Wort und bei Ctrl-U die ganze Zeile
- Viele Programme ahmen dieses Standardverhalten nach
- Außerhalb von Texteditoren fällt keine Ausnahme ein
Regel 6: Farben bei Ausgabe in eine Pipe ausschalten
- Die meisten Programme deaktivieren Farben, wenn das Ausgabziel eine Pipe ist
- Beispiele
rg blah hebt blah bei Ausgabe ins Terminal hervor, schaltet die Hervorhebung aber aus, wenn in eine Pipe oder Datei ausgegeben wird
ls --color=auto verwendet Farben bei Ausgabe ins Terminal, aber nicht bei Ausgabe in eine Pipe
- Je nachdem, ob ins Terminal ausgegeben wird, kann sich auch das Format ändern
ls ordnet Dateien bei Terminal-Ausgabe in Spalten an
- ripgrep gruppiert Treffer zusammen mit Überschriften
- Wenn man Farben erzwingen möchte, kann man die Programmausgabe mit
unbuffer wie ein tty wirken lassen
unbuffer rg blah | less -R
- Manche Programme können Farben mit einem
--color-Flag erzwingen
- Das obige Beispiel ist auch mit
rg --color=always | less -R möglich
Regel 7: - bedeutet stdin oder stdout
- Übergibt man statt eines Dateinamens
-, liest ein Programm normalerweise in der passenden Richtung von stdin oder schreibt nach stdout
- Um Python-Code aus der Zwischenablage mit
black zu formatieren und wieder zu kopieren, kann man etwa Folgendes ausführen
pbpaste | black - | pbcopy
pbpaste ist ein Mac-Programm; unter Linux kann man mit xclip etwas Ähnliches tun
- Die meisten Programme scheinen diese Vorgehensweise zu implementieren, wenn sie sinnvoll ist; es kann aber viele Ausnahmen geben
Warum es lange dauert, diese Regeln zu lernen
- Solche Regeln lernt man nicht auf einmal explizit, sondern über mehrere Schritte hinweg
- Zuerst lernt man, dass es eine Regel gibt, etwa „
Ctrl-C beendet ein Programm“
- Dann stößt man auf Ausnahmen, zum Beispiel dass
find beendet wird, less aber nicht
- Unbewusst erkennt man das Muster, dass nicht-interaktive Programme beendet werden, interaktive Programme aber möglicherweise nur die aktuelle Aufgabe abbrechen
- Erst später kann man es als explizite Regel formulieren
- Viel Verständnis rund ums Terminal bleibt weiterhin auf der Ebene unbewusster Mustererkennung
- Wenn man Regeln schriftlich festhält, können andere Terminal-Verhalten etwas schneller lernen
1 Kommentare
Meinungen auf Hacker News
Außerdem sollten Terminalprogramme die standardmäßigen Vordergrund-/Hintergrundfarben der Nutzer respektieren und sie nicht ohne Grund ändern.
Selbst wenn Farben verwendet werden, muss der Text unabhängig vom Standardhintergrund/-vordergrund und von der Terminal-Farbpalette lesbar bleiben; wenn Bedeutung nur über Farbe vermittelt wird, geht sie beim Kopieren/Einfügen oder in Terminals ohne Farbunterstützung verloren.
Auch Emojis und unnötige Nicht-ASCII-Zeichen sollten mit Bedacht eingesetzt werden, und bei scrollender Standardausgabe ist es besser, wichtige Informationen wie vorübergehend angezeigte Warnungen oder Dateinamen nicht wieder zu löschen.
Wenn man viele auffällige Farben und Animationen verwenden will, sollte man zumindest eine leicht auffindbare Konfigurationsoption zum Abschalten anbieten.
Ich weiß nicht, ob die eigentliche Ursache behoben wurde, aber ich musste in mehreren Open-Source-Projekten Workaround-PRs erstellen, die die ursprüngliche künstlerische Absicht etwas zurücknahmen: https://github.com/docker-archive/toolbox/issues/695
Man kann allerdings benutzerdefinierte Farben anbieten.
Im Wesentlichen ist es eher eine Zusammenstellung von Dingen, von denen man annehmen kann, dass sie fast alle Terminalprogramme bereits befolgen.
Manche Autoren gehen heute davon aus, dass Nutzer gepatchte Fonts verwenden, und bauen ihre Tools entsprechend, was die Benutzbarkeit stark beeinträchtigen kann.
Guter Artikel.
Da erwähnt wurde, dass solche Konventionen schwer zu lernen sind, empfehle ich auch die Command Line Interface Guidelines, meine bevorzugte Referenz beim Nachdenken über CLIs: https://clig.dev
Sie enthalten die Regeln aus dem Originaltext, etwa Beenden mit Ctrl-C,
-für Standardeingabe und Farben in Pipes abschalten, sowie vieles mehr.Sie umfasst POSIX-, GNU- und Unix-Material, den Heroku-CLI-Guide und den Guide für 12-Factor-CLI-Apps.
Der Punkt „normale Ausgabe gehört auf stdout, Fehler auf stderr“ ist wirklich wichtig.
Standardausgabe sollte ausschließlich für die Daten vorgesehen sein, die das Programm erzeugen soll. Wenn der Nutzer JSON-Daten angefordert hat, darf auf der Standardausgabe exakt nur dieses JSON-Objekt stehen und absolut nichts anderes.
Der Name Standardfehler ist eigentlich etwas unglücklich; es ist eher ein „Standard-Benutzer-Stream“ für alle Nachrichten, die der Nutzer im Terminal lesen soll. Dazu gehören nicht nur Fehlermeldungen, sondern auch Statusmeldungen und ausführliche Ausgaben.
So lässt sich die Programmausgabe reibungslos an andere Programme pipen, während Nicht-Daten-Informationen weiterhin ins Terminal gehen oder an eine andere Stelle umgeleitet werden können.
Es wäre gut, wenn Programme leicht neue, zweckgebundene Terminal-Verbindungs-File-Deskriptoren anlegen und deren Nummern im Manual dokumentieren könnten, ähnlich wie Exit-Codes. Aus Kompatibilitätsgründen könnten die Standardwerte an stdout oder stderr gehen; ich denke darüber nach, damit etwas zu experimentieren.
Es gibt 6 verschiedene Streams, und jeder davon kann separat abgefangen oder anders konfiguriert werden: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_output_streams?view=powershell-7.4
grepnicht funktioniert.Noch ein paar Ergänzungen: Wenn readline-/Emacs-Keybindings wie Ctrl-E und Ctrl-W neu für dich sind, ist es gut zu wissen, dass die meisten macOS-Eingabequellen diese Keybindings verwenden.
Unter macOS kannst du Ctrl-E, Ctrl-W und Ctrl-U direkt jetzt in der Adressleiste des Browsers ausprobieren.
Wenn du ein Kommandozeilenprogramm verwendest, das keinerlei Zeilenbearbeitung unterstützt, kannst du
rlwrapinstallieren und die REPL darunter ausführen. Standard ML of New Jersey hat zum Beispiel zwar eine REPL, aber keine Zeilenbearbeitung; das lässt sich mitrlwrap smlnjausgleichen.Zu „nicht mehr als 16 Farben verwenden“ würde ich noch weiter gehen und sagen: „nicht mehr als 8 Farben verwenden oder Farben zumindest konfigurierbar machen“. Beliebte Farbschemata wie Solarized und das Standard-Base-16 verwenden „helle“ Farben teils für mehrere Graustufen, sodass das, was der Autor für hellgrün hält, in der Praxis derselbe Grauton wie normaler Text sein kann.
Bei meinem Terminalemulator Alacritty ist es genauso, und auch die meisten Datei-Explorer mit Tabs verhalten sich so. Soweit ich mich erinnere, gilt das inzwischen wohl auch für Windows Explorer, aber ich habe Windows schon länger nicht mehr benutzt.
Die Konvention, eine REPL mit Ctrl-D zu beenden, bringt mich in GHCi jedes Mal ins Stolpern
Normalerweise drücke ich Ctrl-D, bin verwirrt, weil nichts passiert, erinnere mich dann, dass das in GHCi nicht geht, und führe
:qausDann hat Ctrl-D am Anfang der Eingabe ein unsichtbares Zeichen eingefügt, sodass der Fehler „lexical error at character '\EOT'“ erscheint, und ich muss
:qnoch einmal ohne unsichtbares Präfix eingeben, damit GHCi beendet wirdIch bin mir nicht einmal sicher, was die andere Methode wäre; gibt es eine Einstellung, die dieses Verhalten ändert?
Was noch fehlt, ist das Verhalten,
~als Home-Verzeichnis zu behandelnDas scheint keine POSIX-API zu sein, sondern eine Shell-Funktion. In Go funktioniert
os.ReadFile("~/.bashrc")nicht, aber in der Shell wirdecho ~/~/~zu etwas wie/home/jrockway/~/~expandiertyourprogram ~/path/to/filefunktioniert immer, aber in einer REPL, die nach einem Dateinamen fragt, möglicherweise nicht. In der Praxis unterstützen viele Programme das, daher würde ich es zu den Dingen zählen, die „die meisten TUI-Programme tun“Bei der Wort-Expansion ist die Tilde-Expansion der erste Schritt; danach folgen Feldaufteilung, Pfadnamen-Expansion und Entfernen von Anführungszeichen
Die relevante Shell-Spezifikation steht unter https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_06, die vollständige Spezifikation ist unter https://pubs.opengroup.org/onlinepubs/9799919799/ zu finden
Wenn Tools Tilde-Expansion hinzufügen, müssen sie das Home-Verzeichnis des aktuellen Nutzers kennen, was die Implementierung komplizierter macht, und es kann zu unvollständigen Implementierungen kommen, bei denen nur
~, aber nicht~namefunktioniertIn POSIX-Shells wird
~namezum Home-Verzeichnis dieses Nutzers expandiert, daher müsste man die Home-Verzeichnisse beliebiger Nutzer inklusive Systemnutzer nachschlagen. Je nach NSS-Konfiguration kann das deutlich komplexer sein, als/etc/passwdzu lesenBei einer interaktiven TUI kann es nützlich sein, Tilde-Expansion zu implementieren, aber dann sollte sie so vollständig wie in der Shell sein. CLI-Programme sollten solche magischen Expansionen besser nicht vornehmen
Es stellen sich Fragen wie: Was passiert mit einer Datei namens
"~", was mit"~username", wie escaped bzw. quotet man das, was passiert, wenn$HOMEfehlt oder nicht dem tatsächlichen Home entspricht, und wie verhält es sich unter Windows?Eine weniger bekannte Tatsache: Wenn man bei ssh nach einem Zeilenumbruch
"~."eingibt, erzwingt der Client das Schließen der Verbindung, und der Server kann nichts dagegen tun; daher sollte man das besser nicht in seinen Workflow einbauen./myprogram --config=~/.config/myprogramNatürlich erlauben viele Flag-Parser die von der Shell expandierte Form
./myprogram --config ~/.config/myprogram, sodass sie das entweder wissen oder zufällig unterstützenAber wegen Muscle Memory tippe ich unbedingt das
=. Das ist wohl eine Angewohnheit aus der Flag-Parsing-Bibliothek bei einem früheren Arbeitgeber, und aus demselben Grund schreibe ich auch ständig--underscores_like_thisstatt--hyphens-like-normal-peopleBesonders weil
..auf Ebene der OS-API behandelt wird, liegt die Erwartung nahe, dass~unter POSIX genauso funktionieren würdeDer Hauptgrund, warum ich CLIs gegenüber GUIs und manchmal auch TUIs deutlich bevorzuge, ist, dass sie sich viel konsistenter anfühlen
Es gibt Konventionen, und in einer CLI ist es viel einfacher, alle Konventionen einzuhalten, als eine gute GUI zu entwerfen. Dadurch ist die Qualität am Ende tendenziell höher
Ich denke viel darüber nach, wie man diese Eigenschaft in GUIs übertragen könnte, aber die bisher besten Antworten sind ungefähr „viel Aufwand“ oder „die Erwartungen senken“
Andere Betriebssysteme haben es nicht geschafft, eine Meta-Taste für GUI-Shortcuts zu etablieren, sodass es viel mehr Konflikte wie
ctrl-cgibtDie Defaults des Application-Frameworks, auf dem native macOS-Apps basieren, liefern viele gemeinsame Tastaturaktionen mit. Wenn man die Standard-Menüleistenklasse und Standardbefehle verwendet, bekommt man Command O, N, Q, X, C, V usw. fast gratis dazu, und wenn man Standard-Textfelder verwendet, entstehen auch Navigationen mit Command/Option/Fn + Pfeiltasten automatisch
Windows ist in dieser Hinsicht ebenfalls ziemlich gut, aber dass die primäre Modifikatortaste Ctrl ist, kann die Nutzung terminalbasierter Apps stören, und die verschiedenen UI-Frameworks innerhalb des OS scheinen unterschiedliche Defaults zu haben
In der Linux-Welt wäre das wohl nur möglich, wenn jemand ein vollständiges Application-Framework entwirft, Distributionen es standardisieren und Apps portiert werden. Window-Manager wollen nicht vorschreiben, was innerhalb eines Fensters passiert, GUI-Toolkits wollen keine OS-weiten Defaults definieren, und Desktop-Umgebungen sowie Distributionen wollen nicht die Aufgabe übernehmen, ein UI-Framework durchzusetzen oder Ports vorzunehmen
Programme sollten keine Dateien im Home-Verzeichnis anlegen, sondern Einstellungen wie XDG_CONFIG_HOME respektieren
Es geht nicht darum, Anwendungen davon zu überzeugen, wie sie sich verhalten sollten, sondern darum, Verhalten aufzulisten, das man bei Anwendungen häufig sieht
Eine Ausnahme von der Regel „Ctrl-W sollte das letzte Wort löschen“ ist mysql(1)
mysql ist statt gegen readline gegen editline gelinkt, daher löscht Ctrl-W standardmäßig nicht das letzte Wort, sondern alles bis zum Zeilenanfang
Als ich das eine Zeit lang benutzen musste, sind sorgfältig geschriebene Queries komplett verschwunden, und es hat mich wirklich wahnsinnig gemacht
An der Uni hatte ich einmal das Pech, sqlplus ausprobieren zu wollen, um mich mit einer Oracle-Datenbank zu verbinden. Nicht nur funktionierten Terminal-Shortcuts wie Ctrl-W überhaupt nicht, man konnte den Cursor innerhalb der Zeile auch nicht nach vorn bewegen, und es gab keine History-Navigation zu früheren Befehlen
Schon bei einem einzigen Tippfehler musste man alles von Anfang an neu eingeben. Mit einem einzelnen
/konnte man zwar den letzten Befehl unverändert ausführen, aber beim Korrigieren von Tippfehlern half das nichtAuch das Leeren des Bildschirms mit Ctrl-L ging nicht; stattdessen musste man
clear scrdirekt ausführen.clearallein als Befehl wäre wohl zu naheliegend gewesenIch würde gern ergänzen, dass langlebige Prozesse beim Empfang von SIGHUP ihre Konfiguration neu einlesen sollten
Das ist ein großer Unterschied, und leider ist dieser Punkt nicht verbreitet genug, um in die ursprüngliche Liste zu gehören
Langlebige Prozesse, die auf SIGHUP reagieren, laufen normalerweise getrennt und ohne steuerndes TTY, daher ist es etwas weit hergeholt, sie als „Terminalprogramme“ zu bezeichnen
Man muss aufpassen, dass die Anwendung nicht nur den Konfigurationszustand, sondern auch daraus abgeleiteten Zustand nicht versehentlich cachet; sonst geraten beide auseinander
Bei einfachen Apps mag das leicht sein, aber die meisten Apps, die eine Konfigurationsdatei brauchen, sind nicht so einfach