Fonctions et scripts fiables
Objectifs de la séance
- Découper un script en fonctions réutilisables, nommées selon la convention Verbe-Nom
- Comprendre comment une fonction PowerShell renvoie une valeur, et pourquoi ce n'est pas comme en Bash
- Documenter un script avec l'aide commentée, pour le rendre exploitable par
Get-Help - Rendre un script robuste :
Set-StrictMode,try/catch,-ErrorAction Stop,-WhatIf - Découvrir PSScriptAnalyzer, l'équivalent PowerShell de
shellcheck - Faire le bilan des 6 séances précédentes de PowerShell niveau 1
Notions théoriques
Déclarer une fonction
function Get-InfoDisque {
param(
[string]$Lettre = "C:"
)
# ...
}
Comme en Bash, une fonction doit être définie avant d'être appelée dans le fichier.
La convention Verbe-Nom s'applique aussi à vos fonctions
Vos propres fonctions doivent suivre la même convention que les cmdlets natives : Get-, Set-, New-, Remove-, Test- (pour une fonction qui renvoie un booléen), Write-. Get-Verb liste les verbes approuvés par Microsoft ; un verbe non approuvé déclenche un avertissement lorsque la fonction est chargée depuis un module. Le nom reste toujours au singulier.
En entreprise, il est courant de préfixer ses fonctions maison d'un trigramme, par exemple Get-SioInfoDisque, pour les distinguer visuellement des cmdlets natives.
Appeler une fonction
Get-InfoDisque -Lettre "D:"
N'écrivez jamais Get-InfoDisque("D:"). Les parenthèses et les virgules ne sont pas la syntaxe d'appel de PowerShell — ce code passe en réalité un tableau à un seul élément comme unique argument positionnel, ce qui produit un résultat inattendu. C'est le piège n°1 pour qui vient de PHP, Java ou C#.
Ce qu'une fonction renvoie
Voici la vraie différence avec Bash. return $x existe bien en PowerShell, mais une fonction renvoie en réalité tout ce qu'elle n'a pas explicitement absorbé, pas seulement la valeur du return. Exemple :
function Get-Total {
$a = 5
Write-Output "Calcul en cours" # ceci part AUSSI dans le résultat de la fonction !
return $a * 2
}
Ici, Get-Total ne renvoie pas seulement 10 : elle renvoie deux valeurs, la chaîne "Calcul en cours" puis 10. Solutions :
- utiliser
Write-Host(qui n'écrit jamais dans le pipeline) pour les messages destinés à l'utilisateur ; - ou absorber une sortie parasite avec
| Out-Nullou$null = ....
En Bash, return ne renvoie qu'un code de retour numérique (0-255) ; pour renvoyer une donnée, il faut l'afficher avec echo et la capturer avec $(...). En PowerShell, une fonction peut directement renvoyer un objet complet — un tableau, un [PSCustomObject], etc.
La portée des variables
Par défaut, une fonction lit les variables déjà définies dans le script, mais toute affectation à l'intérieur de la fonction crée une variable locale à cette fonction. Les préfixes $script: et $global: permettent d'élargir explicitement la portée si nécessaire.
Préférez toujours faire transiter les données par des paramètres en entrée et un résultat en sortie, plutôt que de faire dépendre une fonction de variables globales : c'est plus lisible et plus facile à tester isolément.
L'aide commentée
L'équivalent PowerShell du usage() de Bash, en beaucoup plus puissant :
<#
.SYNOPSIS
Génère un inventaire du poste Windows.
.DESCRIPTION
Collecte les informations disques, processus et services, puis les exporte.
.PARAMETER Chemin
Dossier de destination du rapport.
.EXAMPLE
.\Get-InfosSysteme.ps1 -Chemin "C:\Rapports"
#>
Placé au-dessus d'une fonction ou en tête d'un script, ce bloc rend Get-Help .\Get-InfosSysteme.ps1 -Full immédiatement utilisable, exactement comme pour une cmdlet native.
Rendre le script strict
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latestinterdit l'usage d'une variable non définie — le cousin PowerShell duset -ude Bash.$ErrorActionPreference = "Stop"et-ErrorAction Stoptransforment une erreur en erreur bloquante — le cousin duset -ede Bash.
try {
Get-Service -Name "ServiceInexistant" -ErrorAction Stop
} catch {
Write-Host "Erreur : $($_.Exception.Message)" -ForegroundColor Red
} finally {
Write-Host "Traitement terminé"
}
Par défaut, une erreur PowerShell est non bloquante : le script continue son exécution après l'avoir affichée. C'est le contraire de l'intuition qu'on peut avoir venant de Bash — pensez toujours à -ErrorAction Stop si vous voulez qu'un try/catch se déclenche réellement.
Filet de sécurité sur les actions destructrices
De nombreuses cmdlets qui modifient ou suppriment quelque chose supportent nativement -WhatIf et -Confirm :
Remove-Item "C:\Temp\*.tmp" -WhatIf
Tester une commande avec -WhatIf avant son exécution réelle est un réflexe qui évite bien des catastrophes : la commande affiche ce qu'elle aurait fait, sans rien modifier.
PSScriptAnalyzer
Install-Module -Name PSScriptAnalyzer -Scope CurrentUser
Invoke-ScriptAnalyzer -Path .\Get-InfosSysteme.ps1
PSScriptAnalyzer relit votre script et signale les alias utilisés, les verbes non approuvés, les mots de passe en clair, les paramètres inutilisés — le parallèle direct avec shellcheck en Bash.
Sécurité et lisibilité
- Jamais de mot de passe en clair dans un script —
Get-CredentialetSecureString(rappel de la séance précédente). - Pas d'alias (
ls,%,?) dans un script destiné à être maintenu. - Indentation à 4 espaces, une action par ligne, commenter le pourquoi d'un choix plutôt que reformuler ce que fait déjà la ligne.
- Encodage UTF-8 (avec BOM pour Windows PowerShell 5.1) pour que les accents s'affichent correctement.
#Requires -Version 5.1en tête de script pour documenter la version minimale attendue.
| Bash | PowerShell |
|---|---|
nom() { ... }, local, return = code numérique | function Verbe-Nom { param(...) }, return = valeur, portée locale par défaut |
set -eu | Set-StrictMode -Version Latest + $ErrorActionPreference = "Stop" |
shellcheck | Invoke-ScriptAnalyzer (module PSScriptAnalyzer) |
echo "Usage : $0 <dossier>" | Aide commentée <# .SYNOPSIS ... #> + Get-Help |
Exemple pratique
Version « brouillon », sans aucune bonne pratique :
$c = "C:\Rapports"
if (!(dir $c)) { md $c }
$f = "$c\rapport.txt"
"ok" > $f
Version corrigée, structurée en fonctions :
<#
.SYNOPSIS
Génère un inventaire système et l'écrit dans un fichier.
.PARAMETER Chemin
Dossier de destination du rapport.
.EXAMPLE
.\Get-InfosSysteme.ps1 -Chemin "C:\Rapports"
#>
param(
[string]$Chemin = "C:\Rapports"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Test-DossierSortie {
param([string]$Dossier)
if (-not (Test-Path -Path $Dossier -PathType Container)) {
New-Item -Path $Dossier -ItemType Directory -Force | Out-Null
}
}
function Write-Journal {
param([string]$Message)
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] $Message"
}
function Get-InfoDisque {
param([string]$Lettre = "C:")
$disque = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='$Lettre'"
return [PSCustomObject]@{
Lettre = $Lettre
LibreGo = [math]::Round($disque.FreeSpace / 1GB, 1)
}
}
# --- Programme principal ---
try {
Test-DossierSortie -Dossier $Chemin
Write-Journal "Dossier de sortie prêt : $Chemin"
$info = Get-InfoDisque -Lettre "C:"
Write-Journal "Espace libre sur C: : $($info.LibreGo) Go"
$info | Export-Csv -Path (Join-Path $Chemin "disque.csv") -NoTypeInformation -Encoding UTF8
Write-Journal "Rapport généré avec succès"
}
catch {
Write-Host "Erreur : $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Un Get-InfosSysteme.ps1 volontairement bâclé vous est fourni ci-dessous : alias partout, aucune vérification, chemin codé en dur.
$c = "C:\Rapports"
if (!(dir $c)) { md $c }
"ok" > "$c\rapport.txt"
Étape 1 — Extraire la vérification du dossier dans une fonction
Le verbe Test- annonce, par convention, une fonction qui vérifie une condition plutôt qu'elle ne modifie quelque chose — même si, ici, elle agit aussi en créant le dossier au passage.
Étape 2 — Écrire Write-Journal pour horodater les messages
En passant systématiquement par Write-Journal plutôt que par des Write-Host disséminés, vous pourrez plus tard rediriger tous les messages vers un fichier de log en modifiant une seule fonction.
Étape 3 — Protéger l'écriture du rapport par un try/catch
Une erreur PowerShell est non bloquante par défaut : sans -ErrorAction Stop sur la commande qui peut échouer, le bloc catch ne se déclencherait jamais.
Étape 4 — Passer le script à PSScriptAnalyzer
Faites de Invoke-ScriptAnalyzer une étape systématique avant de considérer un script terminé, exactement comme shellcheck pour un script Bash.
📌 Une solution
Ce qu'il faut retenir
| Notion | Résumé |
|---|---|
| Premier script | Fichier .ps1, Set-ExecutionPolicy RemoteSigned -Scope CurrentUser, lancement par .\script.ps1 |
| Cmdlets | Convention Verbe-Nom, découverte par Get-Command, Get-Help, Get-Member |
| Variables | $nom = "valeur" (espaces autorisés), typage .NET, "$($obj.Propriete)", tableaux @(...) |
| Pipeline | Transporte des objets, pas du texte : Where-Object, Select-Object, Sort-Object, Export-Csv |
| Entrées | param() en première instruction, paramètres nommés et typés, Read-Host |
| Conditions | if (...) { }, opérateurs -eq, -lt, -like, Test-Path, $null à gauche |
| Boucles | foreach, ForEach-Object, for, while |
| Fonctions | function Verbe-Nom { param(...) }, appel -Parametre valeur, tout ce qui sort est renvoyé |
| Fiabilité | Set-StrictMode, try/catch, -WhatIf, aide commentée, Invoke-ScriptAnalyzer |
Aperçu de la prochaine séance
Vous savez désormais écrire un script PowerShell complet, fiable et documenté : politique d'exécution, variables, pipeline d'objets, paramètres, conditions, boucles, fonctions et bonnes pratiques. La séance suivante, PowerShell niveau 2, ira plus loin en orientant ces bases vers l'administration Windows avancée : la gestion des utilisateurs et des groupes, l'exécution de commandes à distance sur plusieurs machines avec PowerShell Remoting, l'administration d'Active Directory, la gestion d'erreurs avec try/catch, les modules réutilisables, et la génération de rapports planifiés avec le Planificateur de tâches. Vous retrouverez aussi, côté Linux, un pendant à cette recherche de convivialité en ligne de commande dans le cours 50_InterfacesConviviales (dialog/whiptail).