===== 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 :
* ce pour quoi il est prévu ;
* il peut envoyer des informations sur le terminal ;
* il peut recevoir des informations depuis le terminal ;
* il s'arrête et envoie un code de retour.
Pour écrire et lire en utilisant le terminal, on utilisant 3 pseudo fichiers
particuliers (appelés descripteurs de fichiers) :
* stdin (entrée standard, généralement le clavier), également noté 0 ;
* stdout (sortie standard, généralement l'écran), également noté 1 ;
* stderr (sortie d'erreur, qui utilise en général l'écran comme stdout, et sert à diffuser des messages d'erreur), également noté 2.
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 :
* les variables prédéfinies : elles sont définies à la connexion au shell et sont accessibles et modifiables à l'utilisateur ;
* les variables positionnelles : elles contiennent les paramètres d'un script et sont modifiables par l'utilisateur avec la commande "set" ;
* les variables spéciales : elles sont réservées et exclusivement maintenues par le shell ;
* les variables utilisateur : elles sont définies par l'utilisateur dans le fichier de configuration du shell ou dans un script.
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 :
* les noms de variables ne peuvent contenir que des lettres (majuscules ou minuscules), des chiffres et _ ;
* dans l'assignation, il n'y a JAMAIS d'espace à gauche et à droite du = ;
* les caractères d'espacement (\n, ' ', \t, ...) sont des caractères spéciaux, et nécessitent un traitement particulier. Par exemple :
$ 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 \
puis :
$ 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 ou ${var} : retourne la valeur de la variable si elle est définie, sinon rien :
$ var=foo
$ echo ${var}
foo
$ var=
$ echo ${var}
* ${var:-argument} : retourne la valeur actuelle de $var si elle est définie, sinon le résultat de l'expansion de "argument" :
$ arg=bar
$ var=foo
$ echo ${var:-${arg}}
foo
$ var=
$ echo ${var:-${arg}}
bar
* ${var:=argument} : retourne la valeur actuelle de $var si elle est définie, sinon retourne "argument" (non expandé) et assigne "argument" à var :
$ var=foo
$ echo ${var:=bar}
foo
$ var=
$ echo ${var:=bar}
bar
$ echo $var
bar
* ${var:+argument} : retourne argument si la valeur de $var est définie, sinon ne retourne rien :
$ 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 :
* le traitement des variables ;
* les outils standards, non liés directement au shell, mais indispensables pour le scripting.
* [[http://u-classroom.net/files/2009-06-02/shell/annexes.pdf|document pdf]]
* [[http://u-classroom.net/files/2009-06-02/shell/annexes/|lecture en ligne]]
* [[http://u-classroom.net/files/2009-06-02/shell/annexes.latex|sources LaTeX]]