3 points par GN⁺ 2024-12-14 | 1 commentaires | Partager sur WhatsApp
  • Le comportement perçu d’un terminal résulte non seulement du système d’exploitation, du shell et de l’émulateur de terminal, mais aussi des conventions propres à chaque programme ; et ces conventions sont plus cohérentes qu’on ne le pense
  • POSIX traite surtout des interactions entre l’émulateur de terminal, le système d’exploitation et le shell, ainsi que de certains utilitaires de base, mais ne définit pas les comportements détaillés de programmes comme htop
  • Des motifs reviennent sans cesse : Ctrl-C, q, Ctrl-D, les 16 couleurs ANSI, les raccourcis readline, la désactivation des couleurs en sortie vers un pipe, ou encore la signification de - pour stdin/stdout
  • Des comportements qui semblent relever d’une loi naturelle doivent en réalité être implémentés par le programme ou la bibliothèque d’entrée ; c’est le cas, par exemple, de la fermeture avec Ctrl-D dans ipython ou de l’interruption d’une recherche avec Ctrl-C dans prompt-toolkit
  • Quand on crée un nouveau programme en terminal ou qu’on essaie de comprendre le comportement d’un outil, il est plus utile de partir des attentes par défaut et de leurs exceptions que de se dire que « tout est possible »

Les quatre acteurs qui façonnent le comportement d’un terminal

  • Ce qui se passe dans un terminal peut généralement être vu comme le résultat de quatre acteurs
    • le système d’exploitation
    • le shell
    • l’émulateur de terminal
    • le programme en cours d’exécution, comme top, vim ou cat
  • Avec des combinaisons comme bash, GNOME Terminal et Linux, les trois premiers éléments sont relativement bien connus, et certains comportements sont standardisés par POSIX
  • Les programmes individuels semblent pouvoir faire n’importe quoi, mais dans l’usage réel, ils se comportent de manière assez cohérente

Peu de standards, mais des conventions

  • Il existe très peu de véritables standards qui définissent de manière globale le comportement des programmes de terminal
  • Deux références s’en rapprochent
    • POSIX : traite surtout des interactions entre l’émulateur de terminal, le système d’exploitation et le shell, et définit aussi certains comportements d’utilitaires de base comme cp
    • command line interface guidelines : un guide consacré aux conventions des interfaces en ligne de commande
  • Ces règles ne sont pas des prescriptions que les auteurs de programmes doivent absolument suivre ; elles ressemblent davantage à des conventions techniques observées en 20 ans d’usage du terminal
  • Les exceptions sont nombreuses, et elles ont souvent de bonnes raisons d’exister

Certaines règles doivent être implémentées directement par les programmes

  • L’emplacement des fichiers de configuration, la sortie de --help, ou le fait d’écrire la sortie normale sur stdout et les erreurs sur stderr relèvent assez clairement de la responsabilité du programme
  • Mais certains comportements paraissent naturels alors qu’ils doivent en réalité être gérés directement par le programme ou sa bibliothèque d’entrée
  • La fermeture d’un REPL avec Ctrl-D en est un bon exemple
    • cat n’a pas besoin d’implémentation particulière : en mode cooked, le système d’exploitation renvoie EOF
    • ipython, lui, implémente la gestion de Ctrl-D dans le code de prompt-toolkit
  • Comprendre cette responsabilité d’implémentation rend moins surprenantes les petites différences de comportement d’un programme à l’autre

Règle 1 : les programmes non interactifs se terminent avec Ctrl-C

  • Les programmes non interactifs se terminent généralement quand on appuie sur Ctrl-C
  • En effet, si le programme n’installe pas de gestionnaire du signal SIGINT, il se termine par défaut avec Ctrl-C
  • Cette règle ne s’applique pas telle quelle aux programmes interactifs comme python3, bc ou less
    • dans un programme interactif, Ctrl-C peut interrompre l’opération en cours au lieu de quitter le programme
    • par exemple, une recherche dans less ou du code Python en cours d’exécution dans python3
  • La bibliothèque de gestion d’entrée prompt-toolkit, utilisée par ipython, contient du code pour interrompre une recherche avec Ctrl-C

Règle 2 : les TUI se ferment généralement avec q

  • Les programmes en TUI comme less ou htop se ferment généralement quand on appuie sur q
  • Cette règle ne s’applique pas aux programmes pour lesquels quitter avec q ne serait pas naturel
    • tmux
    • les éditeurs de texte

Règle 3 : les REPL se ferment avec Ctrl-D sur une ligne vide

  • Les REPL comme python3 ou ed se ferment généralement quand on appuie sur Ctrl-D sur une ligne vide
  • Si on exécute un programme comme cat en mode cooked, le système d’exploitation renvoie EOF pour Ctrl-D sur une ligne vide
  • sqlite3, python3, fish, bash et d’autres n’utilisent pas réellement le mode cooked, mais implémentent ce raccourci pour reproduire le comportement par défaut
  • On peut voir des exemples d’implémentation à deux endroits
  • Le REPL Erlang est une exception : il ne se ferme pas avec Ctrl-D

Règle 4 : ne pas dépasser les 16 couleurs de base

  • Les programmes de terminal utilisent en général peu de couleurs au-delà des 16 couleurs ANSI de base
  • Spécifier directement une couleur hexadécimale risque fortement d’entrer en conflit avec la couleur de fond choisie par l’utilisateur
    • par exemple, un texte en #EEEEEE est presque invisible sur fond blanc, mais peut convenir sur fond sombre
  • En se limitant aux 16 couleurs de base, il est plus probable que l’utilisateur ait configuré ces couleurs dans son émulateur de terminal pour qu’elles s’accordent bien avec son fond
  • Cette approche réduit aussi les hypothèses sur les couleurs prises en charge par l’émulateur de terminal
  • Il existe des exceptions, notamment les éditeurs de texte
    • Helix utilise par défaut un fond violet qui n’est pas une couleur ANSI de base
    • Helix n’est pas considéré comme un programme « essentiel », et les utilisateurs qui n’aiment pas sa palette peuvent changer de thème

Règle 5 : prendre en charge, au moins en partie, les raccourcis readline

  • Quand cela a du sens, presque tous les programmes prennent en charge au moins une partie des raccourcis readline
  • Beaucoup de programmes n’utilisent pas directement readline, mais imitent les raccourcis de style emacs/readline
  • Tous les programmes n’imitent pas exactement readline de la même manière
    • atuin semble utiliser Ctrl-A comme préfixe, donc Ctrl-A ne déplace pas au début de la ligne
  • Ces programmes implémentent leurs propres tampons couper/coller, ce qui permet par exemple d’effacer une ligne avec Ctrl-U puis de la recoller avec Ctrl-Y
  • Il existe aussi des exceptions
    • des programmes comme git, cat ou nc ne proposent pas d’édition de ligne au-delà de backspace, Ctrl-W et Ctrl-U
    • les éditeurs de texte ont chacun leur propre mode d’édition
  • Le sujet est abordé plus en détail dans entering text in the terminal is complicated

Règle 5.1 : Ctrl-W supprime le dernier mot

  • En dehors des éditeurs de texte, je n’ai jamais vu de programme où Ctrl-W ne supprime pas le dernier mot
  • En mode cooked, le système d’exploitation supprime par défaut le dernier mot avec Ctrl-W, et toute la ligne avec Ctrl-U
  • Beaucoup de programmes reproduisent ce comportement par défaut
  • En dehors des éditeurs de texte, il n’y a pas d’exception qui me vienne à l’esprit

Règle 6 : désactiver les couleurs lors d’une écriture vers un pipe

  • La plupart des programmes désactivent les couleurs quand la sortie est envoyée vers un pipe
  • Exemples
    • rg blah met blah en évidence dans la sortie vers le terminal, mais désactive cette mise en évidence quand la sortie part vers un pipe ou un fichier
    • ls --color=auto utilise des couleurs quand il écrit dans le terminal, mais pas quand il écrit vers un pipe
  • Le format de sortie peut aussi changer selon qu’il s’agit ou non d’un terminal
    • ls organise les fichiers en colonnes dans une sortie terminal
    • ripgrep regroupe les correspondances avec leur en-tête
  • Si l’on veut absolument conserver les couleurs, unbuffer permet de faire croire au programme qu’il écrit vers un tty
unbuffer rg blah |  less -R
  • Certains programmes permettent aussi de forcer les couleurs avec l’option --color
    • l’exemple ci-dessus fonctionne aussi avec rg --color=always | less -R

Règle 7 : - signifie stdin ou stdout

  • En général, si on passe - à la place d’un nom de fichier, le programme lit sur stdin ou écrit sur stdout selon ce qui est approprié
  • Pour formater avec black du code Python présent dans le presse-papiers puis le recopier, on peut faire comme ceci
pbpaste | black - | pbcopy
  • pbpaste est un programme Mac ; sous Linux, on peut faire quelque chose de similaire avec xclip
  • Il semble que la plupart des programmes implémentent ce comportement quand cela a du sens, même s’il peut y avoir beaucoup d’exceptions

Pourquoi ces règles prennent du temps à s’apprendre

  • Ces règles s’acquièrent rarement de manière explicite et d’un seul coup ; on les apprend plutôt en plusieurs étapes
    • on découvre d’abord qu’il existe une règle, par exemple « Ctrl-C arrête un programme »
    • puis on rencontre des exceptions, comme le fait que find s’arrête mais pas less
    • on finit par repérer inconsciemment le schéma : dans les programmes non interactifs, cela quitte ; dans les programmes interactifs, cela peut seulement interrompre l’action en cours
    • ce n’est que plus tard qu’on peut formuler cela comme une règle explicite
  • Une grande partie de notre compréhension du terminal reste encore au stade de la reconnaissance implicite de motifs
  • Mettre ces règles par écrit peut aider d’autres personnes à apprendre un peu plus vite le comportement du terminal

1 commentaires

 
GN⁺ 2024-12-14
Commentaires sur Hacker News
  • En plus, les programmes de terminal doivent respecter les couleurs de premier plan et d’arrière-plan par défaut de l’utilisateur et ne pas les changer sans raison
    Même s’ils utilisent des couleurs, le texte doit rester lisible quels que soient l’arrière-plan, le premier plan et la palette du terminal, et si le sens n’est transmis que par la couleur, il disparaît lors d’un copier-coller ou dans un terminal sans prise en charge des couleurs
    Il faut aussi utiliser avec prudence les emoji et les caractères non ASCII superflus, et dans une sortie standard défilante, il vaut mieux éviter d’effacer des informations importantes comme des avertissements affichés temporairement ou des noms de fichiers
    Si l’on veut vraiment utiliser beaucoup de couleurs flashy et d’animations, il faut au minimum fournir une option de configuration facile à désactiver

    • Il y a quelques années, avec Docker, j’ai subi un bug extrêmement pénible où un script affichait des emoji sur la sortie standard, ce qui faisait planter le terminal interactif et arrêtait même le conteneur
      Je ne sais pas si la cause profonde a été corrigée, mais cela m’a conduit à proposer des PR de contournement dans plusieurs projets open source, au prix d’un petit renoncement à l’intention artistique d’origine : https://github.com/docker-archive/toolbox/issues/695
    • En réalité, rendre les couleurs lisibles quels que soient l’arrière-plan, le premier plan et la palette du terminal est quasiment impossible
      En revanche, on peut proposer des couleurs personnalisées
    • Comme le précise explicitement le texte d’origine, ces règles ne sont pas prescriptives mais relèvent de constats techniques
      Elles ressemblent davantage à une mise en ordre de ce que l’on peut généralement supposer déjà respecté par tous les programmes de terminal
    • Il existe une tentative de standardisation : https://bixense.com/clicolors/
    • Les emoji et les caractères non ASCII inutiles méritent encore plus d’être soulignés
      Certains auteurs, de nos jours, créent des outils en supposant que l’utilisateur emploie une police patchée, ce qui nuit fortement à l’utilisabilité
  • Bon article
    Puisqu’il dit qu’il est difficile d’apprendre ces conventions, je recommande aussi Command Line Interface Guidelines, qui est ma référence préférée quand je réfléchis aux CLI : https://clig.dev
    On y retrouve les règles du texte d’origine, comme quitter avec Ctrl-C, accepter - sur l’entrée standard, désactiver les couleurs dans un pipe, ainsi que bien d’autres choses

    • Cette ressource est déjà citée au début même de l’article, juste après la table des matières
    • Les lectures complémentaires de CLIG valent aussi le détour : https://clig.dev/#further-reading
      Elles incluent des ressources POSIX, GNU et Unix, le guide CLI de Heroku, ainsi que le guide des applications CLI 12-factor
    • Il y a aussi ceci : https://usage.jdx.dev/
  • Le point « il faut envoyer la sortie normale vers stdout et les erreurs vers stderr » est vraiment essentiel
    La sortie standard doit être réservée uniquement aux données que le programme a été invité à produire. Si l’utilisateur demande des données JSON, la sortie standard doit contenir exactement cet objet JSON et absolument rien d’autre
    Le nom erreur standard est en fait plutôt mal choisi ; c’est plus proche d’un « flux utilisateur standard » vers lequel envoyer tous les messages que l’utilisateur doit lire dans le terminal. Cela inclut non seulement les messages d’erreur, mais aussi les messages d’état et la sortie détaillée
    Ainsi, on peut chaîner proprement la sortie du programme vers un autre programme, tandis que les informations non liées aux données continuent d’aller vers le terminal ou peuvent être redirigées ailleurs
    Ce serait bien si les programmes pouvaient facilement créer de nouveaux descripteurs de fichier reliés au terminal pour un usage spécifique, et documenter leurs numéros dans le manuel comme on le fait pour les codes de sortie. Pour la compatibilité, la valeur par défaut pourrait rester stdout ou stderr, et j’ai envie d’expérimenter un peu avec ça

  • Encore quelques points : si vous découvrez Ctrl-E et Ctrl-W, les raccourcis de type readline/emacs, il est utile de savoir que la plupart des sources de saisie de macOS utilisent ces raccourcis
    Sur macOS, vous pouvez essayer Ctrl-E, Ctrl-W et Ctrl-U dès maintenant dans la barre d’adresse de votre navigateur
    Si vous utilisez un programme en ligne de commande qui ne prend pas du tout en charge l’édition de ligne, vous pouvez installer rlwrap et exécuter le REPL en dessous. Par exemple, Standard ML of New Jersey a un REPL mais pas de fonction d’édition de ligne, donc on peut compenser avec rlwrap smlnj
    À « n’utilisez pas plus de 16 couleurs », j’irais même jusqu’à ajouter « n’utilisez pas plus de 8 couleurs, ou au minimum rendez les couleurs configurables ». Des palettes populaires comme Solarized et Base 16 par défaut emploient parfois les couleurs « claires » pour différentes nuances de gris, si bien que le vert clair imaginé par l’auteur peut en réalité être le même gris que le texte ordinaire

    • Si j’appuie sur Ctrl-W dans la barre d’adresse de macOS, cela ferme l’onglet courant dans la plupart des navigateurs que j’ai utilisés
      C’est aussi le cas dans Alacritty, l’émulateur de terminal que j’utilise, ainsi que dans la plupart des explorateurs de fichiers à onglets. De mémoire, Windows Explorer fait probablement pareil maintenant, mais cela fait assez longtemps que je n’utilise plus Windows
  • La convention qui consiste à quitter un REPL avec Ctrl-D me piège toujours dans GHCi
    En général, j’appuie sur Ctrl-D, rien ne se passe et je reste déconcerté, puis je me rappelle que ça ne marche pas dans GHCi et j’exécute :q
    Mais comme Ctrl-D insère alors un caractère invisible au début de l’entrée, cela provoque l’erreur « lexical error at character '\EOT' », et je dois retaper :q sans ce préfixe invisible pour quitter GHCi

    • Si vous essayez de quitter vi sur un clavier de pays étranger, vous trouverez probablement ça encore plus difficile
    • Je ferme toujours GHCi avec Ctrl-D
      Je ne connais pas vraiment d’autre méthode, mais existe-t-il un réglage pour changer ce comportement ?
    • Vous utilisez Windows ?
  • Un point manquant est le traitement de ~ comme répertoire personnel
    Cela semble être une fonction du shell, et non une API POSIX. En Go, os.ReadFile("~/.bashrc") ne fonctionne pas, alors que dans le shell, echo ~/~/~ s’étend en /home/jrockway/~/~
    yourprogram ~/path/to/file fonctionne toujours, mais dans un REPL qui demande un nom de fichier, ce n’est pas forcément le cas. En pratique, beaucoup de logiciels le prennent en charge, donc cela mérite sans doute de figurer dans la catégorie « ce que font la plupart des programmes TUI »

    • L’expansion du tilde fait partie de l’expansion des mots du shell : https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_06_01
      Dans l’expansion des mots, l’expansion du tilde est la première étape, suivie du découpage en champs, de l’expansion des chemins et de la suppression des guillemets
      La spécification shell concernée se trouve à https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_06, et la spécification complète est disponible sur https://pubs.opengroup.org/onlinepubs/9799919799/
    • À mon avis, cela devrait rester une fonction du shell
      Si un outil ajoute l’expansion du tilde, il doit connaître le répertoire personnel de l’utilisateur courant, ce qui complique l’implémentation et peut conduire à des implémentations incomplètes où ~ fonctionne mais pas ~name
      Dans un shell POSIX, ~name s’étend vers le répertoire personnel de cet utilisateur, ce qui implique de pouvoir retrouver le répertoire personnel de n’importe quel utilisateur, y compris les comptes système. Selon la configuration NSS, cela peut être bien plus complexe que lire /etc/passwd
      Pour une TUI interactive, implémenter l’expansion du tilde peut être utile, mais dans ce cas elle devrait être aussi complète que celle du shell. Un programme CLI ferait mieux d’éviter ce genre d’expansion « magique »
    • Si vous l’ajoutez à votre propre REPL, il faut vraiment prendre au sérieux la documentation, l’échappement et la gestion des guillemets
      Que faire s’il existe un fichier nommé "~", qu’en est-il de "~username", comment gérer l’échappement et les guillemets, que faire si $HOME n’existe pas ou diffère du vrai répertoire personnel, et comment gérer cela sous Windows ?
      Fait moins connu : dans ssh, si vous tapez "~." après un retour à la ligne, le client force la fermeture de la connexion et le serveur ne peut rien y faire, donc mieux vaut ne pas intégrer cela dans un workflow
    • Le cas qui me pose toujours problème est une forme comme ./myprogram --config=~/.config/myprogram
      Bien sûr, beaucoup d’analyseurs d’options permettent d’utiliser la forme que le shell développe, ./myprogram --config ~/.config/myprogram, donc ils le prennent en charge parce qu’ils le savent ou par hasard
      Mais ma mémoire musculaire me fait toujours taper =. C’est sans doute une habitude prise avec la bibliothèque d’analyse d’options d’un ancien employeur, et pour la même raison j’utilise toujours --underscores_like_this au lieu de --hyphens-like-normal-people
    • Bonne remarque
      En particulier, comme .. est géré au niveau de l’API de l’OS, on peut facilement s’attendre à ce que POSIX fasse de même pour ~
  • La principale raison pour laquelle je préfère largement la CLI à la GUI, ou parfois à la TUI, est qu’elle me paraît bien plus cohérente
    Il existe des conventions, et respecter toutes les conventions dans une CLI est bien plus facile que concevoir une bonne GUI. En conséquence, la qualité tend à être meilleure
    Je réfléchis souvent à la manière de transposer cette propriété dans les GUI, mais jusqu’ici les meilleures réponses semblent être « beaucoup d’efforts » ou « revoir les attentes à la baisse »

    • Je pense qu’une grande partie de la raison est que, sauf sur macOS, les conventions GUI ne sont pas fortement implémentées dans l’OS et ne sont pas faciles à adopter telles quelles
      Les autres OS n’ont pas réussi à imposer une touche méta pour les raccourcis GUI, donc les conflits comme ctrl-c sont bien plus fréquents
      Les valeurs par défaut des frameworks applicatifs sur lesquels reposent les apps natives macOS fournissent beaucoup d’interactions clavier communes. Si vous utilisez la classe standard de barre de menus et les commandes par défaut, Command O, N, Q, X, C, V, etc. arrivent quasiment gratuitement, et si vous utilisez des champs de texte standard, la navigation avec Command/Option/Fn + flèches est également fournie automatiquement
      Windows est aussi plutôt correct sur ce point, mais le fait que la touche de modification par défaut soit Ctrl peut gêner l’usage d’apps basées sur le terminal, et les différents frameworks UI internes à l’OS semblent avoir des valeurs par défaut différentes
      Dans le monde Linux, cela demanderait sans doute que quelqu’un conçoive un framework applicatif complet, que les distributions le standardisent et que les applications y soient portées. Les gestionnaires de fenêtres ne veulent pas dicter le comportement à l’intérieur des fenêtres, les toolkits GUI ne veulent pas définir des valeurs par défaut à l’échelle de tout l’OS, et ni les environnements de bureau ni les distributions ne semblent vouloir imposer ou porter un framework UI
  • Les programmes ne devraient pas ajouter de fichiers dans le répertoire personnel et devraient respecter des réglages comme XDG_CONFIG_HOME

    • D’accord, mais ce texte est plus descriptif que prescriptif, comme l’auteur essaie clairement de le préciser
      Il ne cherche pas à convaincre de la manière dont les applications devraient se comporter, mais à lister des comportements fréquemment observés dans les applications
  • Il existe une exception à la règle « Ctrl-W doit supprimer le dernier mot » : mysql(1)
    mysql est lié à editline plutôt qu’à readline, donc par défaut Ctrl-W supprime toute la ligne jusqu’au début, et non le dernier mot
    À l’époque où j’ai dû l’utiliser pendant un certain temps, voir des requêtes rédigées avec soin disparaître d’un coup me rendait franchement fou

    • Ça pouvait être pire
      À l’université, j’ai eu le malheur d’essayer sqlplus pour me connecter à une base de données Oracle, et non seulement les raccourcis du terminal comme Ctrl-W ne fonctionnaient pas du tout, mais en plus il était impossible d’avancer le curseur dans la ligne et il n’y avait aucun historique pour revenir aux commandes précédentes
      À la moindre faute de frappe, il fallait tout retaper depuis le début ; on pouvait bien entrer / pour réexécuter la dernière commande telle quelle, mais cela n’aidait en rien à corriger une erreur
      Ctrl-L ne permettait pas non plus d’effacer l’écran ; il fallait exécuter clear scr à la main. J’imagine que taper simplement clear était trop évident
  • J’aimerais ajouter que les processus de longue durée devraient relire leur configuration lorsqu’ils reçoivent SIGHUP

    • Ce n’est pas une liste de choses « à faire », mais plutôt une liste de choses « généralement observées »
      La différence est importante et, malheureusement, ce point n’est pas assez universel pour figurer dans la liste d’origine
    • À strictement parler, cela sort probablement du cadre de cet article
      Les processus de longue durée qui réagissent à SIGHUP s’exécutent généralement détachés, sans TTY de contrôle, donc les appeler des « programmes de terminal » est un peu discutable
    • Ce comportement n’est-il pas en réalité difficile à implémenter ?
      Il faut veiller à ce que l’application ne mette pas en cache par inadvertance non seulement l’état de configuration, mais aussi l’état dérivé de cette configuration, faute de quoi les deux finissent par diverger
      Cela peut être simple pour une application très basique, mais la plupart des applications qui ont besoin d’un fichier de configuration ne le sont pas autant