lundi 1 novembre 2010

redirections (encore)

Les descripteurs de fichiers
Le répertoire /proc contient un sous-répertoire pour chaque processus qui tourne sur le système, sous-répertoire nommé d'après le pid de ce processus.
Chacun de ces répertoires contient à son tour un sous-répertoire fd où se trouvent les descripteurs de fichier ouverts par le processus.
Sachant que echo $$ renvoie le pid du shell bash courant, examinons quels y sont les descripteurs de fichier ouverts:

/proc/1897/fd/0 correspond à l'entrée standard (stdin)
/proc/1897/fd/1 correspond à la sortie standard (stdout)
/proc/1897/fd/2  correspond à l'erreur standard (stderr)

Ce sont les descripteurs de fichiers par défaut.
Nous avons déjà traité de stdout et stderr dans le billet précédent.
Quant à l'entrée standard, elle correspond au clavier.
Lorsque nous exécutons une commande, il lui correspond un processus qui est différent du processus du shell dans lequel la commande a été lancée. Ce processus hérite des descripteurs tels qu'ils existent au niveau du shell, puis les redirections éventuelles sont effectuées. Mais comment avoir un état de la situation? Il faudrait connaître le pid de ce processus! Et bien pas vraiment: il suffit d'examiner le contenu du répertoire /proc/self/fd:


Nous voyons que /proc/self/fd est en fait /proc/2310/fd où 2310 est le pid du processus d'exécution de la commande. Ce répertoire /proc/2310/fd n'existe que pendant la durée de vie du processus. Les messages d'erreurs de la première commande ls (comme celui généré par le fait que le fichier tata est inexistant) n'apparaissent pas à l'écran. Ils ont été redirigés vers le fichier erreurs, ce que montre bien l'examen du contenu de /proc/2310/fd. La deuxième commande ls a hérité des descripteurs du shell qui sont restés inchangés et cette fois le message d'erreur (prouvant que /proc/2310 n'existe plus) arrive à l'écran.

Les canaux de communication avant et après un pipe.
Dans le cas d'un pipe, chacune des deux commandes, avant et après le pipe, a son propre  système de canaux de communication. En principe stdout de la commande en aval est connecté à stdin de la commande en amont, comme dans ce cas simple où l'entrée standard de la commande tr n'est plus le clavier, mais la sortie standard de la commande cat:


Mais en cas de redirections, la situation peut-être plus compliquée:


Ici, stdout en aval (processus 2109) a été redirigé vers le fichier output et c'est le descripteur 2 auquel le canal 1 a été affecté qui est connecté au pipe. En amont (processus 2110), stdin (fd/0) est connecté au pipe. Bien sûr, le pipe ici ne sert absolument à rien. En effet la commande ll en aval ne produit aucun message d'erreur qui serait envoyé dans le canal 1. Donc rien ne transite par le pipe et de toute façon ls en amont ignore stdin. Cette ligne de commandes n'a d'autre utilité que de permettre la visualisation de connections en amont et en aval d'un pipe.

Filtrer stderr.
Imaginons maintenant que nous désirions filtrer les messages d'erreurs avec grep. Nous voudrions aussi que tout arrive à l'écran, aussi bien stderr que stdout. Dans le billet précédant nous avons proposé une solution à ce genre de problème. En voici une autre:


correspondant au schéma:
Cette solution, comme celle du billet précédant, demande un descripteur supplémentaire (3). Mais cette fois, il est créé au niveau de la commande et il n'existe que pendant la durée d'exécution de la commande, alors qu'auparavant il avait été défini au niveau du shell.
Il ne faut pas se laisser leurrer par le schéma et lui accorder une signification qu'il n'a pas: les différentes instructions de redirections s'évaluent bien de gauche à droite. La première instruction 3>&1 établit un lien du descripteur 3 vers le canal 1, lien vers le canal 1 qui sans cela serait perdu suite à la deuxième instruction qui lie le descripteur 1 au canal 2. La dernière instruction 2>&3  affecte au descripteur 2 le canal précédemment affecté au descripteur 3, c'est-à-dire le canal 1.

Rediriger stdout au niveau du shell
Si nous redirigeons stdout vers un fichier au niveau d'un shell bash, chaque commande lancée dans ce shell, héritant des descripteurs existants, enverra son output dans ce fichier. Plus rien n'arrivera à l'écran, sauf les messages d'erreurs. Avant de faire pointer le descripteur 1 vers le fichier, il convient de sauvegarder le lien vers le canal 1:
toto@rigel:~/test$ exec 3>&1 1> output
De la sorte on pourra restaurer la situation initiale en faisant pointer le descripteur 1 vers le canal associé au descripteur 3 et en clôturant ensuite ce descripteur 3:
toto@rigel:~/test$ exec 1>&3 3>&-
Procédons:



Rediriger stdin
Nous n'avons pas jusqu'à présent donné d'exemple de redirection de stdin.
Revenons sur la commande où nous avons mis en majuscules le contenu du fichier erreurs. On aurait pu procéder comme ceci:


< erreurs est mis pour 0< erreurs: l'entrée standard a été redirigée vers le fichier erreurs. On peut enchaîner les redirections comme ceci:


La sortie standard est redirigée vers Erreurs qui va donc contenir le message modifié.
Ce genre d'expression:
commande < fichier-in > fichier-out
est relativement courant.
Continuons avec un autre exemple:


La commande read est en attente de ce que nous allons saisir au clavier pour le mettre dans la variable readvar. Nous saisissons par exemple "hello", puis nous tapons sur Enter afin d'envoyer le texte saisi. Puis nous vérifions le contenu de readvar:

Si nous voulons lire le fichier toto dont voici le contenu:

Je m'appelle Toto.
Je vais à l'école avec Tata
Nous allons à pied car ce n'est pas loin.

nous devons rediriger stdin vers ce fichier.
Procédons:

readvar contient la première ligne du fichier toto. Si nous relançons la commande read rien ne changera. Ce sera encore la première ligne qui sera lue, car à chaque fois le fichier toto est réouvert et la lecture reprend au début . En fait ce qu'il faudrait, c'est pouvoir rediriger stdin vers le fichier toto au niveau du shell bash, comme cela a été fait pour stdout. Mais c'est impossible (plantage garanti). Par contre nous pouvons procéder au niveau d'un script qui contiendra les commandes read, comme dans le script suivant (appelé readtoto):

#!/bin/bash

exec 4<&0 < toto # sauvegarde du lien vers le canal 0
                 # redirige stdin vers toto
sleep 10
read readvar
echo $readvar
read readvar
echo $readvar

exec 0<&4 4<&-   # retablit stdin et clôture fd/4

La commande sleep est là pour nous permettre d'examiner la situation depuis un autre shell bash. Procédons après avoir rendu le script exécutable (chmod +x readtoto):

echo $PPID nous a donné le pid du processus parent et pendant que le script était arrêté sur la commande sleep nous avons lancé la commande pstree (dans un autre shell).


La commande sleep tourne dans un processus à part, de même que les deux commandes read suivantes qui vont à chaque fois hériter des canaux de communication définis au niveau du processus 2569 (readtoto). Comme toto est ouvert une seule fois en début de script, les différentes lignes du fichier sont lues les unes après les autres.

Ouverture en lecture avec un descripteur
Il existe une autre façon de procéder pour arriver au même résultat: ouvrir toto en lecture en le reliant à un nouveau descripteur qui est alors créé et ajouter après chaque read une instruction redirigeant stdin vers le fichier lié à ce nouveau descripteur.



(La dernière commande est la commande de fermeture du fichier)

De nouveau le fichier est ouvert une seule fois ce qui nous permet de lire au delà de la première ligne.

Ouverture en écriture avec un descripteur
Nous ouvert le fichier toto en lecture via un descripteur.
Il est possible de procéder de la sorte pour ouvrir un fichier en écriture.
Ouvrons par exemple le fichier erreurs en écriture en l'affectant au descripteur 4.
Attention, de ce fait nous supprimons le contenu éventuel du fichier.
Nous commençons par écrire dans le fichier via une redirection de stdout vers le fichier affecté au descripteur 4, ensuite via une redirection de stderr:


On pourrait arriver au même résultat sans utiliser fd/4 et en remplaçant chaque >&4 par >> erreurs.