4 Punkte von GN⁺ 2023-09-10 | 1 Kommentare | Auf WhatsApp teilen
  • Ein langfristiges Erklärprojekt, das die Quellstruktur von GNU coreutils 8.3 Utility für Utility durchgeht und sich an Programmieranfänger richtet, die das Design von Kommandozeilen-Tools lernen möchten
  • Kein Nutzungshandbuch, sondern Begleitmaterial zum Lesen des Quellcodes; behandelt Namespaces, Ausführungsabläufe, Designentscheidungen, Algorithmen sowie die UNIX- und frühe Coreutils-Genealogie der einzelnen Befehle
  • coreutils besteht aus vielen kleinen C-Programmen mit jeweils einem Zweck; daher sollten auch aus moderner Sicht raue Codemuster wie globale Variablen, Makros, goto und lange Funktionen im historischen Kontext gelesen werden
  • Der gemeinsame Ablauf führt über Initialisierung, Options-Parsing, Eingabeverarbeitung, Ausführung von Systemaufrufen und Fehlerbehandlung; getopt_long(), fts und Helfer aus gnulib tauchen wiederholt auf
  • Vor Beiträgen oder Änderungen sollte zuerst geprüft werden, ob sich das Ziel mit vorhandenen Tools reproduzieren lässt, ob Abwärtskompatibilität gebrochen wird und wie stark es von POSIX abweicht

Umfang des Erklärprojekts zu GNU coreutils

  • Ein langfristiges Projekt zur Erklärung aller Utilities von GNU coreutils 8.3
  • Zielgruppe sind Programmieranfänger, die sich das Design von Kommandozeilen-Utilities ansehen
  • Am besten eignet sich das Material als Hintergrundinformation beim Lesen des source code eines interessierenden Utilities
  • Dieses Material ist kein Benutzerhandbuch; für die tatsächliche Nutzung sollten die man pages herangezogen werden
  • Das Projekt besteht aus mehreren Phasen
    • Phase 1: Abschluss der Seiten zu Namespace und Ausführungsüberblick für jedes Utility
    • Phase 2: Abschluss wichtiger Designentscheidungen und Algorithmen, der UNIX- und frühen Coreutils-Genealogie, nützlicherer Source-Walkthroughs und einer Visualisierung der Quellcode-Evolution
    • Phase indefinite: Zeilenweise Code-Walkthroughs der einzelnen Utilities bleiben ein langfristiges Vorhaben
    • Ein GitHub repo zum Sammeln zeilenweiser Notizen ist verfügbar

Erklärseiten pro Utility

  • Jeder Befehlsname verweist auf eine Detailseite, die das jeweilige Utility dekodiert
  • Jede Seite enthält Diskussion, Quellcode und Walkthrough
  • Fett markierte Utilities sind in Phase 2 erweiterte Einträge
  • Die Liste enthält GNU-coreutils-Befehle wie arch, base64, cat, chmod, cp, date, dd, df, ls, rm, sort, tail, tr, wc, yes und weitere

Voraussetzungen vor dem Lesen des Codes

  • Viele coreutils-Utilities haben eine fast 30-jährige Geschichte und wurden über lange Zeit von vielen Personen verändert
  • Die meisten sind kleine Programme, die auf einen einzelnen Zweck zugeschnitten sind
    • In der Regel handelt es sich um Programme aus einer einzelnen Quelldatei
    • Der Code ist nicht darauf ausgelegt, ewig zu überleben oder über seine Rolle hinaus zu wachsen
    • Man sieht Muster wie globale Variablen, Makros, goto und lange Funktionen mit verschachtelten switch-Anweisungen/Schleifen
  • POSIX-Kenntnisse sind wichtig, um den Code zu verstehen
    • Ein guter Einstieg sind die Utility Syntax Guidelines
    • POSIX definiert die Interoperabilität von Ein- und Ausgabe, überlässt die tatsächliche Arbeitsweise aber der Implementierung
    • GNU coreutils folgt POSIX nicht zwingend streng, doch Konzepte wie Berechtigungsbits, uid/gid, Umgebungsvariablen und Exit-Status sind tief verankert
  • Wegen Portabilitätsproblemen stützt sich coreutils stark auf gnulib
    • Fast jedes Utility enthält gnulib-Funktionen, die dafür ausgelegt sind, gemeinsame Probleme auf mehreren Systemen zu behandeln
  • coreutils setzt bei der Ausführung die Unterstützung einer Shell wie bash, zsh oder ksh voraus
    • Die Shell startet Utilities per fork/clone, übergibt Argumente, richtet die Umgebung ein, leitet Ein- und Ausgabe über Pipes um und bewahrt Rückgabewerte auf
  • GNU coreutils entstand ursprünglich aus drei Paketen für Shell-, Text- und Datei-Utilities; Utilities desselben Typs teilen viele Designmuster

Grundlegende Designmuster

  • Die meisten CLI-Utilities haben gemeinsam drei Phasen
    • eine Setup-Phase, die Flags, Optionen, Lokalisierung und Ähnliches vorbereitet
    • eine Argument-Parsing-Phase, die Eingaben liest und Ausführungsparameter festlegt
    • eine Verarbeitungs- und Ausführungsphase, die Eingaben vorbereitet und an einen oder mehrere Systemaufrufe übergibt
  • Während der Ausführung können an mehreren Stellen Bedingungen geprüft werden und Fehler auftreten
    • Unterschiedliche Exit-Status können andeuten, wo das Problem liegt
    • EXIT_FAILURE wird häufig als allgemeiner Fehlerstatus verwendet
  • Nach einem Fehler wird dem Benutzer Feedback gegeben
  • Utility-Varianten lassen sich in drei Gruppen einteilen
    • Trivial utilities: Definieren einige Zeilen Makros, includen dann den Quellcode eines anderen Utilities und erzwingen per Makro eine bestimmte Ablaufsteuerung. Beispiele sind arch, dir, vdir
    • Wrapper utilities: Übergeben nach Setup und Options-Parsing Kommandozeilenoptionen direkt als Argumente an Systemaufrufe. Das Ergebnis des Systemaufrufs wird zum Ergebnis des Utilities; eigene Verarbeitung ist gering. Beispiele sind link, whoami, hostid, logname
    • Full utilities: Vollständige Form mit Setup, Options- und Argument-Parsing, Verarbeitung von Eingabedaten und Ausführung mehrerer Systemaufrufe; die meisten Utilities gehören hierzu

Gemeinsame Initialisierung und Options-Parsing

  • Alle Utilities haben am Anfang von main() eine kurze Initialisierungsprozedur
    • initialize_main (&argc, &argv);
    • set_program_name (argv[0]);
    • setlocale (LC_ALL, "");
    • bindtextdomain (PACKAGE, LOCALEDIR);
    • textdomain (PACKAGE);
    • atexit (close_stdout);
  • Dieser Vorverarbeitungscode erledigt Verwaltungsaufgaben wie Internationalisierung und das Registrieren von Aktionen beim Beenden; er beeinflusst nicht das Kernverhalten des jeweiligen Utilities
  • Beim Parsen von Kommandozeilenoptionen spielt die Getopt-Toolfamilie eine zentrale Rolle
    • Kurze Optionen verwenden das Präfix -, lange Optionen das Präfix --
    • Kurze Optionen werden als String definiert, lange Optionen über eine struct
    • Im String für kurze Optionen wird bei Optionen ohne Argument nur das Zeichen geschrieben, bei Pflichtargumenten folgt :, bei optionalen Argumenten ::
    • Der String für kurze Optionen von kill, Lln:s:t, bedeutet, dass L, l und t keine Argumente haben, während n und s Argumente benötigen
    • In allen Utilities wird getopt_long() verwendet, um die nächste Option zurückzugeben
    • optind zeigt auf die Position des nächsten Arguments im Array argv[]
    • optarg ist ein Zeichenzeiger auf den Wert eines Optionsarguments

Dateisystem-Traversierung und Helfer für Systemaufrufe

  • Unix-artige Systeme unterstützen oft die Bibliothek fts, um Dateisystem-Traversierung einfacher zu handhaben
    • Mit fts_open() oder xfts_open() wird aus einem Pfad eine FTS-Struktur erstellt
    • Datei- oder Verzeichnisknoten des Baums werden durch FTSENT-Strukturen dargestellt
    • Ein Aufruf von fts_read() erzeugt ein FTSENT; dieser Prozess ist die Baum-Traversierung
    • Das Feld FTSENT->fts_info wird häufig zur Beschreibung von Einträgen verwendet und entscheidet, wie ein Eintrag verarbeitet wird
  • coreutils verwendet neben dem, was libc bereitstellt, viele Wrapper für Systemaufrufe und Helfer
  • Bei Schreibvorgängen tauchen neben dem write()-Systemaufruf-Wrapper und libc-Funktionen wie fwrite() auch nicht standardisierte Funktionen wie full_write() auf
    • full_write() versucht Schreibvorgänge weiter, solange kein harter Fehler auftritt
    • safe_write() wiederholt den write()-Systemaufruf auch bei Unterbrechungen
    • Es gibt auch Schreibhelfer, die nur in einem einzelnen Utility verwendet werden, etwa iwrite() in dd oder cwrite() in split

Gemeinsame Funktionen und Code-Idiome

  • Alle Utilities verwenden mindestens die drei Funktionen main(), usage() und _()
  • usage() zeigt Hilfe mit Eingabeparametern, Bedeutung und korrekter Syntax an
  • _() ist ein in system.h definiertes Makro und verbindet einfache Strings mit der Native-Language-Support-Funktion von GNU gettext.h
    • Strings, die Benutzern angezeigt werden, werden in der Regel mit diesem Makro umschlossen
  • In den meisten nicht trivialen Utilities wiederholen sich gemeinsame Codezeilen
    • #include "system.h" definiert systemabhängige Makros, Variablen und nicht standardisierte Utility-Funktionen
    • PROGRAM_NAME definiert den offiziellen Namen des Utilities und wird bei der Versionsprüfung verwendet
    • AUTHORS definiert die Autoren des Utilities und wird bei der Versionsprüfung verwendet
    • emit_try_help() gibt nach einer fehlgeschlagenen Ausgabe einen Hinweis auf die Hilfe aus
    • emit_ancillary_info (PROGRAM_NAME) gibt nach befehlsspezifischer Ausgabe gemeinsame zusätzliche Hilfeinformationen aus
    • initialize_main(&argc, &argv) behandelt die eingebaute Wildcard-Expansion von VMS und wird in den meisten anderen Betriebssystemen entfernt
    • atexit(close_stdout) registriert eine Funktion, die beim Programmende gepufferte Streams flusht und schließt
  • Es treten auch C-Idiome auf, die Anfängern fremd sein können
    • !! ist ein doppelter unärer NOT-Operator und dient dazu, Werte in boolesche Werte zu zwingen
    • do { ... } while (0) ist ein Nicht-Schleifen-Muster, das Makros mit mehreren Anweisungen umschließt, damit die Tokenisierung nach der Präprozessor-Ersetzung sicher bleibt

Wartungsweise und Prüfungen vor Beiträgen

  • Aktive Projekte wie coreutils entwickeln sich im Allgemeinen über drei Wartungsströme weiter
  • Projektweite Änderungen sind große Änderungen an Struktur und Abhängigkeiten aller Utilities
    • 1995 wurde über das GNU-gettext project Native Language Support hinzugefügt, wodurch das Makro _() in die meisten Textausgabezeilen eingeführt wurde
    • 1996 wurde die Internationalisierungsunterstützung erweitert, und in main() kamen mehrere Initialisierungscodezeilen hinzu
    • 1995 wurde der Usage-Ausgabe eine kurze Beschreibung des Utility-Zwecks hinzugefügt
    • 2003 wurde VMS-Wildcard-Unterstützung hinzugefügt, sichtbar in der Funktion initialize_main()
    • 2016 ersetzte das Makro die() die meisten exit()- und error()-Funktionen, um Compiler-Warnungen in Fehlerpfaden zu vermeiden
  • Utility-spezifische Änderungen lassen sich in Bugfixes, neue Funktionen und Optimierungen einteilen
    • join, sort und uniq waren bis zu einem Patch von 2016 anfällig für Overflow-Angriffe
    • Zu df wurde 2013 die Option --output hinzugefügt
    • yes wurde durch besseres Buffering in der Performance verbessert
  • Jährliche Wartung umfasst mindestens die Aktualisierung der Copyright-Jahre aller Utilities
    • Dazu kommen administrative Änderungen wie die Aktualisierung der FSF-Adresse, die keine Auswirkung auf die Ausführung haben
  • Beitragende sollten zuerst die GNU project page, die contribution guidelines, rejected features und die mailing list archives prüfen
  • Vor dem Schreiben von Code sind drei Fragen zu klären
    • Lässt sich dieselbe Funktion mit vorhandenen Tools reproduzieren?
    • Bricht der Beitrag die Abwärtskompatibilität?
    • Weicht das vorgeschlagene Verhalten stark von POSIX ab?
  • Wenn Unsicherheit besteht, wird empfohlen, die Community über die Mailingliste zu fragen

Interessante Details

  • Das kürzeste Utility ist false mit 2 Zeilen; es liegt gleichauf mit arch, dir und vdir
  • Das kürzeste eigenständig ausführbare Utility ist true mit 80 Zeilen; die erste Version ist fast ein minimales C-Programm
  • Das längste Utility ist ls mit 5308 Zeilen
  • Viele Utilities reichen bis zu Research UNIX der 1970er zurück, einige sogar bis Multics
  • Der älteste geistige Vorfahr ist der CTSS-Befehl LISTF um 1963, der später zu ls verkürzt wurde
  • Die eigenwillige Syntax von dd erinnert an die job control language von OS/360 aus den frühen 1960er-Jahren
  • sort ist das einzige Utility, das Multithreading nutzt
  • fmt zeigt Optimierung von Zeilen und Absätzen mithilfe von Funktionskosten
  • yes implementiert Hochleistungsausgabe mit einem seitenbündig ausgerichteten Speicherpuffer
  • df verwendet Gerätemetadaten, während du alle Dateien prüft; deshalb ist df schneller
  • cksum hat zwei Einstiegspunkte: für normale Ausführung und für die Erzeugung der CRC-32-Tabelle
  • echo hat keine Fehlerbedingungen
  • Das Design von test und expr unterscheidet sich stark von der üblichen Utility-Struktur
  • su wurde ursprünglich in coreutils/shellutils gepflegt

Bemerkenswerte Implementierungen

Hinweis zur Unterstützung

  • Persönliche Unterstützung ist nicht eingerichtet
  • Wer Zeit oder Geld teilen möchte, kann die Free Software Foundation unterstützen

1 Kommentare

 
GN⁺ 2023-09-10
Hacker-News-Meinungen
  • Wenn man sagt: „Viele dieser Utilities sind fast 30 Jahre alt und wurden in dieser Zeit von vielen Leuten geändert“, aber zugleich: „Sie wurden nicht dafür entworfen, über eine lange Lebensdauer oder ihre Rolle hinaus zu wachsen“, würde ich gern Beispiele für Programme sehen, die nach Ansicht des Autors mit einer langen Lebensdauer im Sinn entworfen wurden.
    Mich interessiert, ob es Programme sind, die 30 Jahre überlebt haben, oder solche, von denen man annimmt, dass sie die nächsten 30 Jahre halten werden.

    • Die Frage ist, was genau ihre Rolle ist. Ich frage mich, ob es eine Grenze dafür gibt, was man mit solchen Tools tun kann.
      Um eine kleine selbstgebaute Programmiersprache zu testen, habe ich ein Test-Framework mit bash und coreutils gebaut. Anfangs hatte ich ein ungutes Gefühl, keine „richtige“ Sprache zu verwenden, aber in der Praxis funktionierte es sehr gut und konnte auch parallel ausgeführt werden.
      Das Einzige, was ich nicht testen konnte, war argv[0] des Programms. Mit keiner Kombination ließ sich das gewünschte Verhalten exakt herstellen, daher habe ich einen Feature Request samt Patch für env an coreutils geschickt: https://lists.gnu.org/archive/html/coreutils/2023-08/msg0006...
      Es sieht so aus, als würde es übernommen; damit bekommt ein altes Programm eine neue Funktion.
    • Gemeint ist hier wohl eher, dass sie dafür gebaut wurden, „lange aktiv weiterentwickelt zu werden“.
      Die vom Autor aufgezählten Praktiken sind Dinge, die bei großen Programmen normalerweise dafür kritisiert werden, dass sie mit wachsender Größe die Wartbarkeit verschlechtern.
    • Vermutlich war „lange Entwicklungslebensdauer“ gemeint. Also im Sinne von Utilities, die man im Grunde einmal schreibt und lange nutzt; das ist auch ein Vorteil des Ansatzes „eine Sache gut machen“.
    • Seltsamerweise habe ich „long life“ als ein Programm verstanden, das dauerhaft läuft. Also als Gegensatz zu Tools mit klarer Eingabe, kurzer Ausführung und klarer Ausgabe.
    • Emacs?
  • Nützliche Referenzen:
    Wie GNU coreutils getestet wird: https://www.pixelbeat.org/docs/coreutils-testing.html
    Untersuchung der einzelnen coreutils-Befehle: https://ratfactor.com/slackware/pkgblog/coreutils
    Kommandozeilen-Textverarbeitung mit GNU Coreutils: https://learnbyexample.github.io/cli_text_processing_coreuti... — mein E-Book über mehr als 20 Textverarbeitungs-Tools

  • Verwandte Beiträge:
    Decoded: GNU Coreutils (2018) - https://news.ycombinator.com/item?id=29871037 - Januar 2022, 7 Kommentare
    Decoded: GNU coreutils (2019) - https://news.ycombinator.com/item?id=26411908 - März 2021, 38 Kommentare
    Decoded: GNU Coreutils - https://news.ycombinator.com/item?id=20328650 - Juli 2019, 55 Kommentare

  • Interessanterweise wird, wenn man coreutils unter macOS per Homebrew installiert, das coreutils-od als god(1) installiert, weil macOS bereits od(1) hat.

  • Falls der Autor mitliest: Es gibt mindestens einen Fehler. Die Kurzbeschreibung auf der shred-Seite[0] ist tatsächlich die Beschreibung von csplit[1]; sie sollte eher „Dateiinhalte überschreiben, um sie zu verbergen, und optional löschen“ lauten.
    [0]: https://maizure.org/projects/decoded-gnu-coreutils/shred.htm...
    [1]: https://maizure.org/projects/decoded-gnu-coreutils/csplit.ht...

  • Ich wusste nicht, dass es so etwas gibt; cool. Selbst etwas so Einfaches wie yes ist ziemlich interessant, um zu sehen, wie der Grundcode eines Utilities, das auf die Standardausgabe schreibt, aufgebaut ist.
    https://maizure.org/projects/decoded-gnu-coreutils/yes.html

    • Mein Punkt ist, dass selbst ein simples Utility es verdient, gut geschrieben und optimiert zu sein.
      Es ist zum Beispiel schön, ein Minimalbeispiel zu haben, an dem man lernen kann, wie man Ausgaben sehr schnell schreibt. Da das Programm ohnehin sehr kurz ist, spielt die Zeilenzahl kaum eine Rolle; wenn der Autor weiß, wie es geht, sollte man es meiner Meinung nach so schnell wie möglich machen, damit es nicht im Weg steht.
    • Eine Go-Implementierung zum Spaß sieht so aus:
      package main
      
      import "flag"
      
      func main() {  
      yes := flag.String("m", "y", "message")  
      flag.Parse()  
      for {  
      println(*yes)  
      }  
      }  
      
  • Als Liste grundlegender Utilities ziemlich interessant. Obwohl ich UNIX schon sehr lange benutze, hatte ich von Dingen wie shred, shuf oder factor noch nie gehört.
    Ich bekomme Lust, einmal

    sudo find / -type f -exec shred {} \;  
    

    auszuführen und zu sehen, wie lange es dauert, bis es sich selbst umbringt. Natürlich in einer virtuellen Maschine oder auf einem Gerät, das sich leicht neu flashen lässt.

    • Etwas Ähnliches habe ich mit dd ausprobiert; es lief bis zum Ende durch und war ziemlich ernüchternd.
      Ich hatte gehofft, dass es abstürzt oder zumindest die Verbindung abreißt, aber Kernel, sshd und bash waren noch im Speicher, sodass ich wieder an einem Prompt landete, an dem man nichts mehr richtig machen konnte.
    • Unter Linux kommt es wahrscheinlich ziemlich weit. In gerade laufende ausführbare Dateien und Bibliotheken kann nicht geschrieben werden (ETXTBUSY), daher kann shred weder sich selbst noch find zerstören.
  • Mir gefällt, dass /bin/true tatsächlich fehlschlagen und false zurückgeben kann. Technisch gesehen ist ein Aufruf von „Not /bin/false“ also robuster: https://github.com/coreutils/coreutils/blob/master/src/true....
    Natürlich weiß ich, dass das kaum jemals passiert, aber es ist einfach witzig.

    • Eine weitere interessante Sache am Befehl true ist, dass er viel komplizierter geworden ist als nötig.
      Zunächst eine Übung:
      touch mytrue  
      chmod u+x mytrue  
      ./mytrue  
      echo "error code for mytrue is $?"  
      
      true begann buchstäblich genau so. Ziemlich zenmäßig.
      Die erste Änderung kam wegen der Rechtsabteilung. Jeder Code musste einen Copyright-Hinweis haben, auch eine leere Datei. Also wurde aus einer Datei mit nichts darin eine Datei, die nur einen Copyright-Hinweis enthielt, und es stellte sich die Frage: „Kann man ein Copyright auf nichts beanspruchen?“ AT&T hat es versucht.
      Danach sagte jemand, Programme sollten wohldefiniert sein und sich nicht auf zufälliges Unix-Verhalten stützen, und zu diesem Zeitpunkt war das ziemlich vernünftig. Also bekam true schließlich Code, und dieser Code war exit 0.
      Dann sagte jemand, System-Utilities sollten statt in der Shell in C geschrieben werden, weil das schneller sei, und OpenBSD hat noch ein gutes Beispiel dieser Form: http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr....
      Irgendwann mischte sich die GNU-Bürokratie ein und sagte, jedes Programm müsse ein -h-Flag unterstützen, und danach, jedes Programm müsse Locales unterstützen. Dadurch ist das heutige GNU-true erstaunlicherweise 80 Zeilen lang: https://github.com/coreutils/coreutils/blob/master/src/true....
      Man kann das in Ordnung finden, aber für ein Programm, das per Definition „nichts tut und erfolgreich ist“, ist das eine Menge Code.
      http://trillian.mit.edu/~jc/humor/ATT_Copyright_true.html
    • Passiert das nicht nur, wenn man es mit Optionen ausführt? Einfach true auszuführen scheint keine Auswirkungen zu haben, sofern der Kommentar nicht irreführend ist.
  • Wenn man ein Programmieranfänger ist und ein Gefühl dafür entwickeln will, wie man Datenstrukturen und Algorithmen sinnvoll einsetzt: Gibt es hier etwas, worauf man besonders achten sollte?

    • Die Cleverness in coreutils liegt größtenteils weniger in Datenstrukturen und Algorithmen als darin, effektiv mit dem Kernel zusammenzuspielen.
      Zum Beispiel wird copy_file_range() statt read()/write() verwendet, um Daten nicht in den Userspace kopieren zu müssen. Das ist eher Software Engineering als Informatik.
    • Vielleicht verstehe ich das falsch, aber ich sehe nicht, was Datenstrukturen und Algorithmen mit Kommandozeilen-Tools zu tun haben.
  • Vielleicht habe ich den Sinn dieser Seite verpasst, aber gibt es dafür nicht schon jeweils eine man page oder info-Seite?

    • Es geht nicht um Nutzungsinformationen; diese Seite behandelt im Detail, wie die Programme intern funktionieren.