105.2 Leçon 2
Certification : |
LPIC-1 |
---|---|
Version : |
5.0 |
Thème : |
105 Shells et scripts shell |
Objectif : |
105.2 Personnaliser ou écrire des scripts simples |
Leçon : |
2 sur 2 |
Introduction
En règle générale, les scripts shell servent à automatiser les opérations liées aux fichiers et aux répertoires, les mêmes opérations que celles qui peuvent s’effectuer manuellement en ligne de commande. Cependant, la portée des scripts shell ne se limite pas aux documents d’un utilisateur, étant donné que la configuration et l’interaction avec certains éléments d’un système d’exploitation Linux s’effectuent également à l’aide d’une série de scripts.
Le shell Bash comporte toute une série de commandes intégrées (builtins ou primitives du shell) fort pratiques pour écrire des scripts, mais toute la puissance des scripts repose sur la combinaison des commandes intégrées de Bash avec les nombreux outils en ligne de commande disponibles sur un système Linux.
Tests étendus
En tant que langage de script, Bash sert principalement à manipuler des fichiers. C’est pourquoi la commande interne test
de Bash dispose de toute une série d’options qui permettent d’évaluer les propriétés de tout ce qui constitue le système de fichiers (notamment les fichiers et les répertoires). Ces tests permettent, par exemple, de vérifier si les fichiers et les répertoires nécessaires à l’exécution d’une tâche particulière sont présents et peuvent être lus. Ensuite, en combinaison avec une structure conditionnelle if
, les actions appropriées sont exécutées en fonction du résultat du test.
La commande test
peut évaluer des expressions en utilisant deux syntaxes différentes : les expressions de test peuvent être fournies en argument à la commande test
ou elles peuvent être placées entre crochets, avec la commande test
donnée implicitement. Ainsi, le test pour évaluer si /etc
est un répertoire valide peut s’écrire test -d /etc
ou [ -d /etc]
:
$ test -d /etc $ echo $? 0 $ [ -d /etc ] $ echo $? 0
Comme le confirme le code de sortie de la variable spéciale $?
— une valeur de 0 signifie que le test a réussi — les deux formes ont évalué /etc
en tant que répertoire valide. En admettant que le chemin vers un fichier ou un répertoire est enregistré dans la variable $VAR
, les expressions suivantes peuvent être utilisées comme arguments de test
ou entre crochets :
-a "$VAR"
-
Vérifie si VAR existe dans le système de fichiers et est un fichier.
-b "$VAR"
-
Vérifie si VAR existe et est un fichier spécial en mode bloc.
-c "$VAR"
-
Vérifie si VAR existe et est un fichier spécial en mode caractère.
-d "$VAR"
-
Vérifie si VAR existe et est un répertoire
-e "$VAR"
-
Vérifie si VAR existe dans le système de fichiers.
-f "$VAR"
-
Vérifie si VAR existe et est un fichier ordinaire.
-g "$VAR"
-
Vérifie si VAR existe et a son bit SGID positionné.
-h "$VAR"
-
Vérifie si VAR existe et est un lien symbolique.
-L "$VAR"
-
Vérifie si VAR existe et est un lien symbolique (comme
-h
). -k "$VAR"
-
Vérifie si VAR existe et son bit collant (sticky) est positionné.
-p "$VAR"
-
Vérifie si VAR existe et est un tube nommé.
-r "$VAR"
-
Vérifie si VAR existe et est lisible pour l’utilisateur en cours.
-s "$VAR"
-
Vérifie si VAR existe et n’est pas vide.
-S "$VAR"
-
Vérifie si VAR existe et est un fichier socket.
-t "$VAR"
-
Vérifie si VAR est ouvert dans un terminal.
-u "$VAR"
-
Vérifie si VAR existe et son bit SUID positionné.
-w "$VAR"
-
Vérifie si VAR existe et est accessible en écriture pour l’utilisateur en cours.
-x "$VAR"
-
Vérifie si VAR existe et est exécutable pour l’utilisateur en cours.
-O "$VAR"
-
Vérifie si VAR existe et appartient à l’utilisateur en cours.
-G "$VAR"
-
Vérifie si VAR existe et appartient au groupe effectif de l’utilisateur en cours.
-N "$VAR"
-
Vérifie si VAR existe et a été modifié depuis le dernier accès.
"$VAR1" -nt "$VAR2"
-
Vérifie si VAR1 est plus récent (newer than) que VAR2 en fonction de la date de dernière modification.
"$VAR1" -ot "$VAR2"
-
Vérifie si VAR1 est plus ancien (older than) que VAR2.
"$VAR1" -ef "$VAR2"
-
Vérifie si VAR1 est un lien symbolique vers $VAR2.
Il est préférable d’utiliser les guillemets doubles autour d’une variable testée car, si la variable est vide, elle peut provoquer une erreur de syntaxe pour la commande test
. Les options de test requièrent un argument d’opérande et une variable vide sans guillemets provoquerait une erreur due à l’absence de cet argument. Il existe également des tests pour des variables texte arbitraires, tels que ceux décrits ci-dessous :
-z "$TXT"
-
Vérifie si la variable
TXT
est vide (taille zéro). -n "$TXT"
outest "$TXT"
-
Vérifie si la variable
TXT
n’est pas vide. "$TXT1" = "$TXT2"
ou"$TXT1" == "$TXT2"
-
Vérifie si
TXT1
est égal àTXT2
. "$TXT1" != "$TXT2"
-
Vérifie si
TXT1
n’est pas égal àTXT2
. "$TXT1" < "$TXT2"
-
Vérifie si
TXT1
vient avantTXT2
par ordre alphabétique. "$TXT1" > "$TXT2"
-
Vérifie si
TXT1
vient aprèsTXT2
par ordre alphabétique.
Chaque langage peut avoir des règles différentes en ce qui concerne l’ordre alphabétique. Pour obtenir des résultats cohérents, quels que soient les paramètres de localisation du système sur lequel le script est exécuté, il est préférable de définir la variable d’environnement LANG
à C
, comme dans LANG=C
, avant d’effectuer des opérations qui impliquent l’ordre alphabétique. Cette définition va également afficher les messages du système dans la langue d’origine, elle ne doit donc être utilisée que dans le contexte du script.
Les comparaisons numériques ont leur propre jeu de tests :
$NUM1 -lt $NUM2
-
Vérifie si
NUM1
est plus petit que (less than)NUM2
. $NUM1 -gt $NUM2
-
Vérifie si
NUM1
est plus grand que (greater than)NUM2
. $NUM1 -le $NUM2
-
Vérifie si
NUM1
est plus petit ou égal à (less or equal)NUM2
. $NUM1 -ge $NUM2
-
Vérifie si
NUM1
est plus grand ou égal à (greater or equal)NUM2
. $NUM1 -eq $NUM2
-
Vérifie si
NUM1
est plus égal à (equal)NUM2
. $NUM1 -ne $NUM2
-
Vérifie si
NUM1
n’est pas égal à (not equal)NUM2
.
Tous les tests peuvent recevoir les modificateurs suivants :
! EXPR
-
Vérifie si l’expression
EXPR
est fausse. EXPR1 -a EXPR2
-
Vérifie si les deux expressions
EXPR1
etEXPR
sont vraies. EXPR1 -o EXPR2
-
Vérifie si au moins l’une des deux expressions est vraie.
Une autre structure conditionnelle, case
, peut être considérée comme une variante de la structure if. L’instruction case
va exécuter une liste de commandes données si un élément spécifié — le contenu d’une variable, par exemple — se trouve dans une liste d’éléments séparés par des pipes (la barre verticale |
) et finissant par )
. L’exemple de script ci-dessous montre comment la structure case
peut être utilisée pour indiquer le format de paquet logiciel correspondant à une distribution Linux donnée :
#!/bin/bash DISTRO=$1 echo -n "Distribution $DISTRO uses " case "$DISTRO" in debian | ubuntu | mint) echo -n "the DEB" ;; centos | fedora | opensuse ) echo -n "the RPM" ;; *) echo -n "an unknown" ;; esac echo " package format."
Chaque liste de motifs et de commandes associées doit être terminée par ;;
, ;&
, ou ;;&
. Le dernier motif, un astérisque, correspondra si aucun autre motif n’a été trouvé auparavant. L’instruction esac
(case à l’envers) met fin à la structure case
. En admettant que l’exemple de script précédent ait été nommé script.sh
et qu’il soit exécuté avec opensuse
comme premier argument, le résultat suivant sera généré :
$ ./script.sh opensuse Distribution opensuse uses the RPM package format.
Tip
|
Bash a une option nommée |
L’élément recherché et les motifs sont soumis à l’expansion du tilde, à l’expansion des paramètres, à la substitution de commandes et à l’expansion arithmétique. Si l’élément recherché est spécifié avec des guillemets, ces derniers seront supprimés avant que la correspondance ne soit établie.
Les structures de boucles
Les scripts sont souvent utilisés pour automatiser des tâches répétitives, en exécutant le même jeu de commandes jusqu’à ce qu’un critère d’arrêt soit satisfait. Bash possède trois instructions de boucles — for
, until
et while
— conçues pour des structures de boucles légèrement distinctes.
La structure for
parcourt une liste donnée d’éléments — généralement une liste de mots ou d’autres segments de texte séparés par des espaces — en exécutant le même ensemble de commandes sur chacun de ces éléments. Avant chaque itération, l’instruction for
assigne l’élément courant à une variable, qui peut alors être utilisée par les commandes correspondantes. Le processus est répété jusqu’à ce qu’il n’y ait plus d’éléments. Voici la syntaxe de la structure for
:
for VARNAME in LIST do COMMANDS done
VARNAME
est un nom de variable shell aléatoire et LIST
représente n’importe quelle séquence de termes séparés. Les caractères de délimitation valides pour séparer les éléments de la liste sont définis par la variable d’environnement IFS
et sont par défaut les caractères espace, tabulation et retour chariot. La liste des commandes à exécuter est délimitée par les instructions do
et done
de sorte que les commandes peuvent occuper autant de lignes que nécessaire.
Dans l’exemple suivant, la commande for
récupère chaque élément de la liste fournie — une séquence de nombres — et l’affecte à la variable NUM
, un élément à la fois :
#!/bin/bash for NUM in 1 1 2 3 5 8 13 do echo -n "$NUM is " if [ $(( $NUM % 2 )) -ne 0 ] then echo "odd." else echo "even." fi done
Dans l’exemple, une structure if
imbriquée est utilisée en conjonction avec une expression arithmétique pour évaluer si le nombre dans la variable NUM
est pair ou impair. En admettant que l’exemple de script ci-dessus ait été nommé script.sh
et qu’il se trouve dans le répertoire courant, le résultat suivant sera généré :
$ ./script.sh 1 is odd. 1 is odd. 2 is even. 3 is odd. 5 is odd. 8 is even. 13 is odd.
Bash gère également un format alternatif pour les structures for
, avec la notation en double parenthèse. Cette notation ressemble à la syntaxe de l’instruction for
du langage de programmation C et se révèle particulièrement utile pour travailler avec des tableaux :
#!/bin/bash SEQ=( 1 1 2 3 5 8 13 ) for (( IDX = 0; IDX < ${#SEQ[*]}; IDX++ )) do echo -n "${SEQ[$IDX]} is " if [ $(( ${SEQ[$IDX]} % 2 )) -ne 0 ] then echo "odd." else echo "even." fi done
Cet échantillon de script génère exactement le même résultat que l’exemple ci-dessus. En revanche, au lieu d’utiliser la variable NUM
pour stocker un élément à la fois, c’est la variable IDX
qui est utilisée pour suivre l’index actuel du tableau en ordre croissant, en commençant par 0 et en l’incrémentant continuellement tant qu’il est inférieur au nombre d’éléments dans le tableau SEQ
. L’élément actuel est récupéré à partir de sa position dans le tableau avec ${SEQ[$IDX]}
.
De la même manière, la structure until
exécute une séquence de commandes jusqu’à ce qu’une commande de test — comme la commande test
elle-même — se termine avec l’état 0 (succès). Par exemple, la même structure de boucle de l’exemple précédent peut être implémentée avec until
comme ceci :
#!/bin/bash SEQ=( 1 1 2 3 5 8 13 ) IDX=0 until [ $IDX -eq ${#SEQ[*]} ] do echo -n "${SEQ[$IDX]} is " if [ $(( ${SEQ[$IDX]} % 2 )) -ne 0 ] then echo "odd." else echo "even." fi IDX=$(( $IDX + 1 )) done
Les structures until
requièrent certes plus d’instructions que les structures for
, mais elles sont plus adaptées aux critères d’arrêt non numériques fournis par les expressions test
ou toute autre commande. Il convient d’inclure des actions qui garantissent un critère d’arrêt valide, comme l’incrémentation d’une variable compteur, faute de quoi la boucle risque de s’exécuter indéfiniment.
L’instruction while
ressemble à l’instruction until
, sauf que while
continue à réitérer l’ensemble des commandes si la commande de test se termine avec l’état 0 (succès). Par conséquent, l’instruction until [ $IDX -eq ${#SEQ[*]} ]
de l’exemple précédent équivaut à while [ $IDX -lt ${#SEQ[*]} ]
, étant donné que la boucle est censée se répéter tant que l’index du tableau est inférieur au nombre total d’éléments dans le tableau.
Un exemple plus élaboré
Imaginons qu’un utilisateur souhaite synchroniser régulièrement ses fichiers et répertoires avec un autre périphérique de stockage, monté sur un point de montage quelconque du système de fichiers, et qu’un système de sauvegarde classique soit considéré comme excessif. Comme il s’agit d’une activité destinée à être exécutée périodiquement, c’est un bon cas de figure pour automatiser une application à l’aide d’un script shell.
La mission est simple : synchroniser tous les fichiers et répertoires contenus dans une liste, depuis un répertoire d’origine renseigné comme premier argument du script vers un répertoire de destination renseigné comme second argument du script. Pour faciliter l’ajout ou la suppression d’éléments de la liste, celle-ci sera conservée dans un fichier séparé, ~/.sync.list
, avec un élément par ligne :
$ cat ~/.sync.list Documents To do Work Family Album .config .ssh .bash_profile .vimrc
Le fichier contient un assortiment de fichiers et de répertoires, dont certains avec des espaces dans le nom. C’est un scénario approprié pour la commande interne mapfile
de Bash, qui va analyser n’importe quel contenu textuel donné et créer une variable de type tableau à partir de celui-ci, en plaçant chaque ligne comme un élément de tableau distinct. Le fichier sera nommé sync.sh
, et contiendra le script suivant :
#!/bin/bash set -ef # List of items to sync FILE=~/.sync.list # Origin directory FROM=$1 # Destination directory TO=$2 # Check if both directories are valid if [ ! -d "$FROM" -o ! -d "$TO" ] then echo Usage: echo "$0 <SOURCEDIR> <DESTDIR>" exit 1 fi # Create array from file mapfile -t LIST < $FILE # Sync items for (( IDX = 0; IDX < ${#LIST[*]}; IDX++ )) do echo -e "$FROM/${LIST[$IDX]} \u2192 $TO/${LIST[$IDX]}"; rsync -qa --delete "$FROM/${LIST[$IDX]}" "$TO"; done
La première action du script consiste à redéfinir deux options du shell avec la commande set
: l’option -e
terminera l’exécution immédiatement si une commande se termine avec un code d’état non nul et l’option -f
désactive les métacaractères. Les deux options peuvent être abrégées en -ef
. Cette étape n’est pas obligatoire, mais elle permet de diminuer les risques de comportements inattendus.
Les instructions du script qui concernent l’application à proprement parler peuvent être organisées en trois parties :
-
Récupérer et vérifier les paramètres du script
La variable
FILE
correspond au chemin du fichier qui contient la liste des éléments à copier :~/.sync.list
. Les variablesFROM
etTO
correspondent respectivement aux chemins d’origine et de destination. Puisque ces deux derniers paramètres sont fournis par l’utilisateur, ils sont soumis à un simple test de validation effectué par la structureif
: si l’un des deux n’est pas un répertoire valide — évalué par le test[ ! -d "$FROM" -o ! -d "$TO" ]
— le script affichera un message d’aide succinct et se terminera avec un état de sortie de 1. -
Charger la liste des fichiers et des répertoires
Une fois que tous les paramètres sont définis, un tableau avec les éléments à copier est créé avec la commande
mapfile -t LIST < $FILE
. L’option-t
demapfile
supprime le caractère de fin de ligne de chaque ligne avant de l’inclure dans la variable tableau nomméeLIST
. Le contenu du fichier renseigné par la variableFILE
—~/.sync.list
— est lu via une redirection d’entrée. -
Effectuer la copie et informer l’utilisateur
Une boucle
for
avec double parenthèse parcourt la grille d’éléments, avec la variableIDX
qui garde la trace de l’incrémentation de l’index. La commandeecho
informe l’utilisateur de chaque élément copié. Le caractère unicode échappé —\u2192
— pour le caractère flèche droite est présent dans le message de sortie, et l’option-e
de la commandeecho
doit donc être utilisée. La commandersync
ne copie sélectivement que les parties modifiées du fichier d’origine, son utilisation est donc recommandée pour de telles tâches. Les options-q
et-a
, condensées en-qa
, neutralisent les messagesrsync
et activent le mode archive, qui préserve toutes les propriétés du fichier. L’option--delete
va faire en sorte quersync
supprime un élément dans la destination qui n’existe plus dans l’origine, elle doit donc être utilisée avec précaution.
En admettant que tous les éléments de la liste existent dans le répertoire personnel de l’utilisateur carol
, /home/carol
, et que le répertoire de destination /media/carol/backup
pointe vers un périphérique de stockage externe monté, la commande sync.sh /home/carol /media/carol/backup
va générer le résultat suivant :
$ sync.sh /home/carol /media/carol/backup /home/carol/Documents → /media/carol/backup/Documents /home/carol/"To do" → /media/carol/backup/"To do" /home/carol/Work → /media/carol/backup/Work /home/carol/"Family Album" → /media/carol/backup/"Family Album" /home/carol/.config → /media/carol/backup/.config /home/carol/.ssh → /media/carol/backup/.ssh /home/carol/.bash_profile → /media/carol/backup/.bash_profile /home/carol/.vimrc → /media/carol/backup/.vimrc
L’exemple suppose également que le script est exécuté par root
ou par l’utilisateur carol
, étant donné que la plupart des fichiers seraient illisibles pour d’autres utilisateurs. Si sync.sh
n’est pas dans un répertoire figurant dans la variable d’environnement PATH
, il doit être invoqué avec son chemin complet.
Exercices guidés
-
Comment peut-on utiliser la commande
test
pour vérifier si le chemin d’un fichier stocké dans la variableFROM
est plus récent que celui d’un fichier dont le chemin est stocké dans la variableTO
? -
Le script suivant est censé afficher une séquence de chiffres de 0 à 9, mais au lieu de cela, il affiche 0 indéfiniment. Que doit-on faire pour obtenir le résultat escompté ?
#!/bin/bash COUNTER=0 while [ $COUNTER -lt 10 ] do echo $COUNTER done
-
Admettons qu’un utilisateur écrive un script qui nécessite une liste ordonnée de noms d’utilisateurs. La liste classée qui en résulte est présentée comme ceci sur son ordinateur :
carol Dave emma Frank Grace henry
Or, cette même liste est classée de la manière suivante sur l’ordinateur de son collègue :
Dave Frank Grace carol emma henry
Comment expliquer les différences de classement entre les deux listes ?
Exercices d’approfondissement
-
Comment peut-on utiliser tous les arguments en ligne de commande du script pour initialiser un tableau Bash ?
-
Comment se fait-il que, contre toute attente, la commande
test 1 > 2
soit considérée comme vraie ? -
Comment un utilisateur pourrait-il modifier temporairement le séparateur de champ par défaut en le remplaçant uniquement par le caractère de retour à la ligne, tout en ayant la possibilité de revenir à son contenu d’origine ?
Résumé
Cette leçon aborde en détail les tests disponibles pour la commande test
et d’autres structures conditionnelles et de boucles, nécessaires pour écrire des scripts shell plus élaborés. Un script simple de synchronisation de fichiers est donné comme exemple d’application pratique d’un script shell. La leçon comprend les points suivants :
-
Tests étendus pour les structures conditionnelles
if
etcase
. -
Structures de boucles du shell :
for
,until
etwhile
. -
Itération dans les tableaux et les paramètres.
Voici les commandes et les procédures abordées :
test
-
Effectue une comparaison entre les éléments fournis à la commande.
if
-
Une structure logique utilisée dans les scripts pour évaluer quelque chose comme étant vrai ou faux, puis pour brancher l’exécution de la commande en fonction des résultats.
case
-
Évaluer plusieurs valeurs par rapport à une seule variable. L’exécution de la commande de script est alors effectuée en fonction du résultat de la commande
case
. for
-
Répète l’exécution d’une commande en fonction d’un critère donné.
until
-
Répète l’exécution d’une commande jusqu’à ce qu’une expression soit évaluée comme fausse.
while
-
Répète l’exécution d’une commande pendant qu’une expression donnée est évaluée comme vraie.
Réponses aux exercices guidés
-
Comment peut-on utiliser la commande
test
pour vérifier si le chemin d’un fichier stocké dans la variableFROM
est plus récent que celui d’un fichier dont le chemin est stocké dans la variableTO
?La commande
test "$FROM" -nt "$TO"
renverra un code d’état de 0 si le fichier dans la variableFROM
est plus récent que le fichier dans la variableTO
. -
Le script suivant est censé afficher une séquence de chiffres de 0 à 9, mais au lieu de cela, il affiche 0 indéfiniment. Que doit-on faire pour obtenir le résultat escompté ?
#!/bin/bash COUNTER=0 while [ $COUNTER -lt 10 ] do echo $COUNTER done
La variable
COUNTER
doit être incrémentée, ce qui peut se faire avec l’expression arithmétiqueCOUNTER=$(( $COUNTER + 1 ))
qui permettra d’atteindre le critère d’arrêt pour terminer la boucle. -
Admettons qu’un utilisateur écrive un script qui nécessite une liste ordonnée de noms d’utilisateurs. La liste classée qui en résulte est présentée comme ceci sur son ordinateur :
carol Dave emma Frank Grace henry
Or, cette même liste est classée de la manière suivante sur l’ordinateur de son collègue :
Dave Frank Grace carol emma henry
Comment expliquer les différences de classement entre les deux listes ?
Le tri se base sur les paramètres linguistiques du système actuel. Pour éviter les incohérences, les tâches de classement doivent être effectuées avec la variable d’environnement
LANG
fixée àC
.
Réponses aux exercices d’approfondissement
-
Comment peut-on utiliser tous les arguments en ligne de commande du script pour initialiser un tableau Bash ?
Les commandes
PARAMS=( $* )
ouPARAMS=( "$@" )
créent un tableau nomméePARAMS
avec tous les arguments. -
Comment se fait-il que, contre toute attente, la commande
test 1 > 2
soit considérée comme vraie ?L’opérateur
>
est conçu pour les tests de chaînes de caractères, pas pour les tests numériques. -
Comment un utilisateur pourrait-il modifier temporairement le séparateur de champ par défaut en le remplaçant uniquement par le caractère de retour à la ligne, tout en ayant la possibilité de revenir à son contenu d’origine ?
Une copie de la variable
IFS
peut être stockée dans une autre variable :OLDIFS=$IFS
. Le nouveau séparateur de ligne est alors défini avecIFS=$'\n'
et la variableIFS
peut être rétablie avecIFS=$OLDIFS
.