Aspects avancés de la programmation shell
Présentation
Ce chapitre présente d’autres fonctionnalités utilisées en programmation shell qui viennent compléter celles abordées au chapitre Les bases de la programmation shell. Lorsque les scripts donnés en exemple ne sont pas compatibles avec l’interpréteur utilisé par le lecteur, nous invitons ce dernier à récupérer les exemples à partir de l’espace de téléchargement, qui sont fournis pour différents shells présentés dans ce livre.
Comparatif des variables $* et $@
1. Utilisation de $* et de $@ sans guillemets
Les variables $* et $@ contiennent la liste des arguments d’un script shell. Lorsqu’elles ne sont pas entourées par des guillemets, elles sont équivalentes.
Exemple
Le script test_var1.sh affiche la valeur de chaque argument de la ligne de commande :
$ nl test_var1.sh
1 #! /usr/bin/ksh
2 # compatibilité du script : posix, ksh, bash
3
4 compteur=1 ;
5 # $* : tous les espaces sont vus comme séparateurs de mots
6 for arg in $* # Equivalent a $@
7 do
8 echo "Argument $compteur : $arg"
9 compteur=$(( compteur + 1 ))
10 done
Voici un exemple d’appel du script :
$ test_var1.sh a b c "d e f" g
Première étape : le shell courant traite les caractères de protection avant de lancer le script. À ce niveau, les espaces internes à "d e f" sont protégés et ne sont pas vues comme séparateurs de mots, mais comme des caractères quelconques.
Deuxième étape : le shell enfant qui interprète le script substitue $* (ou $@) par a b c d e f g.
for arg in $@
ou
for arg in $*
for arg in a b c d e f g
Les espaces qui étaient protégés au niveau du shell de travail ne le sont plus au niveau du shell enfant. Voici le résultat de l’exécution...
Manipulation de variables
posix |
ksh |
bash |
La manipulation de variables a été abordée au chapitre Les bases de la programmation shell - Les variables utilisateur. Cette section présente de nouvelles fonctionnalités disponibles au niveau des shells bash et ksh.
1. Longueur de la valeur contenue dans une variable
Syntaxe
${#variable}
Exemple
$ var="ma chaine"
$ echo ${#var}
9
$
2. Retirer le plus petit fragment en début de chaîne
Syntaxe
${variable#modele}
où modele est une chaîne de caractères pouvant inclure les caractères spéciaux *, ?, [], ?(expression), +(expression), *(expression), @(expression), !(expression) (cf. chapitre Mécanismes essentiels du shell - Substitution de noms de fichiers).
Le caractère # signifie "Chaîne la plus courte possible en début de chaîne".
Exemple
Afficher la variable ligne sans son premier champ :
$ ligne="champ1:champ2:champ3"
$ echo ${ligne#*:}
champ2:champ3
L’expression " *: " signifie : 0 à n caractères suivis du caractère ":".
3. Retirer le plus grand fragment en début de chaîne
Syntaxe
${variable##modèle}
Les caractères ## signifient "Chaîne la plus longue possible en début de chaîne".
Exemple
Afficher le dernier champ de la variable ligne :
$ ligne="champ1:champ2:champ3"
$ echo ${ligne##*:}
champ3
L’expression " *: " signifie : 0 à n caractères suivis du caractère " : ".
4. Retirer le plus petit fragment en fin de chaîne
Syntaxe
${variable%modèle}
Le caractère "%" signifie "Chaîne la plus courte possible fin de chaîne".
Exemple
Afficher la variable ligne sans son dernier...
Tableaux indicés numériquement
ksh |
bash |
Les shells récents permettent de travailler avec des tableaux à une dimension. Les éléments d’un tableau sont indicés à partir de 0. Le terme "indice" est synonyme ici de "clé numérique".
1. Définition et initialisation d’un tableau
Les éléments d’un tableau peuvent être assignés de manière globale ou un par un.
bash |
Syntaxe
Définition d’un tableau :
declare -a nomtableau
Initialisation globale d’un tableau :
nomtableau=( val1 val2 ... valn )
Définition et initialisation globale d’un tableau :
declare -a nomtableau=( val1 val2 ... valn )
Exemples
$ declare -a tab
$ tab=( 10 11 12 13 mot1 mot2 )
ou
$ declare -a tab=( 10 11 12 13 mot1 mot2 )
En bash, la commande typeset est un synonyme de declare.
ksh 88 et 93 |
Syntaxe
Définition d’un tableau :
set -A nomtableau
Définition et initialisation globale d’un tableau :
set -A nomtableau val1 val2 ... valn
Exemple
$ set -A tab 10 11 12 mot1 mot2
ksh93 |
Autre syntaxe ksh93
Définition et initialisation globale d’un tableau :
nomtableau=( val1 val2 ... valn )
2. Assigner un élément de tableau
Les éléments peuvent être initialisés, modifiés ou ajoutés de cette façon :
Syntaxe
nomtableau[indice]=valeur
Exemple
$ tab[0]=10
$ tab[2]=12
Cette façon de procéder crée directement le tableau si celui-ci n’a pas été défini. La syntaxe de définition vue plus haut (section Définition et initialisation d’un tableau) est plus claire, mais n’est pas obligatoire.
3. Valeur d’un élément
Syntaxe
${nomtableau[indice]}
Exemple
Affichage de l’élément d’indice 0 :
$ echo...
Tableaux associatifs
ksh93 |
bash4 |
Les tableaux associatifs sont des tableaux dont les clés sont des chaînes de caractères. Ils se manipulent comme les tableaux indicés numériquement, à la différence qu’il est impossible d’incrémenter les clés puisque ces dernières ne sont pas numériques.
1. Définition et initialisation d’un tableau associatif
bash4 |
Nous retrouvons la même commande que pour les tableaux indicés numériquement, mais avec l’option -A.
Définition d’un tableau :
$ declare -A tabAssoc # declare et typeset sont synonymes en bash
Initialisation d’un tableau :
$ tabAssoc=([nom]="Deffaix Rémy" [prenom]=Christine)
Définition et initialisation d’un tableau (declare ou typeset) :
$ declare -A tabAssoc=([nom]="Deffaix Rémy" [prenom]=Christine)
ksh93 |
bash4 |
$ typeset -A tabAssoc
$ tabAssoc=([nom]="Deffaix Rémy" [prenom]=Christine)
ou
$ typeset -A tabAssoc=([nom]="Deffaix Rémy" [prenom]=Christine)
2. Afficher la valeur associée à une clé
$ echo ${tabAssoc[nom]}
Deffaix Rémy
3. Afficher la liste des clés
$ echo ${!tabAssoc[*]}
nom prenom
4. Afficher la liste des valeurs
$ echo ${tabAssoc[*]}
Deffaix Rémy Christine
5. Boucler sur un tableau associatif
$ for cle in ${!tabAssoc[*]}
> do
> echo "Clé : $cle , Valeur : ${tabAssoc[$cle]}"
> done
Clé : nom , Valeur : Deffaix Rémy
Clé : prénom , Valeur : Christine
Initialisation des paramètres positionnels avec set
La commande set appelée sans option mais suivie d’arguments affecte ces derniers aux paramètres positionnels ($1, $2, ..., $*, $@, $#). Cela permet de manipuler facilement le résultat de substitutions diverses.
Exemple
Exécution de la commande date :
$ date
ven. mars 18 11:23:50 CET 2022
Le résultat de la commande date est affecté aux paramètres positionnels :
$ set $(date)
$ echo $1
ven.
$ echo $2
mars
$ echo $4
11:23:50
$ echo $*
ven. mars 18 11:23:50 CET 2022
$ echo $#
6
$
Les fonctions
Les fonctions servent à regrouper des commandes qui ont besoin d’être exécutées à plusieurs reprises pendant le déroulement d’un script.
1. Définition d’une fonction
La définition d’une fonction doit être faite avant son premier appel.
Première syntaxe
bourne |
posix |
ksh |
bash |
Les parenthèses indiquent au shell que mafonction est une fonction.
Définition de la fonction :
mafonction() {
commande1
commande2
...
}
Appel de la fonction :
mafonction
Deuxième syntaxe
ksh |
bash |
Le mot-clé function remplace les parenthèses utilisées dans la première syntaxe.
Définition de la fonction :
function mafonction {
commande1
commande2
...
}
Appel de la fonction :
mafonction
Dans un script contenant des fonctions, les commandes situées en dehors des corps de fonction sont exécutées séquentiellement.
Pour que les commandes localisées dans une fonction soient exécutées, il faut faire un appel de fonction. Une fonction peut être appelée aussi bien à partir du programme principal qu’à partir d’une autre fonction.
Exemples
Utilisation de la première syntaxe :
$ nl fonc1.sh
1 f1() { # Déefinition de la fonction
2 echo "Dans f1"
3 }
4 echo "1ère commande"
5 echo "2ème commande"
6 f1 # Appel de la fonction
7 echo "3ème commande"
$ fonc1.sh
1ère commande
2ème commande
Dans f1
3ème commande ...
Commandes d’affichage
1. La commande print
ksh |
Cette commande apporte des fonctionnalités qui n’existent pas avec echo.
a. Utilisation simple
Exemple
$ print Imprimante en panne
Imprimante en panne
$
b. Suppression du saut de ligne naturel de print
Il faut utiliser l’option -n.
Exemple
$ print -n Imprimante en panne
Imprimante en panne$
c. Afficher des arguments commençant par le caractère "-"
Exemple
Dans l’exemple suivant, la chaîne de caractères -i fait partie du message. Malheureusement, print interprète -i comme une option, et non comme un argument :
$ print -i : Option invalide
ksh: print: bad option(s)
$ print "-i : Option invalide"
ksh: print: bad option(s)
Il ne sert à rien de mettre des protections autour des arguments de print. En effet, "-" n’est pas un caractère spécial du shell, il ne sert donc à rien de le protéger. Il n’est pas interprété par le shell, mais par la commande print.
Avec l’option - de la commande print, les caractères qui suivent seront, quelle que soit leur valeur, interprétés comme des arguments.
Exemple
$ print - "-i : Option invalide"
-i : Option invalide
$
d. Écrire sur un descripteur particulier
L’option -u permet d’envoyer un message sur un descripteur particulier.
print -udesc message
où desc représente le descripteur de fichier.
Exemple
Envoyer un message sur la sortie d’erreur standard avec print :
$ print -u2 "Message d'erreur"
Envoyer un message sur la sortie d’erreur standard avec echo :
$ echo "Message d'erreur" 1>&2
Comparaison des deux commandes :
-
L’option -u2 de la commande print lui indique qu’il faut envoyer le message sur la sortie d’erreur standard.
-
La commande echo écrit...
Gestion des entrées/sorties d’un script
1. Redirection des entrées/sorties standards
La commande interne exec permet de manipuler les descripteurs de fichier du shell courant. Utilisée à l’intérieur d’un script, elle permet de rediriger de manière globale les entrées/sorties de celui-ci.
Rediriger l’entrée standard d’un script
exec 0< fichier1
Toutes les commandes du script placées derrière cette directive et qui lisent leur entrée standard vont extraire leurs données à partir de fichier1. Il n’y aura donc plus d’interaction avec le clavier.
Rediriger la sortie standard et la sortie d’erreur standard d’un script
exec 1> fichier1 2> fichier2
Toutes les commandes du script placées derrière cette directive et qui écrivent sur leur sortie standard enverront leurs résultats dans fichier1. Celles qui écrivent sur leur sortie d’erreur standard enverront leurs erreurs dans fichier2.
Rediriger la sortie standard et la sortie d’erreur standard d’un script dans un même fichier
exec 1> fichier1 2>&1
Toutes les commandes du script placées derrière cette directive enverront leurs résultats et leurs erreurs dans fichier1 (cf. chapitre Mécanismes essentiels du shell - Redirections).
Premier exemple
Le script batch1.sh envoie sa sortie standard dans /tmp/resu et sa sortie d’erreur standard dans /tmp/log :
$ nl batch1.sh
1 #! /bin/bash
2 # compatibilité du script : posix, ksh, bash
3 exec 1> /tmp/resu 2> /tmp/log
4 echo "Début du traitement: $(date)"
5 ls ...
La commande eval
Syntaxe
eval expr1 expr2 ... exprn
La commande eval permet de faire subir à une ligne de commande une double évaluation. Elle prend en argument une suite d’expressions sur laquelle elle effectue les opérations suivantes :
-
Première étape : les caractères spéciaux contenus dans les expressions sont traités. Le résultat du traitement aboutit à une ou plusieurs autres expressions : eval autre_exp1 autre_exp2 ... autre_expn. L’expression autre_exp1 représentera la commande Unix à lancer dans la deuxième étape.
-
Deuxième étape : eval va lancer la commande autre_exp1 autre_exp2 ... autre_expn. Mais auparavant, cette ligne de commande va subir une nouvelle évaluation. Les caractères spéciaux sont traités, puis la commande lancée.
Exemple
Définition de la variable nom qui contient "christie" :
$ nom=christie
Définition de la variable var qui contient le nom de la variable définie ci-dessus :
$ var=nom
Comment faire afficher la valeur "christie" en se servant de la variable var ? Dans la commande suivante, le shell substitue $$ par le PID du shell courant :
$ echo $$var
17689var
Dans la commande suivante, le nom de la variable est isolé. Cela ne pourra pas fonctionner non plus : le shell génère une erreur de syntaxe car il ne peut traiter les deux caractères "$" simultanément :
$ echo ${$var}
ksh: ${$var}: bad substitution
Il est indispensable d’utiliser la commande eval :
$ eval echo \$$var
christie
$
L’ordre d’évaluation du shell est donné au chapitre Les bases de la programmation shell - Interprétation d’une ligne de commande.
Mécanisme de la commande eval
Première étape...
Gestion des signaux
Les dispositions du shell courant vis-à-vis des signaux peuvent être modifiées en utilisant la commande trap.
1. Principaux signaux
Nom du signal |
N° |
Signification |
Disposition par défaut d’un processus sur réception du signal |
Disposition modifiable ? |
HUP |
1 |
Rupture d’une ligne de terminal. Lors d’une déconnexion, le signal est reçu par d’éventuels processus lancés en arrière-plan à partir du shell concerné. |
Mort |
oui |
INT |
2 |
Généré à partir du clavier (voir paramètre intr de la commande stty -a). Utilisé pour tuer le processus qui tourne en avant plan. |
Mort |
oui |
TERM |
15 |
Généré via la commande kill. Utilisé pour tuer un processus. |
Mort |
oui |
KILL |
9 |
Généré via la commande kill. Utilisé pour tuer un processus. |
Mort |
non |
Dans les commandes, les signaux peuvent être exprimés sous forme numérique ou symbolique. Les signaux HUP, INT, TERM et KILL possèdent la même valeur numérique sur toutes les plates-formes Unix, ce qui n’est pas le cas de tous les signaux. La forme symbolique est donc préférable.
2. Ignorer un signal
Syntaxe
trap '' sig1 sig2
Exemple
Le shell courant a pour PID 18033 :
$ echo $$
18033
L’utilisateur demande au shell d’ignorer l’éventuelle réception des signaux HUP et TERM :
$ trap '' HUP TERM
Envoi des signaux HUP et TERM (syntaxe tous shells) :
$ kill -HUP 18033
$ kill -TERM 18033
Les signaux sont ignorés, le processus shell ne meurt donc pas :
$ echo $$
18033
$
Remarque sur la commande interne kill : si le signal est exprimé sous forme symbolique (par exemple TERM au lieu de 15), la norme POSIX propose d’utiliser l’option -s (disponible en bash et ksh93) de la commande...
Gestion de menus avec select
ksh |
bash |
Syntaxe
select var in item1 item2 ... itemn
do
commandes
done
La commande interne select est une structure de contrôle de type boucle qui permet d’afficher de manière cyclique un menu. La liste des items, item1 item2 ... itemn, sera affichée à l’écran à chaque tour de boucle. Les items sont indicés automatiquement. La variable var sera initialisée avec l’item correspondant au choix de l’utilisateur.
Cette commande utilise également deux variables réservées :
-
La variable PS3 représente le prompt utilisé pour la saisie du choix de l’utilisateur. Sa valeur par défaut est #?. Elle peut être modifiée à la convenance du développeur.
-
La variable REPLY qui contient l’indice de l’item sélectionné.
La variable var contient le libellé du choix et REPLY l’indice de ce dernier.
Exemple
$ nl menuselect.sh
1 #! /bin/bash
2 # compatibilité du script : ksh, bash
3 function sauve {
4 echo "Vous avez choisi la sauvegarde"
5 # Lancement de la sauvegarde
6 }
7 function restaure {
8 echo "Vous avez choisi la restauration"
9 # Lancement de la restauration
10 }
...
Analyse des options d’un script avec getopts
bourne |
posix |
ksh |
bash |
Syntaxe
getopts liste-options-attendues option
La commande interne getopts permet à un script d’analyser les options qui lui ont été passées en argument. Chaque appel à getopts analyse l’option suivante de la ligne de commande. Pour vérifier la validité de chacune des options, il faut appeler getopts à partir d’une boucle.
Définition d’une option
Pour getopts, une option est composée d’un caractère précédé d’un signe "+" ou "-".
Exemple
"-c" et "+c" sont des options, tandis que "christie" est un argument :
# gestuser.sh -c christie
# gestuser.sh +c
Une option peut fonctionner seule ou être associée à un argument.
Exemple
Voici le script gestuser.sh qui permet d’archiver et de restaurer des comptes utilisateur. Les options -c et -x signifient respectivement "Créer une archive" et "Extraire une archive". Ce sont des options sans argument. Les options -u et -g permettent de spécifier la liste des utilisateurs et la liste des groupes à traiter. Elles doivent être suivies d’un argument.
# gestuser.sh -c -u christie,bob,olive
# gestuser.sh -x -g cours -u christie,bob
Pour tester si les options et arguments passés au script gestuser.sh sont ceux attendus, le développeur écrira :
getopts "cxu:g:" option
Explication des arguments de getopts :
-
Premier argument : les options sont citées l’une derrière l’autre. Une option suivie d’un ":" signifie que c’est une option à argument.
-
Deuxième argument : option est une variable utilisateur qui sera initialisée avec l’option en cours de traitement.
Un appel à getopts récupère...
Gestion d’un processus en arrière-plan
bourne |
posix |
ksh |
bash |
La commande wait permet au shell d’attendre la terminaison d’un processus lancé en arrière-plan.
Syntaxes
Attendre la terminaison du processus dont le PID est donné en argument :
$ wait pid1
Attendre la terminaison de tous les processus lancés en arrière-plan à partir du shell courant :
$ wait
En ksh et en bash le processus peut également être exprimé par son numéro de tâche (cf chapitre Mécanismes essentiels du shell - Processus en arrière-plan - Contrôle de tâches (jobs)).
Exemple
Le script attendProc.sh lance une sauvegarde en arrière-plan. Pendant que celle-ci se déroule, le shell effectue d’autres actions. Puis il attend la fin de la sauvegarde avant de lancer une vérification de la bande :
$ nl attendProc.sh
1 #! /bin/bash
2 # compatibilité du script : bourne, posix, ksh, bash
3 # Lancement d'une commande de sauvegarde en arrière-plan
4 find / | cpio -ocvB > /dev/rmt/0 &
5 echo "Le PID du processus en arrière-plan est : $!"
6 # Pendant que la commande de sauvegarde se déroule,
7 # le script fait autre chose
8 echo "Début des autres actions"
9 ...
10 echo "Fin des autres actions"
11 # Attente de la fin de la sauvegarde pour passer à la suite ...
Compatibilité d’un script entre bash et ksh
Cette section traite de la manière d’écrire un script pour qu’il soit compatible avec les shells bash et ksh.
1. Récupérer le nom du shell interpréteur du script
Nous allons avoir besoin de tester si le script est en cours d’interprétation par le bash ou le ksh. Voici l’instruction qui permet de faire ce test :
$$ représente le PID du shell courant, tail permet de récupérer la dernière ligne du résultat, et awk permet de récupérer le dernier champ de la ligne (cf. chapitre Le langage de programmation awk).
$ shell=$( ps -p $$ | tail -1 | awk '{ print $NF}' )
$ echo $shell
bash
$
Nous listons ci-après deux incompatibilités classiques entre ksh et bash et nous finirons par la manière d’écrire un script unique grâce au nom du shell que nous venons de récupérer ci-dessus.
2. Gestion des séquences d’échappement avec echo
Dans le comportement par défaut du bash, l’option -e est indispensable pour que les séquences d’échappement soient interprétées. En ksh, aucune option n’est nécessaire.
Exemple en ksh
$ echo " a\nb"
a
b
$
Exemple en bash
$ echo -e " a\nb"
a
b
$
Pour éviter d’avoir à utiliser l’option -e en bash, il faut activer l’option xpg_echo à l’aide de la commande shopt.
Exemple en bash
$ shopt -s xpg_echo
$ echo "a\nb"
a
b
$
3. Gestion des caractères étendus
Comme nous l’avons étudié dans les chapitres précédents, l’option extglob doit être activé...
Script d’archivage incrémental et transfert SFTP automatique
1. Objectif
Il s’agit d’écrire un script qui sauvegarde de manière incrémentale le contenu d’un répertoire d’une machine de production. Les fichiers de sauvegarde (archives cpio compressées) seront transférés sur un serveur de sauvegarde (serveur venus), dans un répertoire dont le nom dépend du mois et de l’année de sauvegarde.
Répertoires de la machine de production :
-
/root/admin/backup : répertoire des scripts de sauvegarde.
-
/home/document : répertoire des documents à sauvegarder.
-
/home/lbackup : répertoire local des archives. Ce répertoire sera nettoyé tous les mois.
Répertoires de la machine de sauvegarde :
-
/home/dbackup/2022/01 : archives du mois de janvier 2022.
-
/home/dbackup/2022/02 : archives du mois de février 2022.
Dans l’exemple présenté ici, ces répertoires sont créés par avance. Ce n’est pas le script de sauvegarde qui le fait (mais cela est facilement réalisable).
La figure 2 représente l’arborescence des deux serveurs.
La sauvegarde incrémentale utilisera autant de niveau de sauvegarde qu’il y a de jours dans le mois. Par principe, une sauvegarde de niveau 0 (sauvegarde de tous les fichiers du répertoire /home/document) est faite le premier jour de chaque mois. Les jours suivants, seuls les fichiers modifiés depuis le jour précédent seront archivés.
Des fichiers de niveau sont utilisés (niveau0, niveau1...). Ceux-ci témoignent de la date à laquelle la sauvegarde a été réalisée.
Exemple
Le 01/01/2022 : Sauvegarde de niveau 0 : création du fichier témoin "niveau0" et sauvegarde de tous les fichiers situés sous...
Exercices
Les fichiers fournis pour les exercices sont disponibles dans le répertoire dédié au chapitre sous l’arborescence Exercices/fichiers.
1. Fonctions
a. Exercice 1 : fonctions simples
Commandes utiles : df, who.
Écrire un script audit.sh :
-
Écrire une fonction users_connect qui affichera la liste des utilisateurs actuellement connectés.
-
Écrire une fonction disk_space qui affichera l’espace disque disponible.
-
Le programme principal affichera le menu suivant :
- 0 - Fin
- 1 - Afficher la liste des utilisateurs connectés
- 2 - Afficher l'espace disque
Votre choix :
-
Saisir le choix de l’utilisateur et appeler la fonction adéquate.
b. Exercice 2 : fonctions simples, statut de retour
Commandes filtres utiles : awk, tr -d (cf. chapitre Les commandes filtres). Autres commandes utiles : df, find.
Écrire un script scrute_sf.sh :
-
Programme principal :
-
Le programme principal affichera le menu suivant :
0 - Fin
1 - Supprimer les fichiers de taille 0 sous mon répertoire d'accueil
2 - Contrôler l'espace disque du SF racine
Votre choix :
-
Saisir le choix de l’utilisateur.
-
Le choix 0 provoquera la terminaison du script.
-
Le choix 1 provoquera l’appel de la fonction balaye.
-
Le choix 2 provoquera l’appel de la fonction pas_d_espace.
-
En fonction de la valeur renvoyée par la fonction, afficher le message adéquat.
-
Écrire la fonction balaye : recherche, à partir du répertoire d’accueil de l’utilisateur, tous les fichiers ayant une taille à 0 dans le but de les supprimer (avec demande de confirmation pour chaque fichier).
-
Écrire la fonction pas_d_espace : cette fonction teste le taux d’occupation du système de fichiers racine et renvoie vrai si ce taux est supérieur à 80 %, faux dans le cas contraire.
Exemples d’exécution...