======================= Bash Scripting 01 HOWTO (c) Nicolas Kovacs ======================= Dernière révision : 21 décembre 2015 Ce HOWTO présente les fonctionnalités qui composent les bases de la programmation shell. * Les variables utilisateur * La substitution de variables * La substitution de commandes * Les caractères de protection * Écriture et lancement d'un script shell * Variables réservées du shell * La commande 'read' * Documentation ========================= Les variables utilisateur ========================= Le shell permet de définir ou redéfinir des variables qui conditionnent l'environnement de travail de l'utilisateur. Il est également possible de définir d'autres variables, dites variables utilisateur, qui vont permettre de stocker des informations nécessaires à l'exécution d'un script. Nommer une variable ------------------- Voici les règles à utiliser pour attribuer un nom à une variable : - le premier caractère fait partie de l'ensemble [a-zA-Z_] ; - les caractères suivants sont pris dans l'ensemble [a-zA-Z0-9_]. Définir une variable -------------------- Une variable est définie dès qu'elle est initialisée. Le contenu d'une variable est considéré par le shell comme une suite de caractères. $ var1=mot1 $ echo $var1 mot1 /!\ Il ne faut pas mettre d'espace autour du symbole d'affectation '='. Si l'on affecte une valeur contenant au moins un espace, il faut protéger ce dernier, étant donné que c'est un caractère spécial du shell (séparateur de mots sur la ligne de commande) : $ var2='mot1 mot2 mot3' $ echo $var2 mot1 mot2 mot3 Une variable qui n'a jamais été initialisée est vide. $ echo $var3 La commande interne 'unset' permet de retirer la définition d'une variable. $ var=12 $ echo $var 12 $ set | grep var var=12 $ unset var $ echo $var $ set | grep var Il faut faire attention en concaténant le contenu d'une variable et d'une chaîne de caractères à ce que le shell interprète correctement le nom de la variable. Dans l'exemple ci-dessous, le caractère souligné '_' fait partie du nom de la première variable : $ fic=resu $ datejour=20151020 $ newfic=$fic_$datejour $ echo $newfic 20151020 Pour le shell, la première variable se nomme 'fic_' puisque le caractère souligné '_' est autorisé à l'intérieur du nom d'une variable. Celle-ci est donc substituée par sa valeur (donc vide), puis concaténée avec le contenu de la variable 'datejour' Pour faire comprendre au shell quels sont les caractères qui font partie du nom de la variable, il faut entourer le nom de cette dernière avec des accolades { } : $ fic=resu $ datejour=20151020 $ newfic=${fic}_$datejour $ echo $newfic resu_20151020 La commande interne 'typeset' permet de déclarer explicitement une variable comme étant un nombre entier. Cette déclaration est facultative, mais elle permet d'avoir un contrôle sur la valeur stockée et rend les calculs plus rapides. $ typeset -i nb=1 $ echo $nb 1 ============================ La substitution de variables ============================ Le shell offre la possibilité d'attribuer une valeur par défaut aux variables non initialisées ou, au contraire, initialisées. Expression ${variable:-valeur} ------------------------------ - Si la variable n'est pas vide, l'expression est substituée par 'variable' - Si la variable est vide, l'expression est substituée par 'valeur' $ fic=/tmp/kikinovak.log $ echo "Le fichier traité sera : ${fic:-/tmp/default.log}." Le fichier traité sera : /tmp/kikinovak.log. $ unset fic $ echo "Le fichier traité sera : ${fic:-/tmp/default.log}." Le fichier traité sera : /tmp/default.log. $ echo $fic Expression ${variable:=valeur} ------------------------------ - Si la variable n'est pas vide, l'expression est substituée par '$variable'. - Si la variable est vide, 'variable' est affectée avec 'valeur' et l'expression est substituée par 'valeur' $ fic=/tmp/kikinovak.log $ echo "Le fichier traité sera : ${fic:=/tmp/default.log}." Le fichier traité sera : /tmp/kikinovak.log. $ unset fic $ echo "Le fichier traité sera : ${fic:=/tmp/default.log}." Le fichier traité sera : /tmp/default.log. $ echo $fic /tmp/default.log Expression ${variable:+valeur} ------------------------------ - Si la variable n'est pas vide, l'expression est substituée par 'valeur' - Si la variable est vide, l'expression est substituée par '$variable', donc vide. $ a=1 $ echo "Expression : ${a:+99}" Expression : 99 $ unset a $ echo "Expression : ${a:+99}" Expression : Expression ${variable:?message} ------------------------------- - Si la variable n'est pas vide, l'expression est substituée par '$variable'. - Si la variable est vide, le shell affiche le nom de la variable suivi de la chaîne de caractères 'message' $ echo $var $ echo ${var:?"non définie"} -bash: var: non définie Message par défaut : $ echo ${var:?} -bash: var: parameter null or not set Définition de la variable : $ var=définie $ echo ${var:?"non définie"} définie ============================ La substitution de commandes ============================ Les caractères de substitution permettent de remplacer une commande par l'affichage résultant de son exécution. Ce mécanisme est utilisé pour insérer dans une ligne de commande Unix le résultat d'une autre commande. Dans l'exemple qui suit, les commandes 'logname' et 'uname' sont remplacées par leur résultat avec l'exécution de la commande 'echo'. $ echo Vous êtes l\'utilisateur `logname` sur la machine `uname -n`. Vous êtes l'utilisateur kikinovak sur la machine alphamule.microlinux.lan. On préférera la syntaxe alternative : $ echo Vous êtes l\'utilisateur $(logname) sur la machine $(uname -n). Vous êtes l'utilisateur kikinovak sur la machine alphamule.microlinux.lan. Dans l'exemple qui suit, on initialise une variable 'monuid' avec l'UID de l'utilisateur 'kikinovak' $ grep kikinovak /etc/passwd kikinovak:x:1000:100:Niki Kovacs,,,:/home/kikinovak:/bin/bash $ grep kikinovak /etc/passwd | cut -d: -f3 1000 $ monuid=$(grep kikinovak /etc/passwd | cut -d: -f3) $ echo $monuid 1000 ============================ Les caractères de protection ============================ Les caractères de protection servent à faire perdre la signification des caractères spéciaux du shell. Il existe trois jeux de caractères ayant chacun leur fonctionnalité propre : - les simples quotes ' ' - l'antislash \ - les guillemets " " Les simples quotes ' ' ---------------------- Les simples quotes (ou apostrophes) retirent la signification de tous les caractères spéciaux du shell. Les quotes doivent être en nombre pair sur une ligne de commande. Quelques exemples ci-dessous. /!\ Les simples quotes ne se protègent pas elles-mêmes. La variable $HOME est substituée par sa valeur : $ echo $HOME /home/kikinovak Le caractère '$' perd sa signification spéciale : $ echo '$HOME' $HOME Le caractère '*' est substitué par les noms de fichier du répertoire : $ echo * f1 f2 fic fic1.txt FIC.c Fic.doc fIc.PDF Le caractère '*' perd sa signification spéciale : $ echo '*' * Le shell s'attend à trouver un nom de fichier derrière une redirection : $ echo > bash: Erreur de syntaxe près du symbole inattendu « newline » Le caractère '>' perd sa signification spéciale : $ echo '>' > Le shell exécute la commande 'logname' et la remplace par son résultat : $ echo Bonjour $(logname) Bonjour kikinovak La séquence de caractères '$( )' perd sa signification spéciale : $ echo 'Bonjour $(logname)' Bonjour $(logname) Protection de plusieurs caractères spéciaux : $ echo '* ? > < >> << | $HOME $(logname) &' * ? > < >> << | $HOME $(logname) & La quote ne se protège pas elle-même. Pour le shell, la commande n'est pas terminée. Il affichera le prompt secondaire (PS2) tant que les quotes seront en nombre impair : $ echo 'La quote n'est pas protégée.' > Le caractère antislash '\' -------------------------- Le caractère antislash '\' retire la signification spéciale du caractère qui le suit. Là encore, quelques exemples. Les caractères '*' et '$' deviennent des caractères ordinaires : $ echo Voici une \* et une variable \$HOME. Voici une * et une variable $HOME. L'antislash se protège de lui-même : $ echo \\ \ L'antislash retire la signification spéciale de la quote : $ echo L\'antislash protège la quote. L'antislash protège la quote. Le premier antislash protège le deuxième, le troisième protège le '$' : $ echo \\\$HOME \$HOME Les guillemets " " ------------------ Les guillemets " " retirent la signification de tous les caractères spéciaux du shell sauf '$', '` `' et '$( )', '\' et lui-même. $ echo "> et | sont protégés par les guillemets, la valeur de $HOME est \ substituée, la commande $(logname) est exécutée, l'antislash protège le \ caractère suivant, ce qui permet d'afficher un \"." > et | sont protégés par les guillemets, la valeur de /home/kikinovak est substituée, la commande kikinovak est exécutée, l'antislash protège le caractère suivant, ce qui permet d'afficher un ". En pratique, il est fréquent d'encadrer les arguments de la commande 'echo' avec des guillemets. ======================================= Écriture et lancement d'un script shell ======================================= Définition ---------- Un script shell est un fichier texte contenant des commandes Unix internes ou externes ainsi que des mots clés du shell. Il n'y a pas de convention imposée pour le nom d'un script shell. Il peut avoir une extension, mais ce n'est pas obligatoire. Néanmoins, il est assez fréquent de choisir l'extension '.sh'. Voici un exemple de script : $ nl premier.sh 1 pwd 2 cd /tmp 3 pwd 4 ls Exécution du script : $ sh premier.sh /home/kikinovak /tmp blueman-applet-1000 Exécution d'un script par un shell enfant ----------------------------------------- Dans la majorité des cas, les scripts doivent être exécutés par l'intermédiaire d'un shell enfant. Ceci a pour avantage de ne pas modifier l'environnement du shell courant. Pour lancer un script shell, il existe trois méthodes qui produiront un résultat équivalent. Première méthode : $ sh premier.sh C'est la méthode utilisée précédemment. On appelle la commande 'sh' en lui demandant d'interpréter le script 'premier.sh'. Dans ce cas, la permission de lecture est suffisante sur le fichier 'premier.sh' : $ ls -l premier.sh -rw-r--r-- 1 kikinovak users 20 Oct 20 16:26 premier.sh Deuxième méthode : $ sh < premier.sh Le shell est un programme qui lit sur son entrée standard. Il est donc possible de connecter celle-ci sur le fichier 'premier.sh'. Là encore, la permission de lecture est suffisante. Cette syntaxe est peu utilisée. Troisième méthode : $ chmod u+x premier.sh $ ls -l premier.sh -rwxr--r-- 1 kikinovak users 20 Oct 20 16:26 premier.sh $ ./premier.sh C'est la méthode la plus utilisée. Dans ce cas, le script est considéré comme la commande, et donc - comme pour toute commande externe - il est nécessaire de posséder les droits d'exécution sur le fichier. Par défaut, le script sera interprété par un shell enfant identique au shell courant. Sur la première ligne de l'interpréteur, la directive '#!' permet d'imposer l'interpréteur du script. Le chemin de celui-ci devra être exprimé en absolu. Reprenons notre exemple en spécifiant l'interpréteur Bash : $ nl premier.sh 1 #! /bin/bash 2 pwd 3 cd /tmp 4 pwd 5 ls /!\ Les caractères '#' et '!' sont écrits respectivement en colonne 1 et 2. L'espace entre le '!' et le chemin absolu de l'interpréteur est facultatif. Commentaires ------------ Un commentaire commence par le caractère '#' et se termine à la fin de la ligne. Les lignes blances ainsi que les indentations (espaces, tabulations) sont ignorées. /!\ Une exception à cette règle : si la première ligne du fichier commence par '#!', le shell s'attend à trouver le nom de l'interpréteur du script juste derrière. Ajoutons des commentaires à notre script : $ nl premier.sh 1 #! /bin/bash 2 # 3 # Voici mon premier script 4 # comportant des commentaires. 5 pwd # Afficher le répertoire courant 6 cd /tmp # Changement de répertoire 7 pwd # 8 ls # Liste des fichiers du répertoire courant ============================ Variables réservées du shell ============================ Dans un script, un certain nombre de variables réservées sont accessibles en lecture. Ces variables sont initialisées par le shell et véhiculent des informations de nature diverse. Les paramètres positionnels --------------------------- Les scripts shell sont capables de récupérer les arguments passés sur la ligne de commande à l'aide de variables spéciales nommés 'paramètres positionnels' - $# représente le nombre d'arguments reçus par le script ; - $0 représente le nom du script ; - $1 représente la valeur du premier argument, $2 la valeur du deuxième, et ainsi de suite jusqu'à $9 ; - $* et $@ représentent la liste de tous les arguments. Voici un exemple de script qui affiche la valeur de chaque paramètre positionnel. $ nl monscript.sh 1 #!/bin/bash 2 echo "Ce script a reçu $# arguments." 3 echo "Le nom du script est : $0" 4 echo "Mon premier argument est : $1" 5 echo "Mon deuxième argument est : $2" 6 echo "Mon troisième argument est : $3" 7 echo "Voici la liste de tous mes arguments : $*" Appel de 'monscript.sh' avec six arguments : $ ./monscript.sh f1 f2 f3 f4 /tmp/fic.txt 123 Ce script a reçu 6 arguments. Le nom du script est : ./monscript.sh Mon premier argument est : f1 Mon deuxième argument est : f2 Mon troisième argument est : f3 Voici la liste de tous mes arguments : f1 f2 f3 f4 /tmp/fic.txt 123 Appel de 'monscript.sh' avec trois arguments : $ ./monscript.sh 12 + 24 Ce script a reçu 3 arguments. Le nom du script est : ./monscript.sh Mon premier argument est : 12 Mon deuxième argument est : + Mon troisième argument est : 24 Voici la liste de tous mes arguments : 12 + 24 /!\ Les arguments de la ligne de commande doivent être séparés les uns des autres par au moins un espace ou une tabulation. La commande 'shift' ------------------- La commande 'shift' permet de décaler la liste des arguments d'une ou de plusieurs positions vers la gauche. Dans l'exemple ci-dessous, le script 'decal.sh' est censé recevoir en premier argument le nom d'un répertoire puis, à partir du deuxième argument, un nombre quelconque de noms de fichiers. Dans le contexte de ce programme, le premier argument ne subira pas le même traitement que les suivants. Le nom du répertoire est sauvegardé dans une variable, puis la commande 'shift' fait sortir le nom du répertoire. Ceci permet de récupérer dans la variable $* uniquement la liste des fichiers, qui pourra être traitée ultérieurement dans une boucle. $ nl decal.sh 1 #!/bin/bash 2 # 3 # Affichage des variables avant décalage 4 echo "### Avant shift ###" 5 echo "1er argument : $1" 6 echo "2ème argument : $2" 7 echo "3ème argument : $3" 8 echo "4ème argument : $4" 9 echo "Tous les arguments : $*" 10 echo -e "Nombre d'arguments : $#\n" 11 # Sauvegarde du premier argument dans la variable rep 12 rep=$1 13 # Décalage d'un cran à gauche 14 shift 15 # Affichage des variables après décalage 16 echo "### Après shift ###" 17 echo "1er argument : $1" 18 echo "2ème argument : $2" 19 echo "3ème argument : $3" 20 echo "4ème argument : $4" 21 echo "Tous les arguments : $*" 22 echo -e "Nombre d'arguments : $#\n" 23 # Changement de répertoire 24 cd $rep 25 # Traitement de chaque fichier contenu dans $* 26 for fic in $* 27 do 28 echo "Sauvegarde de $fic..." 29 done Lancement du script : $ chmod u+x decal.sh $ ./decal.sh /tmp f1 f2 f3 f4 f5 f6 ### Avant shift ### 1er argument : /tmp 2ème argument : f1 3ème argument : f2 4ème argument : f3 Tous les arguments : /tmp f1 f2 f3 f4 f5 f6 Nombre d'arguments : 7 ### Après shift ### 1er argument : f1 2ème argument : f2 3ème argument : f3 4ème argument : f4 Tous les arguments : f1 f2 f3 f4 f5 f6 Nombre d'arguments : 6 Sauvegarde de f1... Sauvegarde de f2... Sauvegarde de f3... Sauvegarde de f4... Sauvegarde de f5... Code de retour d'une commande ----------------------------- Toutes les commandes Unix retournent un code d'erreur. Ce dernier est un entier compris entre 0 et 255. Du point de vue du shell, 0 représente la valeur 'vrai' (succès de la commande), toute valeur supérieure à 0 représente la valeur 'faux' (échec de la commande). Le code d'erreur de la dernière commande exécutée est contenu dans la variable spéciale $?. Dans l'exemple qui suit, la commande 'grep' retourne 'vrai' lorsque la chaîne recherchée est trouvée, et 'faux' dans le cas contraire. $ grep kikinovak /etc/passwd kikinovak:x:1000:100:Niki Kovacs,,,:/home/kikinovak:/bin/bash $ echo $? 0 $ grep zorglub /etc/passwd $ echo $? 1 À l'intérieur d'un script shell, le test du code de retour d'une commande permet d'orienter le flux d'exécution. Un script shell est une commande, il doit donc lui aussi retourner un code. La commande 'exit' permet de terminer un script tout en renvoyant un code d'erreur : ... exit 0 # Terminaison du script avec renvoi du code 0 (succès) Ou bien : ... exit 1 # Terminaison du script avec renvoi du code 1 (échec) Autres variables spéciales -------------------------- La variable spéciale $$ représente le PID du shell qui interprète le script. Cette variable garde une valeur constante pendant toute la durée d'exécution du script. Dans l'exemple qui suit, le script 'trouve.sh' crée un fichier résultat qui porte un nom différent à chaque fois que le script est lancé. Le script prend deux arguments : un nom de répertoire et un nom de fichier. $ nl trouve.sh 1 #!/bin/bash 2 ficresu=/tmp/atrouve.$$ 3 find "$1" -name "$2" 2> /dev/null 1> $ficresu 4 echo "Contenu du fichier $ficresu : " 5 more $ficresu Premier exemple d'exécution du script : $ chmod u+x trouve.sh $ ./trouve.sh / passwd Contenu du fichier /tmp/atrouve.821 : /boot/initrd-tree/bin/passwd /boot/initrd-tree/etc/passwd /usr/bin/passwd /etc/passwd Deuxième exemple d'exécution du script : $ ./trouve.sh /bin/ g* Contenu du fichier /tmp/atrouve.825 : /bin/gzexe /bin/gzip /bin/getopt /bin/ginstall /bin/gawk-4.1.0 /bin/gunzip /bin/gawk /bin/grep /bin/groups Chaque nouvelle exécution du script génère donc un fichier résultat portant un nom unique : $ ls -l /tmp/atrouve* -rw-r--r-- 1 kikinovak users 86 oct. 21 09:30 /tmp/atrouve.821 -rw-r--r-- 1 kikinovak users 107 oct. 21 09:30 /tmp/atrouve.825 Il est tout à fait possible de lancer une commande en arrière-plan à partir d'un script. Le PID de cette commande est alors stocké dans la variable spéciale $!. $ nl arriereplan.sh 1 #!/bin/bash 2 find / -name "$1" 1> /tmp/res 2> /dev/null & 3 date 4 echo "*********************************" 5 echo "Résultat de la commande ps :" 6 ps 7 echo "*********************************" 8 echo "PID de find : $!" 9 exit 0 Et voilà ce que donne le lancement d'une commande en arrière-plan à partir d'un script shell : $ ./arriereplan.sh passwd mer. oct. 21 09:40:12 CEST 2015 ********************************* Résultat de la commande ps : PID TTY TIME CMD 771 pts/0 00:00:00 bash 838 pts/0 00:00:00 arriereplan.sh 839 pts/0 00:00:00 find 841 pts/0 00:00:00 ps ********************************* PID de find : 839 ================== La commande 'read' ================== Lecture au clavier ------------------ La commande 'read' lit son entrée standard et affecte les mots saisis dans la ou les variable(s) dont le nom est passé en argument. La liste des caractères séparateurs de mots utilisés par 'read' est stockée dans la variable d'environnement IFS, qui contient par défaut les caractères espace, tabulation (\t) et saut de ligne (\n). Le mot saisi est stocké dans la variable 'var1' : $ read var1 Bonjour $ echo $var1 Bonjour Tous les mots saisis sont stockés dans la variable 'var1' : $ read var1 Bonjour tout le monde ! $ echo $var1 Bonjour tout le monde ! Le premier mot est stocké dans 'var1' le deuxième dans 'var2' : $ read var1 var2 Au revoir $ echo $var1 Au $ echo $var2 revoir Le premier mot est stocké dans 'var1' et tout le reste dans 'var2' : $ read var1 var2 Au revoir tout le monde ! $ echo $var1 Au $ echo $var2 revoir tout le monde ! Le mot est stocké dans 'var1' et 'var2' est vide : $ read var1 var2 Merci $ echo $var1 Merci $ echo $var2 Code de retour -------------- La commande 'read' renvoie un code 'vrai' (0) si elle ne reçoit pas l'information 'fin de fichier' ([Ctrl]+[D]). $ read var Voici ma saisie $ echo $? 0 $ echo $var Voici ma saisie Si l'utilisateur tape immédiatement sur la touche [Entrée], il a saisi la chaîne vide. La variable est vide mais le code est 'vrai' (0) : $ read var [Entrée] $ echo $? 0 $ echo $var Si l'utilisateur appuie sur la combinaison de touches [Ctrl]+[D], il envoie l'information 'fin de fichier' à la commande. La variable est vide et le code est 'faux' (1) : $ read var [Ctrl]+[D] $ echo $? 1 $ echo $var La variable IFS --------------- Cette variable contient par défaut les caractères espace, tabulation (\t) et saut de ligne (\n). La commande 'od' ('octal dump' permet de voir la valeur de chaque octet contenu dans IFS. Le caractère '\c' permet de ne pas récupérer dans le tube le saut de ligne de la commande 'echo' : $ echo "$IFS\c" | od -c 0000000 \t \n \ c \n 0000006 Le contenu de la variable IFS peut être modifié. Dans l'exemple qui suit, on va commencer par sauvegarder la valeur actuelle de IFS pour une restauration ultériere : $ OLDIFS="$IFS" Modification de IFS : $ IFS=":" Le caractère 'espace' redevient un caractère normal, tandis que le caractère ':' devient le séparateur de mots : $ read var1 var2 var3 mot1:mot2 mot3:mot4 $ echo $var1 mot1 $ echo $var2 mot2 mot3 $ echo $var3 mot4 Restauration de la valeur initiale de IFS : $ IFS="$OLDIFS" /!\ Les expressions '$IFS' et '$OLDIFS' doivent obligatoirement être placées entre guillemets " " pour que les caractères internes ne soient pas interprétés par le shell. ============= Documentation ============= - Programmation shell sous Unix/Linux, Deffaix-Rémy, pp. 121 - 157 ------------------------------------------------------------------------------ # vim: syntax=txt