Tutoriels gratuits et ouverts à toutes et à tous · conçus au Lycée Chevrollier — AngersÀ propos

Série « Parcours PowerShell » · partie 4 / 8

PowerShell niveau 3 — Conditions et boucles

Faire réagir et répéter : if, switch, foreach et traitement en masse : niveau 3 du parcours PowerShell, avec exercices corrigés et QCM.

Systèmes✎ B. Jousseaume📅 17 octobre 2026⏱ 5 h📶 Intermédiaire

1. Les conditions

$espaceLibreGo = [math]::Round((Get-PSDrive C).Free / 1GB, 1)

if ($espaceLibreGo -lt 10) {
    Write-Warning "Espace critique : $espaceLibreGo Go"
}
elseif ($espaceLibreGo -lt 30) {
    Write-Host "Espace à surveiller : $espaceLibreGo Go" -ForegroundColor Yellow
}
else {
    Write-Host "Espace suffisant : $espaceLibreGo Go" -ForegroundColor Green
}
  • La condition est entre parenthèses, le bloc entre accolades.
  • L’accolade ouvrante reste sur la ligne du if : PowerShell est sensible à cette mise en forme.
  • Les opérateurs sont ceux du niveau 1 : -eq, -lt, -like, -match, combinés par -and, -or, -not.

Ce qui est considéré comme vrai

ValeurÉvaluée comme
$true, un nombre différent de 0, une chaîne non videVrai
$false, 0, "", $null, un tableau videFaux

On peut donc écrire if ($services) { ... } pour dire « si la recherche a donné au moins un résultat ».

Le switch

Pour comparer une même valeur à plusieurs cas, switch est plus lisible qu’une cascade de elseif :

$etat = (Get-Service -Name Spooler).Status

switch ($etat) {
    "Running" { Write-Host "Le spouleur fonctionne" }
    "Stopped" { Write-Host "Le spouleur est arrêté" }
    default   { Write-Host "État inattendu : $etat" }
}

# switch accepte aussi des motifs et des conditions
switch -Wildcard ($nomFichier) {
    "*.log"  { "Journal" }
    "*.csv"  { "Tableur" }
    default  { "Autre" }
}

2. Répéter sur une collection

Deux écritures coexistent, et la différence compte en administration.

# 1. foreach : on charge tout en mémoire, puis on parcourt
$services = Get-Service
foreach ($service in $services) {
    Write-Host "$($service.Name) : $($service.Status)"
}

# 2. ForEach-Object : on traite chaque objet au fil du pipeline
Get-Service | ForEach-Object {
    Write-Host "$($_.Name) : $($_.Status)"
}
foreachForEach-Object
PositionDans un scriptDans un pipeline
VariableCelle que l’on nomme$_ (ou $PSItem)
MémoireTout est chargé d’abordTraitement au fil de l’eau
Gros volumesRisque de saturationAdapté (des milliers d’objets)
Vitesse sur petit volumePlus rapideUn peu plus lent

Règle simple : foreach pour quelques centaines d’éléments dans un script, ForEach-Object quand les données arrivent par le pipeline, par exemple 50 000 lignes de journal.

while, do…while, for

# Attendre qu'un service démarre, avec une limite
$essais = 0
while ((Get-Service Spooler).Status -ne "Running" -and $essais -lt 10) {
    Start-Sleep -Seconds 2
    $essais++
}

# do : le bloc s'exécute au moins une fois
do {
    $reponse = Read-Host "Continuer ? (o/n)"
} while ($reponse -notin @("o", "n"))

# for : compteur classique
for ($i = 1; $i -le 5; $i++) {
    New-Item -Path "C:\Temp\test$i.txt" -ItemType File -Force | Out-Null
}

3. Interrompre : break et continue

foreach ($ligne in Get-Content "C:\Temp\comptes.txt") {
    if ($ligne -eq "") { continue }        # ligne vide : on passe à la suivante
    if ($ligne -eq "#FIN") { break }       # on sort de la boucle
    Write-Host "Traitement de $ligne"
}

Dans un ForEach-Object, break et continue ne se comportent pas comme dans foreach : préfère y filtrer en amont avec Where-Object.

4. Traiter un fichier CSV

C’est le scénario d’administration le plus courant : une liste fournie par le secrétariat ou un export, que l’on traite ligne par ligne.

# eleves.csv :  Nom;Prenom;Classe
Import-Csv "C:\Temp\eleves.csv" -Delimiter ";" | ForEach-Object {
    $identifiant = ($_.Prenom.Substring(0, 1) + $_.Nom).ToLower()
    [PSCustomObject]@{
        Identifiant = $identifiant
        Affichage   = "$($_.Prenom) $($_.Nom)"
        Classe      = $_.Classe
    }
} | Export-Csv "C:\Temp\comptes.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation

5. Mini-projet : le rangement du dossier Téléchargements

6. Bilan

Exercices corrigés

Les boucles d’administration se testent toujours en deux temps : d’abord une version qui affiche ce qu’elle ferait, ensuite une version qui agit. Les corrigés respectent systématiquement cet ordre.

Exercice 3.1 — Décider

Voir le corrigé
# 1.
if ((Get-Service Spooler).Status -eq "Running") { "Démarré" } else { "Arrêté" }

# 2.
$chemin = Read-Host "Chemin du fichier"
if (Test-Path $chemin) { "Le fichier existe" } else { "Introuvable" }

# 3.
$tailleMo = 42
switch ($tailleMo) {
    { $_ -lt 1 }    { "Petit fichier"; break }
    { $_ -le 100 }  { "Fichier moyen"; break }
    default         { "Gros fichier" }
}

# 4.
$estAdmin = (New-Object Security.Principal.WindowsPrincipal(
    [Security.Principal.WindowsIdentity]::GetCurrent())
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if ($estAdmin) { "Console administrateur" } else { "Console utilisateur" }

Explication

Dans un switch, { $_ -lt 1 } est une condition et non une valeur : cela permet de classer des intervalles. Le break empêche l’évaluation des cas suivants.

Exercice 3.2 — Répéter

Voir le corrigé
# 1.
foreach ($s in Get-Service | Where-Object Status -eq "Stopped") {
    "$($s.Name) - démarrage : $($s.StartType)"
}

# 2.
New-Item C:\Temp\Essai -ItemType Directory -Force | Out-Null
for ($i = 1; $i -le 5; $i++) {
    New-Item "C:\Temp\Essai\fichier$i.txt" -ItemType File -Force | Out-Null
}

# 3.
foreach ($f in Get-ChildItem C:\Temp\Essai -Filter *.txt) {
    Get-Date | Out-File $f.FullName -Encoding utf8
    "--- $($f.Name) ---"
    Get-Content $f.FullName
}

# 4.
$essais = 0
while (-not (Test-Path "C:\Temp\declencheur.txt") -and $essais -lt 15) {
    Start-Sleep -Seconds 1
    $essais++
}
if (Test-Path "C:\Temp\declencheur.txt") { "Fichier détecté après $essais seconde(s)" }
else { "Délai dépassé" }

Explication

Out-Null sert à ne pas polluer la sortie avec les objets renvoyés par New-Item. La boucle d’attente combine deux conditions : l’état recherché et le nombre d’essais.

Exercice 3.3 — Générer des identifiants

Voir le corrigé
function Remove-Accent {
    param([string]$Texte)
    ($Texte.Normalize([Text.NormalizationForm]::FormD) -replace "\p{Mn}", "")
}

$existants = @{}
Import-Csv "C:\Temp\eleves.csv" -Delimiter ";" | ForEach-Object {
    $base = (Remove-Accent ($_.Prenom.Substring(0, 1) + $_.Nom)).ToLower() -replace "[^a-z0-9]", ""
    $identifiant = $base
    $n = 1
    while ($existants.ContainsKey($identifiant)) {
        $n++
        $identifiant = "$base$n"
    }
    $existants[$identifiant] = $true

    [PSCustomObject]@{
        Identifiant = $identifiant
        Affichage   = "$($_.Prenom) $($_.Nom)"
        Classe      = $_.Classe
    }
} | Format-Table -AutoSize

Explication

La normalisation retire les accents en décomposant les caractères (é devient e + un signe diacritique), puis en supprimant les signes. Le dictionnaire $existants mémorise les identifiants déjà attribués pour gérer les doublons.

Pour aller plus loin

Pense aussi à la longueur maximale : SamAccountName est limité à 20 caractères dans Active Directory. Un Substring de sécurité évitera une erreur au niveau 6.

Mini-projet

Voir le corrigé
$source = "$env:USERPROFILE\Downloads"
$simulation = $true          # passer à $false pour déplacer réellement

$categories = "Images", "Documents", "Archives", "Installeurs", "Divers"
foreach ($c in $categories) {
    New-Item -Path (Join-Path $source $c) -ItemType Directory -Force | Out-Null
}

$compteur = @{}
Get-ChildItem $source -File | ForEach-Object {
    $categorie = switch ($_.Extension.ToLower()) {
        { $_ -in ".jpg", ".jpeg", ".png", ".gif", ".webp" } { "Images"; break }
        { $_ -in ".pdf", ".docx", ".xlsx", ".pptx", ".txt", ".csv" } { "Documents"; break }
        { $_ -in ".zip", ".7z", ".rar", ".tar", ".gz" } { "Archives"; break }
        { $_ -in ".exe", ".msi" } { "Installeurs"; break }
        default { "Divers" }
    }

    "$($_.Name) -> $categorie"
    if (-not $simulation) {
        Move-Item $_.FullName (Join-Path $source $categorie) -Force
    }
    $compteur[$categorie] = 1 + $compteur[$categorie]
}

""
"--- Récapitulatif ---"
$compteur.GetEnumerator() | Sort-Object Name | Format-Table Name, Value -AutoSize

Explication

La variable $simulation est le cœur du corrigé : le même code sert à vérifier puis à agir, sans le réécrire. Le compteur en table de hachage s’incrémente même pour une catégorie jamais vue, car $null + 1 vaut 1 en PowerShell.

Pour aller plus loin

Une amélioration utile : en cas de conflit de nom, ajouter un horodatage au fichier déplacé plutôt que de l’écraser.

🧠 Testez vos connaissances

5 questions. Répondez puis cliquez sur « Corriger ». Rien n’est enregistré : c’est de l’auto-évaluation.

1. Quelle boucle convient pour traiter 50 000 lignes venant d’un pipeline ?

💬 ForEach-Object traite au fil de l’eau, tandis que foreach charge tout en mémoire d’abord.

2. Que représente $_ dans Where-Object { $_.Status -eq "Running" } ?

💬 On l’écrit aussi $PSItem. Il n’a de sens que dans un bloc { } du pipeline.

3. Quel risque présente une boucle while qui attend un état ?

💬 Toute boucle d’attente doit avoir un compteur d’essais ou un délai maximal.

4. Dans un switch PowerShell, que se passe-t-il sans break ?

💬 C’est une différence avec beaucoup d’autres langages : break arrête l’évaluation des cas suivants.

5. Quelle est la bonne méthode avant un traitement en masse ?

💬 Une erreur dans une boucle ne fait pas une victime mais des centaines. La sauvegarde est utile, mais elle ne remplace pas la vérification.

© 2026 Bruno Jousseaume. Ce cours est publié sous licence CC BY-NC-SA 4.0 : vous pouvez le partager et l’adapter en citant l’auteur et un lien vers cette page, sans usage commercial, et en partageant vos adaptations sous la même licence.

Pour aller plus loin

Bastion Guacamole : dépanner les connexions RDP

Une démarche méthodique pour diagnostiquer une connexion RDP qui échoue à travers le bastion : journal de guacd, tableau de diagnostic, NLA et NTLM, réseau, fiche de suivi d’incident.

Avancé⏱ 1 h 30 · 8 octobre 2026