Passer d’un script à un « bon » script (PowerShell)

Bonjour à tous,

Dans cet article « un peu particulier » qui ne va pas être réellement un tutoriel, je vais essayer de vous pousser à la réflexion pour passer d’un script « simple » à un « bon » script.

Avant de rentrer dans le sujet, je vais poser le contexte de cet article afin de comprendre pourquoi j’ai écrit ces lignes.

Je suis mentor sur OpenClassrooms et dans un des parcours de formation, les étudiants doivent écrire plusieurs scripts. Avec un peu de recul maintenant, je me suis aperçu qu’ils y arrivent, mais avec des scripts que je qualifierai de « basique », en s’arrêtant au strict minimum sans réelle réflexion sur leur code.

En gros voici l’énoncé du script que l’on va réaliser :

Réaliser un script PowerShell, qui permet d’extraire dans un fichier (texte ou csv) les utilisateurs membres d’un groupe Active Directory, le script devra pouvoir s’exécuter de façon silencieuse ou/(et) interactive.

Avant de se lancer dans le script, on va définir silencieux et interactif :

  • Silencieux : le script n’a pas besoin d’interaction avec l’utilisateur
  • Interactif : le script demandera des informations à l’utilisateur

Pour illustrer le code, je vais me baser sur mon groupe GG_Super_heros, qui comme le montre la capture ci-dessous est composé d’un utilisateur et d’autre groupe :

Je vais directement me lancer dans le script PowerShell, mais avant, il est impératif d’avoir les bases d’algorithmie

Souvent, voici le premier script proposé par les étudiants :

La première chose à dire, c’est Merci GOOGLE ! Alors oui cela répond plus ou moins à la question, car effectivement, cela exporte les membres du groupe GG_Super_heros dans un fichier CSV de façon silencieuse …

Faire un script d’une ligne, statique sans contrôle, y a-t-il réellement un intérêt, car autant ouvrir une fenêtre PowerShell et taper le code non ?

De plus dans mon cas, cela ne va totalement répondre à l’énoncé, car dans mon groupe, j’ai des groupes et je ne vais donc pas avoir que des utilisateurs.

Partant de ce bout de code, je vais expliquer pas à pas comment réaliser un « bon » script.

Qu’est-ce qu’un « bon » script ? Il faut définir cela, c’est un script qui sera dynamique (on pourra changer le groupe, le nom du fichier …), il pourra être silencieux ou interactif (ce n’est pas toujours possible) et on traitera les erreurs (que ce passe t il si le groupe n’existe pas ?) et surtout il répondra à l’énoncé dans tous les cas, c’est-à-dire avoir les utilisateurs qui sont membres du groupe et on va commencer par là.

Je vais traiter la partie export dans le fichier à la fin, je vais donc faire en sorte de supprimer la sortie fichier et avoir une sortie écran en supprimant l’export et je vais ajouter le type d’objet.

Voici donc le script de départ :

Voici la sortie console :

Premier constat, on ne répond plus l’énoncé, car j’ai des groupes et pas que des utilisateurs.

Pour corriger ce problème rien de bien compliqué la solution est donnée, il suffit d’aller faire un tour ici : Get-ADGroupMember (ActiveDirectory) | Microsoft Docs pour trouver la réponse et ajouter le paramètre -Recursive pour ne plus qu’avoir les utilisateurs.

Ci qui nous donne :

Voici maintenant le résultat :

C’est déjà mieux, mais pas encore bon, on n’a pas balayé le cas si dans le groupe j’ai des utilisateurs et des ordinateurs, si j’ajoute un ordinateur dans le groupe et que je passe cette commande, j’aurai l’ordinateur.

Il faut maintenant que l’on filtre le résultat, pour ne garder que les utilisateurs, pour cela on va ajouter un filtre de sortie where à notre cmdlet.

En powershell ajoute des options de sortie ave le | .

Pour le filtre (condition), on va utiliser where comme en SQL.

Ce qui nous donne maintenant :

Maintenant la Cmdlet Get-ADGroupMember retourne les utilisateurs qui sont dans le groupe.

On va maintenant passer à l’amélioration de notre script.

La première étape va être de stocker le nom du groupe dans une variable et d’utiliser cette variable dans la cmdlet Get-ADGroupMember.

Ce qui nous donne le script suivant :

En utilisant une variable, il est plus simple de changer le groupe, mais ce n’est pas très dynamique.

Ce que l’on va faire maintenant, c’est de déclarer la variable en paramètre et ce qui va nous mettre d’appeler notre script de cette manière :

fichier-powershell.ps1 -NomGroupe "GG_Super_heros"

Le code du script maintenant :

La première étape de notre script est terminée, nous avons script dynamique qui s’exécute de façon « silencieuse ».

Avant complexifié le script, nous allons rentre le script interactif et silencieux, comme cela il répond à l’énonce (et même plus).

Pour faire cela, nous allons utiliser une condition (if) afin de tester au début si la variable $NomGroupe est vide, si elle est vide, on demande à l’utilisateur de saisir le nom du groupe.

Afin de faire cela, nous allons utiliser Read-Host.

Ce qui donne :

Si on s’arrête là, le script répond à la demande, il suffit d’ajouter l’export CSV, mais c’est loin d’être parfait ! On pourrait avant de faire de récupérer les utilisateurs, s’assurer que le groupe Active Directory ? Et si, il n’existe pas, sortir du script avec un code erreur.

On va ajouter cette vérification, pour cela on récupère le groupe avec la cmdlet Get-ADGroup et on va utiliser try / catch pour gérer l’erreur et si la commande est en erreur, on affiche un message et on arrête le script.

Voici maintenant le script :

Avec ce script, on arrête le script si le groupe n’existe pas, ce qui évite l’affichage d’un message d’erreur.

Il ne reste plus qu’à faire l’export en CSV en ajoutant le | Export-CSV … Et vue que l’on est sympa, on va afficher un message en indiquant l’emplacement et le nom du fichier. Je vais aussi ajouter le nom du groupe dans le nom du fichier, ce qui permet de jouer plusieurs fois le script sans écraser le fichier.

Ce qui donne :


Je vais m’arrêter ici pour cet article, le but étant de vous faire prendre conscience que le développement d’un script nécessite de la réflexion pour aboutir à quelque chose de complet qui va couvrir un maximum de cas.

Si vous souhaitez l’améliorer c’est possible voici ce qui pourrait être ajouté :

  • Stocké dans une variable le type de lancement (silencieux / interactif)
  • Si le script est interactif et que le groupe n’existe pas, demander une nouvelle saisie utilisateur

Bon courage pour vos futur script.



Notable Replies

  1. Bonjour, comme toujours Post très intéressant. Ce que j’apprécie particulièrement, c’est la démarche didactique.
    On passe d’un ensemble de cmdlets basiques, à l’optimisation de la requête, puis à la gestion des erreurs.
    j’ai cependant quelques remarques que je veux constructive.

    Si nous prenons ceci :

    Get-ADGroupMember -Identity $NomGroupe -Recursive | Select name, objectClass | Where { $_.objectClass -eq "user" }
    

    Nous pouvons voir 3 cmdlets qui sont chainées via le pipeline. La première ramasse une collection d’objects, la seconde filtre sur cette collection pour ne conserver que 2 propriétés, et enfin la 3ème, filtre une seconde fois pour ne conserver que les « Users ».
    Ce n’est pas totalement optimisé en terme de requête. Il aurait fallut bâtir la requête ainsi :

    Get-ADGroupMember -Identity $NomGroupe -Recursive |  Where { $_.objectClass -eq "user" | Select name, objectClass  }
    

    Dans le cas présent (et en toute franchise, je n’ai pas vérifié via Measure-Command), mais il y a une grande règle sui s’exprime ainsi « Filter Left, Format right ». Il faut filtrer le plus possible à gauche, et la présentation (format) c’est à la fin. Tout ça pour passer une collection moins importante dans le pipeline pour les cmdlets suivantes.
    La démarche est donc la suivante :

    • Est-ce que la 1ère cmdlet a un paramètre -Filter ? Si oui, on filtre sur celle-ci. Ce n’est pas le cas, dans l’exemple présenté

    • Filtrage sur la seconde cmdlet avec Where-Object

    • Et enfin format, avec Select-Object pour la 3ème.

    1. Il faut éviter d’utiliser des alias dans les scripts (dans un shell, on fait ce qu’on veut, mais dans les scripts, il faut éviter). Ca rend les scripts moins lisibles et compréhensibles. Des scripts blindés de %, ? et autres alias, deviennent juste du charabia pour ceux qui les lisent et essaient de les comprendre.

    2. Concernant le « Error Handling » (gestion des erreurs).
      Très bien l’utilisation du Try… Catch, … mais tu as oublié 2 points. le premier est primordial, le second plus « anecdotique ».

    • La (ou les) cmdlet qui est passée dans le Try doivent être suivi impérativement du paramètre -ErrroAction Stop, … sinon cela ne passera jamais au Catch.

    • Bien le message d’erreur dans le Catch, mais tu aurais pu également « trapper » l’erreur avec $_ ou $_.Exception.Message ou encore $Error[0] (la dernière des erreurs).

    1. Si on destine ce script à un usage interactif, le Write-Host (avec ou sans paramètre en sus comme -ForegroundColor xxxx) est OK. Cependant, si le script est destiné à un usage silencieux, il faut éviter le Write-Host et lui préférer des Write-Output, Write-Information, Write-Error, car il n’y a pas de host dans ce cas.
      Un moyen simple de s’apercevoir de tout ceci est d’utiliser le module PSScriptAnalyzar. Une seule cmdlet à connaitre Invoke-ScriptAnalyzer -Path \monscript.ps1 et en console, cela retourne les Avertissements et Erreurs par rapport au bonnes pratiques. Ce module est à ajouter dans PS ou ISE, mais est en standard avec VisualStudio Code.

    Dans le cas d’un usage silencieux (tâche planifiée par ex.), on peut demander l’exécution de script.ps1 + passer les paramètres, ou directement script.ps1 pour peu que les valeurs des paramètres (section param) aient une valeur par défaut.

    1. Le Exit à la fin, … on peut s’en passer, C’est fini, cela stoppe tout seul :slight_smile:

    2. Tu as une seule variable collée en dur dans ton script (le nom du fichier de sortie). Tu aurais pu également la même en param() avec sa valeur par défaut (la convention de nommage que tu as défini). Ainsi, pas la peine de se « palucher » tout le code pour modifier le nom du fichier de sortie :slight_smile: … et cela aurait introduit la notion de « valeur par défaut » pour certains paramètres.

    3. J’allais oublier ce point également. Tu testes ta cmdlet Get-ADGroupMember mais pas le path de sortie… Tu pourrais soit créer le path (du répertoire) s’il n’existe pas, ou peut-être mieux t’affranchir de tout cela et utiliser la variable automatique $PSScriptRoot (path ou est le script), car ça on est certain que cela existe (voire même un sous répertoire que tu pourrais créer (ex. Sortie) dans $PSScriptRoot.

    Passons sur ces critiques, que je veux constructive, et sur les points très positifs :

    • Il est (bien trop) rare de voir une section param dans les scripts (dans les fonctions, oui, mais dans les scripts cela reste rare), … et pourtant qu’est-ce que c’est bien d’avoir tous les paramètres en début de script présentés comme cela. Excellent donc :slight_smile:

    • Les variables que tu crées ont des noms représentatifs que ce qu’elles contiennent. Ca c’est un point notable, que l’on voit malheureusement trop peu fréquemment.

    Olivier
    P.S. : Et une tartine de plus :-), décidément on ne se refait pas.

  2. Avatar for rdrit rdrit says:

    Bonjour Olivier,

    Merci pour ton commentaire constructif, j’espère qu’il sera lu avant autant d’attention que l’article, tu facilites le travail des étudiants s’ils souhaitent optimiser et corriger le script :wink:

    Après le but de l’article n’était pas forcément de leur fournir le script « parfait », mais donner une méthodologie de réflexion…

    On aurait aussi pu dire que le script manque de commentaire … (ce qui est volontaire dans le contexte global)

    Romain

Continue the discussion at community.rdr-it.io

Participants