Master SAR 2006-2007, Université Pierre & Marie Curie. Alexandre Duret-Lutz, Emmanuel Saint-James. [retour à l'index des TD]

Administration et Architecture des Systèmes, TD 1

Shell avancé

Les systèmes Unix sont en bonne partie administrables sans programmation de bas niveau, par des fichiers de configurations et de scripts écrits à l'aide du langage de commandes Shell.

Il existe plusieurs variantes de shells, toutes facilement compilables dans les différentes versions d'Unix. Celle est installée par défaut sur la plupart des systèmes GNU/Linux, comme à l'ARI, est GNU bash (Bourne Again Shell). Ce langage est standardisé, donc toutes les implémentations de Shell dites POSIX compliant respectent ce standard.

Cette première séance est consacrée à une revue assez exhaustive de ce langage. Nous utiliserons bash, qui possède de très nombreuses fonctionnalités qui ne font pas partie du standard, et qui peuvent donc ne pas fonctionner avec d'autres shells. Nous les éviterons la plupart du temps, mais certaines extensions sont trop pratiques pour être passées sous silence. Nous marquerons ces extensions clairement avec [non-standard]. Pratiques en mode interactif, il vaudra mieux les éviter lorsque vous écrivez des scripts qui doivent fonctionner sur d'autres systèmes.

Fondamentalement, Shell est un langage de manipulation de chaînes de caractères (en particulier les noms de fichiers et leur contenu quand ils sont textuels), le mécanisme de base étant de remplacer une chaîne par une autre : on remplace un caractère spécial par son expansion, une variable par sa valeur, une commande Unix par le flux de sortie qu'elle calcule etc.

Le Shell traite les commandes qu'on lui soumet en plusieurs étapes qu'il est important de connaître si l'on veut anticiper les conséquences, notamment syntaxiques, de ce qu'on écrit.

  1. Le shell lit les commandes sur son entrée standard, en paramètre de l'option -c, ou à partir d'un fichier passé en paramètre.

  2. Le shell sépare son entrée en lexèmes (une unité lexicale telle qu'un mot ou un opérateur). C'est l'analyse lexicale.

  3. Le shell analyse son entrée pour y reconnaître des commandes simples, ou des commandes composées. C'est l'analyse syntaxique. La substitution des alias est effectuée pendant cette analyse.

  4. Le shell effectue plusieurs types de substitutions sur les différents parties d'un commande. Sont réalisées en même temps :

    • les substitutions d'accolades (ex. mkdir dir{1,2,3}{,b}) [non-standard]
    • les substitutions de tildes (ex.: cp ~adl/.emacs ~toto/.emacs)
    • les substitutions de variables (ex.: echo $DISPLAY)
    • les substitutions de commandes (ex.: mkdir `date -I`)
    • les substitutions arithmétiques (ex.: echo $((33 * 4)))

    Puis le résultat est découpé (en général sur les espaces) pour former une liste de mots.

    • des substitutions de noms de fichiers sont effectuées
    • les protections ("...", '...', \) s'il y en avait, sont supprimées
  5. Le shell prend en compte les opérateurs de redirection (repérés lors de l'analyse lexicale), puis les supprime de la commande.

  6. Le shell exécute la commande en lui passant ses paramètres. Il doit alors décider s'il s'agit d'une fonction, d'une commande interne au shell, ou d'un exécutable externe.

  7. Sauf contre-ordre (&), le shell attend que la commande se termine avant de passer à la suivante.

Ce recueil d'exercices est rythmé par ces étapes.

Note

Cette feuille est exceptionnellement longue et il n'est pas question de tout faire en TD. Essayez de ne vous occuper que des sections marquées d'une étoile (*) dans la liste qui suit, et s'il vous reste du temps, enchaînez avec les deux étoiles. Les autres sections discutent d'aspects qui doivent vous être familiers et que vous pourrez réviser par vous même.

Sommaire

1   Lecture des commandes

Question

Faites afficher Bonjour ! à un Shell en lui passant la commande echo Bonjour ! de trois façons différentes (et non-interactives).

Solution

$ echo 'echo Bonjour !' | sh
Bonjour !
$ sh -c 'echo Bonjour !'
Bonjour !
$ echo 'echo Bonjour !' > commandes
$ sh commandes
Bonjour !

2   Analyse Syntaxique

Le premier mot est comparé à l'ensemble des mot-clés connus:

!     elif    for     until
case  else    if      while
do    esac    in      {
done  fi      then    }

Certains de ces mot-clés introduisent une commande complexe ({, case, for, ...), d'autre sa continuation (in, then, ...) ou sa terminaison (fi, done, ...).

Question

Expliquez les comportements du shell pour les 5 entrées suivantes:

  • if [ x = x ] then echo oui fi
  • if [ x = x ] then echo oui; fi
  • if [ x = x ]; then echo oui fi
  • if [ x = x ]; then echo oui; fi
  • if [ x = x ]; then echo "c'est compris ?"; fi

Solution

$ if [ x = x ] then echo oui fi
>

Le shell a lu la commande suivant if jusqu'à l'indication de sa fin, qui est ici la fin de ligne. La commande [ sera donc lancé avec les 8 mots suivants comme paramètres. Le shell étant maintenant un then, suite obligatoire du if.

$ if [ x = x ] then echo oui; fi
bash: syntax error near unexpected token `fi'

Comme précédemment, le shell a lu la commande suivant if jusqu'à l'indication de sa fin, qui est ici le ;. Il attend ensuite un if mais y trouve un fi inattendu.

$ if [ x = x ]; then echo oui fi
>

Il y a du progrès. La commande passée à if est correctement délimitée. Le shell trouve son then, qui peut être suivi d'autant de commandes qu'on le souhaite, jusqu'à rencontrer un elif ou un fi. Ici le fi fait visiblement partie des argument de echo, donc le shell attend d'autres commandes.

$ if [ x = x ]; then echo oui; fi
oui
$ if [ x = x ]; then echo "c'est compris ?"; fi
c'est compris ?

Deux constructions if/then/fi complètes.

Question

Où trouver la page de manuel de la commande [ ?

Solution

$ man test

Les commandes [ et test sont synonymes, à ceci près que la première voudra un ] en dernier argument. Ainsi [ "$x" = on ] peut tout aussi bien s'écrire test "$x" = on.

Le premier argument de la structure if est en fait n'importe quelle commande Unix, dont on teste le code d'erreur. Par exemple, la commande grep cherche un motif donné comme premier argument dans les fichiers donnés comme arguments suivants (ou le flux d'entrée s'ils sont absents). Son code d'erreur ne signale une erreur que si le motif n'a été trouvé dans aucun des fichiers.

Question

Si l'on suppose que le fichier primes contient la liste des 10 premiers nombres premiers, que donnera

if grep 9 primes; then
  echo oui
fi

Solution

$ if grep 9 primes; then echo oui; fi
19
29
oui

En pratique on utilisera plus volontiers if grep -q 9 primes; then afin de supprimer l'affichage de grep.

Note

Le code d'erreur de la dernière commande est accessible par la variable $?

$ cd /toto; echo $?
bash: cd: /todo: No such file or directory
1
$ cd; echo $?
0

2.1   Séparateurs de commandes

Outre ;, quatre autres opérateurs permettent de séquencer des commandes.

Opérateur Signification
cmd1; cmd2 exécute cmd2 après la terminaison de cmd1
cmd1 && cmd2 exécute cmd2 uniquement après le succès de cmd1
cmd1 || cmd2 exécute cmd2 uniquement après l'échec de cmd1
cmd1& cmd2 exécute cmd2 sans attendre la fin de cmd1
cmd1 | cmd2 exécute cmd1 et cmd2, en dirigeant la sortie du premier sur l'entrée du second

Question

Réécrivez la commande suivante sans utiliser if

if grep 9 primes; then
  echo oui
fi

Solution

grep 9 primes && echo oui

2.2   Groupements de commandes *

Deux syntaxes permettent de grouper des commandes. Cela permet par exemple d'utiliser des groupes de commandes comme opérandes de && ou ||, ou bien d'appliquer des redirections sur un ensemble de commandes, ou encore d'utiliser plusieurs commandes à un endroit où une seule attendue (p.ex. en argument d'un if).

Syntaxe Signification
{ liste de commandes... ; } Exécute la liste de commandes.
( liste de commandes... ) Idem, mais dans un sous-shell.

Comme on l'a déjà dit, { et } sont considérés comme des mot-clefs et sont donc reconnu uniquement en début de phrase. Le ; (un & ou un retour à la ligne conviendraient aussi bien) avant } est donc indispensable pour que } ne soit pas interprété comme un argument. Le problème n'existe pas avec les parenthèses, qui sont des opérateurs.

Le code de retour d'un groupe de commande est celui de sa dernière commande.

Question

Comparez les exécutions des trois commandes suivantes

$ [ "$UID" = 0 ] || ( echo "vous n'êtes pas root"; exit 1 ); echo $?
$ [ "$UID" = 0 ] || { echo "vous n'êtes pas root"; false; }; echo $?
$ [ "$UID" = 0 ] || { echo "vous n'êtes pas root"; exit 1; }; echo $?

Solution

La variable $UID contient votre numéro d'utilisateur. L'utilisateur root porte toujous le numéro 0. La commande [ "$UID" = 0 ] a donc toutes les chances d'échouer lors de vos essais à l'ARI...

Les deux premières commandes affichent

vous n'êtes pas root
1

La première exécute echo "vous n'êtes pas root"; exit 1 dans un sous-shell. Ce shell termine avec le code de retour indiqué (1), qui devient celui du bloc parenthèsé.

La seconde exécute echo "vous n'êtes pas root"; false dans le shell courant. La commande false est une commande dans le code d'erreur est toujours 1. Le code du bloc {...} est celui de sa dernière commande exécutée, donc 1.

La dernière commande affiche echo "vous n'êtes pas root"; exit 1 dans le shell courant. La chaîne vous n'êtes pas root est donc affichée, puis le shell se termine avec 1 pour code d'erreur. Le dernier echo $? n'est pas l'occasion de s'exécuter.

Question

Réécrivez la commande suivante sans utiliser d'accolades

$ x=0; while { x=$((x + 1)); echo $x; [ $x -le 10 ]; }; do :; done

Note

La commande : est la commande qui ne fait rien. Elle sert dans les cas où la syntaxe du shell réclame une commande dont nous n'avons pas besoin, comme ici entre do et done.

Solution

Facile, les {...;} ne servaient à rien...

$ x=0; while x=$((x + 1)); echo $x; [ $x -le 10 ]; do :; done

est tout à fait légitime. On peut mettre une liste de commandes entre while ... do et if ... then. Seul le code de retour de la dernière commande est déterminant.

Vous avez sans doute trouvé l'écriture suivante, plus classique :

$ x=0; while [ $x -le 10 ]; do x=$((x + 1)); echo $x; done

Notez que pour produire des séquences de chiffres on a souvent intérêt à recourir à la commande seq plutôt qu'à écrire des boucles. Ici :

$ seq 1 10

2.3   Définitions de fonctions *

Une définition de fonction prend l'une des deux formes suivantes :

nom() { commandes...; }
nom() ( commandes... )

selon que l'on souhaite que les commandes soient exécutées dans le shell courant ou dans un sous-shell. Au sein des commandes, les paramètres $1, $2, ... seront remplacés par les paramètres passés à la commande lors de son appel.

Question

Écrivez une fonction cdd qui prend deux arguments et change de répertoire en remplaçant le premier argument par le second dans le nom du répertoire courant. (Il s'agit de se déplacer et non de renommer.) Par exemple :

$ pwd
/home/adl/test/dir1a
$ cdd 1 2
$ pwd
/home/adl/test/dir2a

Cette opération peut-elle être effectuée par un script ?

Solution

cdd() { cd `echo $PWD | sed "s/$1/$2/g"; }

Un script ne pourrait pas changer le répertoire courant du shell qui l'a lancé.

3   Les substitutions

3.1   Substitutions des accolades [non-standard]

Quand { ne constitue pas seul le premier mot d'une phrase (pour les groupement de commandes), il peut être utilisé pour factoriser plusieurs mots. Ainsi

$ echo dir{1,2}/foo.{c,h}
dir1/foo.c dir1/foo.h dir2/foo.c dir2/foo.h

Ces accolades peuvent être imbriquées.

Question

Comment utiliseriez-vous cette technique pour factoriser la commande suivante ?

$ mkdir d1 d1/d2 d1/d2/d3 d1/d2/d3/d4

Solution

$ mkdir d1{,/d2{,/d3{,/d4}}}

Bien sûr, personne n'écrit cela en pratique ! Il est plus simple d'écrire

$ mkdir -p d1/d2/d3/d4

Note

Même si on l'utilise souvent pour générer des noms de fichiers, il est important de réaliser que cette fonctionnalité travaille uniquement sur des chaînes de caractères et n'a rien à voir avec l'existence ou non de fichiers sur le disque.

3.2   Substitutions des tildes

En début de mot, ~utilisateur est remplacé par le chemin absolu du répertoire de cet utilisateur. Si l'utilisateur n'est pas précisé, ~ désigne votre propre répertoire.

Question

Demandez le numéro d'étudiant de votre binôme et affichez l'adresse de son répertoire.

Solution

$ echo ~12345678

3.3   Substitutions des variables

En dehors des blocs protégés par des apostrophes, $VAR et ${VAR} sont tous les deux remplacés par la valeur de la variables VAR. L'emploi des accolades s'impose quand la référence à la variable est immédiatement suivie d'autres lettres:

$ x=A
$ echo ${x}B$xC$x
ABA

Le shell possède plusieurs variables prédéfinies dont la consultation ou modification (selon les variables) peut être utile.

$?
Le code d'erreur de la dernière commande exécutée.
$$
Le numéro de processus du shell courant.
$#
Le nombre de paramètres passés au script ou à la fonction.
$@
La liste des paramètres passés au script ou à la fonction.
$3
Le troisième paramètre passé au script ou à la fonction.
$HOME
le répertoire de l'utilisateur. Même expansion que ~, sauf que ce dernier ne s'utilise qu'en début de mot.
$PATH
Une chaîne de répertoires séparés par des :, stipulant où et dans quel ordre doivent être cherchées les commandes exécutables.
$PWD
contient le chemin absolu du répertoire courant (plus efficace que d'exécuter la commande homonyme en minuscule).
$PS1

une chaîne décrivant le prompt du shell, dans laquelle les séquence d'échappement suivantes sont reconnues :

Séquence remplacée par
\H le nom de la machine
\w le nom du répertoire courant
\! le numéro d'historique de la commande qui sera tapée

Consultez le manuel de GNU Bash pour la liste complète de ces séquences.

Nous reviendrons sur ces variables.

Outre $VAR et ${VAR}, les substitutions suivantes s'avèrent parfois utiles :

Syntaxe Effet
${VAR-val} remplacée par val si VAR n'est pas définie, utilise la valeur de VAR autrement
${VAR+val} remplacée par val si VAR est définie, par la chaîne vide autrement
${VAR%motif} remplacée par la valeur de VAR, privée de son plus petit suffixe respectant le motif
${VAR%%motif} remplacée par la valeur de VAR, privée de son plus grand suffixe respectant le motif
${VAR#motif} remplacée par la valeur de VAR, privée de son plus petit préfixe respectant le motif
${VAR##motif} remplacée par la valeur de VAR, privée de son plus grand préfixe respectant le motif
${#VAR} longueur (en caractères) de la valeur de VAR

Question

Réécrivez les commandes suivantes à l'aide de ces substitutions, sans utiliser ni basename ni dirname

$ F=/bin/cat
$ dirname $f
/bin
$ basename $f
cat

Solution

$ F=/bin/cat
$ echo ${f%/*}
/bin
$ echo ${f##*/}
cat

3.4   Substitutions de processus

Une chaîne comme `commande...` ou $(commande...) est remplacée par l'affichage de cette commande. (De ces deux syntaxes, seule la seconde peut être imbriquée.) Ainsi

$ ls -F
$ date -I
2006-08-18
$ mkdir `date -I`
$ ls -F
2006-08-18/

l'affichage de date -I a été utilisé comme argument de mkdir.

Question

Quelle est la différence entre ces deux commandes ?

$ expr 1 + 3
$ echo `expr 1 + 3`

Solution

Le résultat est le même, mais les mécanismes employés par la seconde sont beaucoup plus coûteux. (Employer cette seconde syntaxe, c'est surtout montrer qu'on ne comprend pas très bien ce qu'on fait.)

Question

Les commandes suivantes sont-elles identiques ?

$ cat fichier
$ echo `cat fichier`

Solution

Non! echo affiche tous ses arguments (ici les mots de fichier) séparés par un espace. Avec la deuxième commande on perd les séquences d'espaces multiples, les tabulations, ainsi que les retours à la ligne.

3.5   Substitutions arithmétiques *

Le shell peut effectuer des opérations arithmétiques avec la commande expr (coût prohibitif) ou la syntaxe $((expression)).

L'expression considérée peut référencer les variables du shell, qu'il n'est pas nécessaire de faire précéder par le dollar usuel. Les opérateurs mathématiques sont alors disponibles, indépendamment de leur signification habituelle pour le shell.

Question

En supposant que x vaut 2 et y vaut 3, que feront

$ echo $x * $y > 6
$ echo $((x * $y > 6))

Solution

Dans la première commande, le shell remplace $x par 2, * par la liste des fichiers du répertoire courant, et $y par 3. Il crée ensuite un fichier appelé 6, puis exécute la commande echo en lui passant les arguments précédemment calculés, et en redirigeant sa sortie vers le fichier 6. Après cette commande on trouvera donc dans le fichier 6 les chiffres 2 et 3 séparés par une liste de fichiers.

Dans la deuxième commande le shell calcule le produit de x et y (l'emploi ou non du $ est sans importance), et compare à 6. Comme jusqu'à preuve du contraire 6 n'est pas strictement supérieur à 6, l'expression $((x * $y > 6)) est remplacée par 0 (c'est-à-dire faux).

3.6   Substitution des noms de fichiers

À ce stade, un mot où les caractères spéciaux ?, * et crochets apparaissent non protégés sera interprété comme une expressions régulières et remplacé par les entrées du répertoire courant qui vérifie cette expression s'il y en a (ou laissé tel quel s'il n'y en a pas).

Question

Prévoyez l'affichage des commandes suivantes:

$ touch a00.c b00.c b01.c c10.c d11.h
$ echo *.c
$ echo *1*
$ echo b?1.c
$ echo c?1.c
$ echo [a-z]?1*

Solution

$ touch a00.c b00.c b01.c c10.c d11.h
$ echo *.c
a00.c b00.c b01.c c10.c
$ echo *1*
b01.c c10.c d11.h
$ echo b?1.c
b01.c
$ echo c?1.c
c?1.c
$ echo [a-z]?1*
b01.c d11.h

Il faut noter que lorsque l'expression ne correspond à aucun nom de fichier, comme dans le cas de c?1.c, elle est conservée telle quelle.

4   Protection des caractères spéciaux et découpages des arguments

Les guillemets et apostrophes permettent de délimiter des portions de la ligne de commande sur lesquelles on interdit certains substitutions.

Opération xyz... "xyz..." 'xyz...'
substitutions d'accolades    
substitutions de tildes    
substitutions de variables  
substitutions de commandes `cmd`  
substitutions de commandes $(cmd)  
substitutions arithmétiques  
interprétation de *, ?, et [...]    
découpage des arguments sur espaces    
interprétation opérateurs >, &&, |, ...    

Le apostrophes préservent leur contenu quel qu'il soit, pourvu qu'il ne contienne pas lui-même d'apostrophe. Autrement dit, seul le caractère ' est interprété spécialement lorsque le shell traite une chaîne entre apostrophes: les chaîne qui contiennent un ' ne peuvent pas être représentées entre apostrophes.

Lorsque le shell traite une chaîne entre guillemets, il réagit spécialement sur quatre caractères:

En dehors des guillemets et apostrophes, un \ préserve la valeur littérale de n'importe quel caractère, sauf du retour à la ligne.

Question

Proposez deux syntaxes d'invocation de la commande echo pour afficher chacune des commandes ci-dessous. L'un des appels ne devra jamais utiliser de "..." et l'autre appel ne devra pas utiliser de '...'. (Bien sûr on ne vous demande pas de retirer les ' et " qui font partie de la chaîne à afficher.)

  • Est-ce que 3 * 5 = 15 ?
  • Je m'appelle "Alexandre"
  • Je n'ai que $3 en banque.
  • #include <stdio.h>
  • prog:10: option `foobar' inconnue

Solution

Voici trois solutions pour chaque phrase. La première basée sur '...', la seconde sur "..." et la troisième n'utilisant ni l'un ni l'autre.

echo 'Est-ce que 3 * 5 = 15 ?'
echo "Est-ce que 3 * 5 = 15 ?"
echo Est-ce que 3 \* 5 = 15 \?

echo 'Je m'\''appelle "Alexandre"'
echo "Je m'appelle \"Alexandre\""
echo Je m\'appelle \"Alexandre\"

echo 'Je n'\''ai que $3 en banque.'
echo "Je n'ai que \$3 en banque."
echo Je n\'ai que \$3 en banque.

echo '#include <stdio.h>'
echo "#include <stdio.h>"
echo \#include \<stdio.h\>

echo 'prog:10: option `foobar'\'' inconnue'
echo "prog:10: option \`foobar' inconnue"
echo prog:10: option \`foobar\' inconnue

Seule exception à ces règles : "$@" est équivalent à "$1" "$2" "$3" ... et non "$1 $2 $3...".

5   Substitution des alias *

La substitution des alias concerne uniquement le premier mot d'une commande. Elle permet de raccourcir le nom d'une commande fréquemment utilisée, ou bien de forcer l'emploi d'options. Par exemple

$ alias ls='ls -F' rm='rm -i'

Question

Quelle est la différence entre les deux lignes suivantes

$ alias la='ls -la'
$ alias 'la=ls -la'

Solution

Il n'y en a pas. Les appostrophe ne servent qu'a empêcher le shell de couper l'argument en deux sur l'espace : leur position n'est pas importante tant qu'elles encadrent l'espace.

Question

Expliquer le problème de l'alias suivant, dont l'objectif est de compter le nombre d'appels à la commande cd. Le problème n'est pas visible dans l'exemple, mais pensez aux différentes façon de combiner des commandes. Est-il possible d'écrire cet alias correctement ?

$ alias 'cd=t=$((t + 1)); cd'
$ cd toto; cd ..
$ echo $t
2

Solution

Les problèmes découlent de l'emploi de ; au sein de l'alias, et surviennent lorsqu'on combine cd a une autre commande avec && ou ||. Par exemple

$ [ -d dir ] && cd dir
cd: no such file or directory: dir

En effet après substitution de l'alias, la commande ci-dessus s'est transformée ainsi

$ [ -d dir ] && t=$((t + 1)); cd dir

Le && ne porte sur l'affectation et non le cd qui suit.

Le premier réflexe est d'essayer de changer ; en && ou ||, mais cela n'aide pas lorsqu'on combine cd avec l'autre opérateur. De façon générale, la substitution d'alias n'est pas l'outil idéal pour remplacer une commande par plusieurs. On fera mieux d'utiliser une fonction.

Question

Réécrivez l'alias cd à l'aide d'une fonction et vérifiez que le problème soulevé par l'alias disparaît.

Solution

Le piège de l'appel récursif à cd est évité avec la commande builtin [non-standard] qui demande au shell d'utiliser sa définition interne pour la commande qui suit

$ cd() { $((t + 1)); builtin cd "$@"; }

6   Opérateurs de redirection *

cmd... >f Écrase le fichier f avec la sortie standard de cmd....
cmd... >>f Ajoute au fichier f la sortie standard de cmd....
cmd... <f Connecte l'entrée standard de la commande cmd au fichier f.
cmd... <<mot
...
mot
Écrit dans un fichier temporaire les lignes suivant la commande, jusqu'à la première ligne contenant uniquement mot (ce mot peut-être librement choisi), et connecte ce fichier à l'entrée standard de cmd....
cmd1... | cmd2... Connecte la sortie standard de la commande cmd1... à l'entrée standard de cmd2....

À part |, ces opérateurs peuvent être placés n'importe où sur une ligne de commande. (ex. >f echo Salut toto est strictement équivalant à echo Salut >f toto.)

>, >>, < et << acceptent en préfixe un numéro descripteur de fichier. Les descripteurs de fichiers suivant existent systématiquement:

flux correspondant
0 entrée standard (lecture)
1 sortie standard (écriture)
2 sortie d'erreur (écriture)

Par exemple la commande cmd... 2>f redirige la sortie d'erreur de cmd... vers de le fichier f.

Le numéro utilisé par défaut pour > et >> est bien sûr 1, tandis que celui utilisé par < et << est 0.

Les descripteurs de fichiers peuvent être copiés avec N>&M ou N<&M: le descripteur numéro M est alors copié à la place du descripteur numéro N. Par exemple

Question

Sachant que l'opérateur | connecte la sortie standard de la commande de gauche à l'entrée standard de la commande de droite avant que les opérateurs >, >>, < et << soient pris en compte, expliquez les redirections de la commande suivante:

cmd1... 2>&1 >out | tac >err

Note

La commande tac est un cat inversé. Elle affiche les lignes des fichiers (passés en argument ou sur son entrée standard) à l'envers.

Solution

Voici l'évolution des descripteurs de fichiers au fur et à mesure que les opérateurs sont pris en compte.

cmd1... | tac
cmd1 tac
0 terminal 0 TUBE1
1 terminal 1 terminal
2 TUBE1 2 terminal
cmd1... 2>&1 | tac
cmd1 tac
0 terminal 0 TUBE1
1 TUBE1 1 terminal
2 TUBE1 2 terminal
cmd1... 2>&1 >out | tac
cmd1 tac
0 terminal 0 TUBE1
1 out 1 terminal
2 TUBE1 2 terminal
cmd1... 2>&1 >out | tac >err
cmd1 tac
0 terminal 0 TUBE1
1 out 1 err
2 TUBE1 2 terminal

Autrement dit la sortie standard de cmd1... est envoyée dans out, tandis que sa sortie d'erreur est retournée par tac puis envoyée dans err.

De nouveaux descripteurs peuvent être créés par le shell avec la commande exec. Il seront hérités par toutes les commandes lancées par le shell.

Par exemple la boucle suivante lit une ligne de chaque fichier en parallèle, et s'arrête dès qu'elle atteint la fin de l'un des deux fichiers :

exec 3<fichier1 4<fichier2
while read ligne1 0<&3 && read ligne2 0<&4 ; do
  ...
done

Question

Le script suivant écrit une ligne sur sa sortie standard, et une sur sa sortie d'erreur. Sauvegardez-le sous le nom t, et marquez-le comme exécutable.

#!/bin/sh
echo Sortie standard
echo "Sortie d'erreur" 1>&2

Sachant que la commande grep --color . peut être utilisée pour afficher son entrée standard en couleur, écrivez un script showerr qui prend en argument n'importe quelle commande et affiche sa sortie d'erreur de cette commande en couleur (sur la sortie d'erreur de showerr) et la sortie standard inchangée. Ainsi

$ ./showerr ./t >f

devra envoyer Sortie standard dans f, et afficher Sortie d'erreur en couleur.

N'utilisez aucun fichier temporaire.

Solution

#!/bin/sh
exec 3>&1
"$@" 2>&1 1>&3 | grep --color . 1>&2

7   Fichiers de configuration *

Un shell peut être lancé en mode interactif (c'est le cas de celui qui tourne dans un terminal) ou non-interactif (quand il exécute un script). La gestion de l'historique du shell est quelque chose qui ne fonctionne qu'en mode interactif, par exemple. De même, on souhaite généralement définir ses alias uniquement en mode interactif, pour ne pas risquer de troubler un script shell écrit par quelqu'un d'autre.

D'autre part on distingue aussi le shell de login (celui que l'on obtient lorsqu'on se connecte en console, ou que l'on se logue à distance avec ssh) des autres shells lancés par la suite. Aujourd'hui, avec les logins graphiques qui démarrent directement un gestionnaire de fenêtre la notion de shell de login est devenu un peu floue. Certains environnements font en sorte que chaque terminal ouvert lance un shell de login, d'autres pas.

Les variables d'environnements telles que PATH sont en général définies par le shell de login, et héritées par tous les autres.

Selon le mode dans lequel il a été lancé, le shell exécute le contenu de différents fichiers de configuration.

Fichier lu login non-login interactif non-login non-interactif
/etc/profile    
~/.bash_profile ou ~/.bash_login ou ~/.profile ✔ premier trouvé    
/etc/bash.bashrc    
~/.bashrc    
$BASH_ENV    

Ce tableau est évidement spécifique à Bash.

Question

Supposons qu'un utilisateur ait installé plusieurs programmes dans son répertoire ~/usr/bin/. Comment doit-il modifier sa variable $PATH pour les rendre exécutables directement, et dans quel(s) fichier(s) doit il faire cette modification ?

Solution

L'utilisateur ajoute la ligne

PATH=$HOME/usr/bin:$PATH

dans l'un des fichiers ~/.bash_profile, ~/.bash_login, ou ~/.profile, selon ce qu'il utilise.

Il est normalement inutile faire cette modification dans ~/.bashrc puisque les autres shells lancés depuis le shell de login hériteront de cette variable d'environnement.

Question

Si un utilisateur souhaite définir l'alias ls='ls -v -F -b -T 0 --color=auto', quel fichier doit-il modifier pour que cela soit effectif dans tous les shells interactifs ?

Solution

La commande

alias ls='ls -v -F -b -T 0 --color=auto'

devrait être ajoutée à ~/.bash_profile, ~/.bash_login, ou ~/.profile (selon ce qui est déjà utilisé) pour les shells de login, mais aussi dans .bashrc pour les autres shells.

Devoir maintenir deux configurations est assez pénible. Bien souvent, on ajoutera le ligne suivante à la fin de ~/.bash_profile (ou ~/.bash_login ou ~/.profile...)

[ -f ~/.bashrc ] && . ~/.bashrc

afin que ~/.bashrc soit aussi exécuté par les shells de logins.

Le fichier ~/.bash_profile (ou...) est alors utilisé pour définir les variables d'environnements, et ~/.bashrc pour le reste (définitions d'alias et de fonctions principalement).

Question

Pourquoi est-ce que

$ export BASH_ENV=$HOME/.bashrc

est généralement une très mauvaise idée?

Solution

Les shells non interactifs sont utilisés par tous les script shells que l'on peut lancer. On ne souhaite pas que leur configuration diffère d'un utilisateur à l'autre. Par exemple si un utilisateur a définit un alias sur cd, celui-ci ne doit pas influencer un script qui croit appeler le vrai cd.

La situation peut être encore pire si le fichier de configuration donne une valeur fixe à une variable d'environnement telle que PATH, car de nombreux scripts modifient cette variable pour lancer d'autres scripts et échoueront si modification de la variable est annulée dans chaque sous-shell.

8   L'historique *

Le shell garde l'historique de toutes les commandes qu'on lui a faites exécuter. Cet historique peut être parcouru avec les flèches haut et bas du clavier. Il peut être affiché et manipulé avec les commandes (internes au shell) history et fc.

Question

Modifiez la définition de votre prompt pour qu'elle ressemble à quelque chose comme

10 ~/usr $

10 correspond au numéro d'historique de la commande qui va être tapée, et ~/usr désigne le répertoire dans lequel vous vous trouvez.

Solution

PS1='\! \w $'

Bash, ainsi que de nombreux shells, supporte une substition d'historique lorsqu'il est en mode interactif. Le caractère introduisant cette substitution est ! et peut être protégé par un \ ou entre '...'.

Expression Remplacement
!4 La commande numéro 4 dans l'historique.
!-4 La quatrième commande en partant de la fin de l'historique.
!! La dernière commande (= !-1)
!ma La dernière commande commençant par ma.
!?ma La dernière commande contenant ma.

Ces expressions désignent des entrées complètes de l'historique (autrement dit des commandes entières). Il est possible d'affiner cette sélection en les faisant suivre des suffixes suivants:

Suffixe Signification
:3 Le troisième argument, c'est-à-dire le quatrième mot (:0 désigne le nom de la commande)
^ Le premier argument (= :1)
$ Le dernier argument
:2-4 Les deuxième troisième et quatrième arguments.
* Tous les arguments

Ainsi !!$ désigne le dernier argument de la dernière commande. (Notez que le raccourci clavier Alt+. insère lui aussi le dernier mot de commande précédente.)

Enfin ce suffixe peut lui-même être suivi d'un modificateur pour altérer la sélection. Nous nous limiterons aux deux modificateurs suivants.

Modificateur Effet
:s/motif/remplacement/ Remplace la première occurrence de motif par remplacement
:gs/motif/remplacement/ Remplace toutes les occurrences de motif par remplacement

Question

On suppose que la commande suivante vient à l'instant d'être exécutée par erreur

$ mv /home/adl/usr/share/foo/bar.1 /var/local/quux/bar/foo.1

Exécutez la commande inverse en vous servant des commandes des substitutions d'historique pour ne pas retaper les noms de fichiers.

Solution

$ mv !!:2 !!:1

Question

Malheur ! Je viens de taper la longue commande suivante

$ make check TESTS='req.text reqd.text reqd2.text rulepat.text scripts.text scripts2.text seenc.text sinclude.text space.text specflg.text'

Mais je me suis trompé en entrant le nom des tests dont l'extension est .test et non .text. J'ai déjà exécuté cette commande (qui a échoué à cause de mon erreur) donc elle est dans l'historique. Comment puis-je utiliser les substitutions d'historique pour corriger mes bêtises ?

Solution

$ !!:gs/text/test/

9   Options utiles *

Les options suivantes du shell peuvent être spécifiées sur la ligne de commande lorsqu'on exécute le script (p.ex. sh -v script), où à l'intérieur d'un script à l'aide de la commande set (p.ex. set -v).

Option Signification
-e Quitte dès qu'une commande simple retourne un code d'erreur non nul.
-x Affiche chaque commande avant de l'exécuter, et après toutes les substitutions.
-u Signale toute référence à une variable non initialisée.
-v Affiche chaque ligne lue par le shell sur la sortie d'erreur.

Question

$ ldd
ldd: missing file arguments
Try `ldd --help' for more information.
$ which ldd
/usr/bin/ldd
$ file /usr/bin/ldd
/usr/bin/ldd: Bourne-Again shell script text executable

Utilisez l'option -x du shell pour compter le nombre d'appels à la commande echo effectués par ldd pour afficher son message d'erreur.

Solution

$ sh -x /usr/bin/ldd 2>&1 | grep '+ echo ' | wc -l
2

Question

Comment simplifieriez-vous le script suivant ?

com1 || exit 1
if com2 && com3; then
  com4 || exit 1
else
  exit 1
fi

Solution

set -e
com1
com2
com3
com4

Ou bien sûr

com1 &&
com2 &&
com3 &&
com4

Question

Comment faire en sorte que tous les scripts que vous lancez (et qu'ils lancent eux-même) se mettent automatiquement en mode set -x ?

Solution

Ces scripts sont exécutés par des shells non-interactifs. Ces shells exécutent donc n'importe quel fichier indiqué par $BASH_ENV avant d'exécuter le script lui-même.

$ echo 'set -x' > set-x
$ BASH_ENV=$PWD/set-x
$ export BASH_ENV
$ ldd
+ TEXTDOMAIN=libc
+ TEXTDOMAINDIR=/usr/share/locale
+ RTLDLIST='/lib/ld-linux.so.2 /lib64/ld-linux-x86-64.so.2'
+ warn=
+ bind_now=
+ verbose=
+ filename_magic_regex='((^|/)lib|.so$)'
+ test 0 -gt 0
+ add_env='LD_TRACE_LOADED_OBJECTS=1 LD_WARN= LD_BIND_NOW='
+ add_env='LD_TRACE_LOADED_OBJECTS=1 LD_WARN= LD_BIND_NOW= LD_LIBRARY_VERSION=$verify_out'
+ add_env='LD_TRACE_LOADED_OBJECTS=1 LD_WARN= LD_BIND_NOW= LD_LIBRARY_VERSION=$verify_out LD_VERBOSE='
+ test '' = yes
+ set -o pipefail
+ case $# in
+ echo ldd: 'missing file arguments'
ldd: missing file arguments
+ echo 'Try `ldd --help'\'' for more information.'
Try `ldd --help' for more information.
+ exit 1

10   Liste de commandes à connaître

On suppose que vous connaissez les commandes suivantes. Si ce n'est pas le cas, documentez-vous rapidement !

11   Shebang **

Lorsqu'un fichier exécutable commence par les caractères #! (shebang), le système exécute la commande qui suit en lui passant comme paramètre le nom du fichiers, puis les autres paramètres indiqués sur la ligne de commande.

Ainsi si le fichier toto commence par #!/bin/sh, la commande

$ ./toto titi

provoque l'exécution de /bin/sh ./toto titi.

Question

Le script suivant lance un shell uniquement pour lancer sed. C'est une perte de temps. Réécrivez-le sans utiliser de shell.

#! /bin/sh
sed -n '1!G;$p;h' "$@"

Au passage, reconnaissez-vous la commande qu'il imite ?

Solution

#! /bin/sed -nf
1!G;$p;h

Ce script se comporte comme la commande tac : il écrit son entrée standard (ou les fichiers qu'on lui indique) lignes par lignes mais à l'envers.

12   Exercices d'écriture de scripts ou fonctions **

Question

En utilisant la commande history de bash, écrivez une fonction stathist qui affiche le nom des des dix commandes que vous avez le plus fréquemment employées dans votre session, avec leur nombre d'occurrences. Par exemple

$ stathist
   21 echo
   15 make
   11 ls
    9 history
    7 info

Est-il possible d'écrire stathist sous la forme d'un script shell ?

Solution

stathist() {
  history |
  awk '{ print $2 }' |
  sort |
  uniq -c |
  sort -rn |
  head -n 5
}

Cette fonction fait référence à des propriété du shell courant (l'historique) auquel un script externe n'aurait pas accès.

Il est possible de configurer bash pour que plusieurs instances partagent leur historique, mais de toute façon l'historique n'est pas active dans les shells non-intéractifs.

Question

Écrivez un script shell prenant deux fichiers en paramètre, et fusionnant ces deux fichiers à la façon de paste (mais sans utiliser paste). Vérifiez que votre script fonctionne correctement même lorsque les deux fichiers indiqués n'ont pas le même nombre de lignes.

Solution

#!/bin/sh

exec 3< $1 4< $2

while true; do
  read ligne3 <&3
  c=$?
  read ligne4 <&4
  c=$c$?
  [ "$c" = 11 ] && break
  echo "$ligne3     $ligne4"
done

Question

Généralisez le script de la question précédente pour qu'il accepte un nombre quelconque d'arguments, comme le fait la commande paste. Attention, les numéros de flux indiqués à gauche de < ou à droite de <\& doivent impérativement être des entiers littéraux (c'est-à-dire pas des variables), il vous faudra utiliser eval pour contourner ce problème.

Solution

#!/bin/sh
n=3
for i in "$@"; do
  eval "exec $n< $i"
  # La variable $r contient bout à bout tous les blocs
  # d'instructions du style
  #   read ligne3 <&3
  #   c=$c$?
  #   read ligne4 <&4
  #   c=$c$?
  #   ...
  r="${r}read ligne$n <&$n; c=\$c\$?;"
  # construit une chaîne de n `1'.
  uns=1$uns
  # La variable $e contient la commande d'affichage, de la forme
  #   echo "$ligne3""       $ligne4""       $ligne5"...
  if [ -z "$e" ]; then
    e="echo \"\$ligne$n\""
  else
    e=$e"\" \$ligne$n\""
  fi
  n=$((n + 1))
done

while true; do
  c=
  eval "$r"
  [ "$c" = "$uns" ] && break
  eval "$e"
done