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.
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)
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.
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).
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 !
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}
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 é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 :