Conception des paramètres et modularisation des scripts PowerShell — Du « script qui fonctionne » au « script que l'on peut transmettre »

· · PowerShell, Windows, Script, Automatisation, Amélioration opérationnelle, Efficacité opérationnelle, Valorisation des actifs existants, Ligne de commande

« Le script PowerShell écrit par la personne en charge fonctionne, mais elle seule sait le toucher. » « Les noms de serveur et les chemins sont écrits en dur un peu partout dans le code, si bien qu’il faut réécrire le corps du script à chaque changement d’environnement. » « Un mauvais argument passe silencieusement et le script s’exécute quand même — on ne s’en aperçoit qu’après coup. » — Dans les consultations sur l’automatisation opérationnelle, ce n’est très souvent pas le script lui-même qui pose problème, mais la façon de le transmettre et de le faire grandir.

Tant que le script tourne uniquement sur votre propre poste, un « script qui fonctionne » avec des variables écrites en dur ne pose aucun problème. Mais dès qu’il s’agit de le mettre dans le planificateur de tâches, de le transmettre à un collègue ou de le réutiliser sur plusieurs serveurs, c’est la conception des paramètres et l’organisation du traitement commun qui déterminent la qualité. Heureusement, PowerShell fournit dès l’origine, en tant que fonctionnalités du langage, tout l’outillage nécessaire pour atteindre ce « script que l’on peut transmettre » : le bloc param, les attributs de validation, l’aide basée sur les commentaires, et les modules.

Cet article part du « script qui fonctionne » déjà en place chez les responsables informatiques et les équipes d’exploitation des petites et moyennes entreprises, et organise une démarche pratique pour en élever la qualité par étapes, dans l’ordre suivant : conception des paramètres → validation des entrées → aide et prise en charge de -WhatIf → modularisation en .psm1 → partage interne. Nous prenons PowerShell 7.x comme référence, tout en ajoutant au fil du texte les précautions propres aux environnements limités à Windows PowerShell 5.1.

1. La conclusion, d’abord

  • Déclarez les paramètres dans un bloc param et ajoutez [CmdletBinding()] pour en faire une « fonction avancée » : c’est le point de départ. Les paramètres communs (-Verbose, -ErrorAction, etc.) s’ajoutent automatiquement, et transmettre un paramètre non défini provoque une erreur de liaison, ce qui évite l’accident d’une faute de frappe ignorée silencieusement.1
  • Déclarez les arguments obligatoires avec [Parameter(Mandatory)] et donnez-leur toujours un type. Un appel qui omet un paramètre obligatoire s’arrête avant l’exécution, et une valeur de type incorrect est également rejetée avant l’exécution.2
  • Confiez la vérification du format aux attributs de validation (ValidateSet/ValidateRange/ValidateScript/ValidateNotNullOrEmpty) plutôt qu’à une instruction if. Si la validation échoue, la fonction n’est pas appelée, ce qui élimine structurellement le risque de « traitement qui s’exécute à moitié puis se casse ». Le principe est de faire remonter les erreurs le plus tôt possible, dès l’entrée.2
  • Déclarez les arguments on/off avec [switch]. Un paramètre indicateur maison qui reçoit $true ou $false sous forme de chaîne est une source d’accidents pour l’appelant.2
  • En écrivant l’aide basée sur les commentaires (.SYNOPSIS/.EXAMPLE), Get-Help fonctionne aussi pour vos propres commandes. S’affranchir du « lisez le code pour savoir comment l’utiliser » est la condition minimale pour transmettre un script à quelqu’un d’autre.3
  • Pour les fonctions qui modifient quelque chose, déclarez SupportsShouldProcess pour prendre en charge -WhatIf/-Confirm. Que l’appelant puisse vérifier l’étendue de l’impact avant l’exécution est, parmi les dispositifs de sécurité d’un script d’exploitation, celui qui offre le meilleur rapport coût-bénéfice.4
  • Extrayez dans un module .psm1 les fonctions réutilisées par plusieurs scripts, et placez-le sous $env:PSModulePath. Si l’emplacement est correct, le chargement se fait automatiquement, sans besoin d’Import-Module. Il suffit d’ajouter le manifeste psd1 au « stade de la distribution ».5678
  • Gérez les versions des modules et des scripts avec Git, et prêtez attention à la politique d’exécution lors d’une distribution par dossier partagé. Un script situé sur un chemin UNC peut se voir refuser l’exécution par RemoteSigned.9

2. Le bloc param et [CmdletBinding()] — la porte d’entrée vers la « fonction avancée »

Voici d’abord un exemple typique du « script qui fonctionne » que l’on rencontre couramment sur le terrain.

# Exemple courant : variables écrites en dur. Il faut réécrire le corps du script à chaque changement d'environnement
$logDir = "D:\Logs\AppA"
$days = 90
Get-ChildItem $logDir -Filter *.log |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$days) }

Réécrivons cela avec un bloc param et [CmdletBinding()].

[CmdletBinding()]
param(
    # Obligatoire. Un appel qui l'omet s'arrête avant l'exécution
    # (Remarque : [string] exprime l'intention, ce n'est pas un rejet. Les valeurs comme les nombres sont
    #  converties automatiquement en chaîne ; pour un rejet strict, utilisez les attributs de validation vus plus loin)
    [Parameter(Mandatory)]
    [string]$LogDir,

    # Donnez aux arguments facultatifs une valeur par défaut sûre pour l'exploitation
    [int]$Days = 90
)

Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$Days) }

[CmdletBinding()] est une déclaration signifiant « faire fonctionner cette fonction (ce script) selon les mêmes règles qu’une applet de commande compilée » ; une fonction qui le porte devient une fonction avancée (advanced function). Cela produit trois effets.1

  • Les paramètres communs s’ajoutent automatiquement. -Verbose, -Debug, -ErrorAction, -ErrorVariable, etc. deviennent utilisables par l’appelant sans que vous ayez à les implémenter vous-même. Vous obtenez directement le comportement standard où Write-Verbose dans le script ne s’affiche que si -Verbose est spécifié.
  • Une erreur d’argument est bloquée avant l’exécution. Dans une fonction avancée, transmettre un nom de paramètre non défini, ou un argument excédentaire sans paramètre positionnel correspondant, fait échouer la liaison des paramètres. L’accident où une faute de frappe comme -Dyas 30 est ignorée silencieusement et le script s’exécute avec la valeur par défaut disparaît.1
  • $PSCmdlet devient utilisable. C’est la porte d’entrée vers des fonctionnalités réservées aux applets de commande, comme ShouldProcess, abordé plus loin.10

Lorsqu’un argument marqué Mandatory est omis, PowerShell demande une saisie avant l’exécution. C’est le premier dispositif de sécurité qui empêche l’accident du « argument obligatoire oublié qui s’exécute quand même avec une valeur par défaut ».2 Il y a toutefois un point de vigilance : ce comportement de « demander une saisie » s’accorde mal avec une exécution sans surveillance. Si un argument obligatoire manque lors d’un lancement depuis le planificateur de tâches, la tâche peut rester bloquée sur une invite à laquelle personne ne peut répondre. Pour une exécution sans surveillance, la règle de base est de lancer le script avec -NonInteractive, afin qu’il échoue immédiatement avec une erreur au lieu d’afficher une invite.

Comme il n’est pas toujours évident de savoir « où l’écrire », voici un exemple de remplissage de l’onglet « Action » du planificateur de tâches (pour une exécution avec PowerShell 7).

Champ Exemple de saisie
Programme/script C:\Program Files\PowerShell\7\pwsh.exe
Ajouter des arguments (facultatif) -NoProfile -NonInteractive -File "C:\Scripts\Remove-OldAppLog.ps1" -LogDir "D:\Logs\AppA" -Days 90
Commencer dans (facultatif) C:\Scripts

Pour une exécution sous Windows PowerShell 5.1, le champ Programme est powershell.exe. Trois points sont à retenir : utiliser -NoProfile pour éliminer les écarts d’environnement et le ralentissement du démarrage dus au chargement du profil ; utiliser -NonInteractive pour éviter l’attente d’invite mentionnée plus haut ; et entourer de guillemets le chemin du script comme la valeur de chaque argument, dès qu’un espace peut s’y trouver. Tout ce qui suit -File est transmis comme argument du script, donc les paramètres propres au script doivent être placés après -File. Si vous laissez « Commencer dans (facultatif) » vide, le répertoire de travail prend l’emplacement par défaut : indiquez-le impérativement pour un script qui utilise des chemins relatifs.

Notez par ailleurs que, lorsque vous nommez et publiez une fonction, adoptez le format Verbe-Nom et choisissez le verbe parmi les verbes approuvés que l’on peut consulter avec Get-Verb. Un verbe non approuvé fonctionne quand même, mais un avertissement s’affiche à l’importation du module.11

3. La validation des entrées, à l’entrée — faire remonter les erreurs tôt avec les attributs Validate

Écrire la vérification du format des arguments dans une instruction if au sein du corps de la fonction tend à laisser passer des oublis de vérification, ou des bogues où « un effet de bord s’exécute avant la vérification ». PowerShell permet de faire cohabiter la validation avec la déclaration du paramètre, grâce aux attributs de validation. La validation s’effectue avant l’appel de la fonction, et en cas d’échec, aucune ligne du corps ne s’exécute.2

[CmdletBinding()]
param(
    # N'accepte que les dossiers qui existent. $_ est la valeur soumise à validation
    [Parameter(Mandatory)]
    [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
    [string]$LogDir,

    # Limite la durée de rétention à une plage de 1 à 3650 jours. Évite l'accident où 0 ou un nombre négatif viserait « tous les fichiers »
    [ValidateRange(1, 3650)]
    [int]$Days = 90,

    # Fige les choix possibles. La complétion par tabulation fonctionne aussi
    [ValidateSet('Zip', 'Move', 'ReportOnly')]
    [string]$Mode = 'ReportOnly',

    # Rejette la chaîne vide et $null. L'équipement de base d'un argument de type chaîne
    [ValidateNotNullOrEmpty()]
    [string]$ReportName = 'log-report',

    # Un on/off se déclare en switch. $true si spécifié, $false si omis
    [switch]$IncludeSubfolders
)

Voici un repère pour choisir entre ces attributs.

Attribut Usage Exemple typique sur le terrain
ValidateSet Limite la valeur à un ensemble fixe de choix et active la complétion par tabulation2 Mode de fonctionnement, nom d’environnement (Dev/Test/Prod)
ValidateRange Limite une plage de valeurs numériques ou de dates2 Durée de rétention, nombre de tentatives, numéro de port
ValidateScript Validation par un script arbitraire ; échoue avec $false ou une exception2 Vérification de l’existence d’un chemin, ordre chronologique de dates
ValidateNotNullOrEmpty Rejette $null, la chaîne vide et les collections vides2 Presque tous les arguments de type chaîne
ValidatePattern Vérification du format par expression régulière2 Numéro de bon, règles de nommage des noms d’hôte

Deux précisions. D’abord, l’ordre de déclaration des attributs de validation demande de l’attention : les écrire après le type peut faire valider la valeur avant conversion de type et provoquer un échec inattendu, si bien que l’ordre attribut → type → nom de variable est la bonne pratique recommandée par la documentation officielle.2 Ensuite, l’argument ErrorMessage de ValidateScript (message d’erreur personnalisé) est une fonctionnalité apparue avec PowerShell 6 et n’est pas disponible sous Windows PowerShell 5.1.2 Dans un environnement mixte incluant du 5.1, il est plus prudent soit de lever une exception (throw) dans le script de validation pour afficher votre propre message, soit de conserver le message par défaut.

Dès que vous commencez à écrire une validation élaborée avec ValidateScript, c’est aussi le signal qu’il est temps d’écrire des tests. Faire reposer la vérification du fonctionnement de la logique de validation elle-même sur le modèle présenté dans « Mise en place de tests PowerShell avec Pester » la rend moins susceptible de se casser.

4. Les bases de l’entrée par pipeline — ValueFromPipeline et le bloc process

Si votre fonction peut elle aussi s’utiliser dans un pipeline, comme Get-Content servers.txt | Test-AppServer, vous pouvez la combiner avec les mêmes réflexes que pour les commandes standard de PowerShell. Deux éléments suffisent : la déclaration de ValueFromPipeline, et un bloc process.2

function Test-AppServer {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string[]]$ComputerName
    )
    begin   { $results = @() }   # Exécuté une seule fois, avant le traitement du pipeline
    process {
        # Exécuté pour chaque élément reçu du pipeline
        foreach ($name in $ComputerName) {
            $results += [pscustomobject]@{
                ComputerName = $name
                # -ComputerName fonctionne à la fois en 5.1 et en 7 (-TargetName, ajouté en 7, n'existe pas en 5.1)
                Reachable    = Test-Connection -ComputerName $name -Count 1 -Quiet
            }
        }
    }
    end     { $results }         # Exécuté une seule fois, à la fin
}

Il n’y a qu’un seul point à retenir : si la fonction reçoit une entrée par pipeline, écrivez le traitement dans le bloc process. Sans bloc process, faire transiter plusieurs valeurs par le pipeline aboutit au bogue classique où seul le dernier élément est traité.10 begin et end sont facultatifs, donc en cas d’hésitation, retenir « le corps dans process, begin/end seulement s’il faut agréger » suffit amplement en pratique.

Comme ce piège est difficile à ressentir sans exemple, voici un cas à éviter, placé en regard.

# Exemple à éviter : pas de bloc process. Même en faisant transiter 3 éléments par le pipeline, seul le dernier est traité
function Test-AppServerBad {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string[]]$ComputerName
    )
    # Si aucun de begin/process/end n'est écrit, le corps entier est traité comme un bloc end et ne s'exécute qu'« une seule fois, à la fin ».
    # Les éléments du pipeline sont liés au paramètre un par un, donc au moment d'atteindre end, il ne reste que le dernier
    foreach ($name in $ComputerName) {
        [pscustomobject]@{ ComputerName = $name }
    }
}

'SV01', 'SV02', 'SV03' | Test-AppServerBad   # → une seule ligne, SV03. SV01 et SV02 sont abandonnés silencieusement
'SV01', 'SV02', 'SV03' | Test-AppServer      # → 3 lignes sont retournées (la version avec bloc process ci-dessus)

Ce qui est gênant, c’est qu’aucune erreur n’est levée. De plus, si vous passez les valeurs en argument comme dans Test-AppServerBad -ComputerName 'SV01','SV02','SV03', les 3 éléments sont traités correctement, ce qui fait que vous ne remarquerez rien si vous vérifiez le fonctionnement uniquement par appel avec arguments. Dès que vous écrivez une fonction qui reçoit une entrée par pipeline, incluez systématiquement un test qui fait transiter plusieurs éléments.

5. Faire fonctionner Get-Help et -WhatIf — la condition minimale pour transmettre un script

5.1. L’aide basée sur les commentaires

Un utilisateur de PowerShell, face à une commande inconnue, commence toujours par lancer Get-Help. Que votre propre fonction s’inscrive ou non dans cette culture dépend de la présence d’une aide basée sur les commentaires. Il suffit d’écrire des commentaires portant des mots-clés spéciaux pour que Get-Help affiche l’aide dans le même format que pour les applets de commande standard.3

Voici la liste des mots-clés les plus utilisés. Il n’est pas nécessaire de tous les écrire : le minimum, ce sont .SYNOPSIS et .EXAMPLE, et pour transmettre le script à quelqu’un, il suffit d’ajouter progressivement .DESCRIPTION et .PARAMETER.3

Mot-clé Ce qu’il faut écrire
.SYNOPSIS Résumé en une ligne. Apparaît en tête de Get-Help
.DESCRIPTION Description détaillée. Les prérequis et les effets de bord y trouvent leur place
.PARAMETER nom_du_paramètre Description de chaque paramètre. Faites suivre le mot-clé du nom du paramètre
.EXAMPLE Exemple d’utilisation. Commande à exécuter sur la première ligne, explication sur les lignes suivantes. Peut être répété
.INPUTS Type d’objet acceptable par pipeline
.OUTPUTS Type d’objet retourné
.NOTES Compléments : auteur, date de mise à jour, limitations connues, etc.
.LINK Commandes ou URL associées. La première URL devient la cible de Get-Help -Online

5.2. SupportsShouldProcess et -WhatIf

Pour une fonction qui supprime, déplace ou modifie un paramétrage, déclarez [CmdletBinding(SupportsShouldProcess)]. Cela suffit à ajouter automatiquement les paramètres -WhatIf et -Confirm ; dans le corps, la valeur de retour de $PSCmdlet.ShouldProcess() permet de décider s’il faut réellement exécuter la modification.4

Voici, intégrant les deux, la forme finale d’une fonction d’une qualité que l’on peut transmettre.

function Remove-OldAppLog {
    <#
    .SYNOPSIS
    Supprime, dans le dossier indiqué, les fichiers journaux dont la durée de rétention est dépassée.

    .DESCRIPTION
    Supprime les fichiers *.log dont LastWriteTime est plus ancien que la durée de rétention.
    Avec -WhatIf, vous pouvez vous limiter à vérifier les fichiers ciblés.

    .EXAMPLE
    Remove-OldAppLog -LogDir 'D:\Logs\AppA' -Days 90 -WhatIf
    Affiche seulement les fichiers ciblés par la suppression, sans rien supprimer réellement.
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
        [string]$LogDir,

        [ValidateRange(1, 3650)]
        [int]$Days = 90
    )
    process {
        $limit = (Get-Date).AddDays(-$Days)
        # -File exclut les dossiers (évite de supprimer par erreur un dossier dont le nom se termine par « .log »)
        Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
            Where-Object { $_.LastWriteTime -lt $limit } |
            ForEach-Object {
                # ShouldProcess renvoie $false lors de -WhatIf, ou en cas de refus via -Confirm
                if ($PSCmdlet.ShouldProcess($_.FullName, "Suppression")) {
                    Remove-Item -LiteralPath $_.FullName
                }
            }
    }
}

En tapant Remove-OldAppLog -LogDir D:\Logs\AppA -WhatIf, seule une liste « What if : … » s’affiche et rien n’est supprimé. Lorsque vous transmettez un script qui modifie des données, joignez-y systématiquement la procédure de répétition avec -WhatIf — c’est le modèle d’exploitation que ce site recommande sans cesse. Un exemple d’intégration dans un véritable script de nettoyage de journaux est détaillé dans « PowerShell en pratique — automatiser en toute sécurité l’investigation, l’archivage et la mise en rapport des journaux ».

Notez par ailleurs que l’article officiel met en garde contre l’excès de confiance dans l’idée que -WhatIf se propage toujours jusqu’aux commandes appelées en interne. Pour en être sûr, transmettez explicitement -WhatIf:$WhatIfPreference aux commandes internes comme Remove-Item.4

6. Extraire le traitement commun dans un module .psm1

6.1. .psm1 et Export-ModuleMember

À mesure que vos fonctions se développent, vous voudrez réutiliser les mêmes fonctions depuis plusieurs scripts. Les multiplier par copier-coller fait qu’une correction ne se propage plus à toutes les copies : regroupez donc les fonctions communes.

L’option intermédiaire, avant cela, est le dot-sourcing. En exécutant un script précédé d’un point et d’un espace, ce script s’exécute dans la portée de l’appelant, et les fonctions ou variables qui y sont définies restent disponibles telles quelles chez l’appelant.12

# Charge Common.ps1, qui contient les fonctions communes (le point et l'espace en tête forment le dot-sourcing)
. C:\Scripts\Common.ps1

# Les fonctions définies dans Common.ps1 peuvent être appelées directement
Remove-OldAppLog -LogDir 'D:\Logs\AppA' -WhatIf

C’est pratique, mais l’appelant doit connaître le chemin physique du fichier, et vous ne pouvez pas choisir ce qui est exposé (fonctions, variables et alias affluent tous tels quels). Cela suffit pour découper un script en plusieurs parties, mais dès que vous voulez réutiliser la même fonction depuis un deuxième script, c’est le signal qu’il faut passer à la modularisation.

Créer un module est étonnamment simple : il suffit d’enregistrer le fichier contenant les fonctions avec l’extension .psm1.5

# AppOpsTools.psm1 — module commun des outils d'exploitation internes
function Remove-OldAppLog { <# fonction du chapitre précédent #> }
function Get-AppLogSummary { <# fonction d'agrégation #> }

# Fonction d'assistance interne. Non exposée à l'extérieur
function ConvertTo-InternalPath { <# ... #> }

# Déclare explicitement les fonctions à exposer. Sans cela, toutes les fonctions sont exposées
Export-ModuleMember -Function Remove-OldAppLog, Get-AppLogSummary

Si vous n’écrivez pas Export-ModuleMember, toutes les fonctions et tous les alias du module sont exportés (pas les variables). C’est facultatif, mais expliciter ce qui est exposé est considéré comme une bonne pratique.13 Garder les fonctions d’assistance internes cachées laisse une marge de manœuvre pour les refactoriser librement par la suite.

6.2. L’emplacement — $env:PSModulePath et le chargement automatique

Placez le module sous l’un des dossiers listés dans $env:PSModulePath, dans un dossier portant le même nom que le module (AppOpsTools\AppOpsTools.psm1). Si le nom du dossier et le nom de base du fichier ne correspondent pas, il n’est pas reconnu comme un module.65 Les emplacements par défaut sont les suivants ; le fait que les chemins diffèrent entre Windows PowerShell 5.1 et PowerShell 7 est un piège classique sur le terrain.6

Portée PowerShell 7 Windows PowerShell 5.1
Utilisateur courant (CurrentUser) $HOME\Documents\PowerShell\Modules $HOME\Documents\WindowsPowerShell\Modules
Tous les utilisateurs (AllUsers) $env:ProgramFiles\PowerShell\Modules $env:ProgramFiles\WindowsPowerShell\Modules

Plutôt que de mémoriser ce tableau, il est plus sûr de vérifier directement dans votre environnement. Cela se fait en une ligne.

# Affiche les chemins de recherche de votre environnement, un par ligne (le séparateur sous Windows est le point-virgule)
$env:PSModulePath -split ';'

# Écriture qui récupère le séparateur depuis l'environnement. À privilégier si vous touchez aussi à macOS/Linux avec PowerShell 7
$env:PSModulePath -split [System.IO.Path]::PathSeparator

Bien que le nom $env:PSModulePath soit le même, son contenu diffère complètement entre 5.1 et 7. La cause d’un « module introuvable » est le plus souvent que l’emplacement où vous l’avez placé ne figure pas dans le chemin de recherche de cet environnement : exécutez d’abord cette ligne avant de chercher ailleurs.

Placé au bon endroit, PowerShell importe automatiquement le module dès la première exécution d’une de ses commandes, sans qu’il soit nécessaire d’écrire Import-Module (le chargement automatique des modules).7 L’utilisateur peut alors se servir de la commande « comme si elle était présente dès le départ ». Le rythme habituel consiste à vérifier le fonctionnement d’un module en cours de mise au point avec Import-Module en chemin complet, puis à le placer sous PSModulePath une fois qu’il est stabilisé.

Deux précautions dépendantes de l’environnement. Le dossier Documents réel peut avoir été déplacé par une redirection de dossier OneDrive, auquel cas les modules à portée utilisateur se retrouvent aussi sous OneDrive.6 Par ailleurs, le placement à portée « tous les utilisateurs » exige des droits administrateur. Si vous placez le module sur un serveur, optez pour la portée AllUsers et assurez-vous qu’il reste visible depuis le compte d’exécution du planificateur de tâches : cela évite le problème « ça marche sur mon poste mais pas sur le serveur ».

6.3. Le manifeste psd1, au « stade de la distribution »

Le manifeste de module (.psd1) est un fichier de type table de hachage décrivant des métadonnées comme la version du module ou ses dépendances ; il n’est pas obligatoire. La seule clé obligatoire du manifeste est ModuleVersion.8 Tant que le module reste utilisé au sein de votre propre équipe, le .psm1 seul suffit ; générez le manifeste avec New-ModuleManifest lorsque vient le stade de le distribuer à d’autres services ou de gérer les versions de façon plus rigoureuse.8

New-ModuleManifest -Path .\AppOpsTools\AppOpsTools.psd1 `
    -RootModule 'AppOpsTools.psm1' `
    -ModuleVersion '1.0.0' `
    -FunctionsToExport 'Remove-OldAppLog', 'Get-AppLogSummary' `
    -PowerShellVersion '5.1'

Le psd1 généré prend la forme d’un modèle assorti de commentaires, ce qui permet de ne développer que les clés nécessaires.8

6.4. Partage interne et points clés de la gestion des versions

  • Conservez l’original dans Git. Les scripts et les modules étant du texte, ils s’accordent bien avec Git ; pouvoir retracer « qui a changé quoi, quand et pourquoi » constitue en soi la fiabilité d’un script d’exploitation. Faire correspondre la mise à jour de ModuleVersion avec un commit facilite l’identification de la version présente sur un serveur.
  • La forme de base de la distribution consiste à « copier depuis un dossier partagé vers PSModulePath sur chaque machine ». Copier le dossier du module dans son intégralité permet une installation manuelle.7 Une configuration qui ajoute directement un chemin de dossier partagé à PSModulePath n’est pas recommandée en usage courant, compte tenu du principe qu’un partage peut être « lent, coupé, ou absent » (voir en détail « Les pièges des lecteurs réseau et des chemins UNC »).
  • Prêtez attention à la relation avec la politique d’exécution. La politique RemoteSigned par défaut autorise les scripts créés localement même non signés, mais sur les systèmes qui ne distinguent pas les chemins UNC des chemins Internet, un script situé sur un chemin UNC peut se voir refuser l’exécution. Par ailleurs, les fichiers marqués comme provenant d’un téléchargement sont bloqués et nécessitent soit un déblocage avec Unblock-File, soit une signature.9 Si vous envisagez de généraliser la distribution en interne, vérifiez la combinaison avec la signature de code dans « Politique d’exécution et signature de scripts PowerShell ».

7. Les règles de l’art en pratique (tableau de décision)

Sujet Options Repère de décision
Réception des arguments Variables en dur / Bloc param Dès qu’un script est utilisé deux fois ou plus, ou par quelqu’un d’autre, param s’impose. Les valeurs par défaut doivent pencher du « côté sûr »2
[CmdletBinding()] Ne pas l’ajouter / L’ajouter Toujours l’ajouter pour ce qui est transmis ou mis en exploitation. Une faute de frappe est bloquée avant l’exécution1
Vérification des entrées Instruction if dans le corps / Attributs de validation La vérification du format d’un seul paramètre va dans les attributs. Seule la validation combinée reste dans le corps2
Dispositif de sécurité pour les traitements modificateurs Argument -TestMode maison / SupportsShouldProcess Ne créez pas votre propre indicateur. Reposez-vous sur -WhatIf/-Confirm standard4
Gestion du traitement commun Copier-coller / Dot-sourcing / Module .psm1 Modularisez dès que le partage démarre avec un deuxième script. Explicitez les fonctions publiques avec Export-ModuleMember513
Emplacement du module Dossier quelconque + Import-Module / Sous PSModulePath Placez ce qui est utilisé régulièrement à l’emplacement prévu pour bénéficier du chargement automatique. Portée AllUsers sur un serveur67
Manifeste psd1 Le créer dès le départ / Le créer au stade de la distribution New-ModuleManifest au moment de sortir du cadre de l’équipe ou de gérer les versions plus rigoureusement8

8. Conclusion

  • Faire d’un script une fonction avancée grâce au bloc param et à [CmdletBinding()] est le point de départ d’un « script que l’on peut transmettre ». Les paramètres communs s’ajoutent, et une erreur d’argument est bloquée avant l’exécution.
  • Le typage, [Parameter(Mandatory)], des valeurs par défaut du côté sûr et les attributs de validation font remonter les erreurs tôt, dès l’entrée. ValidateSet améliore aussi le confort d’utilisation grâce à la complétion par tabulation.
  • L’entrée par pipeline se reçoit avec la combinaison ValueFromPipeline et bloc process. Oublier process fait que seul le dernier élément est traité.
  • L’aide basée sur les commentaires fait fonctionner Get-Help, et SupportsShouldProcess fait accepter -WhatIf aux fonctions qui modifient quelque chose. Pouvoir faire une répétition constitue le dispositif de sécurité de l’exploitation.
  • Extrayez les fonctions communes dans un .psm1, explicitez ce qui est exposé avec Export-ModuleMember, et placez-le sous PSModulePath. Attention à la différence de chemins entre 5.1 et 7.
  • Le manifeste psd1 se crée au stade de la distribution. Gérez l’original avec Git, et vérifiez la politique d’exécution (la relation entre chemin UNC et RemoteSigned) lors d’une distribution par dossier partagé.

Articles connexes

Domaines de conseil associés

合同会社小村ソフト (Komura Software LLC) prend en charge l’organisation et la modularisation de scripts PowerShell devenus dépendants d’une seule personne, la revue de conception de scripts d’automatisation opérationnelle, ainsi que la mise en place de mécanismes de distribution interne et de gestion des versions. N’hésitez pas à nous consulter, même pour un simple inventaire d’actifs de scripts qui « fonctionnent mais que personne ne peut plus toucher ».

Références

  1. Microsoft Learn, about_Functions_CmdletBindingAttribute. Sur le fait que l’attribut CmdletBinding fait fonctionner une fonction comme une applet de commande compilée, que les paramètres communs s’ajoutent automatiquement, que $PSCmdlet devient utilisable, que la liaison échoue pour un paramètre inconnu ou un argument positionnel non pris en charge, et que SupportsShouldProcess ajoute les paramètres Confirm/WhatIf.  2 3 4

  2. Microsoft Learn, about_Functions_Advanced_Parameters. Sur les spécifications de l’attribut Parameter et de Mandatory, ValueFromPipeline, le paramètre switch, ainsi que des attributs de validation comme ValidateSet/ValidateRange/ValidateScript/ValidateNotNullOrEmpty/ValidatePattern ; sur le fait que la fonction n’est pas appelée en cas d’échec de la validation ; sur le fait que déclarer les attributs avant le type est une bonne pratique ; et sur le fait que l’argument ErrorMessage de ValidateScript date de PowerShell 6.  2 3 4 5 6 7 8 9 10 11 12 13 14 15

  3. Microsoft Learn, about_Comment_Based_Help. Sur le fait qu’écrire une aide basée sur les commentaires avec des mots-clés comme .SYNOPSIS/.DESCRIPTION/.PARAMETER/.EXAMPLE fait que Get-Help l’affiche dans le même format que l’aide XML, et sur les règles de placement propres aux scripts et aux fonctions.  2 3

  4. Microsoft Learn, Everything you wanted to know about ShouldProcess. Sur le fait que la seule spécification de SupportsShouldProcess crée automatiquement -WhatIf/-Confirm, sur la façon d’écrire une bifurcation avec $PSCmdlet.ShouldProcess(), et sur la recommandation de ne pas trop compter sur la propagation de -WhatIf et de le transmettre explicitement aux commandes internes.  2 3 4

  5. Microsoft Learn, How to Write a PowerShell Script Module. Sur le fait qu’enregistrer un fichier avec l’extension .psm1 suffit à en faire un module de script, qu’il doit être enregistré dans un dossier portant le même nom que le script, que par défaut toutes les fonctions sont exposées et les variables ne le sont pas, et sur la recommandation d’expliciter les fonctions exposées avec Export-ModuleMember.  2 3 4

  6. Microsoft Learn, about_PSModulePath. Sur le fait que $env:PSModulePath est la liste des dossiers de recherche de modules, que les chemins par défaut des portées CurrentUser/AllUsers diffèrent entre PowerShell 7 et Windows PowerShell 5.1, et que l’emplacement de Documents peut changer avec OneDrive ou une redirection de dossier.  2 3 4 5

  7. Microsoft Learn, about_Modules. Sur le fait qu’un module situé sous PSModulePath est importé automatiquement à la première exécution d’une commande (chargement automatique des modules), sur la méthode d’installation manuelle consistant à copier le dossier du module dans son ensemble, et sur les emplacements de placement par défaut des modules.  2 3 4

  8. Microsoft Learn, New-ModuleManifest. Sur le fait que le manifeste de module (.psd1) est une table de hachage décrivant le contenu, les attributs et les prérequis du module, sans être obligatoire, que la seule clé obligatoire est ModuleVersion, et que New-ModuleManifest génère un modèle utilisable comme gabarit.  2 3 4 5

  9. Microsoft Learn, about_Execution_Policies. Sur le fait que RemoteSigned autorise les scripts créés localement même non signés mais exige une signature pour les scripts provenant d’Internet, que sur les systèmes qui ne distinguent pas les chemins UNC des chemins Internet, un script situé sur un chemin UNC peut se voir refuser l’exécution par RemoteSigned, et sur le déblocage via Unblock-File.  2

  10. Microsoft Learn, about_Functions_Advanced_Methods. Sur les méthodes de traitement d’entrée begin/process/end disponibles dans une fonction avancée, et sur le fait que la méthode ShouldProcess s’appelle depuis le bloc process et requiert une déclaration via l’attribut CmdletBinding.  2

  11. Microsoft Learn, Approved Verbs for PowerShell Commands. Sur le fait que le nom d’une commande doit suivre le format Verbe-Nom, sur la liste des verbes approuvés et la façon de la consulter avec Get-Verb, et sur l’affichage d’un avertissement à l’importation d’un module contenant un verbe non approuvé. 

  12. Microsoft Learn, about_Scripts. Sur le fait qu’un script s’exécute par défaut dans sa propre portée, où les fonctions, variables, alias et lecteurs créés n’existent que dans la portée du script, et sur le fait qu’exécuter le script avec un point et un espace en tête du chemin (le « dot-sourcing ») l’exécute dans la portée actuelle, les éléments créés restant dans la session après l’exécution. 

  13. Microsoft Learn, Export-ModuleMember. Sur le fait qu’Export-ModuleMember est l’applet de commande qui spécifie les membres exportés depuis un module de script, que sans spécification les fonctions et les alias sont exportés mais pas les variables, et que c’est facultatif mais considéré comme une bonne pratique pour exprimer l’intention de l’auteur.  2

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Que change l'ajout de [CmdletBinding()] au bloc param d'un script PowerShell ?
La fonction ou le script est alors traité comme une « fonction avancée » (advanced function) et acquiert le même comportement qu'une applet de commande compilée. Concrètement, les paramètres communs comme -Verbose ou -ErrorAction sont ajoutés automatiquement, la variable $PSCmdlet devient utilisable, et transmettre un paramètre non défini ou un argument positionnel excédentaire provoque une erreur de liaison. Comme un argument mal orthographié n'est plus ignoré silencieusement, ajouter cet attribut est la base pour tout script d'exploitation.
Faut-il écrire la vérification des arguments d'un script avec les attributs Validate ou avec une instruction if ?
La règle de base consiste à confier la vérification du format d'un paramètre aux attributs de validation comme ValidateSet, ValidateRange ou ValidateScript. La validation s'exécute avant le corps de la fonction : si la valeur est invalide, aucune ligne du traitement ne s'exécute et une erreur est levée immédiatement, ce qui évite l'accident classique du « traitement qui s'exécute à moitié puis se casse ». ValidateSet offre en outre l'avantage pratique de la complétion par tabulation. En revanche, la validation combinant plusieurs paramètres ou dépendant de l'état d'exécution doit être effectuée par une instruction if dans le corps de la fonction.
Où faut-il placer un module PowerShell fait maison (.psm1) ?
Placez-le dans un dossier portant « le même nom que le module », lui-même situé sous l'un des dossiers listés dans $env:PSModulePath. Pour un usage personnel, l'emplacement par défaut sous PowerShell 7 est $HOME\Documents\PowerShell\Modules ; pour un usage partagé par tous les utilisateurs, c'est $env:ProgramFiles\PowerShell\Modules. Sous Windows PowerShell 5.1, les chemins diffèrent et utilisent WindowsPowerShell\Modules dans les deux cas — attention à ce point. En le plaçant à cet endroit, il est chargé automatiquement à la première exécution de la commande, sans qu'il soit nécessaire d'écrire Import-Module.
Faut-il obligatoirement créer un manifeste de module (.psd1) ?
Ce n'est pas obligatoire. Un simple fichier .psm1 sans manifeste fonctionne déjà comme module. Le manifeste devient nécessaire au « stade de la distribution », lorsque vous voulez porter des métadonnées comme le numéro de version, la version de PowerShell requise, les modules dont il dépend, ou la liste explicite des commandes exportées ; il se génère alors avec New-ModuleManifest. La seule clé obligatoire du manifeste est ModuleVersion, donc il suffit de commencer avec une configuration minimale et de l'enrichir au besoin.
Pourquoi un script placé dans un dossier partagé interne se retrouve-t-il bloqué par la politique d'exécution ?
La politique par défaut RemoteSigned autorise l'exécution des scripts créés localement même non signés, mais exige une signature pour les scripts marqués comme provenant d'Internet. La documentation officielle précise elle-même que, sur les systèmes configurés sans distinction entre chemin UNC et chemin Internet, un script situé sur un chemin UNC peut se voir refuser l'exécution par RemoteSigned. Si vous envisagez de généraliser la distribution en interne, étudiez la combinaison signature de code et AllSigned, ou la configuration de la zone intranet.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog