Chapitre 719 minDébutant
Gestion des erreurs : try/catch et set -euo pipefail
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- distinguer erreurs bloquantes et non bloquantes en PowerShell ;
- attraper une erreur avec
try,catchetfinally; - protéger un script Bash avec
set -euo pipefailen connaissant ses pièges ; - journaliser et renvoyer un code de sortie exploitable.
4 exercices corrigés1 schéma
Gérer les erreurs, c'est décider ce que fait un script quand une commande échoue : s'arrêter, réessayer, passer à la suite en le notant, mais jamais continuer à l'aveugle. En PowerShell, cela passe par try, catch et finally, avec -ErrorAction Stop ; en Bash, par le code de retour des commandes, le mode set -euo pipefail et trap. Ce chapitre explique les deux mécanismes et leurs pièges, puis donne un modèle de script qui journalise et renvoie un code de sortie exploitable.
Pourquoi c'est vital
Un script de nettoyage fait Set-Location D:\Temp puis Remove-Item * -Recurse. Le jour où le disque D: n'est pas monté, Set-Location échoue, le script continue dans le dossier où il se trouvait… et supprime son contenu. En Bash, c'est le célèbre cd "$dossier" && rm -rf * écrit sans le &&. Une erreur non gérée ne fait pas que « planter » : elle fait continuer le script dans un état que personne n'a prévu.
Les deux sortes d'erreurs en PowerShell
PowerShell distingue deux sortes d'erreurs, et la différence est la clé de tout le reste.
- Une erreur non bloquante (non-terminating) : la cmdlet signale le problème en rouge et continue.
Get-ChildItemsur une liste de dossiers dont l'un n'existe pas affiche une erreur pour celui-là et liste les autres. C'est le comportement de la plupart des cmdlets. - Une erreur bloquante (terminating) : l'exécution s'arrête et l'erreur remonte jusqu'à un
catch, ou arrête le script s'il n'y en a pas.throw, une erreur de syntaxe, une exception .NET (division par zéro, fichier verrouillé lu par une méthode .NET) en produisent.
-ErrorAction et $ErrorActionPreference
| Valeur | Effet sur une erreur non bloquante |
|---|---|
Continue (défaut) | Affiche l'erreur, l'ajoute à $Error, continue |
SilentlyContinue | N'affiche rien, l'ajoute à $Error, continue |
Ignore | N'affiche rien, ne l'enregistre pas, continue |
Stop | La transforme en erreur bloquante |
Inquire | Demande quoi faire (inutilisable dans un script non surveillé) |
-ErrorAction agit sur une seule commande ; $ErrorActionPreference fixe la valeur par défaut pour toutes les commandes du script. La bonne pratique pour un script d'administration est de commencer par $ErrorActionPreference = 'Stop' : toute erreur arrête le script, sauf là où tu as prévu de la traiter.
try, catch, finally
$source = '\\srv-fichiers\Exports\stock.csv'
try {
$lignes = Import-Csv -Path $source -Delimiter ';' -ErrorAction Stop
Write-Output "$($lignes.Count) lignes importées"
}
catch [System.IO.FileNotFoundException], [System.Management.Automation.ItemNotFoundException] {
Write-Warning "Fichier introuvable : $source"
}
catch {
Write-Warning "Erreur inattendue : $($_.Exception.Message)"
}
finally {
Write-Output "Fin du traitement de l'import"
}
- Dans un
catch,$_est l'erreur :$_.Exception.Messagedonne son message,$_.Exception.GetType().FullNameson type exact (utile pour écrire uncatchciblé),$_.InvocationInfo.ScriptLineNumberla ligne qui a échoué. - Les
catchtypés se placent avant lecatchgénéral : PowerShell prend le premier qui correspond. Un mêmecatchpeut lister plusieurs types, séparés par des virgules. Pour connaître le type d'une erreur, provoque-la puis tape$Error[0].Exception.GetType().FullName. finallys'exécute toujours, erreur ou pas : c'est là qu'on ferme une connexion, qu'on supprime un fichier temporaire, qu'on écrit la fin du journal.
Pour l'annuaire, les erreurs ont leurs propres types. Tester l'existence d'un compte se fait ainsi :
try {
$compte = Get-ADUser -Identity 'jlefevre' -ErrorAction Stop
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
$compte = $null # le compte n'existe pas : ce n'est pas une panne, c'est une réponse
}
Lever ses propres erreurs
throw produit une erreur bloquante avec ton message. Utilise-le quand une condition rend la suite impossible ; Write-Error produit une erreur non bloquante, pour signaler un problème sur un élément sans arrêter le traitement des autres.
if (-not (Test-Path 'D:\')) {
throw 'Le disque D: est absent : nettoyage annulé'
}
Les programmes externes et $LASTEXITCODE
Un programme qui n'est pas une cmdlet (robocopy.exe, ping.exe, git, ssh) ne produit pas d'erreur PowerShell quand il échoue : il renvoie un code de sortie, que PowerShell range dans $LASTEXITCODE. C'est au script de le lire.
robocopy D:\Partages \\srv-journaux\Copies\Partages /MIR /R:2 /W:5 /LOG:C:\Scripts\Logs\robocopy.log
if ($LASTEXITCODE -ge 8) {
throw "robocopy a échoué (code $LASTEXITCODE)"
}
robocopy est un piège célèbre : ses codes de 0 à 7 signifient un succès (1 : des fichiers ont été copiés, 2 : des fichiers en trop ont été détectés…), et seuls les codes 8 et plus signalent un échec. Lis toujours la documentation du programme avant d'interpréter son code. Dans PowerShell 7.4 et suivants, la variable $PSNativeCommandUseErrorActionPreference = $true transforme tout code différent de 0 en erreur PowerShell, ce qui convient à la plupart des programmes, mais pas à robocopy.
Journaliser et renvoyer un code de sortie
Un script qui tourne la nuit doit laisser une trace et dire, par son code de sortie, s'il a réussi. Voici un modèle de script PowerShell d'administration que l'on peut réutiliser :
#Requires -Version 5.1
[CmdletBinding()]
param(
[string]$Journal = "C:\Scripts\Logs\$(Get-Date -Format 'yyyy-MM-dd')-export.log"
)
$ErrorActionPreference = 'Stop'
function Write-Journal {
param([string]$Message, [ValidateSet('INFO', 'ATTENTION', 'ERREUR')][string]$Niveau = 'INFO')
$ligne = '{0} [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Niveau, $Message
Add-Content -Path $Journal -Value $ligne -Encoding UTF8
Write-Verbose $ligne
}
try {
Write-Journal 'Début'
# … le travail du script, chaque étape journalisée
Write-Journal 'Fin sans erreur'
exit 0
}
catch {
Write-Journal "$($_.Exception.Message) (ligne $($_.InvocationInfo.ScriptLineNumber))" -Niveau ERREUR
exit 1
}
Pour un dépannage ponctuel, Start-Transcript -Path C:\Scripts\Logs\session.txt enregistre tout ce qui s'affiche dans la session jusqu'à Stop-Transcript.
Les erreurs en Bash : le code de retour
Bash n'a pas d'exceptions. Une commande qui échoue renvoie un code différent de 0, affiche souvent un message sur la sortie d'erreur… et le script continue à la ligne suivante. Sans précaution :
cd /srv/sauvegardes/anciennes
rm -rf ./* # si le cd a échoué, on supprime le contenu du dossier courant
La correction minimale relie les deux commandes : cd /srv/sauvegardes/anciennes && rm -rf ./*, ou mieux cd /srv/sauvegardes/anciennes || exit 1. Mais vérifier chaque commande à la main est fastidieux : c'est le rôle de set -e.
set -euo pipefail, le mode strict
Écrite juste après le shebang, la ligne set -euo pipefail change trois comportements de Bash :
| Option | Sans elle | Avec elle |
|---|---|---|
-e (errexit) | Une commande échoue, le script continue | Le script s'arrête dès qu'une commande échoue |
-u (nounset) | Une variable non définie vaut une chaîne vide | Utiliser une variable non définie arrête le script |
-o pipefail | Le code d'un pipe est celui de la dernière commande | Le code d'un pipe est celui de la dernière commande qui a échoué |
Les trois protègent de vraies catastrophes. Avec -u, une faute de frappe ne devient pas un chemin dangereux :
set -u
rm -rf "/srv/sauvegardes/$DOSIER" # faute : la variable s'appelle DOSSIER
./purge.sh : ligne 3 : DOSIER : variable sans liaison
Sans -u, la commande serait devenue rm -rf "/srv/sauvegardes/". Avec pipefail, un grep motif fichier-absent | sort n'est plus considéré comme réussi parce que sort a réussi.
Les pièges de set -e
set -e n'est pas magique. Il connaît des exceptions qu'il faut connaître pour ne pas croire un script protégé alors qu'il ne l'est pas, ou pour comprendre pourquoi il s'arrête sans raison apparente.
- Une commande testée ne déclenche pas l'arrêt : dans
if commande,while commande,commande && …oucommande || …, l'échec est attendu, Bash continue. C'est voulu :if cp "$f" /srv/ 2>/dev/null; then … else echo "copie impossible"; fifonctionne. grepqui ne trouve rien échoue (code 1).nb=$(grep -c "Failed password" fichier)arrête le script s'il n'y a aucune ligne, alors que zéro est une réponse normale. Écrisnb=$(grep -c "Failed password" fichier || true).((n++))arrête le script quandnvaut 0 sous Bash 4.1 et suivants : l'expression vaut l'ancienne valeur, 0, que Bash considère comme un échec. Écrisn=$((n + 1)), une affectation qui réussit toujours.local v=$(commande)masque l'échec de la commande (chapitre 6) : déclare et affecte sur deux lignes.
trap : nettoyer et signaler
trap exécute une commande quand le script reçoit un signal ou se termine. Deux usages reviennent dans tous les scripts sérieux : nettoyer à la sortie, quelle qu'elle soit, et signaler la ligne qui a échoué.
#!/usr/bin/env bash
set -Eeuo pipefail
tmp=$(mktemp -d) # un dossier temporaire unique
nettoyer() { rm -rf "$tmp"; }
trap nettoyer EXIT # à la sortie, normale ou sur erreur
trap 'echo "ERREUR ligne $LINENO" >&2' ERR # à chaque commande qui échoue
tar -czf "$tmp/config.tar.gz" /etc/nginx
cp "$tmp/config.tar.gz" /srv/sauvegardes/
trap … EXIT est l'équivalent du finally de PowerShell : le dossier temporaire disparaît même si le script s'arrête au milieu. L'option -E (dans set -Eeuo pipefail) fait hériter le piège ERR aux fonctions.
Codes de sortie et journal système
Un script Bash se termine avec le code de sa dernière commande, ou avec celui donné par exit. Les conventions :
| Code | Sens habituel |
|---|---|
0 | Succès |
1 | Erreur générale |
2 | Mauvaise utilisation (arguments manquants ou invalides) |
126 | Commande trouvée mais non exécutable |
127 | Commande introuvable |
128 + n | Arrêt par le signal n (130 pour Ctrl+C, signal 2) |
Pour journaliser, la commande logger écrit dans le journal du système, que journalctl lit ensuite, avec une étiquette qui permet de retrouver les messages du script :
logger -t sauvegarde-config "Archive créée : $archive"
logger -t sauvegarde-config -p user.err "Échec de la copie vers srv-journaux"
journalctl -t sauvegarde-config --since today
C'est la meilleure destination sur un serveur Debian : les messages sont horodatés, centralisables vers srv-journaux avec le reste des journaux, et la supervision peut alerter sur le niveau err.
Exercices
Exercice 1 sur 4 Le catch qui ne se déclenche pas Réussi À revoir
Ce script doit afficher « Dossier introuvable » si D:\Exports n'existe pas. Sur un serveur sans disque D:, il affiche une erreur rouge, puis « 0 fichier(s) », et jamais le message prévu. Explique et corrige.
try {
$fichiers = Get-ChildItem -Path 'D:\Exports' -Filter *.csv
Write-Output "$($fichiers.Count) fichier(s)"
}
catch {
Write-Warning 'Dossier introuvable'
}
Voir la réponseMasquer la réponse de l'exercice 1
Un chemin introuvable est une erreur non bloquante pour Get-ChildItem : la cmdlet affiche l'erreur et continue, $fichiers reste vide, et le catch n'est jamais déclenché puisque rien n'a interrompu le bloc try. $null.Count vaut 0. Il faut rendre l'erreur bloquante :
try {
$fichiers = Get-ChildItem -Path 'D:\Exports' -Filter *.csv -ErrorAction Stop
Write-Output "$($fichiers.Count) fichier(s)"
}
catch {
Write-Warning "Dossier introuvable : $($_.Exception.Message)"
}
Une alternative est de tester avant d'agir : if (-not (Test-Path 'D:\Exports')) { … }. Les deux se complètent : le test traite le cas prévu, le try les cas imprévus (droits insuffisants, partage déconnecté).
Tu avais trouvé ?
Exercice 2 sur 4 Prédire l'arrêt Réussi À revoir
Pour chaque script, dis ce qui s'affiche et quel est le code de sortie. /inexistant n'existe pas.
# Script A
cp /inexistant /tmp/ 2>/dev/null
echo "suite A"
# Script B
set -e
cp /inexistant /tmp/ 2>/dev/null
echo "suite B"
# Script C
set -e
grep motif /inexistant 2>/dev/null | sort
echo "suite C"
# Script D
set -eo pipefail
grep motif /inexistant 2>/dev/null | sort
echo "suite D"
# Script E
set -e
if cp /inexistant /tmp/ 2>/dev/null; then echo "copié"; else echo "copie impossible"; fi
echo "suite E"
Voir la réponseMasquer la réponse de l'exercice 2
| Script | Affichage | Code de sortie |
|---|---|---|
| A | suite A | 0 : le code de echo, la dernière commande |
| B | rien | 1 : cp échoue, set -e arrête le script |
| C | suite C | 0 : sans pipefail, le pipe prend le code de sort, qui réussit |
| D | rien | 2 : avec pipefail, le pipe prend le code de grep (2 : fichier illisible) |
| E | copie impossible puis suite E | 0 : une commande testée par if ne déclenche pas set -e |
Le script C montre pourquoi set -e seul ne suffit pas : l'erreur de grep est perdue dans le pipe.
Tu avais trouvé ?
Exercice 3 sur 4 Compter sans s'arrêter Réussi À revoir
Ce script, lancé chaque matin sur srv-web, doit afficher le nombre de tentatives SSH échouées depuis la veille. Les jours où il n'y en a aucune, il ne s'affiche rien et le script se termine avec le code 1. Pourquoi ? Corrige-le.
#!/usr/bin/env bash
set -euo pipefail
nb=$(journalctl -u ssh --since yesterday --no-pager | grep -c "Failed password")
echo "Tentatives échouées : $nb"
Voir la réponseMasquer la réponse de l'exercice 3
Quand aucune ligne ne correspond, grep -c affiche bien 0, mais renvoie le code 1. Avec pipefail, le pipe renvoie 1, l'affectation échoue et set -e arrête le script avant le echo. Zéro tentative est pourtant une bonne nouvelle, pas une erreur. Correction :
nb=$(journalctl -u ssh --since yesterday --no-pager | grep -c "Failed password" || true)
|| true rend l'ensemble réussi ; nb contient toujours la sortie de grep -c, donc 0. Sur Debian 13, le service SSH s'appelle ssh : journalctl -u ssh lit ses messages dans le journal du système.
Tu avais trouvé ?
Exercice 4 sur 4 Un script PowerShell qui rend compte Réussi À revoir
Complète ce script de copie pour que : toute erreur l'arrête ; un dossier source absent produise le message « Source absente : D:\Exports » dans le journal ; le code de sortie soit 0 en cas de succès et 1 en cas d'échec ; la ligne « Fin » soit écrite dans le journal dans tous les cas.
$source = 'D:\Exports'
$journal = 'C:\Scripts\Logs\copie.log'
Copy-Item -Path "$source\*.csv" -Destination '\\srv-fichiers\Compta\Imports'
Voir la réponseMasquer la réponse de l'exercice 4
$ErrorActionPreference = 'Stop'
$source = 'D:\Exports'
$journal = 'C:\Scripts\Logs\copie.log'
$code = 0
try {
if (-not (Test-Path $source)) { throw "Source absente : $source" }
Copy-Item -Path "$source\*.csv" -Destination '\\srv-fichiers\Compta\Imports'
Add-Content -Path $journal -Value "$(Get-Date -Format s) Copie terminée"
}
catch {
Add-Content -Path $journal -Value "$(Get-Date -Format s) ERREUR $($_.Exception.Message)"
$code = 1
}
finally {
Add-Content -Path $journal -Value "$(Get-Date -Format s) Fin"
}
exit $code
exit est placé après le finally : un exit dans le try ou le catch déclencherait bien le finally, mais garder le code dans une variable rend l'ordre des opérations évident. Le format s de Get-Date donne une date triable, 2026-11-03T02:00:00.
Tu avais trouvé ?