1. Le premier script
Un script est un fichier texte d’extension .ps1. On l’écrit dans Visual Studio Code avec l’extension PowerShell (ISE reste disponible mais n’évolue plus).
# Fichier : Get-EspaceDisque.ps1
$disques = Get-PSDrive -PSProvider FileSystem
foreach ($d in $disques) {
[PSCustomObject]@{
Lecteur = $d.Name
LibreGo = [math]::Round($d.Free / 1GB, 1)
UtiliseGo = [math]::Round($d.Used / 1GB, 1)
}
}
PS> .\Get-EspaceDisque.ps1 # depuis le dossier du script
PS> & "C:\Scripts\Get-EspaceDisque.ps1" # avec un chemin complet
Le .\ est obligatoire : PowerShell n’exécute pas un script du dossier courant sans chemin explicite, afin d’éviter qu’un fichier malveillant déposé dans un dossier ne soit lancé à la place d’une commande attendue.
La stratégie d’exécution
PS> Get-ExecutionPolicy -List
PS> Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
| Valeur | Effet |
|---|---|
Restricted | Aucun script ne s’exécute (valeur par défaut sur les postes clients) |
RemoteSigned | Les scripts locaux s’exécutent ; ceux venant d’Internet doivent être signés |
AllSigned | Tous les scripts doivent être signés numériquement |
Bypass | Aucune restriction — à réserver à des cas précis et maîtrisés |
2. Des paramètres plutôt que des valeurs en dur
param() doit être la première instruction du script (commentaires exceptés).
param(
[Parameter(Mandatory)]
[string]$Dossier,
[ValidateRange(1, 365)]
[int]$JoursAnciennete = 30,
[ValidateSet("Log", "Tmp", "Tous")]
[string]$Type = "Tous",
[switch]$Supprimer
)
$limite = (Get-Date).AddDays(-$JoursAnciennete)
$fichiers = Get-ChildItem $Dossier -File -Recurse |
Where-Object LastWriteTime -lt $limite
if ($Supprimer) {
$fichiers | Remove-Item -WhatIf
} else {
$fichiers | Select-Object FullName, LastWriteTime
}
PS> .\Clean-Dossier.ps1 -Dossier C:\Temp -JoursAnciennete 90
PS> .\Clean-Dossier.ps1 -Dossier C:\Temp -Supprimer
| Attribut | Rôle |
|---|---|
[Parameter(Mandatory)] | PowerShell réclame la valeur si elle manque |
[ValidateSet(...)] | Limite aux valeurs listées, avec complétion par Tab |
[ValidateRange(a,b)] | Borne une valeur numérique |
[ValidateNotNullOrEmpty()] | Refuse une valeur vide |
[switch] | Option présente ou absente, sans valeur |
3. Les fonctions
function Get-EspaceDisque {
[CmdletBinding()]
param(
[string]$Lettre = "C"
)
Write-Verbose "Lecture du lecteur $Lettre"
$d = Get-PSDrive -Name $Lettre -ErrorAction Stop
[PSCustomObject]@{
Lecteur = $Lettre
LibreGo = [math]::Round($d.Free / 1GB, 1)
PourcentLibre = [math]::Round($d.Free / ($d.Free + $d.Used) * 100, 1)
}
}
Get-EspaceDisque -Lettre C -Verbose
[CmdletBinding()]transforme la fonction en fonction avancée : elle accepte alors-Verbose,-ErrorAction,-WhatIf… gratuitement.- Nomme tes fonctions Verbe-Nom avec un verbe approuvé (
Get-Verb), et un préfixe maison pour le nom :Get-LycEspaceDisque. - Une fonction renvoie des objets : tout ce qui n’est pas capturé sort de la fonction. Utilise
Write-Hostseulement pour un affichage destiné à l’humain.
Les canaux de sortie
| Commande | Usage | Visible par défaut |
|---|---|---|
Write-Output | Données exploitables (c’est la sortie du pipeline) | Oui |
Write-Host | Message décoratif pour l’utilisateur | Oui, mais non capturable |
Write-Verbose | Détail de fonctionnement | Avec -Verbose |
Write-Warning | Avertissement | Oui |
Write-Error | Erreur non bloquante | Oui |
throw | Erreur bloquante | Oui |
Rendre une fonction compatible -WhatIf
function Remove-FichierAncien {
[CmdletBinding(SupportsShouldProcess)]
param([string]$Chemin)
if ($PSCmdlet.ShouldProcess($Chemin, "Suppression")) {
Remove-Item $Chemin -Force
}
}
Remove-FichierAncien -Chemin C:\Temp\vieux.log -WhatIf
4. Gérer les erreurs
PowerShell distingue les erreurs non bloquantes (le script continue) des erreurs bloquantes (il s’arrête). -ErrorAction Stop transforme la première en seconde, ce qui permet de l’attraper.
try {
$service = Get-Service -Name "ServiceInexistant" -ErrorAction Stop
$service | Stop-Service -ErrorAction Stop
}
catch [Microsoft.PowerShell.Commands.ServiceCommandException] {
Write-Warning "Service introuvable : $($_.Exception.Message)"
}
catch {
Write-Error "Erreur inattendue : $($_.Exception.Message)"
}
finally {
Write-Verbose "Fin du traitement"
}
$_dans uncatchreprésente l’erreur ;$_.Exception.Messageen donne le texte.- Plusieurs
catchpeuvent se succéder, du plus précis au plus général. $ErrorActionPreference = "Stop"en tête de script rend toutes les erreurs bloquantes : utile pour un script d’administration qui ne doit pas continuer à l’aveugle.- Ne fais jamais
catch { }vide : une erreur avalée est une panne invisible.
5. Journaliser
Un script d’administration doit laisser une trace : ce qui a été fait, quand, par qui, avec quel résultat.
function Write-Journal {
param(
[Parameter(Mandatory)][string]$Message,
[ValidateSet("INFO", "ALERTE", "ERREUR")][string]$Niveau = "INFO",
[string]$Fichier = "$PSScriptRoot\journal.log"
)
$ligne = "{0} [{1}] {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Niveau, $env:USERNAME, $Message
Add-Content -Path $Fichier -Value $ligne -Encoding utf8
}
Write-Journal "Démarrage du nettoyage"
Write-Journal "Espace insuffisant sur D:" -Niveau ALERTE
L’alternative intégrée est Start-Transcript / Stop-Transcript, qui enregistre toute la session : pratique en dépannage, trop verbeux pour un script régulier.
6. Structurer un script d’administration
<#
.SYNOPSIS
Nettoie les fichiers temporaires d'un dossier.
.DESCRIPTION
Supprime les fichiers plus anciens qu'un nombre de jours donné, avec journalisation.
.PARAMETER Dossier
Dossier à nettoyer.
.EXAMPLE
.\Clean-Temp.ps1 -Dossier C:\Temp -JoursAnciennete 60 -WhatIf
.NOTES
Auteur : Bruno Jousseaume — Lycée Chevrollier
#>
[CmdletBinding(SupportsShouldProcess)]
param(...)
#region Fonctions
function Write-Journal { ... }
#endregion
#region Traitement
try { ... }
catch { ... }
#endregion
Ce bloc de commentaire n’est pas décoratif : Get-Help .\Clean-Temp.ps1 -Examples l’affiche comme pour une cmdlet native.
7. Mini-projet : le script de nettoyage
8. Bilan
Exercices corrigés
Un script bien écrit se reconnaît à trois signes : ses paramètres sont validés, ses erreurs sont gérées, et il laisse une trace. Compare ton travail sur ces trois points avant la syntaxe.
Exercice 4.1 — Un script paramétré
Voir le corrigé
<#
.SYNOPSIS
Produit la fiche d'identité de la machine locale.
.EXAMPLE
.\Get-FicheMachine.ps1 -CheminExport C:\Temp\fiche.json -Format JSON
#>
param(
[string]$CheminExport,
[ValidateSet("CSV", "JSON")][string]$Format = "CSV"
)
$os = Get-CimInstance Win32_OperatingSystem
$cs = Get-CimInstance Win32_ComputerSystem
$fiche = [PSCustomObject]@{
Machine = $env:COMPUTERNAME
Utilisateur = $env:USERNAME
Systeme = $os.Caption
PowerShell = $PSVersionTable.PSVersion.ToString()
MemoireGo = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
DisqueCLibreGo = [math]::Round((Get-PSDrive C).Free / 1GB, 1)
DateReleve = Get-Date
}
if ($CheminExport) {
if ($Format -eq "CSV") {
$fiche | Export-Csv $CheminExport -Delimiter ";" -Encoding UTF8 -NoTypeInformation
} else {
$fiche | ConvertTo-Json | Out-File $CheminExport -Encoding utf8
}
}
$fiche
Explication
Le script renvoie un objet, ce qui permet de l’enchaîner : .\Get-FicheMachine.ps1 | Export-Csv ... fonctionne même sans le paramètre d’export. C’est la bonne pratique : une fonction produit des données, l’appelant décide de leur destination.
Exercice 4.2 — Robustesse
Voir le corrigé
function Test-Service {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$Nom)
Write-Verbose "Recherche du service $Nom"
$service = Get-Service -Name $Nom -ErrorAction SilentlyContinue
[PSCustomObject]@{
Nom = $Nom
Existe = [bool]$service
Etat = if ($service) { $service.Status } else { "N/A" }
}
}
function Backup-Fichier {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][string]$Chemin,
[string]$Destination = "$env:USERPROFILE\Sauvegardes"
)
try {
$fichier = Get-Item $Chemin -ErrorAction Stop
if (-not (Test-Path $Destination)) {
New-Item $Destination -ItemType Directory -Force | Out-Null
}
$nouveau = "{0}_{1}{2}" -f $fichier.BaseName, (Get-Date -Format "yyyyMMdd-HHmm"), $fichier.Extension
$cible = Join-Path $Destination $nouveau
if ($PSCmdlet.ShouldProcess($cible, "Copie de sauvegarde")) {
Copy-Item $fichier.FullName $cible -ErrorAction Stop
Write-Verbose "Copié vers $cible"
}
}
catch {
Write-Error "Sauvegarde impossible : $($_.Exception.Message)"
}
}
Explication
Test-Service ne plante jamais car elle utilise -ErrorAction SilentlyContinue puis teste la variable. [bool]$service convertit l’objet (ou $null) en vrai/faux. Backup-Fichier, elle, a besoin de -ErrorAction Stop pour que try/catch intercepte l’erreur.
Pour aller plus loin
# Le même test sur plusieurs services, grâce au pipeline
"Spooler", "BITS", "Inexistant" | ForEach-Object { Test-Service -Nom $_ } | Format-Table -AutoSize
Mini-projet
Voir le corrigé
<#
.SYNOPSIS
Supprime les fichiers temporaires anciens d'un dossier, avec journalisation.
.PARAMETER Dossier
Dossier à nettoyer (obligatoire).
.PARAMETER JoursAnciennete
Âge minimal des fichiers supprimés, en jours (1 à 365, défaut 30).
.EXAMPLE
.\Clean-Temp.ps1 -Dossier C:\Temp -JoursAnciennete 60 -WhatIf
.NOTES
Auteur : Bruno Jousseaume
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][string]$Dossier,
[ValidateRange(1, 365)][int]$JoursAnciennete = 30,
[string[]]$Extension = @("*.tmp", "*.log"),
[string]$Journal = "$PSScriptRoot\clean-temp.log"
)
function Write-Journal {
param([string]$Message, [ValidateSet("INFO", "ALERTE", "ERREUR")][string]$Niveau = "INFO")
"{0} [{1}] {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Niveau, $env:USERNAME, $Message |
Add-Content -Path $Journal -Encoding utf8
}
# Garde-fous
$interdits = @("C:\", "C:\Windows", "C:\Windows\System32", "C:\Program Files", $env:USERPROFILE)
$chemin = (Resolve-Path $Dossier -ErrorAction SilentlyContinue).Path
if (-not $chemin) {
Write-Journal "Dossier introuvable : $Dossier" -Niveau ERREUR
throw "Dossier introuvable : $Dossier"
}
if ($interdits -contains $chemin.TrimEnd("\")) {
Write-Journal "Refus de nettoyer un dossier protégé : $chemin" -Niveau ALERTE
throw "Dossier protégé, nettoyage refusé."
}
$limite = (Get-Date).AddDays(-$JoursAnciennete)
$cibles = Get-ChildItem -Path $chemin -File -Recurse -Include $Extension -ErrorAction SilentlyContinue |
Where-Object LastWriteTime -lt $limite
$octets = ($cibles | Measure-Object Length -Sum).Sum
$supprimes = 0
foreach ($f in $cibles) {
if ($PSCmdlet.ShouldProcess($f.FullName, "Suppression")) {
try {
Remove-Item $f.FullName -Force -ErrorAction Stop
Write-Journal "Supprimé : $($f.FullName)"
$supprimes++
}
catch {
Write-Journal "Échec sur $($f.FullName) : $($_.Exception.Message)" -Niveau ERREUR
}
}
}
$resume = [PSCustomObject]@{
Dossier = $chemin
FichiersTrouves = $cibles.Count
FichiersSupprimes = $supprimes
EspaceLibereMo = [math]::Round($octets / 1MB, 1)
Date = Get-Date
}
Write-Journal "Bilan : $supprimes fichier(s), $($resume.EspaceLibereMo) Mo"
$resume
Explication
Le script suit l’ordre : valider les entrées, refuser les cas dangereux, agir, journaliser, renvoyer un résumé. Resolve-Path transforme un chemin relatif en chemin absolu, ce qui rend la comparaison avec la liste des dossiers interdits fiable.
Pour aller plus loin
Amélioration possible : un paramètre -Corbeille qui déplace au lieu de supprimer, le temps de gagner en confiance.
🧠 Testez vos connaissances
5 questions. Répondez puis cliquez sur « Corriger ». Rien n’est enregistré : c’est de l’auto-évaluation.
© 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.