GNU Coreutils entschlüsselt (2018)
(maizure.org)- 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,
gotound 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(),ftsund 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,yesund 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,
gotound lange Funktionen mit verschachteltenswitch-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,zshoderkshvoraus- 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_FAILUREwird 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
- Trivial utilities: Definieren einige Zeilen Makros, includen dann den Quellcode eines anderen Utilities und erzwingen per Makro eine bestimmte Ablaufsteuerung. Beispiele sind
Gemeinsame Initialisierung und Options-Parsing
- Alle Utilities haben am Anfang von
main()eine kurze Initialisierungsprozedurinitialize_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, dassL,lundtkeine Argumente haben, währendnundsArgumente benötigen - In allen Utilities wird
getopt_long()verwendet, um die nächste Option zurückzugeben optindzeigt auf die Position des nächsten Arguments im Arrayargv[]optargist ein Zeichenzeiger auf den Wert eines Optionsarguments
- Kurze Optionen verwenden das Präfix
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()oderxfts_open()wird aus einem Pfad eineFTS-Struktur erstellt - Datei- oder Verzeichnisknoten des Baums werden durch
FTSENT-Strukturen dargestellt - Ein Aufruf von
fts_read()erzeugt einFTSENT; dieser Prozess ist die Baum-Traversierung - Das Feld
FTSENT->fts_infowird häufig zur Beschreibung von Einträgen verwendet und entscheidet, wie ein Eintrag verarbeitet wird
- Mit
- 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 wiefwrite()auch nicht standardisierte Funktionen wiefull_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()inddodercwrite()insplit
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 insystem.hdefiniertes 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-FunktionenPROGRAM_NAMEdefiniert den offiziellen Namen des Utilities und wird bei der Versionsprüfung verwendetAUTHORSdefiniert die Autoren des Utilities und wird bei der Versionsprüfung verwendetemit_try_help()gibt nach einer fehlgeschlagenen Ausgabe einen Hinweis auf die Hilfe ausemit_ancillary_info (PROGRAM_NAME)gibt nach befehlsspezifischer Ausgabe gemeinsame zusätzliche Hilfeinformationen ausinitialize_main(&argc, &argv)behandelt die eingebaute Wildcard-Expansion von VMS und wird in den meisten anderen Betriebssystemen entferntatexit(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 zwingendo { ... } 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 meistenexit()- underror()-Funktionen, um Compiler-Warnungen in Fehlerpfaden zu vermeiden
- 1995 wurde über das GNU-gettext project Native Language Support hinzugefügt, wodurch das Makro
- Utility-spezifische Änderungen lassen sich in Bugfixes, neue Funktionen und Optimierungen einteilen
join,sortunduniqwaren bis zu einem Patch von 2016 anfällig für Overflow-Angriffe- Zu
dfwurde 2013 die Option--outputhinzugefügt yeswurde 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
falsemit 2 Zeilen; es liegt gleichauf mitarch,dirundvdir - Das kürzeste eigenständig ausführbare Utility ist
truemit 80 Zeilen; die erste Version ist fast ein minimales C-Programm - Das längste Utility ist
lsmit 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
LISTFum 1963, der später zulsverkürzt wurdeLISTFist eines der ursprünglichen 18 Utilities aus dem 1962 design paper
- Die eigenwillige Syntax von
dderinnert an die job control language von OS/360 aus den frühen 1960er-Jahren sortist das einzige Utility, das Multithreading nutztfmtzeigt Optimierung von Zeilen und Absätzen mithilfe von Funktionskostenyesimplementiert Hochleistungsausgabe mit einem seitenbündig ausgerichteten Speicherpufferdfverwendet Gerätemetadaten, währenddualle Dateien prüft; deshalb istdfschnellercksumhat zwei Einstiegspunkte: für normale Ausführung und für die Erzeugung der CRC-32-Tabelleechohat keine Fehlerbedingungen- Das Design von
testundexprunterscheidet sich stark von der üblichen Utility-Struktur suwurde ursprünglich in coreutils/shellutils gepflegt
Bemerkenswerte Implementierungen
- shuf und shred verwenden die ISAAC cipher) für Zufallszahlen
- shuf verwendet Reservoir sampling
- sum verwendet Legacy-Prüfsummen von System V und BSD
- expr verwendet linksassoziative Ausdrucksauswertung
- shred verwendet Secure overwrite
- cksum berechnet eine CRC-32 checksum
- sort verwendet eine Variante von Merge sort
- factor ist ein Utility, dessen mathematische Implementierungsaspekte einen Blick wert sind
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
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.
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ürenvan 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.
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.
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-
odalsgod(1)installiert, weil macOS bereitsod(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
yesist 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
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.
Als Liste grundlegender Utilities ziemlich interessant. Obwohl ich UNIX schon sehr lange benutze, hatte ich von Dingen wie
shred,shufoderfactornoch nie gehört.Ich bekomme Lust, einmal
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.
ddausprobiert; 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,
sshdundbashwaren noch im Speicher, sodass ich wieder an einem Prompt landete, an dem man nichts mehr richtig machen konnte.ETXTBUSY), daher kannshredweder sich selbst nochfindzerstören.Mir gefällt, dass
/bin/truetatsä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.
trueist, dass er viel komplizierter geworden ist als nötig.Zunächst eine Übung:
truebegann 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
trueschließlich Code, und dieser Code warexit 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-trueerstaunlicherweise 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
trueauszufü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?
Zum Beispiel wird
copy_file_range()stattread()/write()verwendet, um Daten nicht in den Userspace kopieren zu müssen. Das ist eher Software Engineering als Informatik.Vielleicht habe ich den Sinn dieser Seite verpasst, aber gibt es dafür nicht schon jeweils eine man page oder info-Seite?