Aller au contenu
Redirections, tubes et codes de sortie

Redirections, tubes et codes de sortie

200 Pratiquer ⏱ 1 h 30 linuxbashubuntu

À la fin, vous saurez

  • Rediriger la sortie standard, la sortie d'erreur et l'entrée standard d'une commande vers et depuis des fichiers
  • Prévoir l'effet de l'ordre des redirections, et expliquer pourquoi sudo echo > fichier échoue
  • Enchaîner des commandes par des tubes et décrire ce que fait le noyau entre elles
  • Lire et utiliser les codes de sortie : $?, &&, ||, PIPESTATUS et pipefail
  • Insérer le résultat d'une commande dans une autre avec la substitution de commande, et fournir un texte avec un here-document
  • Éviter d'écraser un fichier ou d'exposer un secret par une redirection

Prérequis

Testé avec bash 5.2.21 coreutils 9.4 grep 3.11 strace 6.8 ubuntu 24.04 , vérifié le 5 octobre 2026

Pourquoi

Dans la leçon précédente, vous avez écrit cut -d' ' -f5 acces.log | sort | uniq -c sans trop vous demander ce que faisait ce |. Vous avez aussi vu passer > fichier, 2>/dev/null, &&. Ces quelques caractères sont la grammaire du shell, et ils conditionnent la fiabilité de tout ce que vous automatiserez ensuite.

Mal compris, ils produisent des erreurs classiques et coûteuses. Une sauvegarde nocturne « réussie » depuis des mois alors que la commande échouait, parce que le script ne regardait que le code de sortie du dernier programme d'un tube. Un fichier de configuration vidé par un > à la place d'un >>. Des messages d'erreur qui disparaissent d'un journal parce que la redirection était écrite dans le mauvais ordre. Un sudo echo ... > /etc/... qui répond Permission denied alors qu'on a utilisé sudo.

Cette leçon passe sous la grammaire : on va regarder, appel système par appel système, ce que fait le shell quand il lit une redirection ou un tube. Une fois ce mécanisme compris, il n'y a plus de cas particuliers à apprendre par cœur.

Les concepts

Trois flux, trois numéros

Tout processus Unix démarre avec trois canaux ouverts, que la page stdin(3) décrit sous le nom de flux standard (standard streams) :

NuméroNomSensPar défaut
0entrée standard (stdin)ce que le programme litle clavier du terminal
1sortie standard (stdout)les résultatsl'écran du terminal
2sortie d'erreur (stderr)les messages d'erreur et de diagnosticl'écran du terminal

Ces numéros sont des descripteurs de fichiers (file descriptors) : pour le noyau, chaque processus possède une table de fichiers ouverts, et un descripteur est simplement l'indice d'une case de cette table. Un programme qui écrit « dans la sortie standard » écrit en réalité dans le descripteur 1, sans savoir ce qu'il y a derrière : un terminal, un fichier, un tube vers un autre programme. C'est cette ignorance qui rend le système si composable.

La séparation entre sortie standard et sortie d'erreur est essentielle. Quand vous écrivez ls *.log | wc -l, vous voulez compter des noms de fichiers, pas les messages d'erreur. Comme ces messages partent sur le descripteur 2, qui n'est pas dans le tube, ils restent visibles à l'écran, et le comptage reste juste.

Rediriger, c'est rebrancher un descripteur

Une redirection demande au shell de rebrancher un descripteur avant de lancer la commande. Le manuel de Bash le formule ainsi : les descripteurs peuvent être dupliqués, ouverts, fermés et redirigés avant l'exécution d'une commande. La commande elle-même n'en sait rien ; elle écrit dans son descripteur 1 comme d'habitude.

SyntaxeEffet
cmd > fsortie standard vers f, en le vidant d'abord (le fichier est créé s'il n'existe pas)
cmd >> fsortie standard ajoutée à la fin de f
cmd < fentrée standard lue dans f
cmd 2> fsortie d'erreur vers f
cmd 2>&1descripteur 2 copié depuis le descripteur 1 : les erreurs vont là où va la sortie, à cet instant
cmd &> féquivaut à > f 2>&1 (forme propre à Bash)
cmd > /dev/nullsortie jetée : /dev/null est un fichier spécial qui accepte tout et ne garde rien

Le chiffre devant > est le numéro du descripteur ; sans chiffre, > vaut 1> et < vaut 0<.

L'ordre compte

Le manuel de Bash prend lui-même cet exemple, parce que c'est le point qui trompe tout le monde. Les redirections sont traitées de gauche à droite, et 2>&1 copie la destination actuelle du descripteur 1 :

  • cmd > f 2>&1 : d'abord, 1 va dans f ; ensuite, 2 devient une copie de 1, donc va aussi dans f. Tout va dans le fichier.
  • cmd 2>&1 > f : d'abord, 2 devient une copie de 1, qui à cet instant est le terminal ; ensuite, 1 part dans f. Les erreurs restent à l'écran, seule la sortie va dans le fichier.

Pensez à 2>&1 comme à une affectation (« 2 prend la valeur actuelle de 1 »), pas comme à un lien permanent.

Le tube

Un tube (pipe), écrit |, relie la sortie standard d'une commande à l'entrée standard de la suivante. Le noyau fournit pour cela un tampon en mémoire : la page pipe(7) précise que sa capacité est de 65 536 octets par défaut sous Linux. Les commandes d'un tube ne s'exécutent pas l'une après l'autre : elles sont lancées en même temps, chacune dans son processus, et le tampon régule leur rythme :

  • si l'écrivain remplit le tampon, il est bloqué jusqu'à ce que le lecteur consomme ;
  • si le tampon est vide, le lecteur attend ; quand tous les écrivains ont fermé leur extrémité, il reçoit une fin de fichier et peut terminer ;
  • si le lecteur se termine alors que l'écrivain écrit encore, l'écrivain reçoit le signal SIGPIPE (signal 13), qui le termine par défaut.

Cette exécution concurrente explique qu'un zcat journal.gz | grep erreur | head -n 5 s'arrête dès les cinq premières lignes trouvées, même sur une archive de 20 Go : head se termine, grep reçoit SIGPIPE à sa prochaine écriture, puis zcat de même. Rien n'a été lu au-delà du nécessaire.

Les codes de sortie

Chaque commande, en se terminant, renvoie un nombre entre 0 et 255 : son code de sortie (exit status). La convention, rappelée par le manuel de Bash, est que 0 signifie le succès et toute autre valeur un échec. Quelques valeurs sont réservées par le shell, et POSIX en fixe deux :

CodeSignification
0Succès
1, 2, ...Échec ; la signification exacte dépend de la commande (grep : 1 = rien trouvé, 2 = erreur ; diff : 1 = différences)
126La commande existe mais n'a pas pu être exécutée (pas le droit d'exécution, par exemple)
127Commande introuvable
128 + NLa commande a été terminée par le signal N (130 = Ctrl+C, signal 2 ; 141 = SIGPIPE, signal 13 ; 143 = SIGTERM, signal 15)

Le code de la dernière commande est dans la variable spéciale $?. Les opérateurs && (« si succès, alors ») et || (« si échec, alors ») s'appuient dessus, et ; enchaîne sans condition.

Pour un tube, le code de sortie est, par défaut, celui de la dernière commande. Les autres échecs sont silencieux, sauf si l'on active l'option pipefail, qui rend le tube en échec dès qu'une de ses commandes échoue. Longtemps propre à Bash et à quelques autres shells, pipefail a été intégrée à la norme POSIX dans sa version de 2024.

En pratique

Les sorties ont été produites sur Ubuntu 24.04, Bash 5.2, avec LC_ALL=C.UTF-8. Les messages de Bash sont ceux d'un shell interactif. On réutilise le fichier acces.log de la leçon 5 et le fichier app.conf créé dans ~/essais.

Sortie et erreurs, séparément

ls sur un fichier qui existe et un qui n'existe pas produit une ligne sur chaque flux :

$ ls app.conf absent
ls: cannot access 'absent': No such file or directory
app.conf
$ echo $?
2

Le code 2 signale l'erreur, même si ls a affiché le fichier existant. Redirigeons :

$ ls app.conf absent > f1.txt 2>&1
$ cat f1.txt
ls: cannot access 'absent': No such file or directory
app.conf
$ ls app.conf absent 2>&1 > f2.txt
ls: cannot access 'absent': No such file or directory
$ cat f2.txt
app.conf

Exactement ce que prévoit la règle de l'ordre : dans le premier cas, tout est dans le fichier ; dans le second, l'erreur est restée à l'écran. La forme courte de Bash &> est équivalente à la première :

$ ls app.conf absent &> f3.txt

Pour se débarrasser des erreurs d'une commande dont on attend des échecs prévisibles (un find qui traverse des répertoires interdits, par exemple), on les envoie dans /dev/null :

$ find / -name 'signalements*.conf' 2>/dev/null

À n'utiliser qu'en connaissance de cause : jeter les erreurs, c'est aussi s'interdire de les voir le jour où elles disent quelque chose d'important.

Ajouter plutôt qu'écraser

> vide le fichier avant même que la commande ne démarre. Pour ajouter, >> :

$ echo "$(date +%F) : redémarrage de signalements" >> ~/essais/interventions.txt

Une faute de frappe (> au lieu de >>) efface tout l'historique du fichier. L'option noclobber de Bash empêche > d'écraser un fichier existant :

$ set -o noclobber
$ echo test > app.conf
bash: app.conf: cannot overwrite existing file
$ echo test >| copie.txt         # >| force l'écrasement, explicitement

Ajoutée à votre ~/.bashrc (leçon 7), elle protège vos sessions interactives ; >| reste disponible quand l'écrasement est voulu. >> n'est pas concerné par noclobber.

Lire depuis un fichier, un texte, une chaîne

< branche l'entrée standard sur un fichier. Comparez :

$ wc -l acces.log
8 acces.log
$ wc -l < acces.log
8

Dans le premier cas, wc ouvre lui-même le fichier, et connaît donc son nom ; dans le second, c'est le shell qui l'ouvre et le branche sur l'entrée standard, et wc ne lit qu'un flux anonyme. La seconde forme est pratique pour récupérer un nombre seul dans une variable.

Un here-document fournit plusieurs lignes de texte à l'entrée standard, jusqu'à un délimiteur choisi. Le manuel de Bash précise un détail qui a son importance : si le délimiteur est entre apostrophes, le texte est pris littéralement ; sinon, les variables et les substitutions de commande y sont remplacées.

$ cat <<EOF
> Rapport du $(date +%F) : $(grep -c ' 500 ' acces.log) erreurs
> EOF
Rapport du 2026-10-05 : 2 erreurs
$ cat <<'EOF'
> Littéral : $(date)
> EOF
Littéral : $(date)

Les > en début de ligne sont l'invite de continuation du shell, pas des redirections : le shell attend la suite. Le premier here-document a été développé, le second non. C'est ainsi que l'on écrit un fichier de configuration depuis un script, avec la forme littérale quand le texte contient des $ qui ne doivent pas être interprétés.

Une here-string (<<<) fournit une seule chaîne :

$ tr a-z A-Z <<< "bonjour"
BONJOUR

Les tubes et tee

On l'a fait toute la leçon 5 : chaque | branche une sortie sur une entrée. Pour garder une copie de ce qui passe dans un tube, tee écrit son entrée à la fois dans un fichier et sur sa sortie, comme un raccord en T de plomberie :

$ cut -d' ' -f5 acces.log | tee codes.txt | wc -l
8
$ head -n 3 codes.txt
200
201
200

tee -a ajoute au lieu d'écraser. Et pour faire passer les erreurs dans un tube avec la sortie, 2>&1 |, ou sa forme courte |& :

$ ls app.conf absent 2>&1 | wc -l
2
$ ls app.conf absent |& wc -l
2

Pourquoi sudo echo ... > fichier échoue

Vous voulez ajouter une ligne à un fichier qui appartient à root. Le premier réflexe :

$ sudo echo "workers = 4" >> /etc/signalements/app.conf

échoue avec Permission denied. On obtient le même message en essayant d'écrire dans un fichier en lecture seule qui vous appartient :

$ touch fichier-protege && chmod 444 fichier-protege
$ echo ajout >> fichier-protege
bash: fichier-protege: Permission denied

Notez qui parle : bash, pas echo. La redirection est faite par votre shell, avec vos droits, avant de lancer la commande. Dans sudo echo ... >> /etc/..., sudo ne s'applique qu'à echo ; l'ouverture du fichier, elle, est tentée par votre shell non privilégié, et refusée. Deux solutions correctes :

$ echo "workers = 4" | sudo tee -a /etc/signalements/app.conf > /dev/null
$ sudo sh -c 'echo "workers = 4" >> /etc/signalements/app.conf'
  • Avec sudo tee -a, c'est tee, lancé en root, qui ouvre le fichier ; le > /dev/null évite de réafficher la ligne à l'écran.
  • Avec sudo sh -c '...', c'est un shell entier qui tourne en root, redirection comprise. Les apostrophes empêchent votre shell d'interpréter la redirection avant sudo.

La première forme est préférable : seul tee est privilégié, et l'on voit exactement quel fichier est écrit. Pour une vraie modification de configuration, la procédure sudoedit de la leçon 5 reste la bonne.

Enchaîner selon le résultat

$ grep -q ' 500 ' acces.log && echo "des erreurs 500"
des erreurs 500
$ grep -q ' 503 ' acces.log || echo "aucune 503"
aucune 503

grep -q (quiet) n'affiche rien : seul son code de sortie compte, 0 s'il a trouvé, 1 sinon. && n'exécute la suite qu'en cas de succès, || qu'en cas d'échec. On les combine pour écrire des enchaînements sûrs :

$ sudo cp -a app.conf app.conf.$(date +%F) && sudoedit app.conf
$ cd /opt/signalements/courant && ls

Dans la seconde ligne, si le cd échoue (répertoire absent), ls n'est pas exécuté dans un répertoire inattendu. Avec ; à la place de &&, il le serait. Retenez la règle pour plus tard, quand la commande suivante sera un rm : une commande qui dépend du succès de la précédente s'enchaîne par &&.

Attention à la forme a && b || c, souvent lue comme « si a, alors b, sinon c ». Elle exécute c aussi quand a réussit mais que b échoue. Pour un vrai « si alors sinon », il faut un if (cours Bash pour l'automatisation).

Les codes réservés

$ ./app.conf
bash: ./app.conf: Permission denied
$ echo $?
126
$ commandeinexistante
bash: commandeinexistante: command not found
$ echo $?
127

126 : le fichier existe mais n'est pas exécutable. 127 : aucune commande de ce nom. Sur Ubuntu, en session interactive, le message 127 est souvent enrichi de suggestions de paquets à installer (Command 'x' not found, but can be installed with...) : c'est un ajout de la distribution, le code reste 127.

Un processus terminé par un signal renvoie 128 plus le numéro du signal. On lance ici sleep en arrière-plan dans un shell non interactif (pour éviter les messages de suivi des tâches), on lui envoie le signal SIGTERM, et l'on attend sa fin :

$ bash -c 'sleep 5 & kill -TERM $!; wait $!; echo "code=$?"'
code=143

143 = 128 + 15, SIGTERM. La leçon 10 détaille les signaux.

Le tube qui ment : PIPESTATUS et pipefail

Comptons les erreurs 500 dans un journal... dont on s'est trompé de nom :

$ grep ' 500 ' absent.log | wc -l
grep: absent.log: No such file or directory
0
$ echo "code=$? PIPESTATUS=${PIPESTATUS[*]}"
code=0 PIPESTATUS=2 0

Le tube renvoie 0 : wc a parfaitement compté zéro ligne. L'échec de grep (code 2) n'est visible que dans le tableau PIPESTATUS, propre à Bash, qui garde le code de chaque commande du dernier tube. Dans un script de supervision, ce tube signalerait « 0 erreur » tous les jours, sans que personne ne remarque que le journal a changé de nom.

Avec pipefail :

$ set -o pipefail
$ grep ' 500 ' absent.log | wc -l
grep: absent.log: No such file or directory
0
$ echo "code=$?"
code=2

Le tube prend le code de la dernière commande qui a échoué. Le manuel de Bash : avec pipefail, le code de retour du tube est celui de la commande la plus à droite qui a échoué, ou zéro si toutes ont réussi. Le cours Bash pour l'automatisation fait de set -o pipefail (avec set -e et set -u) l'en-tête de tous les scripts.

pipefail a un revers, lié à SIGPIPE :

$ yes | head -n 2
y
y
$ echo "PIPESTATUS=${PIPESTATUS[*]}"
PIPESTATUS=141 0
$ kill -l 13
PIPE

yes écrit y à l'infini ; head se termine après deux lignes ; yes reçoit SIGPIPE et meurt avec le code 141. C'est le fonctionnement normal d'un tube, mais avec pipefail, ce tube est considéré comme un échec. Quand un script utilise head ou grep -q derrière une commande qui écrit beaucoup, ce faux échec est à prévoir.

La substitution de commande

$(commande) est remplacé par ce que la commande affiche, sans le saut de ligne final. On l'a déjà utilisée pour dater des fichiers :

$ echo "Il y a $(wc -l < acces.log) requêtes"
Il y a 8 requêtes
$ n=$(grep -c ' 500 ' acces.log)
$ echo $n
2

Entourez la substitution de guillemets quand son résultat peut contenir des espaces ("$(commande)"), pour éviter le découpage en mots vu à la leçon 4. L'ancienne syntaxe avec des accents graves (`commande`) fonctionne encore, mais s'imbrique mal ; préférez $( ).

Assembler : les erreurs 500 par heure

Tout ensemble, sur le journal de Signalements : les erreurs 500, regroupées par heure.

$ grep ' 500 ' acces.log | cut -c1-13 | sort | uniq -c
      2 2026-10-05T09

cut -c1-13 garde les treize premiers caractères de la date, c'est-à-dire le jour et l'heure. Sur un vrai journal d'une journée, on obtient une ligne par heure où des erreurs se sont produites, ce qui suffit souvent à relier un pic d'erreurs à un déploiement ou à une tâche planifiée. Et pour garder le rapport tout en le regardant :

$ grep ' 500 ' acces.log | cut -c1-13 | sort | uniq -c | tee erreurs-par-heure.txt

Sous le capot

On peut regarder le shell faire. L'outil strace affiche les appels système d'un programme ; on lui demande de suivre Bash et ses processus enfants (-f), en ne gardant que les appels qui nous intéressent, pour deux commandes : une redirection, puis un tube.

$ strace -f -e trace=openat,dup2,execve,pipe2 -o trace.txt \
    bash --norc -c 'ls app.conf > sortie.txt; cut -d" " -f5 acces.log | sort -u' > /dev/null
$ grep -E 'sortie.txt|dup2|pipe2|execve' trace.txt

Voici les lignes utiles, les adresses mémoire et la taille de l'environnement abrégées en … :

598392 execve("/usr/bin/bash", ["bash", "--norc", "-c", "ls app.conf > sortie.txt; cut -d"...], …) = 0
598393 openat(AT_FDCWD, "sortie.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
598393 dup2(3, 1)                       = 1
598393 execve("/usr/bin/ls", ["ls", "app.conf"], …) = 0
598392 pipe2([3, 4], 0)                 = 0
598394 dup2(4, 1)                       = 1
598395 dup2(3, 0)                       = 0
598394 execve("/usr/bin/cut", ["cut", "-d ", "-f5", "acces.log"], … <unfinished ...>
598395 execve("/usr/bin/sort", ["sort", "-u"], … <unfinished ...>

Lisons-les, le premier nombre étant le numéro du processus :

  1. La redirection. Bash (598392) a créé un processus enfant (598393). Dans cet enfant, avant de lancer ls, il ouvre sortie.txt avec les options O_WRONLY|O_CREAT|O_TRUNC : écriture seule, création si besoin, et troncature. Voilà pourquoi > vide le fichier, même si la commande échoue ensuite. Le fichier reçoit le descripteur 3. Puis dup2(3, 1) copie le descripteur 3 dans le 1 : la page dup(2) décrit dup2 comme l'appel qui fait de son second argument une copie du premier. Seulement alors, execve remplace le programme de l'enfant par ls, qui hérite de la table des descripteurs déjà rebranchée. Avec >>, on verrait O_APPEND à la place de O_TRUNC.
  2. Le tube. Bash crée le tube avec pipe2, qui renvoie deux descripteurs : 3 pour lire, 4 pour écrire. Il crée deux enfants. Dans le premier (598394), dup2(4, 1) branche la sortie standard sur l'écriture du tube ; dans le second (598395), dup2(3, 0) branche l'entrée standard sur la lecture du tube. Les deux execve apparaissent entremêlés, marqués unfinished : cut et sort démarrent en même temps.

2>&1 n'est donc rien d'autre qu'un dup2(1, 2), effectué au moment où le shell le lit. L'ordre des redirections, c'est l'ordre des dup2. Et le Permission denied de sudo echo > fichier est un openat refusé, fait par votre shell avant que sudo ne soit même lancé.

On peut aussi observer la table des descripteurs d'un processus, que Linux expose dans /proc/<pid>/fd. Le chemin spécial /proc/self désigne le processus qui le lit ; ici, ls regarde ses propres descripteurs, avec l'entrée branchée sur un fichier, la sortie dans un tube et les erreurs dans un autre fichier :

$ ls -l /proc/self/fd < acces.log 2> erreurs.txt | cat

On y lit, entre autres lignes : 0 -> /home/camille/essais/acces.log, 1 -> pipe:[18124593], 2 -> /home/camille/essais/erreurs.txt. Le numéro entre crochets identifie le tube dans le noyau.

Pièges courants

cmd 2>&1 > fichier. Les erreurs restent à l'écran. L'ordre correct est > fichier 2>&1, ou &> fichier.

> au lieu de >>. Le fichier est vidé avant même le lancement de la commande. set -o noclobber en interactif.

Lire et écrire le même fichier dans une même commande. sort fichier > fichier donne un fichier vide : le shell tronque fichier (le O_TRUNC vu plus haut) avant que sort ne commence à le lire. Même chose avec grep motif f | ... > f. Écrivez dans un fichier temporaire, puis renommez : sort f > f.tmp && mv f.tmp f. (sort -o f f est une exception documentée : sort lit tout avant d'écrire.)

sudo et les redirections. Voir la pratique : sudo tee, ou sudo sh -c '...'.

Un tube qui réussit alors qu'une étape a échoué. Code de la dernière commande seulement : PIPESTATUS pour diagnostiquer, set -o pipefail dans les scripts.

a && b || c utilisé comme un if. c s'exécute aussi si b échoue.

Des sorties qui s'entremêlent. Sortie standard et sortie d'erreur ne sont pas tamponnées de la même façon : redirigées ensemble dans un fichier, leurs lignes peuvent apparaître dans un ordre différent de celui de l'écran. Ne déduisez pas une chronologie fine de leur ordre dans un fichier.

$? lu trop tard. $? est le code de la dernière commande : un echo ou un ls intercalé l'écrase. Sauvegardez-le immédiatement (code=$?) si vous devez l'utiliser plus loin.

Sécurité

  • Les redirections créent des fichiers avec les droits par défaut. commande > rapport.txt crée un fichier lisible par les autres utilisateurs de la machine selon le masque umask (leçon 9). Si la sortie contient un secret (scw iam api-key create ... > cle.json, un export de base), créez le fichier avec des droits restreints avant d'y écrire (umask 077 dans un sous-shell, ou install -m 600 /dev/null cle.json), et supprimez-le dès qu'il ne sert plus.
  • Un secret ne passe pas en argument de commande. Les arguments d'une commande sont visibles par tous les utilisateurs de la machine (ps, leçon 10) et conservés dans l'historique du shell. Préférez l'entrée standard : commande --password-stdin < fichier, ou un tube depuis un gestionnaire de secrets. C'est la raison d'être des options --password-stdin de docker login et de nombreux outils.
  • > fichier en root peut écraser n'importe quoi. Une redirection mal ciblée dans un sudo sh -c ou un shell root peut vider /etc/passwd aussi simplement qu'un fichier d'essai. noclobber aide, la relecture aussi.
  • Les here-documents développent. Un here-document sans apostrophes remplace $VAR et $(...) : un texte qui contient des $ (un mot de passe, un modèle) sera modifié, ou exécutera une commande. Utilisez la forme <<'EOF' pour du texte littéral.
  • 2>/dev/null peut cacher une attaque. Jeter systématiquement les erreurs, c'est aussi masquer des refus d'accès qui méritaient attention. Réservez-le aux erreurs que vous savez attendues.

En production

  • Les services n'écrivent plus dans des fichiers. Sous systemd, un service écrit simplement sur sa sortie standard et sa sortie d'erreur, et systemd les envoie au journal (leçon 11). Dans un conteneur, c'est le même principe : Docker et Kubernetes recueillent les flux 1 et 2 du processus. Les douze facteurs des applications cloud (The Twelve-Factor App) en font une règle : une application traite ses journaux comme un flux d'événements, sans gérer de fichiers.
  • Les scripts de production commencent par set -euo pipefail, et vérifient les codes de sortie de ce qui compte. Un script de sauvegarde dont on ne vérifie pas le code de sortie de chaque étape d'un tube (pg_dump | gzip | aws s3 cp - ...) peut envoyer chaque nuit une archive vide.
  • La supervision lit les codes de sortie. Les tâches planifiées, les jobs de CI et les sondes de santé de Kubernetes jugent du succès par le code de sortie : un script qui renvoie 0 en cas d'échec rend tout l'édifice aveugle.
  • Chez Lyneko, le pipeline de CI (cours CI/CD : les principes) repose sur ce mécanisme : chaque étape est une commande, son code de sortie décide de la suite.

Exercices

1. Prévoir les redirections (niveau 100). Sans les exécuter, dites où vont la sortie et les erreurs de ls app.conf absent dans chaque cas, puis vérifiez : (a) > a.txt ; (b) 2> b.txt ; (c) > c.txt 2> c.txt ; (d) 2>&1 | wc -l ; (e) > /dev/null 2>&1.

Solution

(a) Sortie dans a.txt, erreur à l'écran. (b) Sortie à l'écran, erreur dans b.txt. (c) Les deux dans c.txt, mais le fichier est ouvert deux fois, indépendamment, chaque ouverture avec sa propre position d'écriture : les deux écritures commencent au début du fichier et s'écrasent mutuellement. Sur notre machine, cat -A c.txt a donné ceci :

app.conf$
t access 'absent': No such file or directory$

L'erreur a été écrite en premier, au début du fichier ; la sortie app.conf et son saut de ligne (9 octets) ont ensuite écrasé ses 9 premiers octets, ls: canno. La bonne forme est > c.txt 2>&1, où les deux descripteurs partagent la même ouverture, donc la même position. (d) Les deux lignes passent dans le tube : 2. (e) Rien ne s'affiche ; seul le code de sortie (2) témoigne de l'erreur.

2. Le comptage qui ment (niveau 200). Un script de supervision exécute chaque nuit nb=$(zcat /var/log/signalements/acces.log.1.gz | grep -c ' 500 ') et alerte si nb dépasse 100. Depuis une mise à jour, l'archive s'appelle acces.log.1.zst. Que se passe-t-il ? Corrigez le script pour que l'erreur soit détectée.

Solution

zcat échoue (fichier introuvable) et n'écrit rien ; grep -c compte 0 ligne et renvoie 1 (rien trouvé), mais c'est sans effet : le script ne regarde pas les codes. nb vaut 0, aucune alerte, chaque nuit. Correction : set -o pipefail en tête du script, et une vérification explicite : nb=$(zcat "$fichier" | grep -c ' 500 '); code=$?. Attention : grep -c renvoie 1 quand il ne trouve rien, ce qui n'est pas une erreur ici ; on teste donc [ "$code" -gt 1 ], ou l'on vérifie l'existence du fichier avant. Le cours Bash pour l'automatisation traite ces cas avec if et test.

3. Écrire un fichier système (niveau 200). Vous devez ajouter la ligne log_level = debug à /etc/signalements/app.conf depuis un script lancé par un utilisateur qui a le droit d'utiliser sudo. Écrivez la commande, et expliquez pourquoi sudo echo "log_level = debug" >> /etc/signalements/app.conf ne fonctionne pas.

Solution

echo "log_level = debug" | sudo tee -a /etc/signalements/app.conf > /dev/null. La version avec sudo echo échoue parce que la redirection >> est réalisée par le shell de l'utilisateur, avant le lancement de sudo, avec ses droits à lui : l'openat sur /etc/signalements/app.conf est refusé. sudo ne s'applique qu'à echo, qui n'a besoin d'aucun privilège. Avec tee, c'est le programme lancé par sudo qui ouvre le fichier.

4. Lire une trace (niveau 200). Lancez strace -f -e trace=openat,dup2,pipe2,execve -o t.txt bash --norc -c 'grep 500 acces.log 2>&1 >> rapport.txt', puis retrouvez dans t.txt les appels qui réalisent les redirections. Dans quel ordre apparaissent-ils, et qu'en déduisez-vous sur la destination des erreurs ?

Solution

Dans le processus enfant, on voit d'abord dup2(1, 2) (le 2>&1, qui copie la sortie standard actuelle, c'est-à-dire ce qu'a reçu bash, ici le terminal ou ce vers quoi vous avez redirigé la commande strace), puis l'ouverture de rapport.txt avec O_WRONLY|O_CREAT|O_APPEND suivie de dup2(3, 1), enfin l'execve de grep. Les erreurs vont donc à la destination d'origine de la sortie standard, et seule la sortie normale va dans rapport.txt. On note aussi O_APPEND au lieu de O_TRUNC : >> ajoute.

Récapitulatif

  • Trois flux standard : 0 entrée, 1 sortie, 2 erreurs. Ce sont des descripteurs de fichiers, des cases de la table des fichiers ouverts du processus.
  • Une redirection rebranche un descripteur avant l'exécution : > (tronque), >> (ajoute), <, 2>, 2>&1 (copie la destination actuelle de 1), &>. L'ordre compte : > f 2>&1.
  • La redirection est faite par votre shell, avec vos droits : sudo cmd > f échoue, cmd | sudo tee f fonctionne.
  • Un tube lance ses commandes en même temps, reliées par un tampon du noyau ; quand le lecteur s'arrête, l'écrivain reçoit SIGPIPE (code 141).
  • Code de sortie : 0 succès ; 126 non exécutable ; 127 introuvable ; 128 + N tué par le signal N. $?, &&, ||.
  • Le code d'un tube est celui de sa dernière commande : PIPESTATUS pour voir les autres, set -o pipefail dans les scripts.
  • $(commande) insère une sortie ; un here-document <<EOF développe, <<'EOF' non ; <<< fournit une chaîne.
  • Sous le capot : openat puis dup2 puis execve ; pipe2 et deux enfants pour un tube.

Pour aller plus loin

  • Le chapitre Redirections du manuel de Bash, court et précis, et la section Pipelines pour pipefail et |&.
  • La page pipe(7) du projet Linux man-pages, pour la capacité des tubes, l'atomicité des petites écritures et le comportement en cas de lecteur absent.
  • La section Exit Status for Commands de la norme POSIX, qui fixe les codes 126 et 127.
  • La leçon suivante, sur l'environnement du shell : variables, PATH, et les fichiers qui configurent chaque session.
Voir ma constellation →

Sources