Décodage de GNU Coreutils (2018)
(maizure.org)- Projet d’explication au long cours qui parcourt la structure du code source de GNU coreutils 8.3 utilitaire par utilitaire, conçu pour les programmeurs débutants qui veulent apprendre la conception des outils en ligne de commande
- Ce n’est pas une documentation d’usage, mais un support de lecture du code source ; il couvre aussi, pour chaque commande, l’espace de noms, le flux d’exécution, les choix de conception, les algorithmes, ainsi que la lignée d’UNIX et des premiers Coreutils
- coreutils contient de nombreux petits programmes C à objectif unique ; il faut donc lire, dans leur contexte historique, des motifs de code rugueux au regard moderne comme les variables globales, les macros,
gotoou les fonctions longues - Le flux commun enchaîne initialisation, analyse des options, traitement des entrées, exécution des appels système et gestion des échecs, avec la présence récurrente de
getopt_long(),ftset des helpers gnulib - Avant toute contribution ou modification, il faut d’abord vérifier si le comportement est déjà reproductible avec les outils existants, si cela casse la rétrocompatibilité et dans quelle mesure cela s’écarte de POSIX
Portée du projet d’explication de GNU coreutils
- Projet au long cours qui explique tous les utilitaires de GNU coreutils 8.3
- Le public visé est celui des programmeurs débutants qui veulent examiner la conception des utilitaires en ligne de commande
- Le contenu est particulièrement adapté comme lecture d’accompagnement lorsqu’on parcourt le source code d’un utilitaire donné
- Ce document n’est pas un manuel utilisateur ; pour l’usage réel, il faut se reporter aux man pages
- Le projet est structuré en plusieurs phases
- Phase 1 : pages terminées pour l’espace de noms et la vue d’ensemble d’exécution de chaque utilitaire
- Phase 2 : pages étendues terminées sur les principaux choix de conception et algorithmes, la lignée d’UNIX et des premiers Coreutils, des walkthroughs de code plus utiles et la visualisation de l’évolution du code source
- Phase indefinite : le walkthrough ligne par ligne de chaque utilitaire reste un chantier de long terme
- Un dépôt GitHub est fourni pour collecter des notes ligne par ligne
Pages d’explication par utilitaire
- Chaque nom de commande renvoie vers une page détaillée qui décortique l’utilitaire correspondant
- Chaque page comprend discussion, code source et walkthrough
- Les utilitaires en gras sont les entrées enrichies de la Phase 2
- La liste inclut des commandes GNU coreutils comme
arch,base64,cat,chmod,cp,date,dd,df,ls,rm,sort,tail,tr,wc,yes, etc.
Hypothèses à connaître avant de lire le code
- Beaucoup d’utilitaires coreutils ont une histoire de près de 30 ans et un code modifié sur une longue période par de nombreuses personnes
- Il s’agit le plus souvent de petits programmes conçus pour une tâche unique
- Ce sont généralement des programmes à fichier source unique
- Ils n’ont pas été conçus pour survivre très longtemps ni pour s’étendre bien au-delà de leur rôle initial
- On y trouve des motifs comme les variables globales, les macros,
gotoou de longues fonctions avecswitch/boucles imbriqués
- La connaissance de POSIX est importante pour comprendre le code
- Il est recommandé de commencer par les Utility Syntax Guidelines
- POSIX définit l’interopérabilité des entrées et sorties, mais laisse la manière concrète de faire à l’implémentation
- GNU coreutils peut ne pas suivre POSIX à la lettre, mais des concepts comme les bits de permission, uid/gid, les variables d’environnement ou les états de sortie y sont profondément ancrés
- Pour des raisons de portabilité, coreutils dépend fortement de gnulib
- Presque tous les utilitaires incluent des fonctions gnulib conçues pour traiter des problèmes communs sur plusieurs systèmes
- coreutils s’exécute en supposant le support d’un shell comme
bash,zshouksh- Le shell lance les utilitaires via fork/clone, transmet les arguments, configure l’environnement, redirige les entrées/sorties avec des pipes et conserve la valeur de retour
- GNU coreutils provient à l’origine de trois paquets — shell, texte et fichiers — et les utilitaires d’un même type partagent beaucoup de motifs de conception
Motifs de conception de base
- La plupart des utilitaires CLI suivent en commun trois étapes
- Une phase de configuration qui prépare les drapeaux, options, paramètres de localisation, etc.
- Une phase d’analyse des arguments qui lit l’entrée et détermine les paramètres d’exécution
- Une phase de traitement et d’exécution qui prépare les données d’entrée et les transmet à un ou plusieurs appels système
- Pendant l’exécution, des contraintes sont vérifiées à plusieurs endroits et peuvent entraîner un échec
- Des états de sortie différents peuvent indiquer où se situe le problème
EXIT_FAILUREest souvent utilisé comme état d’échec générique
- Après un échec, l’utilitaire fournit un retour à l’utilisateur
- Les variantes d’utilitaires se répartissent en trois groupes
- Trivial utilities : quelques lignes définissent des macros puis incluent le code source d’un autre utilitaire, les macros imposant un contrôle de flux spécifique ; exemples :
arch,dir,vdir - Wrapper utilities : après configuration et analyse des options, les options de ligne de commande sont transmises directement comme arguments d’appel système ; le résultat de l’appel système devient celui de l’utilitaire, avec peu de traitement propre ; exemples :
link,whoami,hostid,logname - Full utilities : elles couvrent configuration, analyse des options et arguments, traitement des données d’entrée et exécution de plusieurs appels système ; la plupart des utilitaires relèvent de cette catégorie
- Trivial utilities : quelques lignes définissent des macros puis incluent le code source d’un autre utilitaire, les macros imposant un contrôle de flux spécifique ; exemples :
Initialisation commune et analyse des options
- Tous les utilitaires ont une brève procédure d’initialisation au début de
main()initialize_main (&argc, &argv);set_program_name (argv[0]);setlocale (LC_ALL, "");bindtextdomain (PACKAGE, LOCALEDIR);textdomain (PACKAGE);atexit (close_stdout);
- Ce prétraitement gère des tâches d’administration comme l’internationalisation ou l’enregistrement du comportement à la sortie, sans affecter le cœur du comportement propre à chaque utilitaire
- La famille d’outils Getopt joue un rôle central dans l’analyse des options de ligne de commande
- Les options courtes utilisent le préfixe
-, les longues le préfixe-- - Les options courtes sont définies sous forme de chaîne, les longues à l’aide d’une struct
- Dans la chaîne des options courtes, un caractère seul signifie qu’il n’a pas d’argument ;
:indique un argument obligatoire et::un argument facultatif - La chaîne d’options courtes
Lln:s:tdekillsignifie queL,lettn’ont pas d’argument, tandis quenetsen exigent un getopt_long()sert, dans tous les utilitaires, à renvoyer l’option suivanteoptindpointe vers la position du prochain argument dans le tableauargv[]optargest un pointeur de caractères vers la valeur de l’argument d’option
- Les options courtes utilisent le préfixe
Parcours du système de fichiers et helpers d’appels système
- Sur les systèmes de type Unix, la bibliothèque fts est souvent disponible pour faciliter le parcours du système de fichiers
fts_open()ouxfts_open()crée une structureFTSà partir d’un chemin- Les nœuds de fichiers ou répertoires de l’arbre sont représentés par des structures
FTSENT - Un appel à
fts_read()produit unFTSENT; ce processus constitue le parcours de l’arbre - Le champ
FTSENT->fts_infoest souvent utilisé pour décrire l’entrée et décider de sa manière de traitement
- coreutils utilise aussi de nombreux wrappers d’appels système et helpers au-delà de ce que fournit la libc
- Dans les opérations d’écriture, on rencontre, en plus du wrapper de l’appel système
write()et de fonctions libc commefwrite(), des fonctions non standard commefull_write()- full_write() réessaie l’écriture tant qu’aucun échec dur ne se produit
- safe_write() relance l’appel système
write()même en cas d’interruption - Il existe aussi des helpers d’écriture propres à un seul utilitaire, comme
iwrite()dansddoucwrite()danssplit
Fonctions communes et idiomes de code
- Tous les utilitaires utilisent au minimum les trois fonctions
main(),usage()et_() usage()affiche l’aide, avec les paramètres d’entrée, leur signification et la syntaxe correcte_()est une macro définie danssystem.h, qui relie une chaîne simple aux fonctions Native Language Support de GNU gettext.h- Presque toutes les chaînes affichées à l’utilisateur sont encapsulées dans cette macro
- Dans la plupart des utilitaires non triviaux, on retrouve des lignes de code communes
#include "system.h"définit des macros dépendantes du système, des variables et des fonctions utilitaires non standardPROGRAM_NAMEdéfinit le nom officiel de l’utilitaire et sert à la vérification de versionAUTHORSdéfinit les auteurs de l’utilitaire et sert à la vérification de versionemit_try_help()affiche une suggestion d’aide après une sortie en erreuremit_ancillary_info (PROGRAM_NAME)affiche des informations d’aide additionnelles communes après la sortie propre à la commandeinitialize_main(&argc, &argv)gère l’expansion des jokers intégrée à VMS et est supprimé sur la plupart des autres systèmes d’exploitationatexit(close_stdout)enregistre une fonction qui flush et ferme les flux bufferisés à la fin du programme
- On trouve aussi des idiomes C qui peuvent surprendre les débutants
!!est une double négation unaire utilisée pour forcer une valeur en booléendo { ... } while (0)est un motif non itératif qui encapsule des macros à plusieurs instructions afin de sécuriser la tokenisation après substitution par le préprocesseur
Mode de maintenance et vérifications avant contribution
- Un projet actif comme coreutils évolue généralement selon trois grands flux de maintenance
- Les changements transversaux au projet sont de grosses modifications qui touchent la structure et les dépendances de tous les utilitaires
- En 1995, l’ajout du Native Language Support via le projet GNU gettext a introduit la macro
_()dans la plupart des lignes de sortie texte - En 1996, l’extension du support de l’internationalisation a ajouté plusieurs lignes d’initialisation dans
main() - En 1995, une courte description de l’objectif de l’utilitaire a été ajoutée à la sortie d’usage
- En 2003, le support des jokers VMS a été ajouté, visible dans la fonction
initialize_main() - En 2016, pour éviter des avertissements du compilateur sur les chemins d’échec, la macro
die()a remplacé la plupart des appels àexit()eterror()
- En 1995, l’ajout du Native Language Support via le projet GNU gettext a introduit la macro
- Les changements propres à un utilitaire se répartissent entre corrections de bugs, nouvelles fonctionnalités et optimisations
join,sortetuniqétaient vulnérables à des attaques par débordement jusqu’au patch de 2016- L’option
--outputa été ajoutée àdfen 2013 yesa vu ses performances améliorées grâce à un meilleur buffering
- La maintenance annuelle inclut au minimum la mise à jour des années de copyright de tous les utilitaires
- Elle comprend aussi des changements administratifs comme la mise à jour de l’adresse de la FSF, sans impact sur l’exécution
- Avant de contribuer, il est recommandé de consulter la page du projet GNU, les consignes de contribution, les fonctionnalités refusées et les archives de la mailing list
- Trois questions doivent être vérifiées avant d’écrire du code
- Peut-on reproduire la même fonctionnalité avec les outils existants ?
- La contribution casse-t-elle la rétrocompatibilité ?
- Le comportement proposé s’écarte-t-il fortement de POSIX ?
- En cas de doute, il est recommandé de demander à la communauté sur la mailing list
Détails intéressants
- L’utilitaire le plus court est
false, avec 2 lignes, ex aequo avecarch,diretvdir - Le plus court utilitaire autonome est
true, avec 80 lignes, et sa première version est presque un programme C minimal - L’utilitaire le plus long est
ls, avec 5308 lignes - Beaucoup d’utilitaires remontent au Research UNIX des années 1970, et certains jusqu’à Multics
- L’ancêtre conceptuel le plus ancien est la commande CTSS
LISTF, vers 1963, qui sera plus tard raccourcie enlsLISTFfaisait partie des 18 utilitaires d’origine du papier de conception de 1962
- La syntaxe particulière de
ddrappelle le job control language d’OS/360 du début des années 1960 sortest le seul utilitaire à exploiter le multithreadingfmtmontre l’optimisation de lignes et de paragraphes via un coût de fonctionyesmet en œuvre une sortie haute performance avec un tampon mémoire aligné sur les pagesdfest plus rapide queducardfutilise les métadonnées des périphériques alors queduexamine tous les fichierscksumpossède deux points d’entrée : un pour l’exécution normale, un pour générer la table CRC-32echon’a aucune condition d’échec- La conception de
testetexprdiffère fortement de la structure habituelle des utilitaires suétait à l’origine maintenu dans coreutils/shellutils
Implémentations à regarder
- shuf et shred utilisent le chiffre ISAAC) pour les nombres aléatoires
- shuf utilise le Reservoir sampling
- sum utilise les checksums historiques System V et BSD
- expr utilise une évaluation d’expression à associativité gauche
- shred utilise le Secure overwrite
- cksum calcule un CRC-32 checksum
- sort emploie une variante de Merge sort
- factor est un utilitaire intéressant à examiner pour ses aspects d’implémentation mathématique
Informations sur le soutien
- Aucun dispositif n’est en place pour recevoir des soutiens individuels
- Si vous souhaitez partager du temps ou de l’argent, vous pouvez contribuer à la Free Software Foundation
1 commentaires
Avis sur Hacker News
Si l’on dit que « beaucoup de ces utilitaires ont près de 30 ans et ont été modifiés par de nombreuses personnes pendant tout ce temps », tout en ajoutant qu’« ils n’ont pas été conçus pour s’étendre au-delà d’une longue durée de vie ou de leur rôle », j’aimerais voir des exemples de programmes conçus avec une longue durée de vie en tête selon l’auteur
Je serais curieux de connaître aussi bien des programmes qui ont survécu 30 ans que des programmes dont on pense qu’ils tiendront les 30 prochaines années
Pour tester un petit langage de programmation que j’avais créé, j’ai construit un framework de test avec bash et coreutils. Au début, ça me gênait de ne pas utiliser un « vrai » langage, mais en pratique ça fonctionnait très bien, y compris en exécution parallèle
La seule chose que je n’ai pas pu tester était le
argv[0]du programme, et aucune combinaison ne permettait de produire exactement le comportement voulu ; j’ai donc envoyé une demande de fonctionnalité et un patch pourenvdans coreutils : https://lists.gnu.org/archive/html/coreutils/2023-08/msg0006...Il semble que ce sera intégré, ce qui revient à ajouter une nouvelle fonctionnalité à un vieux programme
Les pratiques énumérées par l’auteur sont généralement critiquées, dans les gros programmes, parce qu’elles nuisent à la maintenabilité à mesure que la taille augmente
Quelques ressources utiles :
La façon dont GNU coreutils est testé : https://www.pixelbeat.org/docs/coreutils-testing.html
Exploration de chaque commande coreutils : https://ratfactor.com/slackware/pkgblog/coreutils
Traitement de texte en ligne de commande avec GNU Coreutils : https://learnbyexample.github.io/cli_text_processing_coreuti... — mon ebook qui couvre plus de 20 outils de traitement de texte
Articles liés :
Decoded: GNU Coreutils (2018) - https://news.ycombinator.com/item?id=29871037 - janvier 2022, 7 commentaires
Decoded: GNU coreutils (2019) - https://news.ycombinator.com/item?id=26411908 - mars 2021, 38 commentaires
Decoded: GNU Coreutils - https://news.ycombinator.com/item?id=20328650 - juillet 2019, 55 commentaires
Fait amusant : quand on installe coreutils via Homebrew sur macOS, comme macOS possède déjà
od(1), leodde coreutils est installé sous le nomgod(1)Si l’auteur lit ceci, il y a au moins une erreur. La courte description de la page shred[0] est en réalité celle de
csplit[1], et devrait plutôt être quelque chose comme « écrase le contenu d’un fichier pour le masquer, puis le supprime éventuellement »[0]: https://maizure.org/projects/decoded-gnu-coreutils/shred.htm...
[1]: https://maizure.org/projects/decoded-gnu-coreutils/csplit.ht...
Je ne savais pas que ça existait, c’est chouette. Même quelque chose d’aussi simple que
yesest assez intéressant pour voir comment est écrit le code de base d’un utilitaire qui écrit sur la sortie standardhttps://maizure.org/projects/decoded-gnu-coreutils/yes.html
Par exemple, c’est appréciable d’avoir un exemple minimal pour apprendre à écrire une sortie très rapidement. Comme le programme est très court, le nombre de lignes importe peu ; si l’auteur sait comment faire, autant le rendre aussi rapide que possible pour qu’il ne devienne pas un goulot d’étranglement
Une liste d’utilitaires de base assez amusante. J’utilise UNIX depuis très longtemps, et pourtant je n’avais jamais entendu parler de choses comme
shred,shufoufactorÇa donne envie de lancer un jour
pour voir combien de temps la machine tient avant de se tuer elle-même. Bien sûr, dans une machine virtuelle ou sur un appareil qu’on peut facilement reflasher
dd, et c’est allé jusqu’au bout, ce qui a été assez décevantJ’espérais un crash, ou au moins une déconnexion, mais le noyau ainsi que
sshdetbashétaient toujours en mémoire, donc je suis revenu à une invite où rien ne fonctionnait vraimentETXTBUSY), doncshredne peut pas casser son propre binaire nifindJ’aime l’idée que
/bin/truepuisse en réalité échouer et renvoyer false. Techniquement, appeler « Not/bin/false» devient donc plus robuste : https://github.com/coreutils/coreutils/blob/master/src/true....Je sais bien sûr que c’est un cas qui n’arrive presque jamais, mais ça me fait rire
true, c’est qu’elle est devenue beaucoup plus complexe que nécessaireD’abord, un petit exercice :
truea littéralement commencé comme ça. C’est assez zenLe premier changement est venu du juridique. Tout code devait avoir un avis de copyright, même les fichiers vides. Le fichier qui ne contenait rien est donc devenu un fichier ne contenant qu’un avis de copyright, ce qui soulève la question : « peut-on mettre un copyright sur rien ? ». AT&T a essayé
Ensuite, quelqu’un a dit qu’un programme devait être bien défini et ne pas dépendre d’un comportement accidentel d’Unix, ce qui était assez raisonnable à ce stade.
truea donc finalement reçu du code, et ce code étaitexit 0Puis quelqu’un d’autre a dit que les utilitaires système devraient être écrits en C plutôt qu’en shell pour être plus rapides, et OpenBSD en conserve encore un bon exemple sous cette forme : http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr....
À un moment donné, la bureaucratie GNU est intervenue en décrétant que tous les programmes devaient prendre en charge l’option
-h, puis que tous les programmes devaient gérer les locales, si bien que letruede GNU fait aujourd’hui, étonnamment, 80 lignes : https://github.com/coreutils/coreutils/blob/master/src/true....On peut considérer que c’est acceptable, mais pour un programme qui, par définition, « ne fait rien et réussit », ça fait beaucoup de code
http://trillian.mit.edu/~jc/humor/ATT_Copyright_true.html
truene semble pas affecté, sauf si le commentaire prête à confusionPour un programmeur débutant qui veut développer son intuition sur la façon d’appliquer utilement les structures de données et les algorithmes, y a-t-il quelque chose ici à regarder en particulier ?
Par exemple, utiliser
copy_file_range()au lieu deread()/write()pour éviter de copier les données vers l’espace utilisateur. C’est plus du génie logiciel que de l’informatique théoriqueJe passe peut-être à côté de l’objectif de ce site, mais il n’existe pas déjà des pages man ou des pages info pour chacun d’eux ?