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).
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:
Question
Où trouver la page de manuel de la commande [ ?
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
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
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 $?
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.
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 ?
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
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.
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
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`
Question
Les commandes suivantes sont-elles identiques ?
$ cat fichier $ echo `cat fichier`
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))
À 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*
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.)
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'
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
Question
Réécrivez l'alias cd à l'aide d'une fonction et vérifiez que le problème soulevé par l'alias disparaît.
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.
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.
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 ?
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 ?
Question
Pourquoi est-ce que
$ export BASH_ENV=$HOME/.bashrc
est généralement une très mauvaise idée?
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.
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.
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 ?
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.
Question
Comment simplifieriez-vous le script suivant ?
com1 || exit 1 if com2 && com3; then com4 || exit 1 else exit 1 fi
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 ?
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 ?
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 ?
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.
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.