===== 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]]