Table des matières

Le shell : notions avancées

Introduction

Ce cours est une préparation au cours sur le scripting shell. Le scripting en soit n'est pas compliqué, et peu de connaissances sont nécessaires. Mais le scripting fait appel à des notions particulières, également utilisables en dehors des scripts. Ce sont ces notions que nous aborderons aujourd'hui.

Notez que nous n'utilisons pas le terme 'bash' ou 'zsh', mais 'shell' qui a un sens plus large. Tous les shells existants ont des particularités, mais nombre d'entre eux ont un point commun : POSIX. POSIX est un standard qu'il est bon de respecter. Utiliser une syntaxe compatible POSIX dans vos scripts vous permettra de les faire fonctionner sur n'importe quel système d'exploitation unix.

Interaction d'une commande avec son shell

Sous unix chaque commande utilisateur lancée est attachée à un shell. Démarrez un terminal, démarrez un programme, fermez le terminal, votre programme quittera également. Le programme fait plusieurs choses :

Pour écrire et lire en utilisant le terminal, on utilisant 3 pseudo fichiers particuliers (appelés descripteurs de fichiers) :

Voici quelques exemples. Lorsque vous entrez votre login dans un tty (pas dans gdm ou kdm), le programme 'login' lit vos données sur stdin. Le programme 'ls' utilise stdout ou stderr suivant ce qu'il a à dire. Exemple :

$ ls # va écrire sur stdout
fichier_1 fichier_2 fichier_3

On peut s'assurer de ça en redirigeant la sortie ailleurs que sur l'écran. La 'redirection de flux' s'effectue grâce à l'opérateur '>' :

$ ls 1>/dev/null # 1 (donc stdout) est redirigé vers /dev/null

Le résultat de ls est alors écrit dans le fichier spécial /dev/null (c'est un fichier de type 'caractère' qui se contente d'absorber ce qu'on lui envoie sans rien redonner).

Note : le # dans la ligne de commande, et tout ce qui suit ne sont pas pris en compte par le shell, ce sont des commentaires.

Note : si on ne donne pas l'opérande de gauche à '>', il utilisera stdout. On aurait donc pu écrire :

$ ls >/dev/null

Par contre les messages d'erreur de 'ls' sont toujours envoyés sur stderr, et la redirection n'a pas d'effet :

$ ls fichier_non_existant >/dev/null
ls: cannot access fichier_non_existant: No such file or directory

On peut aussi “cacher” les messages d'erreur en les envoyant vers /dev/null (rappel : 2 correspond à stderr) :

$ ls 2>/dev/null

Il est possible d'envoyer la sortie standard et la sortie d'erreur en même temps vers /dev/null :

$ ls >/dev/null 2>&1

On envoie 2, stderr, vers l'adresse de 1 : stdout. Attention à la syntaxe. '&1' représente bien stdout lorsqu'il est placé à droite de '>'. Utiliser '1' sans le '&' aurait redirigé stderr dans un fichier nommé '1'.

A la place de /dev/null, on peut bien entendu rediriger stdout ou stderr vers un fichier normal. Il est même possible de concaténer (terme barabre signifiant qu'on ajoute à la suite) la sortie au contenu d'un fichier déjà existant avec ».

$ pwd > fichier
$ cat fichier
/home/gauvain
$ date >> fichier
$ cat fichier
/home/gauvain
mardi 28 avril 2009, 00:44:19 (UTC+0200)

Codes de retour des commandes

Mais dans un environnement automatisé (dans un script) les messages d'erreurs n'ont que peu d'intérêt, et ils n'indiquent ni l'échec complet ni la réussite du programme. Le code de retour du programme donne plus d'informations. Lorsque le programme a fini sont exécution, une variable particulière qui contient le code de retour est disponible : $?. Sa valeur peut être affichée grâce à la commande 'echo'. Dès qu'une autre commande est terminée, sa valeur change, donc le résultat est dépendant de ce qui se passe précédemment :

$ ls
fichier_1 fichier_2 fichier_3
$ echo $?
0

$ ls fichier_non_existant
ls: cannot access fichier_non_existant: No such file or directory
$ echo $?
2
$ echo $? # la commande précédente (echo) a fonctionné correctement
0

0 est le code de retour conventionnel lorsqu'une application quitte en ayant fonctionné correctement. Les codes de retour supérieurs à 0 indiquent qu'une erreur s'est produite. La signification dépend du programme. Si le code de retour est supérieur ou égal à 127, c'est qu'une erreur est survenue au niveau du shell :

$ appli_inexistante
bash: appli_inexistante: command not found
$ echo $?
127

Pour exécuter 2 commandes à la suite, on peut les séparer par le symbole ”;”. La 2ème commande sera alors exécutée quel que soit le code de retour de la 1ère.

$ ls toto; cd toto
ls: ne peut accéder toto: Aucun fichier ou dossier de ce type
bash: cd: toto: Aucun fichier ou dossier de ce type

Il n'existe aucun dossier ou fichier “toto”, les 2 commandes ont été exécutées et ont échoué.

Les opérateurs && et || autorisent l'exécution conditionnelle d'une commande suivant la valeur du code de retour de commande précédente.

L'opérateur && n'exécute sa commande de droite que si celle de gauche a un code de retour égal à 0 (donc si elle s'est correctement terminée).

$ ls toto && cd toto
ls: ne peut accéder toto: Aucun fichier ou dossier de ce type

La 1ère commande s'est terminée sur une erreur, la 2ème n'a pas été executée.

L'opérateur || n'exécute sa commande de droite que si celle de gauche a un code de retour différent de 0 (donc si elle s'est terminée sur une erreur).

$ cd toto || echo 'ne peut accéder à toto'
bash: cd: toto: Aucun fichier ou dossier de ce type
ne peut accéder à toto

Le message envoyé par le echo s'affiche car la 1ère commande a échoué

Il est bien entendu possible de chaîner autant d'opérateurs && et || que l'on désire et même de les mélanger dans une même ligne de commande. Les 2 opérateurs ont la même priorité et leur évaluation s'effectue de gauche à droite.

Utiliser des variables

Une variable est une sorte de “case mémoire” qui permet la mémorisation et l'échange d'information. Il existe plusieurs types de variables dans un shell :

Définir une variable est bien entendu possible, la syntaxe est simple :

$ NOM_DE_VARIABLE=mon_texte_ici

Et pour rappeler cette variable :

$ echo $NOM_DE_VARIABLE
mon_texte_ici

La commande 'echo' affiche simplement la liste des arguments, en remplaçant les variables par leur valeur.

Une autre syntaxe d'accès aux variable est la suivante :

$ echo ${NOM_DE_VARIABLE}
mon_texte_ici

Cette 2ème notation est utile en cas de concaténation d'une variable avec un texte ou une autre variable :

$ arbre=sapin
$ echo "un $arbre, des $arbres"
un sapin, des

En effet, $arbres n'existe pas, et est donc vide !

$ echo "un $arbre, des ${arbre}s"
un sapin, des sapins

Il faut faire attention à certaines règles :

$ FOO=bar baz
bash: baz: command not found

Par défaut, le caractère espace est le séparateur de commande (IFS). Ici le shell affecte donc la valeur “bar” à la variable “FOO” puis essaie d'exécuter la commande “baz” qui n'existe pas.

$ FOO="bar baz"
$ echo $FOO
bar baz

Les expressions entourées de ”” ou '' sont interprétées comme un tout. C'est un élément particulièrement important, notamment pour comprendre comme sont interprétés les arguments passés aux scripts. Dans ce cas, le caractère espace n'est plus considéré comme un séparateur de commande, mais comme faisant partie de la chaîne affectée à la variable “FOO”.

$ FOO=bar\ baz
$ echo $FOO
bar baz

Une autre possibilité est d'utiliser le backslash '\', qui est un caractère dit “d'échappement.” Placé devant un caractère spécial, il permet de le traiter comme un caractère normal. Essayez par exemple :

$ echo \
<code>
puis :
<code>
$ echo \\

La commande set permet de visualiser toutes les variables définies pour le shell. La commande unset permet de supprimer une variable :

$ arbre=sapin
$ echo "un $arbre est un arbre"
un sapin est un arbre

$arbre est bien remplacé ici par 'sapin' : les ”” n'empêchent pas de traiter les variables (on parle de substitution, ou encore d'expansion du shell).

$ unset $arbre
$ echo "un $arbre est un arbre"
un  est un arbre

Note : contrairement à d'autres langages, une variable non définie n'est pas un problème pour le shell, et n'aboutira pas sur une erreur. Elle a une valeur vide.

Vous avez très certainement entendu parler des “variables d'environnement”. Ce sont les variables dont le shell fournit une copie à sa descendance, c'est à dire tous les processus qui sont lancés par ce shell.

La commande export permet de placer une variable dans l'environnement du shell :

$ export arbre=sapin
$ animal=chien
$ bash # on lance un nouveau shell depuis le shell courant
$ echo "un $arbre est un arbre"
un sapin est un arbre

$arbre a bien été rendu accessible au le sous-shell.

$ echo "un $animal est un animal"
un  est un animal

$animal n'a pas été passé, car non exporté au sous-shell

$ exit # retour au shell précédent

La commande env permet de visualiser toutes les variables d'environnement définies. Il y en a quelques unes qui sont définies dans le fichier de configuration de votre shell.

$ env
SHELL=/bin/bash
TERM=rxvt-unicode
USER=gauvain
EDITOR=vim
PWD=/home/gauvain
DEBEMAIL=gpocentek@linutop.com
XAUTHORITY=/home/gauvain/.Xauthority
arbre=sapin
...

Le conteneur 'SHELL' a donc la valeur '/bin/bash'. Certaines de ces variables sont définies par le shell lui même (SHELL par exemple), certaines dans son fichier de configuration, d'autres par des logiciels (XAUTHORITY pour X), ou encore manuellement (DEBEMAIL).

Mixer variables et commandes

L'un des intérêts des variables est de pouvoir récupérer directement les éléments écrits sur stdout et stderr par des commandes. L'assignation de variable se fait toujours de la même manière que pour du texte, en utilisant la syntaxe $() pour l'exécution de la commande :

$ dirs=$(ls /boot)
$ echo $dirs
config-2.6.29.1 grub System.map-2.6.29.1 vmlinuz-2.6.29.1

Le résultat d'une commande peut d'ailleurs être utilisé sans passer par une assignation de variable intermédiaire :

$ dirs=$(ls $(find /usr/include -type d))
$ echo $dirs
/usr/include: abiword-2.6 aio.h aliases.h alloca.h ...

On rencontre encore très souvent les symboles `` (backquotes) à la place de $() pour l'exécution d'une commande . Leur utilisation limite les possibilités, puisqu'il est impossible d'imbriquer deux commandes de cette manière. Évitez de les utiliser !

Traitement conditionnel des variables

Une syntaxe particulière permet de traiter une variable suivant son existence :

$ var=foo
$ echo ${var}
foo
$ var=
$ echo ${var}
$ arg=bar
$ var=foo
$ echo ${var:-${arg}}
foo
$ var=
$ echo ${var:-${arg}}
bar
$ var=foo
$ echo ${var:=bar}
foo
$ var=
$ echo ${var:=bar}
bar
$ echo $var
bar
$ var=foo
$ echo ${var:+bar}
bar
$ var=
$ echo ${var:+bar}

Chaînage de commandes avec les pipes

De nombreuses commandes font une tâche bien précise, mais leur résultat peut nécessiter une nouvelle action. Par exemple vous voulez lister les fichiers d'un répertoire X en ne tenant pas compte des fichiers pdf. Vous pouvez utiliser :

$ ls /votre/dossier/X | grep -v \.pdf$

`ls` va lister tous les fichiers et va les afficher sur la sortie standard. Le `pipe` (tuyau, noté | ) va alors récupérer la sortie et la renvoyer vers l'entrée standard (stdin). `grep` prend alors le relai pour n'afficher que les fichiers ne terminant pas par .pdf. Le mécanisme des pipes (|) permet d'enchaîner plusieurs commandes, en connectant la sortie standard d'une commande à l'entrée standard de la suivante. Cela permet par exemple d'enchaîner plusieurs commandes pour enregistrer le résultat final dans une variable :

$ myname=$(getent passwd $USER | cut -d: -f5 | cut -d, -f1)
$ echo $myname
Gauvain Pocentek

Il est possible, grâce à la commande tee, de stocker dans un fichier le résultat intermédiaire d'une commande dans une chaîne de pipes.

$ date | tee fichier1 | cut -d" " -f1
mardi
$ cat fichier1
mardi 28 avril 2009, 00:08:35 (UTC+0200)

La commande tee se contente de passer le contenu de son entrée standard vers sa sortie standard, en la stockant au passage dans le ou les fichiers passés en argument.

Pour finir

Pour éviter un ennuyant catalogue d'autres possibilités, nous avons préparé un document annexe qui vous donnera des informations additionnelles.

Vous trouverez dans ce document :