I. Rappels sur les conditions▲
I-A. Opérateurs de nombres entiers vs opérateurs de chaînes▲
On remarque régulièrement des confusions dans l'utilisation des opérateurs de la commande « test » ou programmes dérivés (crochets ou doubles-crochets), notamment dans la distinction des opérateurs de chaînes et des opérateurs de nombres entiers.
Nous allons donc commencer par rappeler les principaux opérateurs sur nombres entiers :
- -eq : égalité entre les deux opérandes (g = d) ;
- -ne : inégalité entre les deux opérandes (g <> d) ;
- -ge : supériorité de l'opérande de gauche sur l'opérande de droite (g >= d) ;
- -gt : supériorité stricte de l'opérande de gauche sur l'opérande de droite (g > d) ;
- -le : infériorité de l'opérande de gauche sur l'opérande de droite (g <= d) ;
- -lt : infériorité stricte de l'opérande de gauche sur l'opérande de droite (g < d).
Les opérateurs de comparaison de chaînes quant à eux sont les suivants :
- = ou == : égalité entre les deux opérandes ;
- != : inégalité entre les deux opérandes ;
- =~ : test sur expression régulière (l'opérande de droite doit vérifier l'expression régulière passée comme second opérande). Cet opérateur n'existe qu'en Bash à partir de la version 3 et ne s'utilise qu'avec la syntaxe des doubles-crochets (commande « test » étendue intégrée à KSH et Bash).
Exemple de confusion entre les deux types d'opérateurs :
[ ~/test]$ var="01"
[ ~/test]$ [ "$var" = "1" ] && echo "OK" || echo "KO"
KO
[ ~/test]$ [ "$var" -eq "1" ] && echo "OK" || echo "KO"
OKIci on voit qu'une égalité entre les opérandes « 1 » et « 01 » est vraie au sens mathématique du terme tandis qu'elle est fausse si l'on compare les deux opérandes en tant que chaînes de caractères. Il faut donc faire rigoureusement attention à ce que l'on souhaite vérifier afin d'éviter les comportements inattendus.
I-B. Protéger ses opérandes▲
Il est important de prendre l'habitude de protéger ses opérandes, soit en privilégiant la syntaxe des doubles-crochets apparue dans KSH 88, soit en prenant l'habitude de systématiquement encadrer ses variables par des « doubles-quotes ».
Exemples de tests avec des opérandes non protégés provoquant ainsi des erreurs :
[ ~/test]$ var="string with spaces"
[ ~/test]$ [ $var = "string with spaces" ] && echo "OK" || echo "KO"
bash: [: trop d\'arguments
KO
[ ~/test]$ [ "$var" = string with spaces ] && echo "OK" || echo "KO"
bash: [: trop d\'arguments
KOExemples de tests avec des opérandes protégés :
[ ~/test]$ var="string with spaces"
[ ~/test]$ [ "$var" = "string with spaces" ] && echo "OK" || echo "KO"
OK
[ ~/test]$ [[ $var = "string with spaces" ]] && echo "OK" || echo "KO"
OKII. Boucler sur une sortie de commande▲
II-A. Règles générales▲
Malgré une très forte présence de beaucoup de scripts, les syntaxes suivantes de boucles permettant de parcourir des sorties de commandes sont à éviter dans la plupart des cas :
for i in $(commande); do
#...
done
for i in `commande`; do
#...;
doneDe manière générale, il est préférable de privilégier les syntaxes suivantes :
commande|while read -r; do
#...
done
while read -r; do
#...
done < <(command)
while read -r; do
#...
done <<< "$(command)"II-B. Exemples d'erreurs courantes▲
Pour illustrer ce que nous venons d'expliquer dans la partie précédente, nous allons essayer de créer un script qui parcourt un répertoire « test » et qui, pour chaque fichier de ce répertoire, affiche le nom du fichier et ses droits.
Voici le contenu du répertoire « test » :
[ ~/test]$ ls -l
total 0
-rw-rw-r-- 1 idriss idriss 0 août 09 12:44 file1
-rw-rw-r-- 1 idriss idriss 0 août 09 12:44 file2
-rw-rw-r-- 1 idriss idriss 0 août 09 12:44 file with spaces
-rwxrwxr-x 1 idriss idriss 177 août 09 12:54 script.shOn notera donc que ce répertoire comporte un fichier dont le nom contient des espaces. Voici une première version du script utilisant une des syntaxes à éviter :
#!/bin/bash
DIR="/home/idriss/test"
for file in $(ls $DIR); do
# Affichage du nom du fichier et de ses droits
echo "Fichier : "$file" a pour droits : "$(stat -c "%A" "$file")
doneÀ l'exécution de ce script, on obtient le résultat suivant :
[ ~/test]$ ./script.sh
Fichier : file1 a pour droits : -rw-rw-r--
Fichier : file2 a pour droits : -rw-rw-r--
stat: impossible d'évaluer «file»: Aucun fichier ou dossier de ce type
Fichier : file a pour droits :
stat: impossible d'évaluer «with»: Aucun fichier ou dossier de ce type
Fichier : with a pour droits :
stat: impossible d'évaluer «spaces»: Aucun fichier ou dossier de ce type
Fichier : space a pour droits :
Fichier : script.sh a pour droits : -rwxrwxr-xAu vu du résultat, on peut en déduire que la boucle « for » ne parcourt non pas un ensemble de fichiers, mais un ensemble de valeurs ou de mots renvoyés par la commande « ls », ce qui peut s'avérer problématique.
Voici une version fonctionnelle du script :
#!/bin/bash
DIR="/home/idriss/test"
ls $DIR|while read -r; do
# Affichage du nom du fichier et de ses droits
echo "Fichier : $REPLY a pour droits : "$(stat -c "%A" "$REPLY")
doneEt à l'exécution de cette version, on obtient un comportement normal :
[ ~/test]$ ./script.sh
Fichier : file1 a pour droits : -rw-rw-r--
Fichier : file2 a pour droits : -rw-rw-r--
Fichier : file with spaces a pour droits : -rw-rw-r--
Fichier : script.sh a pour droits : -rwxrwxr-xOn notera que la boucle « while » parcourt ici le résultat de la commande « ls » ligne par ligne ce qui nous permet de prendre en compte le fichier dont le nom est composé d'espaces comme une ligne et donc comme un élément parcouru.
Il faut également noter qu'avec la syntaxe du « pipe » (caractère « | »), la boucle while est exécutée dans un sous-shell. Par conséquent toute variable valorisée dans cette boucle perdrait sa valeur à la fin de la boucle (qui correspond ici à la fin de l'exécution du sous-shell).
Prenons par exemple ce script qui tente d'afficher le nom, du dernier fichier, qui comporte des espaces :
#!/bin/bash
DIR="/home/idriss/test"
nomFichierAvecEspace=""
ls $DIR|while read -r; do
[[ $REPLY =~ .*\ .* ]] && nomFichierAvecEspace="$REPLY"
done
echo "Nom du fichier avec des espaces : $nomFichierAvecEspace"À l'exécution de celui-ci :
[ ~/test]$ ./script.sh
Nom du fichier avec des espaces :
[ ~/test]$Une correction possible consiste à déléguer une partie du script au sous-shell :
#!/bin/bash
DIR="/home/idriss/test"
nomFichierAvecEspace=""
ls $DIR|
(
while read -r; do
[[ $REPLY =~ .*\ .* ]] && nomFichierAvecEspace="$REPLY"
done
echo "Nom du fichier avec des espaces : $nomFichierAvecEspace"
)À l'exécution de cette version :
[ ~/test]$ ./script.sh
Nom du fichier avec des espaces : file with spaces
[ ~/test]$Il est également possible d'affecter la sortie d'un sous-shell à une variable de la même façon que pour la sous-exécution d'une commande :
#!/bin/bash
DIR="/home/idriss/test"
fileWithSpaces=""
fileWithSpaces=$(ls $DIR| (
while read -r; do
[[ $REPLY =~ .*\ .* ]] && nomFichierAvecEspace="$REPLY"
done
echo $nomFichierAvecEspace # sortie du sous-shell
))
echo "Nom du fichier avec des espaces : $fileWithSpaces"D'autres alternatives sont possibles :
#!/bin/bash
DIR="/home/idriss/test"
fileWithSpaces=""
while read -r; do
[[ $REPLY =~ .*\ .* ]] && fileWithSpaces="$REPLY"
done < <(ls)
echo "Nom du fichier avec des espaces : $fileWithSpaces"Ou encore :
#!/bin/bash
DIR="/home/idriss/test"
fileWithSpaces=""
while read -r; do
[[ $REPLY =~ .*\ .* ]] && fileWithSpaces="$REPLY"
done <<< "$(ls)"
echo "Nom du fichier avec des espaces : $fileWithSpaces"Enfin, voici un dernier exemple d'erreur déjà rencontrée dans des scripts :
#!/bin/bash
DIR="/home/idriss/test"
lstFile="$(ls $DIR)"
# traitements entre temps qui créent des fichiers dans $DIR
# et qui en suppriment d'autres (ou qui en renomment d'autres...)
touch $DIR"/file3"
rm -rf $DIR"/file with space"
for file in $lstFile; do
# Affichage du nom du fichier et de ses droits
echo "Fichier : "$file" a pour droits : "$(stat -c "%A" "$file")
doneÀ l'exécution de celui-ci :
[ ~/test]$ ./script.sh
Fichier : file1 a pour droits : -rw-rw-r--
Fichier : file2 a pour droits : -rw-rw-r--
stat: impossible d'évaluer «file»: Aucun fichier ou dossier de ce type
Fichier : file a pour droits :
stat: impossible d'évaluer «with»: Aucun fichier ou dossier de ce type
Fichier : with a pour droits :
stat: impossible d'évaluer «spaces»: Aucun fichier ou dossier de ce type
Fichier : space a pour droits :
Fichier : script.sh a pour droits : -rwxrwxr-x
[ ~/test]$ ls
file1 file2 file3 script.shOn peut en déduire la conclusion suivante : ce n'est pas l'instruction « exécuter ls » qui est affectée à la variable, mais le résultat de la sous-exécution de cette commande. Cette commande ne sera donc exécutée qu'au moment de l'affectation et non au moment du parcours par la boucle for. Par conséquent, si le contenu du répertoire change entre temps, la boucle for n'en tiendra pas compte.
II-C. Boucles avec incréments▲
La boucle for sur le retour de la commande « seq » est elle aussi couramment employée dans l'utilisation de boucles incrémentales :
[ ~/test]$ for i in $(seq 1 3); do echo $i; done
1
2
3Bien que cette syntaxe ne pose pas de problèmes à l'exécution, d'autres solutions sont possibles :
[ ~/test]$ for i in {1..3}; do echo $i; done
1
2
3
[ ~/test]$ for (( i=1 ; i<=3 ; i++ )); do echo $i; done
1
2
3III. Éviter les processus inutiles▲
III-A. Éviter la commande « ls » dans les scripts▲
Dans la plupart des cas, les « ls » présents dans les scripts sont superflus aussi bien pour parcourir une liste de fichiers que pour récupérer des informations sur des fichiers.
En effet, il est possible de s'abstraire de cette commande en privilégiant l'utilisation du métacaractère « * » (wildcard) lorsqu'il s'agit de parcourir une liste de fichiers et de la commande « stat » lorsqu'il s'agit de récupérer des informations sur un fichier comme les droits, la taille en mémoire, l'utilisateur propriétaire…
Exemple pour récupérer les droits sur un fichier « file » :
stat -c "%A" file # bonne façon de faire
ls -l file|cut -d" " -f1 # mauvaise façon de faireAutre exemple, reprenons notre script précédent dans sa version corrigée :
#!/bin/bash
DIR="/home/idriss/test"
ls $DIR|while read -r; do
# Affichage du nom du fichier et de ses droits
echo "Fichier : "$REPLY" a pour droits : "$(stat -c "%A" "$REPLY")
doneIl existe une optimisation possible en utilisant le métacaractère « * » (wildcard) :
#!/bin/bash
DIR="/home/idriss/test"
for file in $DIR"/"*; do
# Affichage du nom du fichier et de ses droits
echo "Fichier : "$file" a pour droits : "$(stat -c "%A" "$file")
doneIII-B. Éviter la commande « cat » dans les scripts▲
La commande « cat » ou d'autres commandes telles que « more » ou « less » sont bien souvent employées de manière superflue pour parcourir le contenu d'un fichier. Essayons par exemple de parcourir le fichier « /etc/passwd » afin d'afficher une liste des noms des utilisateurs de l'OS :
#!/bin/bash
# Affichage du nom des utilisateurs dans /etc/passwd
cat /etc/passwd|while read -r; do
name=$(echo $REPLY|awk -F ":" '{print $1}')
echo "Nom : "$name
doneUne optimisation possible :
#!/bin/bash
# Affichage du nom des utilisateurs dans /etc/passwd
while read -r; do
name=$(echo $REPLY|cut -d ":" -f1) # vous remarquerez qu'ici nous évitons d'utiliser un tank pour tuer une mouche
echo "Nom : "$name
done < /etc/passwdBien entendu, ceci aurait suffi pour le même résultat :
awk -F ":" '{print "Nom : "$1}' /etc/passwdIII-C. Profiter de la puissance de Bash▲
Pour les utilisateurs de Bash dans ses récentes versions, il est possible d'éviter les tests sur expressions régulières nécessitant de passer par des commandes telles que « grep », « expr »…
Exemple pour une fonction qui teste si une valeur passée en argument est un entier ou pas :
#!/bin/bash
isInt(){
if echo $1|grep -E "^[0-9]+$" >/dev/null; then
echo "OK"
else
echo "KO"
fi
}
isInt "12345" # écrira "OK"
isInt "chaine" # écrira "KO"Version du script optimisée avec Bash :
#!/bin/bash
isInt(){
if [[ $1 =~ ^[0-9]+$ ]]; then
echo "OK"
else
echo "KO"
fi
}
isInt "12345" # écrira "OK"
isInt "chaine" # écrira "KOBien entendu, à ne pas faire si le script doit être portable et également tourner sur d'autres plateformes Unix non GNU (AIX, Solaris, BSD…).
Par ailleurs, cet exemple aurait également pu être écrit de la façon suivante :
#!/bin/bash
isInt(){
if let $1; then
echo "OK"
else
echo "KO"
fi
}
isInt "12345" # écrira "OK"
isInt "chaine" # écrira "KOIII-D. Autres exemples▲
Filtrer les doublons :
sort fichier|uniq # mauvaise façon de faire
sort -u fichier # bonne façon de faireMélange de sed/grep/awk/… quand une seule commande peut suffire :
[ ~]$ cat fichier
ligne1 chaine value3
ligne2 100 value4
[ ~]$ awk -F " " '{print $2}' fichier|grep -E "^[0-9]+$" # mauvaise façon de faire
100
[ ~]$ awk -F " " '{if($2 ~ /^[0-9]+$/){print $2}}' fichier # bonne façon de faire
100IV. Standardiser l'exécution de vos scripts▲
Il est important de documenter l'utilisation de ses scripts à l'aide d'options standards telles que « -h » ou encore « --help ». Nous allons dans cette partie décrire comment utiliser des options du type « -? » ou « --quelque_chose » à l'aide de l'instruction shell « getopts » ou encore de la commande « getopt ».
Réalisons le script devant fonctionner de la manière suivante :
[ ~]$ ./script.sh
ERREUR : parametres invalides !
utilisez l'option -h pour en savoir plus
[ ~]$ ./script.sh -h
Usage: ./script.sh [options]
-h : afficher l'aide
-b <prenom> : saluer <prenom>
[ ~]$ ./script.sh -b
ERREUR : parametres invalides !
utilisez l'option -h pour en savoir plus
[ ~]$ ./script.sh -bJean
Bonjour Jean
[ ~]$Implémentation du script avec « getopt » :
#!/bin/bash
error(){
echo "ERREUR : parametres invalides !" >&2
echo "utilisez l'option -h pour en savoir plus" >&2
exit 1
}
usage(){
echo "Usage: ./script.sh [options]"
echo "-h : afficher l'aide"
echo "-b <prenom> : saluer <prenom>"
}
traitement(){
echo "Bonjour "$1
}
# Pas de paramètre
[[ $# -lt 1 ]] && error
while getopts ":b:h" option; do
case "$option" in
b) traitement $OPTARG ;;
:) error ;; # il manque une valeur ($option = 'b' ici)
h) usage ;;
*) error ;;
esac
doneOn souhaite maintenant ajouter l'option -help. Il n'est pas possible d'utiliser des options longues avec « getopts », d'où l'utilité de la commande externe « getopt » :
#!/bin/bash
error(){
echo "ERREUR : parametres invalides !" >&2
echo "utilisez l'option -h pour en savoir plus" >&2
exit 1
}
usage(){
echo "Usage: ./script.sh [options]"
echo "--help ou -h : afficher l'aide"
echo "-b <prenom> : saluer <prenom>"
}
traitement(){
echo "Bonjour "$1
}
# Pas de paramètre
[[ $# -lt 1 ]] && error
# -o : options courtes
# -l : options longues
options=$(getopt -o h,b: -l help -- "$@")
# éclatement de $options en $1, $2...
set -- $options
while true; do
case "$1" in
-b) traitement $2
shift 2;; # on décale la liste des options de 2 ($1 et $2 sont remplacés par $3 et $4 s'ils existent)
-h|--help) usage
shift;; # on décale la liste des options de 1
--) # fin des options
shift # on décale la liste des options de 1
break;;
*) error
shift;; # on décale la liste des options de 1
esac
doneV. Liens utiles▲
Voici quelques liens qui vous permettront d'approfondir vos connaissances dans la programmation Shell ou encore de vous entraîner :
Advanced Bash-Scripting Guide (traduction)
Un cours complet sur la programmation Shell
Une liste d'exercices corrigés pour débuter
VI. Remerciements▲
Je tiens tout d'abord à remercier les contributeurs des forums Shell Unix/Linux qui corrigent régulièrement le type d'erreurs évoquées dans ce cours sur les forums et qui m'ont donné l'idée de rédiger ce cours.
Je tiens également à remercier sve@r pour sa relecture technique et ses conseils.
Je tiens enfin à remercier ClaudeLELOUP pour son travail de relecture orthographique.





