Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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, catch et finally ;
  • protéger un script Bash avec set -euo pipefail en 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-ChildItem sur 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.
une cmdlet rencontre une erreurContinue(défaut)affichée en rouge,ajoutée à $ErrorSilentlyContinuerien n'est affiché,ajoutée à $ErrorStopl'erreur devientbloquantele script continue à la ligne suivanteun catch n'est jamais déclenché par ces erreursdans un bloc try ?ouinoncatch { $_ }puis finally, toujoursle scripts'arrête
Seule une erreur bloquante, par exemple avec -ErrorAction Stop, fait sauter l'exécution dans le catch

-ErrorAction et $ErrorActionPreference

ValeurEffet sur une erreur non bloquante
Continue (défaut)Affiche l'erreur, l'ajoute à $Error, continue
SilentlyContinueN'affiche rien, l'ajoute à $Error, continue
IgnoreN'affiche rien, ne l'enregistre pas, continue
StopLa transforme en erreur bloquante
InquireDemande 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

powershell
$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.Message donne son message, $_.Exception.GetType().FullName son type exact (utile pour écrire un catch ciblé), $_.InvocationInfo.ScriptLineNumber la ligne qui a échoué.
  • Les catch typés se placent avant le catch général : PowerShell prend le premier qui correspond. Un même catch peut 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.
  • finally s'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 :

powershell
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.

powershell
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.

powershell
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 :

powershell
#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 :

bash
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 :

OptionSans elleAvec elle
-e (errexit)Une commande échoue, le script continueLe script s'arrête dès qu'une commande échoue
-u (nounset)Une variable non définie vaut une chaîne videUtiliser une variable non définie arrête le script
-o pipefailLe code d'un pipe est celui de la dernière commandeLe 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 :

bash
set -u
rm -rf "/srv/sauvegardes/$DOSIER"    # faute : la variable s'appelle DOSSIER
text
./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 && … ou commande || …, l'échec est attendu, Bash continue. C'est voulu : if cp "$f" /srv/ 2>/dev/null; then … else echo "copie impossible"; fi fonctionne.
  • grep qui 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. Écris nb=$(grep -c "Failed password" fichier || true).
  • ((n++)) arrête le script quand n vaut 0 sous Bash 4.1 et suivants : l'expression vaut l'ancienne valeur, 0, que Bash considère comme un échec. Écris n=$((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é.

bash
#!/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 :

CodeSens habituel
0Succès
1Erreur générale
2Mauvaise utilisation (arguments manquants ou invalides)
126Commande trouvée mais non exécutable
127Commande introuvable
128 + nArrê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 :

bash
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.

powershell
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 :

powershell
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.

bash
# Script A
cp /inexistant /tmp/ 2>/dev/null
echo "suite A"
bash
# Script B
set -e
cp /inexistant /tmp/ 2>/dev/null
echo "suite B"
bash
# Script C
set -e
grep motif /inexistant 2>/dev/null | sort
echo "suite C"
bash
# Script D
set -eo pipefail
grep motif /inexistant 2>/dev/null | sort
echo "suite D"
bash
# 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
ScriptAffichageCode de sortie
Asuite A0 : le code de echo, la dernière commande
Brien1 : cp échoue, set -e arrête le script
Csuite C0 : sans pipefail, le pipe prend le code de sort, qui réussit
Drien2 : avec pipefail, le pipe prend le code de grep (2 : fichier illisible)
Ecopie impossible puis suite E0 : 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.

bash
#!/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 :

bash
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.

powershell
$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
powershell
$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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Fichiers, CSV et JSON : Import-Csv, ConvertFrom-Json et jqRevoir le sommaire du cours