2. Les Fondements : descripteurs de fichiers par défaut, redirections et pipes

L'un des principes fondamentaux d'Unix prévoit de découper les traitements complexes en un certain nombre de commandes simples qui seront coordonnées ou juxtaposées, voire imbriquées, afin de fournir un résultat prévisible et constant pour un contexte donné. [1] :

Pour ce faire, Unix a été conceptuellement doté (et ce au niveau de la librairie C sous-jacente) de mécanismes permettant la collaboration des différentes commandes, unifiées par l'environnement controlant l'exécution de ce flux (généralement un shell), qui vont ainsi transporter d'une commande à l'autre les données textuelles traitées : l'information est transportée sous forme d'un flux textuel

Les descripteurs automatiquement créés pour chaque nouveau processus.
Les redirections.
Les tubes (ou pipes).

2.1. Les descripteurs de fichiers par défaut :

Lors de la création d'un nouveau processus, trois descripteurs de fichiers sont automatiquement créés. Pour s'en assurer, il suffira de compiler et exécuter le petit fichier source C suivant :


#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc, char **argv)
{
	int i, status;
	pid_t pid, fils;
	char c;
	char  fic[256];
	struct stat fs;
	
	pid = getpid();
	printf("PID du processus : %d \n",pid);
	for (i = getdtablesize(); i >= 0; --i)
	{
		if ( ! fstat(i, &fs) )
		{
			printf("- Descripteur %d (inode %d%d - ", i, fs.st_dev, fs.st_ino);
			memset(&fic, 0, sizeof(fic));
			sprintf((char *)&fic, "/proc/%d/fd/%d", pid, i);
			switch (fs.st_mode & S_IFMT)
			{
				case S_IFBLK:  printf("périphérique bloc) :");		break;
				case S_IFCHR:  printf("périphérique caractère) :");	break;
				case S_IFDIR:  printf("répertoire) :");			break;
				case S_IFIFO:  printf("FIFO/tube) :");			break;
				case S_IFLNK:  printf("lien symbolique) :");		break;
				case S_IFREG:  printf("fichier ordinaire) :");		break;
				case S_IFSOCK: printf("socket) :");			break;
				default:       printf("type inconnu...) :");
			}
			printf("\n");
			fils = fork();
			if (fils == 0)
			{
				execl("/bin/ls", "/bin/ls", "-l", fic, NULL);
				_exit (EXIT_FAILURE);
			}
			else if (fils < 0)
				printf("\nErreur lors de l'exécution de la commande ls.\n");
				else
					if (waitpid (fils, &status, 0) != fils)
						status = -1;

				printf("\n");
		}
	}
	printf("Appuyez sur une touche pour terminer l'exécution du programme.");
	c = tolower (fgetc (stdin));
	return 0;
}				

Compilez le code via une commande du style :

td_scripts$ gcc -o descripteurs descripteurs.c

Puis exécutez le programme ainsi obtenu :

td_scripts$ ./descripteurs

 
PID du processus : 5085 
- Descripteur 2 (inode 110 - périphérique caractère) :
lrwx------ 1 francois francois 64 sep 21 17:24 /proc/5085/fd/2 -> /dev/pts/1

- Descripteur 1 (inode 110 - périphérique caractère) :
lrwx------ 1 francois francois 64 sep 21 17:24 /proc/5085/fd/1 -> /dev/pts/1

- Descripteur 0 (inode 110 - périphérique caractère) :
lrwx------ 1 francois francois 64 sep 21 17:24 /proc/5085/fd/0 -> /dev/pts/1

Appuyez sur une touche pour terminer l'exécution du programme.
				

Lors de son exécution, ce petit programme parcourt sa table des descripteurs à l'envers, et affiche ceux qui ont été affectés. Pour chacun des descripteurs ouverts (via la fonction open(...) de la libc), il procède à l'affichage du fichier correspondant par un appel à la commande unix ls. Ce qui nous montre bien, que par défaut, un processus lancé depuis un shell dispose de 3 descripteurs de fichiers vers la console d'appel (dans l'exemple, il s'agit du pseudo-terminal /dev/pts/1) : ces descripteurs sont hérités du shell père qui les a créés lors de son initialisation (login).

En conclusion, par défaut tout nouveau processus dispose de 3 descripteurs de fichiers ouverts sur la même console :

Le fait d'avoir 2 descripteurs distincts ouverts en écriture permettra de traiter distinctement les résultats et les messages d'erreur, même si par défaut ces différents flux utilisent le même terminal.

2.2. Les redirections :

La redirection est le mécanisme offert par le shell permettant d'affecter un flux de données vers un fichier particulier[2] C'est ainsi qu'il sera possible de récupérer le résultat d'une commande dans un fichier, ou encore d'envoyer un fichier vers une commande.

2.2.1. La redirection des sorties (en écriture)

Elle permet donc d'envoyer les affichages normalement affectés à un descripteur donné vers un fichier; et peut prendre deux formes :

La simple redirection, qui s'écrit via le symbole : >, et crée ou écrase le fichier de destination (s'il existe).
La double redirection, qui s'écrit via le symbole : >>, et crée ou concatène le flux sortant vers le fichier de destination.

La syntaxe d'une redirection de sortie est donc la suivante :

macommande [descripteur]>[>] nomfichier où les termes entre crochets sont optionnels.

Exemples :

td_scripts$ ls > toto.txt envoie le résultat de la commande ls dans le fichier toto.txt.

Qu'il est également possible d'écrire sous la forme :

td_scripts$ ls 1> toto.txt dans la mesure où à défaut d'indication, c'est le descripteur 1 qui est utilisé par la redirection de sortie.

td_scripts$ ls > toto.txt; ls -l >> toto.txt envoie le résultat de la commande ls dans le fichier toto.txt, puis y ajoute le résultat de la commande ls -l.

td_scripts$ ls > toto.txt; ls -l > toto.txt envoie le résultat de la commande ls dans le fichier toto.txt, puis l'écrase par le résultat de la commande ls -l.

td_scripts$ ls -l monfichierquinexistepas > toto.txt écrase le fichier toto.txt en n'y écrivant rien dedans (puisque le fichier visé n'existe pas), et affiche le message d'erreur suivant à l'écran :

ls: ne peut accéder monfichier: Aucun fichier ou répertoire de ce type

td_scripts$ ls -l monfichierquinexistepas > toto.txt 2>/dev/null écrase le fichier toto.txt en n'y écrivant rien dedans (puisque le fichier visé n'existe toujours pas), et redirige le message d'erreur vers /dev/null, ce qui en supprime l'affichage.

td_scripts$ ls -l monfichierquinexistepas * > toto.txt 2>toto.txt envoie le résultat de la commande ls et le message d'erreur dans le fichier toto.txt.

td_scripts$ ls -l monfichierquinexistepas * > toto.txt 2>&1 est identique à la précédente forme, si ce n'est que dans ce cas, la syntaxe 2>&1 indique au shell de rediriger les flux destinés au descripteur 2 vers le descripteur 1.

2.2.2. La redirection de l'entrée standard (en lecture)

Elle permet donc d'affecter le contenu d'un fichier en tant que saisie d'une commande; et peut également prendre deux formes :

La simple redirection, qui s'écrit via le symbole : <, et lit des données depuis le fichier spécifié.
La double redirection, qui s'écrit via le symbole : <<, et lit les données présentes entre deux occurences d'une étiquette.

Astuce

Cette forme est essentiellement utilisée dans les script pour utiliser une portion de celui-ci en tant qu'entrée d'une commande ou encore pour transmettre une transcription littérale d'une saisie.

Sa syntaxe est la suivante:

macommande [0]<[<][-][ETIQUETTE] nomfichier où les termes entre crochets sont optionnel.

Exemples :

td_scripts$ mail -s "un sujet" lepape@vatican.com < toto.txt envoie le contenu du fichier toto.txt dans le corps du message électronique à destination du pape.

td_scripts$ mail -s "un sujet" lepape@vatican.com <MSG


					>bla
					>bla
					>bla
					>MSG
est équivalente à la précédente, si ce n'est que la commande mail attend cette fois-ci la saisie du message depuis la console. Le saisie se termine par la saisie en début de ligne de l'étiquette, ici le mot MSG (C'est d'ailleurs le comportement normal de cette commande si rien ne lui est spécifiée - rappelez vous la ligne se terminant par . pour finir un message !).

Soit le bout de script suivant dans toto.sh :


cat <<FIN
	Ceci est message
	sur plusieurs lignes...
FIN

cat <<-EOF
	Ceci est un message
	sur plusieurs lignes...
	dans lequel les tabulations seront omises
	EOF
				
Son exécution par td_scripts$ . toto.sh affiche le contenu :

	Ceci est message
	sur plusieurs lignes...
Ceci est un message
sur plusieurs lignes...
dans lequel les tabulations seront omises
				

Astuce

L'utilisation du symbole - suivant la redirection << permet de supprimer les éventuelles tabulations ayant servi à indenter proprement le code du script.

2.3. Les tubes de communication (ou pipes) :

Un tube est un autre mécanisme du shell permettant de faire communiquer entre eux deux processus, sa syntaxe est la suivante :

macommande1 | macommande2 [| macommande3] [...]

Le tube a donc pour effet d'affecter la sortie standard de la commande située à gauche de l'expression à l'entrée standard de la commande de droite de l'expression ;

Note

Le tube n'a de sens que si les commandes utilisées à droite de l'expression utilisent leur entrée standard, ce qui est notamment le cas pour les commandes ls, echo, cp, rm, mv, etc...

Exemples :

td_scripts$ ls | tee toto.txt envoie le résultat de la commande ls vers la commande tee laquelle affiche sur la console ce qu'elle reçoit sur son entrée standard et l'insère également dans le fichier toto.txt.

td_scripts$ ls | wc -l wc -l compte le nombre de lignes reçues sur son entrée standard (en l'occurence la sortie de ls).

td_scripts$ dpkg -L coreutils | grep bin | pr -4 | sed -e '/^$/d' | sed -e '/^[1-9]/d' affiche sur 4 colonnes les binaires contenus dans le paquet debian coreutils

dont voici le résultat :


/usr/sbin	 			/usr/bin/head		/usr/bin/sort 			/bin/pwd
/usr/sbin/chroot		/usr/bin/tsort		/usr/bin/du			/bin/echo
/usr/bin	 			/usr/bin/chcon 		/usr/bin/paste 		/bin/ls
/usr/bin/tail	 		/usr/bin/expand 		/usr/bin/cksum 		/bin/df
/usr/bin/cut			/usr/bin/tac	 		/usr/bin/factor 		/bin/mv
/usr/bin/users		/usr/bin/dirname 	/usr/bin/pr	 		/bin/chown
/usr/bin/printenv 	/usr/bin/csplit 		/usr/bin/pathchk 	/bin/cp
/usr/bin/sha384su 	/usr/bin/printf 		/usr/bin/link 			/bin/dd
/usr/bin/sha1sum 	/usr/bin/who	 		/usr/bin/ptx 			/bin/readlink
/usr/bin/groups	 	/usr/bin/fold	 		/usr/bin/sha256su	/bin/chgrp
/usr/bin/mkfifo	 	/usr/bin/dircolor 	/usr/bin/sha224su 	/bin/false
/usr/bin/[	 			/usr/bin/tee	 		/usr/bin/base64 		/bin/rm
/usr/bin/md5sum	/usr/bin/tty	 		/usr/bin/pinky 		/bin/true
/usr/bin/whoami	 	/usr/bin/logname 	/usr/bin/stat 			/bin/date
/usr/bin/runcon	 	/usr/bin/od	 		/usr/bin/install 		/bin/sleep
/usr/bin/yes	 		/usr/bin/nl	 		/usr/bin/expr 		/bin/mkdir
/usr/bin/shuf	 		/usr/bin/env	 		/usr/bin/uniq 		/bin/rmdir
/usr/bin/seq	 		/usr/bin/join	 		/usr/bin/wc			/bin/mknod
/usr/bin/tr	 		/usr/bin/sha512su 	/usr/bin/basename 	/bin/sync
/usr/bin/nohup	 	/usr/bin/sum	 		/bin/vdir				/bin
/usr/bin/unexpand 	/usr/bin/shred 		/bin/ln	 			/bin/dir
/usr/bin/id	 		/usr/bin/fmt	 		/bin/uname			/bin/chmod
/usr/bin/hostid	 	/usr/bin/comm	 	/bin/stty	 			/usr/bin/md5sum.t
/usr/bin/nice	 		/usr/bin/split 		/bin/cat	 			/usr/bin/touch
/usr/bin/unlink	 	/usr/bin/test	 		/bin/touch
			

Note

C'est avec l'utilisation des tubes de communication que le notion de filtres Unix prend tout son sens (cf. chapite 5) !

2.4. Le regroupement de commandes :

Il peut être parfois utile de regrouper un certain nombre de commandes, qui seront alors séparées par un caractère point-virgule (;), soit pour en rediriger les sorties vers un même fichier ou vers un tube, soit pour les faire exécuter dans un même environnement.

2.4.1. Regroupement par des parenthèses :

Ce regroupement permet d'agréger les sorties standard (et les sorties d'erreurs) de chaque commande, comme s'il s'agissait d'un seul et unqiue descripteur. Dans ce cas, les commandes sont exécutées classiquement, dans l'ordre d'apparition dans l'expression, par des shells enfants.

Exemple :

td_scripts$ (echo "DEBUT DU FICHIER" ; cat toto.txt ; echo "FIN DU FICHIER) > titi.txt envoie le contenu du fichier toto.txt encadré des lignes "DEBUT..." et "FIN..." dans le fichier titi.txt.

2.4.2. Regroupement par des accolades :

Cet autre type de regroupement permet de faire exécuter la séquence de commandes par le shell courant, et également d'agréger les sorties.

Note

Dans ce cas toutes les commandes (y compris la dernière) doivent être suivies d'un point-virgule (;), de plus un espace est nécessaire après l'accolade ouvrante et avant l'accolade fermante.

Exemple :

td_scripts$ { cd monnouveaurepertoire ; echo "DEBUT DU FICHIER" ; cat toto.txt ; echo "FIN DU FICHIER ; } > titi.txt envoie le contenu du fichier toto.txt encadré des lignes "DEBUT..." et "FIN..." dans le fichier titi.txt, en outre l'environnement du shell sera modifié, le répertoire courant étant maintenant ./monnouveaurepertoire/ .

Notes

[1]

Une commande ne fait qu'une seule chose... mais le fait bien !

[2]

Or, sous Unix, tout est fichier ...