4 points par GN⁺ 2023-09-10 | 1 commentaires | Partager sur WhatsApp
  • 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, goto ou 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(), fts et 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, goto ou de longues fonctions avec switch/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, zsh ou ksh
    • 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_FAILURE est 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

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:t de kill signifie que L, l et t n’ont pas d’argument, tandis que n et s en exigent un
    • getopt_long() sert, dans tous les utilitaires, à renvoyer l’option suivante
    • optind pointe vers la position du prochain argument dans le tableau argv[]
    • optarg est un pointeur de caractères vers la valeur de l’argument d’option

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() ou xfts_open() crée une structure FTS à 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 un FTSENT ; ce processus constitue le parcours de l’arbre
    • Le champ FTSENT->fts_info est 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 comme fwrite(), des fonctions non standard comme full_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() dans dd ou cwrite() dans split

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 dans system.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 standard
    • PROGRAM_NAME définit le nom officiel de l’utilitaire et sert à la vérification de version
    • AUTHORS définit les auteurs de l’utilitaire et sert à la vérification de version
    • emit_try_help() affiche une suggestion d’aide après une sortie en erreur
    • emit_ancillary_info (PROGRAM_NAME) affiche des informations d’aide additionnelles communes après la sortie propre à la commande
    • initialize_main(&argc, &argv) gère l’expansion des jokers intégrée à VMS et est supprimé sur la plupart des autres systèmes d’exploitation
    • atexit(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éen
    • do { ... } 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() et error()
  • Les changements propres à un utilitaire se répartissent entre corrections de bugs, nouvelles fonctionnalités et optimisations
    • join, sort et uniq étaient vulnérables à des attaques par débordement jusqu’au patch de 2016
    • L’option --output a été ajoutée à df en 2013
    • yes a 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 avec arch, dir et vdir
  • 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 en ls
  • La syntaxe particulière de dd rappelle le job control language d’OS/360 du début des années 1960
  • sort est le seul utilitaire à exploiter le multithreading
  • fmt montre l’optimisation de lignes et de paragraphes via un coût de fonction
  • yes met en œuvre une sortie haute performance avec un tampon mémoire aligné sur les pages
  • df est plus rapide que du car df utilise les métadonnées des périphériques alors que du examine tous les fichiers
  • cksum possède deux points d’entrée : un pour l’exécution normale, un pour générer la table CRC-32
  • echo n’a aucune condition d’échec
  • La conception de test et expr diffère fortement de la structure habituelle des utilitaires
  • su était à l’origine maintenu dans coreutils/shellutils

Implémentations à regarder

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

 
GN⁺ 2023-09-10
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

    • Le problème, c’est de savoir quel est exactement ce rôle. Je me demande s’il y a vraiment une limite à ce qu’on peut faire avec ce genre d’outils
      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 pour env dans 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
    • Il faut sans doute comprendre ici que cela signifie « conçu pour être développé activement pendant longtemps »
      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
    • Il voulait probablement dire « longue durée de vie de développement ». Autrement dit, des utilitaires écrits une fois et utilisés longtemps, ce qui est aussi l’un des avantages de l’approche « faire une seule chose et la faire bien »
    • Étrangement, j’ai interprété « long life » comme désignant un programme qui reste en cours d’exécution. Je l’ai compris par opposition à un outil avec une entrée claire, une exécution courte et une sortie claire
    • Emacs ?
  • 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), le od de coreutils est installé sous le nom god(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 yes est assez intéressant pour voir comment est écrit le code de base d’un utilitaire qui écrit sur la sortie standard
    https://maizure.org/projects/decoded-gnu-coreutils/yes.html

    • Mon propos est qu’un utilitaire, même simple, mérite d’être bien écrit et optimisé
      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
    • Voici une implémentation en Go faite pour s’amuser :
      package main
      
      import "flag"
      
      func main() {  
      yes := flag.String("m", "y", "message")  
      flag.Parse()  
      for {  
      println(*yes)  
      }  
      }  
      
  • 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, shuf ou factor
    Ça donne envie de lancer un jour

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

    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

    • J’ai essayé quelque chose de similaire avec dd, et c’est allé jusqu’au bout, ce qui a été assez décevant
      J’espérais un crash, ou au moins une déconnexion, mais le noyau ainsi que sshd et bash étaient toujours en mémoire, donc je suis revenu à une invite où rien ne fonctionnait vraiment
    • Sous Linux, il y a de fortes chances que ça aille jusqu’au bout. On ne peut pas écrire sur les exécutables et bibliothèques en cours d’exécution (ETXTBUSY), donc shred ne peut pas casser son propre binaire ni find
  • J’aime l’idée que /bin/true puisse 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

    • Un autre point intéressant avec la commande true, c’est qu’elle est devenue beaucoup plus complexe que nécessaire
      D’abord, un petit exercice :
      touch mytrue  
      chmod u+x mytrue  
      ./mytrue  
      echo "error code for mytrue is $?"  
      
      true a littéralement commencé comme ça. C’est assez zen
      Le 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. true a donc finalement reçu du code, et ce code était exit 0
      Puis 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 le true de 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
    • Ce n’est pas seulement le cas quand on l’exécute avec des options ? Lancer simplement true ne semble pas affecté, sauf si le commentaire prête à confusion
  • Pour 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 ?

    • Dans coreutils, l’ingéniosité tient surtout à la façon de s’articuler efficacement avec le noyau, plutôt qu’aux structures de données et aux algorithmes
      Par exemple, utiliser copy_file_range() au lieu de read()/write() pour éviter de copier les données vers l’espace utilisateur. C’est plus du génie logiciel que de l’informatique théorique
    • Je me trompe peut-être, mais je ne vois pas vraiment le rapport entre les structures de données et algorithmes et les outils en ligne de commande
  • Je 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 ?

    • Ce ne sont pas des informations d’utilisation : ce site explique en détail comment les programmes fonctionnent en interne