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.
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.
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.
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.
Le shell effectue plusieurs types de substitutions sur les différents parties d'un commande. Sont réalisées en même temps :
Puis le résultat est découpé (en général sur les espaces) pour former une liste de mots.
Le shell prend en compte les opérateurs de redirection (repérés lors de l'analyse lexicale), puis les supprime de la commande.
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.
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.
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 !
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:
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
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
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
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é.
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.
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
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.
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
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.
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).
À 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.
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.)
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...".
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 "$@"; }
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:
n° | 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
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.
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 $
où 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/
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
On suppose que vous connaissez les commandes suivantes. Si ce n'est pas le cas, documentez-vous rapidement !
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.
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