Aller au contenu
Administration Linux medium

Écrire un script shell Bash : le guide complet

30 min de lecture

Ce guide rassemble tout pour écrire un script shell Bash, du shebang aux fonctions. Variables, conditions, boucles, arguments, codes de sortie, débogage : chaque notion est expliquée avec un exemple exécutable, puis approfondie dans une page dédiée. C'est le guide de référence de la section scripting, à lire d'un bout à l'autre pour apprendre, ou à consulter section par section.

  • Créer et exécuter un script avec shebang et chmod +x
  • Utiliser des variables pour stocker et réutiliser des valeurs
  • Écrire des conditions if/then/fi avec [[ ]]
  • Répéter des actions avec for et while
  • Organiser le code avec des fonctions
  • Gérer les arguments $1, $#, $@ et les codes de retour $?
  • Déboguer avec bash -x et shellcheck

Un script Bash est utile dès que vous répétez la même séquence de commandes plus d'une ou deux fois :

  • vérifier l'espace disque sur dix serveurs et envoyer une alerte si un seuil est dépassé
  • créer un utilisateur avec son répertoire home, ses clés SSH et ses permissions en une commande
  • générer un rapport quotidien à partir des logs nginx et l'envoyer par e-mail
  • valider l'état d'un service avant de lancer un déploiement
  • convertir ou renommer un lot de fichiers selon un pattern

En préparation RHCSA, les scripts shell sont au programme de façon explicite : vous devez être capable d'en écrire un à partir de zéro sous contrainte de temps.


La première ligne d'un script indique quel interpréteur l'exécutera. Sans elle, le shell par défaut du système est utilisé, et ce n'est pas forcément Bash.

#!/bin/bash
echo "Bonjour, $USER !"

Enregistrez ce fichier sous bonjour.sh, puis rendez-le exécutable :

Fenêtre de terminal
chmod +x bonjour.sh
./bonjour.sh
Sortie
Bonjour, bob !
  • #!/bin/bash : le shebang, le système lit ce chemin et lance cet interpréteur
  • chmod +x : ajoute le droit d'exécution au propriétaire
  • ./ : exécute le script depuis le répertoire courant

Une variable stocke une valeur pour la réutiliser plus loin. En Bash, la règle qui piège tous les débutants tient dans l'espacement : la déclaration se fait sans espace autour du = (nom="alice", jamais nom = "alice"), car avec des espaces Bash croit exécuter une commande nommée nom. À la lecture, on préfixe le nom par $ ou on l'entoure de ${}, cette dernière forme étant obligatoire dès qu'on colle du texte juste après le nom.

#!/bin/bash
nom="alice"
nombre=42
echo "Utilisateur : $nom"
echo "Valeur : ${nombre}"
Sortie
Utilisateur : alice
Valeur : 42

La substitution de commande capture la sortie standard d'une commande et la place dans une variable. La forme moderne $(...) remplace l'ancienne notation avec des accents graves : elle s'imbrique proprement et se lit sans ambiguïté. C'est le mécanisme qui permet à un script de réagir à l'état réel du système, par exemple la date du jour ou le nombre de comptes présents.

Fenêtre de terminal
date_du_jour=$(date '+%Y-%m-%d')
nb_utilisateurs=$(wc -l < /etc/passwd)
echo "Date : $date_du_jour"
echo "Comptes : $nb_utilisateurs"
Sortie
Date : 2026-04-10
Comptes : 42

La commande read met le script en pause et attend une saisie au clavier, qu'elle range dans la variable indiquée. C'est utile pour un script interactif, mais gardez en tête qu'un script automatisé (cron, CI) n'a personne pour répondre : réservez read aux outils lancés à la main.

Fenêtre de terminal
echo "Entrez votre nom :"
read -r prenom
echo "Bonjour, $prenom !"

-r désactive l'interprétation des backslashes, toujours utiliser read -r.

VariableContenu
$USERNom de l'utilisateur courant
$HOMERépertoire personnel
$PWDRépertoire courant
$RANDOMNombre aléatoire entre 0 et 32767

La structure if/then/fi exécute un bloc si une condition est vraie. Utilisez [[ ]] (double crochets), plus sûr et plus lisible que [ ].

#!/bin/bash
fichier="$1"
if [[ -f "$fichier" ]]; then
echo "Le fichier existe."
else
echo "Le fichier est introuvable."
fi

En administration système, la moitié des conditions portent sur des fichiers : le script existe-t-il, le répertoire est-il présent, ai-je le droit d'écrire ici. Ces opérateurs à une lettre renvoient vrai ou faux et s'utilisent tels quels dans [[ ]]. Le plus courant est -f, qui vérifie qu'un fichier ordinaire existe avant de le lire.

OpérateurCondition vraie si…
-f "$f"$f est un fichier ordinaire
-d "$f"$f est un répertoire
-e "$f"$f existe (fichier ou répertoire)
-r "$f"$f est lisible
-w "$f"$f est modifiable
-x "$f"$f est exécutable

Les tests de chaînes servent surtout à valider un argument : est-il vide, vaut-il exactement telle valeur. Le piège classique est -z contre -n : -z est vrai quand la chaîne est vide, -n quand elle contient quelque chose. Entourez toujours la variable de guillemets, sinon une variable vide fait disparaître l'opérande et provoque une erreur de syntaxe.

OpérateurCondition vraie si…
-z "$s"$s est vide
-n "$s"$s n'est pas vide
"$a" == "$b"les deux chaînes sont égales
"$a" != "$b"les deux chaînes sont différentes

Bash distingue nettement comparaison de nombres et comparaison de chaînes. Pour les nombres, on utilise ces opérateurs à deux lettres (-eq, -lt...), pas les signes < ou > qui, eux, comparent alphabétiquement. Écrire [[ "$a" -gt "$b" ]] compare bien 9 et 10 comme des entiers, là où > placerait « 10 » avant « 9 ».

OpérateurSignification
-eqégal
-nedifférent
-ltinférieur strict
-leinférieur ou égal
-gtsupérieur strict
-gesupérieur ou égal
Fenêtre de terminal
score=85
if [[ $score -ge 90 ]]; then
echo "Excellent"
elif [[ $score -ge 70 ]]; then
echo "Bien"
else
echo "À améliorer"
fi
Sortie
Bien

La boucle for répète un bloc pour chaque élément d'une liste. Cette liste peut être écrite à la main, produite par un motif de fichiers (/etc/*.conf) ou une plage {1..5}. C'est la forme à privilégier quand vous connaissez d'avance l'ensemble des éléments à traiter.

Fenêtre de terminal
for service in nginx ssh cron; do
echo "Service : $service"
done
Sortie
Service : nginx
Service : ssh
Service : cron

Sur les fichiers d'un dossier :

Fenêtre de terminal
for fichier in /etc/*.conf; do
echo "Config : $fichier"
done

Sur une plage numérique :

Fenêtre de terminal
for i in {1..5}; do
echo "Itération $i"
done

Là où for parcourt un ensemble connu, while répète tant qu'une condition reste vraie, sans savoir d'avance combien de tours auront lieu. C'est la boucle à choisir pour attendre qu'un service démarre, ou pour consommer une entrée jusqu'à épuisement. Veillez à ce que la condition finisse par devenir fausse, sinon la boucle tourne indéfiniment.

Fenêtre de terminal
compteur=1
while [[ $compteur -le 3 ]]; do
compteur=$((compteur + 1))
echo "Tour $compteur"
done
Sortie
Tour 2
Tour 3
Tour 4

Lire un fichier ligne à ligne est un besoin quotidien : parcourir une liste de serveurs, traiter un journal, appliquer une action par entrée. La combinaison while IFS= read -r ligne est le patron correct pour le faire sans surprise. Sans le IFS=, Bash supprime les espaces de début et de fin ; sans le -r, il interprète les antislashes. Les deux ensemble garantissent que chaque ligne arrive intacte.

Fenêtre de terminal
while IFS= read -r ligne; do
echo "$ligne"
done < /etc/hostname
  • IFS= : désactive le découpage par espaces/tabulations
  • -r : préserve les backslashes

Une fonction regroupe des instructions réutilisables sous un nom, pour éviter de répéter le même bloc à plusieurs endroits. En Bash, une contrainte vaut d'être connue tout de suite : une fonction doit être déclarée avant la ligne qui l'appelle, car le script est lu de haut en bas. Placez donc vos fonctions en tête de fichier, juste après le shebang.

#!/bin/bash
afficher_entete() {
local titre="$1"
echo ""
echo "=== $titre ==="
}
afficher_entete "Rapport système"
afficher_entete "Fin"
Sortie
=== Rapport système ===
=== Fin ===
  • local : la variable reste locale à la fonction, indispensable pour éviter les effets de bord
  • $1, $2 : arguments passés à la fonction (indépendants des arguments du script)

Une fonction retourne un code de sortie (0–255), pas une chaîne. Pour retourner du texte, utilisez echo et récupérez avec $() :

Fenêtre de terminal
nom_complet() {
local prenom="$1"
local nom="$2"
echo "${prenom} ${nom}"
}
resultat=$(nom_complet "alice" "martin")
echo "Bonjour, $resultat !"
Sortie
Bonjour, alice martin !

Quand vous passez des arguments à votre script, ils sont accessibles via des variables spéciales :

VariableContenu
$0Nom du script
$1, $2Premier, deuxième argument
$#Nombre total d'arguments
$@Tous les arguments (liste)
#!/bin/bash
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <source> <destination>" >&2
exit 1
fi
echo "Script : $0"
echo "Source : $1"
echo "Dest : $2"
echo "Nb args : $#"
Fenêtre de terminal
./copier.sh /etc/hosts /tmp/hosts.bak
Sortie
Script : ./copier.sh
Source : /etc/hosts
Dest : /tmp/hosts.bak
Nb args : 2

Chaque commande se termine avec un code de sortie : 0 = succès, valeur non nulle = erreur. $? contient le code de la dernière commande exécutée.

Fenêtre de terminal
ls /etc/passwd
echo "Code : $?" # 0 : succès
ls /fichier-inexistant 2>/dev/null
echo "Code : $?" # 2 : erreur

Dans un script, exit définit le code retourné à l'appelant :

Fenêtre de terminal
if [[ ! -d "$1" ]]; then
echo "Erreur : '$1' n'est pas un répertoire." >&2
exit 1
fi
# … traitement …
exit 0

Voici un script réaliste qui rassemble tout ce qui précède : une fonction d'affichage, la validation du nombre d'arguments, un test de répertoire, une boucle for de comptage et des codes de sortie explicites. Lisez-le comme un modèle à réutiliser : la ligne set -euo pipefail en tête et la validation d'argument au début sont exactement les réflexes attendus sur un script d'administration sérieux.

#!/bin/bash
# generer-rapport.sh : rapport d'un répertoire
# Usage: ./generer-rapport.sh <repertoire>
set -euo pipefail
afficher_entete() {
local titre="$1"
echo ""
echo "=== $titre ==="
}
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <repertoire>" >&2
exit 1
fi
repertoire="$1"
if [[ ! -d "$repertoire" ]]; then
echo "Erreur : '$repertoire' n'est pas un répertoire." >&2
exit 1
fi
afficher_entete "Rapport : $repertoire"
echo "Date : $(date '+%Y-%m-%d %H:%M')"
echo "Utilisateur: $USER"
echo ""
nb_fichiers=0
for entree in "$repertoire"/*; do
[[ -f "$entree" ]] && nb_fichiers=$((nb_fichiers + 1))
done
echo "Fichiers : $nb_fichiers"
exit 0
Fenêtre de terminal
./generer-rapport.sh /etc
Sortie
=== Rapport : /etc ===
Date : 2026-04-10 07:44
Utilisateur: bob
Fichiers : 158

Fenêtre de terminal
bash -x ./generer-rapport.sh /etc

Chaque ligne exécutée est préfixée de + avec les variables substituées. C'est le moyen le plus rapide de suivre l'exécution pas à pas.

Lancer tout le script avec bash -x devient vite illisible sur un script long. Les directives set -x et set +x encadrent la seule portion qui vous intéresse : le traçage démarre à la première, s'arrête à la seconde. C'est la méthode à privilégier pour isoler une commande suspecte sans noyer la sortie.

Fenêtre de terminal
set -x # début du debug
mv "$src" "$dst"
set +x # fin du debug

shellcheck est un analyseur statique : il lit le script sans l'exécuter et signale erreurs de syntaxe et pièges classiques, à commencer par les variables non quotées. C'est le premier réflexe à adopter avant de lancer un script, et l'outil que les correcteurs d'examen comme les pipelines CI utilisent pour juger la qualité d'un script. Chaque avertissement porte un code (SC2086, etc.) qui renvoie à une explication détaillée en ligne.

Fenêtre de terminal
# Debian/Ubuntu
sudo apt install shellcheck
# RHEL/Fedora
sudo dnf install shellcheck
Fenêtre de terminal
shellcheck mon-script.sh
Exemple de sortie
mon-script.sh:12:8: warning: Double quote to prevent globbing and word splitting. [SC2086]

Les erreurs de scripting débutant se répètent d'un script à l'autre, et la plupart se lisent dès le message d'erreur. Le tableau ci-dessous associe chaque message exact à sa cause la plus fréquente. Deux réflexes couvrent presque tous les cas : vérifier le droit d'exécution et le shebang pour les problèmes de lancement, lancer shellcheck pour les erreurs de syntaxe.

SymptômeCause probableCorrection
Permission denied à l'exécutionDroit +x absentchmod +x mon-script.sh
bad interpreter: No such file or directoryShebang incorrect ou encodage Windows (CRLF)Vérifier #!/bin/bash ; convertir avec dos2unix mon-script.sh
unbound variableVariable non définie utilisée avec set -uInitialiser la variable ou utiliser ${VAR:-}
Rien ne se passe après lancementShebang manquant ou script lancé avec shAjouter #!/bin/bash et lancer avec ./script.sh
syntax error near unexpected tokenfi/done/guillemet manquantLancer shellcheck script.sh pour localiser

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • Un script commence toujours par #!/bin/bash et se rend exécutable avec chmod +x.
  • Les variables se déclarent sans espace autour du = et s'utilisent entre guillemets : "$var".
  • Utilisez [[ ]] pour les tests, plus sûr que [ ].
  • Les fonctions déclarent leurs variables internes avec local.
  • $1, $2, $#, $@ donnent accès aux arguments passés au script.
  • exit 0 = succès, exit 1 = erreur, toujours explicite.
  • bash -x pour déboguer, shellcheck pour détecter les erreurs à l'avance.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn