Votre terminal n'est pas un terminal : Introduction aux flux

Votre terminal n'est pas un terminal : Introduction aux flux

- 19 mins

Article d’origne publié par Lucas Fernandes da Costa sous Licence Copyleft en avril 2019. Traduction en français par mes soins également sous licence Copyleft

NDLR : Je préparais des ateliers sur ce sujet pour la rentrée quand je me suis aperçu que la barrière de la langue pouvait être un frein puissant pour les étudiantes et les étudiants − y compris avec les pré-requis de connaissance pour ce type d'atelier. J'ai donc décidé de traduire cet article. Merci à Maxime Lathuilière d'avoir partagé cet article

J’aime les flux parce que je n’aime pas les logiciels.

J’essaie toujours de construire moins de logiciels. Moins de logiciels signifie que vous passez moins de temps à le mettre à jour, moins de temps à le réparer et moins de temps à y penser. La seule chose meilleure que « moins de logiciels », c’est pas de logiciel du tout.

Les flux (Streams) nous aident à écrire moins de logiciels parce qu’ils permettent aux programmes de communiquer entre eux.

Si les programmes ne peuvent pas communiquer ils doivent alors avoir un très grand nombre de fonctionnalités pour satisfaire les besoins de leurs utilisat⋅rices⋅eurs, créant ainsi plus de logiciels. En permettant la communication entre les processus, les flux incitent à des logiciels plus petits et peuvent même parfois éviter leur conception.

Apprendre à connaître les flux vous aide à mieux comprendre le fonctionnement des systèmes UNIX et à simplifier votre environnement de développement.

Ce que sont les Flux

Les flux ne sont que ça : des flux. De la même façon qu’une rivière a un flux d’eau, les programmes ont des flux de données. De plus, tout comme vous pouvez utiliser des canalisations pour transporter l’eau d’un endroit à un autre, vous pouvez utiliser des tuyaux UNIX pour transporter des données d’un programme à un autre. C’est cette analogie qui a inspiré la conception des flux :

We should have some ways of connecting programs like a garden hose — screw in another segment when it becomes necessary to massage data in another way. This is the way of I/O also”. — Douglas McIlroy

Les flux peuvent être utilisés pour transférer des données vers des programmes et pour en extraire des données.

Sous UNIX, les programmes reçoivent certains flux qui leur sont attachés par défaut, à la fois en entrée et en sortie. Nous appelons ces flux standard.

Il existe trois flux standard différents :

Le programme fortune, par exemple, écrit quelques morceaux de sagesse dans le flux stdout.

$ fortune
It is simplicity that is difficult to make
-- Bertold Brecht

Quand fortune s’est exécuté, il s’est attaché à stdin, stdout et stderr. Puisqu’il n’a pas produit d’erreurs et n’a pas reçu d’entrée externe, il a juste écrit sa sortie dans stdout.

Dessin représentant un carré au milieu avec la commande fortune. À gauche il y a une flèche avec stdin écrit dessus. A droite il y a deux flèches, l'une avec stdout et l'autre avec stderr

cowsay est un autre programme qui écrit à stdout. cowsay prend une chaine de caractère (string) et affiche une vache l’exprimant.

$ cowsay "Brazil has a decent president"
 _______________________________
< Brazil has a decent president >
 -------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Contrairement à fortune, cowsay ne dit pas des choses forcément intelligentes − comme nous venons de le voir. Heureusement, nous pouvons alimenter le flux stdin qui y est rattaché.

Tout ce que nous avons à faire pour rendre cowsay plus intelligent et répéter les citations de fortune est d’utiliser ce que nous appelons un tuyau − représenté par | − pour attacher le stdout de fortune au stdin du cowsay.

$ fortune | cowsay
 _________________________________________
/ A language that doesn't have everything \
| is actually easier to program in than   |
| some that do.                           |
|                                         |
\ -- Dennis M. Ritchie                    /
 -----------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Nous utilisons des tuyaux pour connecter le flux de sortie d’un programme au flux d’entrée d’un autre programme.

Un schéma montrant les flux reliés à la fois à fortune et à cowsay, mais montrant cette fois la flèche stdout de fortune pointant vers la flèche stdin qui va dans la case cowsay.

Vous pouvez voir la sortie de cowsay sur votre écran car, par défaut, votre terminal reçoit les flux standard stdin, stdout et stderr qui y sont attachés.

Les données entrent par stdout et stderr et sortent par l’autre extrémité : votre moniteur (le terminal). De la même façon, l’entrée de votre clavier passe par stdin vers un programme.

Source : Wikipedia

Le programme cat, par exemple, utilise le stdin pour recevoir les entrées de votre clavier et le stdout pour les envoyer en sortie :

$ cat
Everything I write before pressing Enter
Everything I write before pressing Enter
Gets logged right after
Gets logged right after

Un organigramme montrant les données provenant du clavier par stdin et passant par cat, stdout, et stderr vers un moniteur.

Nous pouvons faire plus élaboré en utilisant sed pour remplacer toutes les occurrences de I par We chaque fois que nous appuyons sur Entrer :

$ cat | sed -E "s/I/We/"
I think streams are quite cool.
We think streams are quite cool.

Un organigramme montrant les données circulant depuis le clavier à travers stdin vers cat et sortant par sed, sortie qui entre dans le stdin de sed, sed fonctionnant à son tour, avec un stdout de sed vers à un moniteur.

Aussi, au cas où vous ne le sauriez pas, sed est un éditeur de flux (stream editor).

Comment les flux communiquent avec votre « terminal » ?

Beaucoup de sites Web mis en lien externes dans au dernier billet de blog que j’ai écrit. Dans la section commentaires de l’un d’eux, des personnes on fait remarquer que je n’utilisais pas vraiment de terminal.

Ils avaient tout à fait raison avec leurs commentaires pas-du-tout-pédants. Cependant, voici une photo de moi en 1978 − un peu avant ma naissance − utilisant un terminal série HP 2647A :

Image d'un homme à lunettes utilisant un ancien terminal d'affichage de 1978 − Autopilot [CC BY-SA 3.0], via Wikimedia Commons

Si vous n’êtes pas un voyageur du temps pur et dur comme moi, ce que vous utilisez n’est qu’un émulateur de terminal. Qui aurait pu deviner, n’est-ce pas ?

Les émulateurs de terminaux sont des simulations logicielles de terminaux « réels ». Ces émulateurs vous fournissent une interface pour interagir avec le pilote TTY de Linux1. Le pilote du TTY est responsable de la manipulation des données en provenance et à destination des programmes.

Un diagramme montrant que les données passent du clavier à l'émulateur de terminal et au pilote TTY jusqu'à un programme et peuvent ensuite revenir du programme à l'écran.

Chaque TTY a ses propres flux stdin, stdout et stderr qui lui sont connectés. Ce sont les flux fournis aux programmes pour qu’ils lisent (stdin) et écrivent (stdout et stderr).

Voici une version plus précise de ce qui s’est passé lorsque vous avez lancé cat | sed -E "s/I/We/" dans le dernier exemple :

Un diagramme montrant les données provenant du clavier vers l'émulateur, puis au TTY, puis cat, sed et enfin le retour vers un moniteur.

Comme tout sous UNIX, le tty est un fichier. Chaque instance d’un émulateur de terminal a un fichier tty différent qui lui est associé. Parce que chaque émulateur lit et écrit dans un fichier différent, vous ne voyez pas le résultat des programmes que vous exécutez dans toutes les fenêtres que vous avez ouvertes.

Pour savoir quel tty est associé à une fenêtre de terminal on peut utiliser la commande tty:

Deux fenêtres de terminal s'ouvrent et le résultat de la commande tty dans chacune d'elles montre le chemin vers deux fichiers différents en sortie.

Lorsque vous ouvrez une nouvelle fenêtre de terminal, c’est vers cela que pointent ses flux :

Une image montrant trois cases avec stdout, stderr et stdin et trois flèches pointant vers la droite vers trois autres cases avec /dev/ttys/005 écrites dessus.

Dans l’image ci-dessus, le /dev/ttys/005 n’est qu’un exemple. Cela aurait pu être n’importe quel autre fichier car il y en aura un nouveau pour chaque instance tty.

Redirection

Pour écrire la sortie d’un programme dans un fichier au lieu du tty, vous pouvez diriger le flux stdout ailleurs.

Dans l’exemple ci-dessous, nous écrivons le contenu du répertoire / dans le fichier content_list.txt du dossier /tmp. Nous le faisons en utilisant l’opérateur >, ce qui nous permet de rediriger le flux stdout par défaut.

$ ls / 1> /tmp/content_list.txt

Pour vérifier ce qu’il y a dans /tmp/content_list.txt, vous pouvez maintenant utiliser cat :

$ cat /tmp/content_list.txt
Applications
Library
Network
System
Users
Volumes
bin
cores
dev
etc
home
net
private
sbin
themes
tmp
usr
var

C’est différent de ce qu’aurait fait ls / si vous l’aviez utilisée, la commande ls n’aurait rien écrit sur votre terminal. Au lieu d’écrire dans le fichier /dev/tty que votre émulateur de terminal lit, elle a écrit dans /tmp/content_list.txt.

Un schéma montrant les états avant et après des flux lors de l'utilisation de ls. Initialement, le stdout pointe vers /dev/tty mais après avoir fait une redirection, il pointe vers le chemin du fichier.

On peut obtenir le même effet de redirection en utilisant > au lieu de 1>.

$ ls / > /tmp/content_list.txt

L’omission du numéro préfixé fonctionne parce que le 1 devant > indique le flux que nous voulons rediriger. Dans ce cas, 1 est le descripteur de fichier pour stdout.

Comme le tty n’est qu’un fichier, vous pouvez aussi rediriger un flux stdout d’un terminal vers un autre.

Une image montrant la sortie de cowsay transférée d'un terminal à l'autre en la redirigeant vers le fichier TTY de l'autre terminal.

Si nous voulions rediriger le flux stderr, nous pourrions préfixer son descripteur de fichier (file-descriptor) − qui est 2 − vers >.

$ cat /this/path/does/not/exist 2> /tmp/cat_error.txt

Schéma montrant les états avant et après des flux lors de l'utilisation de cat avec une redirection du flux stderr. Initialement, le </code>stderr</code> pointe vers /dev/tty, mais après avoir fait une redirection, il pointe vers le chemin du fichier.

Maintenant le fichier /tmp/cat_error.txt contient tout ce que cat a écrit dans stderr.

$ cat /tmp/cat_error.txt
cat: /this/path/does/not/exist: No such file or directory

Pour rediriger à la fois stdin et stderr nous pouvons utiliser &>.

$ cat /does/not/exist /tmp/content_list.txt &> /tmp/two_streams.txt

Maintenant /tmp/two_streams contiendra ce qui a été écrit dans stdout et stderr.

$ cat /tmp/two_streams.txt
cat: /does/not/exist: No such file or directory
Applications
Library
Network
System
Users
Volumes
bin
cores
dev
etc
home
installer.failurerequests
net
private
sbin
themes
tmp
usr
var

Schéma montrant qu'au lieu de stdout et stderr pointant vers /dev/tty ils pointent vers le chemin du fichier.

Vous devez être prudent lorsque vous écrivez dans un fichier avec >. L’utilisation d’un simple > remplace le contenu d’un fichier.

$ printf "Look, I have something inside" > /tmp/careful.txt

$ cat /tmp/careful.txt
Look, I have something inside

$ printf "Now I have something else" > /tmp/careful.txt

$ cat /tmp/careful.txt
Now I have something else

Pour ajouter à un fichier au lieu d’écraser son contenu, vous devez utiliser >>.

$ printf "Look, I have something inside" > /tmp/careful.txt

$ cat /tmp/careful.txt
Look, I have something inside

$ printf "\nNow I have one more thing" >> /tmp/careful.txt

$ cat /tmp/careful.txt
Look, I have something inside
Now I have one more thing

Pour la lecture à partir de stdin, on peut utiliser l’opérateur <.

La commande suivante utilise le flux stdin pour alimenter sed avec le contenu de /usr/share/dict/words. sed sélectionne ensuite une ligne aléatoirement et l’écrit dans stdout.

$ sed -n "${RANDOM}p" < /usr/share/dict/words
alloestropha

Puisque le descripteur de fichier de stdin est 0, nous pouvons obtenir le même effet en le préfixant à <.

$ sed -n "${RANDOM}p" 0< /usr/share/dict/words
pentameter

Il est également important de noter la différence entre l’utilisation d’opérateurs de redirection et de tuyaux (NDLR: pipes |). Lorsque nous utilisons des tuyaux, nous attachons la sortie stdout d’un programme au stdin d’un autre programme. Lorsque nous utilisons la redirection, nous changeons l’emplacement vers lequel un flux spécifique pointe au démarrage d’un programme.

Puisque les flux ne sont que des descripteurs de fichiers, nous pouvons créer autant de flux que nous le voulons. Pour cela, nous pouvons utiliser exec pour ouvrir des fichiers sur des descripteurs de fichiers spécifiques.

Dans l’exemple ci-dessous, nous ouvrons /usr/share/dict/words pour lire sur le descripteur 3.

$ exec 3< /usr/share/dict/words

La liste des descripteurs de fichiers de 0 à 3 à gauche. Au-dessus de 0, 1, et 2 nous pouvons voir stdin, stdout, et stderr et ils pointent vers le fichier tty. Le descripteur de fichier 3 pointe vers /usr/share/dict/words

Maintenant nous pouvons utiliser ce descripteur comme stdin pour un programme en utilisant <&.

$ sed -n "${RANDOM}p" 0<&3
dactylic

Ce que fait l’opérateur <& dans l’exemple ci-dessus est de dupliquer le descripteur de fichier 3 et d’en faire une copie à 0 (stdin).

La liste des descripteurs de fichiers incluant les flux standard de 0 à 2 à gauche et un descripteur supplémentaire (3) qui pointe vers /usr/share/dict/words. Le descripteur 0 (stdin) pointe vers le même fichier.

Une fois que vous avez ouvert un descripteur de fichier pour la lecture, vous ne pouvez le « consommer » qu’une seule fois. D’où la raison pour laquelle tenter d’utiliser 3 une nouvelle fois ne fonctionnera pas :

$ grep dactylic 0<&3

Pour fermer un descripteur de fichier nous pouvons utiliser -, comme si nous le copiions dans le descripteur de fichier que nous voulons fermer.

$ exec 3<&-

Tout comme nous pouvons utiliser < pour ouvrir un fichier en lecture, nous pouvons utiliser > pour ouvrir un fichier en écriture.

Dans l’exemple ci-dessous, nous créons un fichier appelé output.txt, l’ouvrons en mode écriture, et dupliquons son descripteur vers 4 :

$ touch /tmp/output.txt
$ exec 4>&/tmp/output.txt

À gauche la liste des descripteurs de fichiers de 0 à 2 et 4 supplémentaires. Au-dessus de 0, 1, et 2 nous pouvons voir stdin, stdout, et stderr et ils pointent vers le fichier tty. Le descripteur de fichier 4 pointe vers /tmp/output.txt

Maintenant si nous voulons que cowsay écrive dans le fichier /tmp/output.txt, nous pouvons dupliquer le descripteur de fichier depuis 4 et le copier dans 1 (stdout)

$ cowsay "Does this work?" 1>&4

$ cat /tmp/output.txt
 _________________
< Does this work? >
 -----------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

La liste des descripteurs de fichiers incluant les flux standard de 0 à 2 à gauche et un descripteur supplémentaire (3) qui pointe vers /usr/share/dict/words. Le descripteur 0 (stdin) pointe vers le même fichier.

Intuitivement, pour ouvrir un fichier en lecture et écriture, vous pouvez utiliser <>. Tout d’abord, créons un fichier appelé /tmp/lines.txt, ouvrons un descripteur r/w pour lui et copions-le dans 5.

$ touch /tmp/lines.txt
$ exec 5<> /tmp/lines.txt

Schéma : A gauche la liste des descripteurs de fichiers de 0 à 2 et 5 supplémentaires. Au-dessus de 0, 1, et 2 nous pouvons voir stdin, </code>stdout</code>, et stderr et ils pointent vers le fichier tty. Le descripteur de fichier 5 points à /tmp/lines.txt

Dans l’exemple ci-dessous, nous copions les 3 premières lignes de /usr/share/dict/propernames dans /tmp/lines.txt.

$ head -n 3 /usr/share/dict/propernames 1>&5
$ cat /tmp/lines.txt
Aaron
Adam
Adlai

Notez que si nous essayions de lire à partir de 5 avec cat, nous n’obtiendrions aucun résultat car lorsque nous avons écrit, nous progressions dans le fichier et 5 est désormais sa limite finale.

$ cat 0<&5

Nous pouvons résoudre ce problème en fermant le 5 et en le rouvrant.

$ exec 5<&-
$ exec 5<> /tmp/lines.txt
$ cat 0<&5
Aaron
Adam
Adlai

Postscriptum

Lors de la génération de nombres aléatoires

Dans les exemples ci-dessus, j’ai utilisé $RANDOM pour générer des nombres aléatoires et les passer à sed afin de sélectionner des lignes aléatoires dans le fichier /usr/share/dict/words.

Vous avez peut-être remarqué que cela vous donne habituellement des mots commençant par a, b ou c. C’est parce que RANDOM est long deux octets et ne peut donc aller que de 0 à 32,767.

Le fichier /usr/share/dict/words compte 235 886 lignes.

$ wc -l /usr/share/dict/words
235886 /usr/share/dict/words

Puisque le plus grand nombre possible généré par RANDOM est environ 7 fois plus petit que /usr/share/dict/words, il n’est pas approprié de sélectionner des mots au hasard. Dans ce billet, il a été utilisé simplement par souci de simplicité.

Sur les appareils TTY et I/O

J’ai intentionnellement omis quelques détails en expliquant que le TTY et l’émulateur de terminal se trouvent entre les périphériques d’I/O et les processus.

Vous trouverez une explication beaucoup plus complète et approfondie de toutes les composantes de ce processus de communication dans ce post extraordinaire de Linus Åkesson intitulé “The TTY Demystified”.

Références et liens utiles

Lucas Fernandes da Costa at London, United Kingdom − Licence Copyleft en avril 2019 .

Remerciements

Notes et commentaires

  1. TTY est parfois traduit en système de téléimprimeur (ATS), notamment au Quèbec http://www.thesaurus.gouv.qc.ca/tag/terme.do?id=12371 

Merci à toutes les personnes qui soutiennent les efforts par leurs dons


Xavier Coadic

Xavier Coadic

Human Collider

rss framagit twitter github mail linkedin stackoverflow