PowerShell Module

PowerShell Module sind ein einfacher und definierter Weg, eine PowerShell Umgebung mit zusätzlichen Befehlen und Commandlets zu erweitern. Allerdings müssen die Namen der bereitgestellten Funktionen natürlich eindeutig sind. Eigene Module sind natürlich auch immer ein Weg, Code einfach zu recyclen. Es gibt aber noch andere Ansätze mit PowerShell modular zu arbeiten, die ich auf Powershell Modular beschrieben habe.

Grundlagen- Wo und wie

PowerShell Module sind einem PowerShell Script sehr ähnlich. Sie haben den gleichen Aufbau und können mit den bekannten Editoren erstellt werden und haben einige deutliche Vorteile:

  • Einbinden und Entfernen bei Bedarf
    Im Gegensatz zu Funktionen aus Skripten können Module zur Laufzeit einfach eingebunden aber auch wieder entladen werden
  • Funktionen exportiert
    Per Default werden alle Funktionen im Modul exportiert. Dies kann aber über "Export-ModuleMember" im Modul selbst gesteuert werden, so das auch "private Funktionen" möglich sind, die aus der Session nicht gefunden und aufgerufen werden können. "Privat" sind diese natürlich nur hinsichtlich des Code. Der Source-Code eines PSM1 ist natürlich einsehbar.
  • Lokale Variablen
    Alle in dem Modul verwendeten Variablen sind per Default nicht außerhalb sichtbar.

Interessant ist, dass die Funktionen z.B. z.B. eigene Variablen und Objekte verwenden können und diese zwischen den Aufrufen bestehen bleiben. Es ist damit zwar noch keine Klasse, weil ein Modul nur einmal eingebunden werden kann, aber für viele Zwecke wird oft nur ein "Objekt" verwendet.

Laden, Finden, Sharen

Wenn Sie ein Modul mit "Add-Module" hinzufügen und sie keinen absoluten Pfad mit angeben, dann sucht die PowerShell ein PSModulePath ab.

Beachten Sie, dass die PowerShell 1-6 einen anderen Pfad haben als PS7 und höher

# PS5
PS C:\> $env:PSModulePath
C:\Users\fcarius\SkyDrive\Dokumente\WindowsPowerShell\Modules;
C:\Program Files\WindowsPowerShell\Modules;
C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules;
C:\Program Files\Common Files\Skype for Business Server 2019\Modules\;
C:\Program Files\Common Files\Skype for Business Online\Modules\;
C:\Program Files (x86)\Microsoft Azure Information Protection\Powershell

In meiner aktuellen PowerShell 7 (Jan 2021) war die Liste noch deutlich länger. (Umbruch zur Lesbarkeit addiert)

# PS7 
PS C:\> $env:psmodulepath
C:\Users\fcarius\OneDrive\Dokumente\PowerShell\Modules;
C:\Program Files\PowerShell\Modules;c:\program files\powershell\7\Modules;
C:\Program Files\WindowsPowerShell\Modules;
C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules;
C:\Program Files\Common Files\Skype for Business Server 2019\Modules\;
C:\Program Files\Common Files\Skype for Business Online\Modules\;
C:\Program Files (x86)\Microsoft Azure Information Protection\Powershell

Interessant ist hier, dass ganz am Anfang ein Verzeichnis in meinem Home-Directory zu finden ist und wenn ich die Ordnerumleitung für OneDrive aktiviere, dann werden die Bibliotheken sogar über den Weg repliziert. Ich habe damit auf allen Clients immer den gleichen Stand. Das kann aber auch "größer" werden.

Auch bei der Abfrage der aktuellen Version gibt es zwei Commandlets, die unterschiedliche Daten liefern

PS C:\> Get-Module powershellget -All | fl version,path

Version : 2.2.3
Path : C:\Users\fcarius\SkyDrive\Dokumente\WindowsPowerShell\Modules\PowerShellGet\2.2.3\PSModule.psm1

PS C:\> Get-InstalledModule powershellget -AllVersions | fl version,installedlocation

Version : 2.2.3
InstalledLocation : C:\Users\fcarius\SkyDrive\Dokumente\WindowsPowerShell\Modules\PowerShellGet\2.2.3

Version : 2.2.5
InstalledLocation : C:\Users\fcarius\SkyDrive\Dokumente\WindowsPowerShell\Modules\PowerShellGet\2.2.5

Die PowerShell beobachtet dabei die Daten in der folgenden Reihenfolge:

  • PSD1 - Manifest-Datei
  • PSM1 - Skriptmodul
  • DLL - Kompiliertes Modul

Ein Modul wird aber immer nur einmal geladen. Gerade wenn Sie Module entwickeln und ändern müssen Sie das Modul in der TestUmgebung immer erst entladen und neu laden oder mit "-force" laden. Bestehende Variablen im Modul werden dann aber natürlich zurück gesetzt.

Wenn man gewissenhaft Module in sich abgeschlossen entwickelt, z.B. auch unabhängig von globalen Voraussetzungen, dann können Sie sich eine Bausteinsammlung aufbauen, die sie immer wieder verwenden können.

Module können sogar verschachtelt werden. Schon fast wie wenn Sie ein bestehendes Module durch eigene Funktionen bereichern können. Wenn ein fertiges Modul ihnen zu viele Parameter hat, können Sie einfach ein eigenes Modul nutzen, welche das andere Modul einbindet und ihre lokal gültigen Parameter vorausfüllt. Dabei sind nur die Commandlets des Hauptmoduls erreichbar.

Libraries, Versionen und Updates

Mit "Install-Module" können sie ganz schnell neue Bausteine in ihre PowerShell einbinden. Die Module kommen aus dem Internet aus mehr oder minder vertrauenswürdigen Quellen wie z.B. https://www.powershellgallery.com/. Hier lohnt sich immer mal ein Blick. Siehe dazu auch PowerShell Gallery. Allerdings machen die Module kein automatisches Update und sie können sogar mehrere Versionen nebeneinander betreiben. Das füllt nicht nur die Festplatte auf sondern könnte auch ein Risiko durch alten fehlerhaften Code beinhalten. Ein Update ist aber auch nicht immer passend, wenn neuere Version Commandlets gestrichen oder verändert haben. Hinzu kommt, dass die Module je nach Powershell-Version, CPU-Version und Benutzer an unterschiedlichen Orten installiert werden, genau genommen müssten Sie eine Tabelle in der folgenden Art führen:

User LocalSystem CurrentUser
BIT-Version 32bit 64bit 32bit 64bit
PowerShell Version 5/6 7 5/6 7 5/6 7 5/6 7

Modulname

 

 

 

 

 

 

 

 

Modulname

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Und das gilt nur für den aktuellen Benutzer:

Es ist nervig, wenn Sie ein Module als Benutzer aktualisieren wollen, was ein Administrator installiert hat. Dann muss auch das Update durch einen Administrator erfolgen.

Tipp: Installieren Sie Module zumindest auf Servern und gemeinsam genutzten Computern als "Administrator", damit alle dort angemeldeten Benutzer die gleichen Versionen nutzen.
Auf individuellen Systemen verzichte ich auf Admin-rechte und zentrale Installation und überlasse es dem Benutzer.

Folgendes Skript prüft alle installierten Module, ob es neuere Versionen gibt:

# Check installed module and show version 
$installedmodules = Get-InstalledModule

foreach ($currentmodul in $installedmodules) {
   Write-Host “Check: $($currentmodul.Name) – $($currentmodul.version)" -nonewline
   $latestAvail = Find-Module $currentmodul.Name
   if ($currentmodul.version -eq $latestAvail.version) {
      Write-host " Current Version installed" -foregroundcolor green
   }
   else {
      Write-host " Newer Version $($latestAvail.version) available” -foregroundcolor yellow
   }
}

Ein "Update-Module" installiert nur die neueste Version aber entfernt keine frühere Version. Es könnte ja Abhängigkeiten zu früheren Versionen geben.

Um Update habe ich mir ein Skript gebaut, welches ich bei Bedarf in der jeweiligen PowerShell ausführen. Ich versuche mich dazu aber schon auf "PowerShell 7 x64" zu beschränken und nutzt nur bei Inkompatibilitäten die andere Versionen. Das Skript prüft nicht auf Abhängigkeiten!

Write-Host "Sammle alle installierten Module"
$installedmodules = get-installedmodule

Write-Host "Starte Verarbeitung"
foreach ($module in $installedmodules) {
   write-host "Modul $($module.name)  -  Check for Updates and try update"
   update-module $module.name -Force
   write-host 
   $latestversion = get-installedmodule -name $module.name
   $allversions = get-installedmodule -name $module.name -allversions
   if ($allversions.count -ge 1) {
      write-host "  Anzahl installierter Versionen $($allversions.count)"
      foreach ($entry in $allversions) {
         if ($entry.version -ne $latestversion.version) {
            write-host "Uninstall Modul: $($entry.name)   Version: $($entry.version)" -nonewline
             $entry | uninstall-module -force
             write-host " DONE"
         }
         else {
            write-host "  Keep current Version $($latestversion.version)"
         }
      }
   }
   else {
      write-host " Only one Version $($latestversion.version)"
   }
}
write-host "done"

Die "Deinstallation" können Sie natürlich auch ganz einfach von Hand durchführen. Einfach das Verzeichnis löschen.

Beispiel einer PSM1-Datei

Es gibt viele Beispiele im Internet, die den Einsatz von Modulen zeigen. Ich möchte mich daher auf eine einfaches Modul als Muster beschränken:

# Modulsample to explain basic modules
[long]$script:test=0  # initialize Variable

function add-Modulsample {
   $script:test++
   write-host "Module1: Sub Plus-Modulsample done: Test=$script:test"
}

function get-Modulsample {
   $script:test
   write-host "Module1: Sub Get-Modulsample done: Test=$script:test"
}

function Set-Modulsample ($value ){
   $script:test = $value
   write-host "Module1: Sub Set-Modulsample done: Test=$script:test"
}

# Es ist ratsam am Ende eine Ausgabe zu generieren. Diese wir beim Laden des Moduls angezeigt
write-host "modulsample: Ready to use"

Diese Datei kann dann überall liegen und einfach mit "import-module <pfad\dati.psm1> importiert werden.

Auf ein paar Dinge möchte ich hier hinweisen.

  • Ausgabe am Anfang/Ende
    Wenn Sie außerhalb der Funktionen, die später als Commandlet agieren auch Code verwenden, dann wird dieser beim Laden des Module direkt ausgeführt. Es ist ratsam hier eine Bildschirmausgabe vorzusehen.
  • Initialisierung
    Sie können in dem Code aber auch schon erste Komponenten Initialisieren. Ich nutze dies im Beispiel für die Variable "Test", welche durch das Prefix "$Script:" innerhalb des Skripts als global definiert wird. Sie könnten hier aber auch z.B. SQL-Connections, PSSessions oder ADSI-Verbindungen aufbauen.
  • Variablen-Scope
    Sie sehen hier auch die Feinheiten beim Einsatz von Variablen. "Test" ist global bezüglich des Skripts, sonst könnte ich die Werte nicht in den Funktionen verändern. Die Variable ist aber nicht in der Session erreichbar, welche diese Modul importiert.

Experimentieren Sie einfach etwas mit Modulen und verändern Sie den Beispielcode, um das unterschiedliche Verhalten zu sehen. Vergessen Sie aber nicht nach Änderungen am Modul diese neu zu laden.

Module und Klassen

Noch sind Module einfach weitere PowerShell-befehle, die Sie nach dem Import nutzen können. Aber damit sind es noch keine Klassen, die mehrfach instanziert werden können und ihre internen Wert und Funktionen und nach extern erreichbare Properties und Methoden haben. Und genau das geht auch mit Modulen. Sie müssen Sie beim "Import-Module" oder New-Module einfach mit "-AsCustomObject" aufrufen. Damit werden aus den Funktionen entsprechende Methoden und exportierte Variablen werden zu Properties.

Hierbei zeigen sich aber zwei Probleme:

  • Methoden sind Funktionen mit Parametern
    Es müssen bei der Methode immer alle Parameter angegeben werden. Die bislang gekannte Flexibilität
  • Funktionsnamen "Verb-Noun" funktionieren nicht
    Zwar wird das Modul als Objekt mit der Methode mit dem Namen importiert aber ist nicht aufrufbar. Das ist ja auch nicht weiter schlimm, wenn man die Funktionen der Moduls dann einfach umbenennt.

Mit beiden Einschränkungen kann man natürlich leben auch wenn Sie nicht schön sind. Um aber nicht in Konflikt mit anderen Namen zu kommen, sollte so ein Modul dann immer mit dem Parameter "CustomObject" geladen werden.

Weitere Links

Advanced Automation using Windows PowerShell 2.0
http://channel9.msdn.com/Events/TechEd/NorthAmerica/2011/WSV406
Einstieg in Module ab 38. Minute

Windows PowerShell Modules - Patterns and Practices - Dan Harman
http://www.youtube.com/watch?v=yDmkXZVD55w
US TEC 2011 PowerShell Deep Dive conference, Windows PowerShell Program Manager Dan Harman

Die PowerShell Module 4:28Min
http://www.youtube.com/watch?v=Tmf7EFUXEhg&feature=related