Chapitre 519 minDébutant
Bash : variables, conditions et boucles
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- utiliser variables, guillemets et arguments sans subir le découpage des mots ;
- lire un code de retour et rediriger les sorties d'une commande ;
- écrire des tests avec
[[ ]]et(( )), puisifetcase; - parcourir des listes, des fichiers et des tableaux avec
foretwhile.
4 exercices corrigés1 schéma
Un script Bash est une suite de commandes Linux enrichie de variables, de tests (if, case) et de boucles (for, while) : le shell remplace les variables par leur texte, lance chaque commande et regarde son code de retour pour savoir si elle a réussi. Ce chapitre présente ces briques, les redirections qui relient les commandes entre elles, et la règle qui évite la moitié des bugs Bash : mettre les variables entre guillemets.
Les variables
serveur="srv-web" # pas d'espace autour du =
seuil=15
aujourdhui=$(date +%F) # la sortie d'une commande : 2026-11-03
echo "Contrôle de $serveur le $aujourdhui"
En Bash, tout est du texte : seuil=15 range les deux caractères 1 et 5. Les calculs se font dans $(( )), qui ne connaît que les entiers :
echo $(( 17 / 5 )) # 3 : division entière
echo $(( 17 % 5 )) # 2 : reste
echo $(( 2 ** 10 )) # 1024 : puissance
x="5"; y=3
echo $(( x + y )) # 8 : dans $(( )), les variables n'ont pas besoin de $
echo "$x$y" # 53 : hors calcul, c'est du texte collé
Par convention, les variables d'environnement et les constantes sont en MAJUSCULES (PATH, HOME, RETENTION_JOURS), les variables du script en minuscules. export NOM=valeur rend une variable visible des programmes lancés par le script ; readonly interdit de la modifier ensuite.
Les guillemets, la règle d'or
| Écriture | Ce que fait Bash | Exemple avec nom="srv-web" |
|---|---|---|
"…" | Remplace $variable et $(commande), garde les espaces | "Serveur $nom" donne Serveur srv-web |
'…' | Garde tout tel quel, rien n'est remplacé | 'Serveur $nom' donne Serveur $nom |
| Sans guillemets | Remplace, puis découpe le résultat aux espaces et développe les jokers * et ? | Danger dès qu'une valeur contient un espace |
Ce découpage est la source de bugs la plus fréquente :
f="rapport final.txt"
touch "$f"
ls $f # Bash comprend : ls rapport final.txt (deux fichiers)
ls "$f" # Bash comprend : ls "rapport final.txt"
ls: impossible d'accéder à 'rapport': Aucun fichier ou dossier de ce nom
ls: impossible d'accéder à 'final.txt': Aucun fichier ou dossier de ce nom
rapport final.txt
Avec rm $f, la même erreur pourrait supprimer deux autres fichiers. D'où la règle : mets toujours tes variables entre guillemets doubles, "$f", sauf si tu veux précisément le découpage. L'outil ShellCheck signale chaque oubli (avertissement SC2086).
Les accolades délimitent le nom de la variable et donnent accès à des transformations utiles, sans appeler de programme externe :
| Écriture | Résultat avec fichier="/var/log/nginx/access.log.2.gz" |
|---|---|
"${fichier}_copie" | /var/log/nginx/access.log.2.gz_copie |
"${fichier##*/}" | access.log.2.gz (supprime le plus long début jusqu'au dernier /) |
"${fichier%/*}" | /var/log/nginx (supprime la fin depuis le dernier /) |
"${fichier%.gz}" | /var/log/nginx/access.log.2 |
"${fichier%%.*}" | /var/log/nginx/access (supprime la plus longue fin depuis le premier .) |
"${#fichier}" | 30 (la longueur) |
"${inconnu:-défaut}" | défaut si la variable est vide ou non définie |
"${nom/web/bdd}" avec nom="srv-web" | srv-bdd |
Les paramètres du script
Un script reçoit des arguments au lancement : ./verif.sh srv-web 15.
| Variable | Contenu |
|---|---|
$0 | Le nom du script |
$1, $2… ${10} | Le premier, le deuxième… argument |
$# | Le nombre d'arguments |
"$@" | Tous les arguments, chacun gardé intact |
$? | Le code de retour de la dernière commande |
$$ | Le numéro du processus du script (pratique pour un nom de fichier temporaire unique) |
Écris toujours "$@" avec ses guillemets pour transmettre les arguments : avec ./copie.sh "rapport final.txt" srv-web, "$@" donne deux arguments, rapport final.txt et srv-web, alors que $@ sans guillemets en donne trois. Le chapitre 6 montre comment traiter des options comme -j 30 avec getopts.
Code de retour, redirections et pipes
Chaque commande Linux se termine par un code de retour (exit status) : 0 si tout s'est bien passé, un nombre de 1 à 255 sinon. C'est sur ce code, et non sur ce qui s'affiche, que Bash prend ses décisions. $? le contient juste après la commande.
grep -q '^root:' /etc/passwd
echo $? # 0 : trouvé
grep -q '^zorro:' /etc/passwd
echo $? # 1 : pas trouvé
&& lance la commande suivante seulement si la précédente a réussi, || seulement si elle a échoué :
mkdir -p /srv/sauvegardes && echo "Dossier prêt"
ping -c 1 -W 2 srv-journaux > /dev/null || echo "srv-journaux ne répond pas"
Chaque programme dispose de trois canaux : l'entrée standard (0, le clavier par défaut), la sortie standard (1, l'écran) et la sortie d'erreur (2, l'écran aussi). Les redirections les branchent ailleurs.
| Écriture | Effet |
|---|---|
commande > fichier | Sortie standard dans le fichier, qui est écrasé |
commande >> fichier | Sortie standard ajoutée à la fin du fichier |
commande 2> erreurs.log | Sortie d'erreur dans un fichier |
commande > journal.log 2>&1 | Les deux sorties dans le même fichier (l'ordre compte : 2>&1 après >) |
commande &> journal.log | Raccourci Bash du précédent |
commande > /dev/null | Jeter la sortie (/dev/null avale tout) |
commande < fichier | Lire l'entrée standard depuis un fichier |
cmd1 | cmd2 | La sortie de cmd1 devient l'entrée de cmd2 |
commande | tee fichier | Afficher et écrire dans le fichier |
Le document en ligne (here-document) envoie plusieurs lignes à l'entrée standard d'une commande, pratique pour écrire un fichier de configuration depuis un script :
cat > /etc/motd <<EOF
Serveur $(hostname) - Sorbier Logistique
Accès réservé au service informatique. Toute connexion est journalisée.
EOF
Avec <<'EOF' (le mot entre apostrophes), rien n'est remplacé à l'intérieur : $(hostname) resterait tel quel.
Les tests : [[ ]] et (( ))
if exécute une commande et regarde son code de retour. La commande la plus utilisée est le test [[ … ]], propre à Bash, qui répond 0 (vrai) ou 1 (faux).
| Test | Vrai si | Exemple |
|---|---|---|
-f fichier | Le fichier existe (fichier ordinaire) | [[ -f /etc/nginx/nginx.conf ]] |
-d dossier | Le dossier existe | [[ -d /srv/sauvegardes ]] |
-e chemin | Le chemin existe, quel que soit son type | [[ -e /dev/sdb ]] |
-s fichier | Le fichier existe et n'est pas vide | [[ -s "$archive" ]] |
-r, -w, -x | Lisible, modifiable, exécutable par toi | [[ -x /usr/bin/rsync ]] |
-z "$v", -n "$v" | Chaîne vide, non vide | [[ -z "$1" ]] |
"$a" == "$b", != | Chaînes égales, différentes ; à droite, * est un joker | [[ $nom == srv-* ]] |
"$a" =~ regex | La chaîne correspond à l'expression régulière | [[ $login =~ ^[a-z]+$ ]] |
$a -eq $b | Nombres égaux (aussi -ne, -lt, -le, -gt, -ge) | [[ $# -eq 2 ]] |
!, &&, || | Non, et, ou | [[ -f "$f" && -s "$f" ]] |
Pour comparer des nombres, l'arithmétique (( … )) est plus lisible : (( libre < 10 )). Attention, dans [[ ]], les signes < et > comparent des chaînes dans l'ordre alphabétique : [[ 10 > 9 ]] est faux, car le caractère 1 vient avant 9. Avec des nombres, utilise -gt ou (( )).
Tu croiseras aussi l'ancienne forme [ … ] (la commande test), seule comprise par sh et dash. Elle exige des guillemets partout et n'accepte ni && ni =~ à l'intérieur : dans un script Bash, préfère [[ ]].
if et case
libre=12
if (( libre < 10 )); then
echo "CRITIQUE : ${libre} Go libres"
elif (( libre < 15 )); then
echo "ATTENTION : ${libre} Go libres"
else
echo "OK : ${libre} Go libres"
fi
ATTENTION : 12 Go libres
if peut tester directement n'importe quelle commande : if grep -q "^$login:" /etc/passwd; then …, if ping -c 1 -W 2 "$hote" > /dev/null; then …. Pas besoin de $? : c'est le code de retour qui est testé.
case compare une valeur à des motifs, comme switch -Wildcard en PowerShell, mais s'arrête au premier motif qui correspond :
case "$1" in
srv-ad*) echo "contrôleur de domaine" ;;
srv-*) echo "serveur membre" ;;
-h|--aide) echo "usage : $0 nom-du-serveur" ;;
*) echo "nom inconnu : $1" ; exit 1 ;;
esac
Les boucles
for … in parcourt une liste de mots, un tableau ou des fichiers :
for s in srv-web srv-journaux; do
echo "Contrôle de $s"
done
for f in /var/log/nginx/*.log; do # les jokers sont développés en noms de fichiers
echo "$f : $(wc -l < "$f") lignes"
done
for ((i = 1; i <= 3; i++)); do # forme avec compteur
printf 'ENT-%03d\n' "$i"
done
ENT-001
ENT-002
ENT-003
printf est plus fiable qu'echo pour mettre en forme : %s pour une chaîne, %d pour un entier, %03d pour un entier sur trois chiffres complété par des zéros, \n pour aller à la ligne.
while read lit un fichier ligne par ligne. C'est la boucle de base pour traiter une liste :
while IFS= read -r serveur; do
[[ -z "$serveur" || "$serveur" == \#* ]] && continue # sauter lignes vides et commentaires
echo "Traitement de $serveur"
done < serveurs.txt
IFS= garde les espaces en début et en fin de ligne, -r empêche read d'interpréter les barres obliques inverses. Le fichier est donné à la boucle par < serveurs.txt après done. until répète jusqu'à ce qu'une condition devienne vraie ; break et continue fonctionnent comme en PowerShell.
Les tableaux
serveurs=(srv-web srv-journaux srv-ad)
echo "${serveurs[0]}" # srv-web
echo "${#serveurs[@]}" # 3 : le nombre d'éléments
serveurs+=(srv-ad2) # ajouter un élément
for s in "${serveurs[@]}"; do echo "- $s"; done
Bash 4 et suivants ont aussi des tableaux associatifs, l'équivalent des tables de hachage, à déclarer avec declare -A :
declare -A passerelle=([POSTES]=192.168.10.126 [SERVEURS]=192.168.10.190 [ADMIN]=192.168.10.222)
echo "${passerelle[SERVEURS]}" # 192.168.10.190
for vlan in "${!passerelle[@]}"; do echo "$vlan -> ${passerelle[$vlan]}"; done
"${!passerelle[@]}" donne les clés, dans un ordre qui n'est pas garanti. macOS fournit encore Bash 3.2, sans tableaux associatifs : un script qui en utilise ne tourne que sur un Linux récent.
Demander une valeur
read -rp "Nom du serveur : " serveur
read -rsp "Mot de passe : " mdp; echo # -s : la saisie ne s'affiche pas
Comme en PowerShell, un script d'administration évite les questions et prend des arguments : il pourra tourner seul, dans cron.
Exercices
Exercice 1 sur 4 Prédire la sortie Réussi À revoir
Donne ce qu'affiche ce script.
#!/usr/bin/env bash
a=7
b="3"
echo "$a$b"
echo $(( a * b ))
echo '$a vaut' "$a"
chemin="/etc/nginx/sites-available/suivi.conf"
echo "${chemin##*/}"
echo "${chemin%/*}"
echo "${chemin##*.}"
[[ 10 -gt 9 ]] && echo "nombres : oui"
[[ 10 > 9 ]] && echo "texte : oui" || echo "texte : non"
Voir la réponseMasquer la réponse de l'exercice 1
73
21
$a vaut 7
suivi.conf
/etc/nginx/sites-available
conf
nombres : oui
texte : non
Les apostrophes empêchent le remplacement de $a. ##*. supprime tout jusqu'au dernier point et garde l'extension. -gt compare des nombres ; > dans [[ ]] compare des chaînes, et 10 vient avant 9 dans l'ordre des caractères.
Tu avais trouvé ?
Exercice 2 sur 4 Vérifier ses arguments Réussi À revoir
Écris le début du script verif-disque.sh, qui attend deux arguments : un point de montage et un seuil en pourcentage. Le script doit afficher un message d'usage et quitter avec le code 2 s'il ne reçoit pas exactement deux arguments, quitter avec le code 2 si le point de montage n'est pas un dossier, et quitter avec le code 2 si le seuil n'est pas un entier.
Voir la réponseMasquer la réponse de l'exercice 2
#!/usr/bin/env bash
# verif-disque.sh POINT_DE_MONTAGE SEUIL_POURCENT
if [[ $# -ne 2 ]]; then
echo "usage : $0 point_de_montage seuil_pourcent" >&2
exit 2
fi
point="$1"
seuil="$2"
if [[ ! -d "$point" ]]; then
echo "erreur : $point n'est pas un dossier" >&2
exit 2
fi
if [[ ! $seuil =~ ^[0-9]+$ ]]; then
echo "erreur : le seuil doit être un entier, reçu « $seuil »" >&2
exit 2
fi
Les messages d'erreur vont sur la sortie d'erreur (>&2) : si quelqu'un redirige la sortie normale du script vers un fichier, les erreurs restent visibles. Le code 2 est l'usage habituel pour « mauvaise utilisation de la commande ». La suite du script, qui mesure vraiment l'occupation, est au chapitre 13.
Tu avais trouvé ?
Exercice 3 sur 4 Les guillemets oubliés Réussi À revoir
Un dossier /srv/partage contient les fichiers compte rendu.odt et budget.ods. Le script suivant doit déplacer chacun d'eux dans /srv/archives. Que se passe-t-il ? Corrige-le.
for f in $(ls /srv/partage); do
mv /srv/partage/$f /srv/archives/
done
Voir la réponseMasquer la réponse de l'exercice 3
$(ls /srv/partage) est découpé aux espaces : la boucle reçoit trois mots, budget.ods, compte et rendu.odt. budget.ods est déplacé, mais mv échoue deux fois (/srv/partage/compte et /srv/partage/rendu.odt n'existent pas), et compte rendu.odt reste en place. La correction laisse Bash développer le joker lui-même, et protège chaque nom par des guillemets :
for f in /srv/partage/*; do
mv -- "$f" /srv/archives/
done
Le joker produit un mot par fichier, espaces compris. -- indique à mv que ce qui suit n'est plus une option, au cas où un nom de fichier commencerait par un tiret. Ne parcours jamais la sortie de ls dans un script.
Tu avais trouvé ?
Exercice 4 sur 4 Compter les lignes d'une liste Réussi À revoir
Le fichier serveurs.txt contient ces cinq lignes :
# serveurs Linux de Sorbier
srv-web
srv-journaux
# fin
Écris une boucle qui affiche chaque serveur, en sautant les lignes vides et les commentaires, puis le nombre de serveurs traités. Donne la sortie.
Voir la réponseMasquer la réponse de l'exercice 4
n=0
while IFS= read -r s; do
[[ -z "$s" || "$s" == \#* ]] && continue
echo "serveur : $s"
n=$((n + 1))
done < serveurs.txt
echo "$n serveur(s)"
serveur : srv-web
serveur : srv-journaux
2 serveur(s)
Le \# protège le dièse, qui commencerait sinon un commentaire. Grâce à done < serveurs.txt, la variable n garde sa valeur après la boucle ; avec cat serveurs.txt | while …, le script afficherait 0 serveur(s).
Tu avais trouvé ?