Redirections, tubes et codes de sortie
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éro | Nom | Sens | Par défaut |
|---|---|---|---|
| 0 | entrée standard (stdin) | ce que le programme lit | le clavier du terminal |
| 1 | sortie standard (stdout) | les résultats | l'écran du terminal |
| 2 | sortie d'erreur (stderr) | les messages d'erreur et de diagnostic | l'é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.
| Syntaxe | Effet |
|---|---|
cmd > f | sortie standard vers f, en le vidant d'abord (le fichier est créé s'il n'existe pas) |
cmd >> f | sortie standard ajoutée à la fin de f |
cmd < f | entrée standard lue dans f |
cmd 2> f | sortie d'erreur vers f |
cmd 2>&1 | descripteur 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/null | sortie 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 dansf; ensuite, 2 devient une copie de 1, donc va aussi dansf. 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 dansf. 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 :
| Code | Signification |
|---|---|
| 0 | Succès |
| 1, 2, ... | Échec ; la signification exacte dépend de la commande (grep : 1 = rien trouvé, 2 = erreur ; diff : 1 = différences) |
| 126 | La commande existe mais n'a pas pu être exécutée (pas le droit d'exécution, par exemple) |
| 127 | Commande introuvable |
| 128 + N | La 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'esttee, lancé enroot, 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 enroot, redirection comprise. Les apostrophes empêchent votre shell d'interpréter la redirection avantsudo.
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 :
- La redirection. Bash (598392) a créé un processus enfant (598393). Dans cet enfant, avant de lancer
ls, il ouvresortie.txtavec les optionsO_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. Puisdup2(3, 1)copie le descripteur 3 dans le 1 : la pagedup(2)décritdup2comme l'appel qui fait de son second argument une copie du premier. Seulement alors,execveremplace le programme de l'enfant parls, qui hérite de la table des descripteurs déjà rebranchée. Avec>>, on verraitO_APPENDà la place deO_TRUNC. - 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 deuxexecveapparaissent entremêlés, marquésunfinished:cutetsortdé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.txtcrée un fichier lisible par les autres utilisateurs de la machine selon le masqueumask(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 077dans un sous-shell, ouinstall -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-stdindedocker loginet de nombreux outils. > fichierenrootpeut écraser n'importe quoi. Une redirection mal ciblée dans unsudo sh -cou un shellrootpeut vider/etc/passwdaussi simplement qu'un fichier d'essai.noclobberaide, la relecture aussi.- Les here-documents développent. Un here-document sans apostrophes remplace
$VARet$(...): 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/nullpeut 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 ffonctionne. - 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 :
PIPESTATUSpour voir les autres,set -o pipefaildans les scripts. $(commande)insère une sortie ; un here-document<<EOFdéveloppe,<<'EOF'non ;<<<fournit une chaîne.- Sous le capot :
openatpuisdup2puisexecve;pipe2et 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
pipefailet|&. - 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.
Sources
- GNU Bash Reference Manual : Redirections
- GNU Bash Reference Manual : Pipelines, Exit Status (page de manuel bash(1))
- POSIX.1-2024, Shell Command Language (2.8.2 Exit Status for Commands)
- Linux man-pages, pipe(7)
- Linux man-pages, dup(2)
- Linux man-pages, stdin(3)
- William Shotts, Learning the Shell : I/O Redirection