PowerShell Modular

Viele Administratoren nutzen PowerShell nur zum Ausführen von einzelnen Befehlen oder Verketten mehrere Befehle per Pipeline. Wer aber auch Skripte in PowerShell schreibt, wird irgendwann bemerken, dass eine Modularisierung aus mehreren Gründen sinnvoll ist:

  • Wiederverwendung
    Code-Teile, die immer wieder erforderlich werden aber in PowerShell nicht vorhanden sind, kann man sich als eigene Funktionen bereitlegen und immer wieder verwenden. Es ist dabei natürlich nur bedingt sinnvoll, diese Funktionen als Textbausteine immer wieder in den eigenen Code einzufügen, da eine spätere Fehlerkorrektur oder Erweiterung dann überall nachgeführt werden muss. Hier ist es besser die Funktionen in Units oder MODULES abzulegen und nur zu referenzieren. "Große" Programmiersprachen machen das über "Imports"- Direktiven schon lange.
  • Verteiltes Arbeiten
    Die Modularisierung von eigenständigen Bausteinen hat aber auch noch den Vorteil, dass mehrere Personen an einem Projekt zusammen arbeiten können und sie sich kaum ins Gehege kommen; zumindest wenn Sie die Schnittstellen bestehen lassen und jemand nicht einen bösen Bug in ein Modul addiert hat.
  • Parallelisieren
    Einige Funktionen können parallel ausgeführt werden. Oft ist es dazu auch erforderlich, diesen Codeabschnitt als eigenes Skript bereit zustellen, z.B. Um es mit Invoke-Command auf viele Systeme parallel los zu lassen.
  • Erweiterbarkeit
    Eine vierte Anforderung habe ich in meinen Skripten ReportWeb, CheckExObject, MSXFAQ-Monitoring, wo ein Hauptprogramm flexibel mehrere Module einbinden muss, die je nach Bedarf eine weitere Verarbeitung durchführen.

Es gibt also genügend Gründe die verschiedenen Optionen zu kennen, mit denen PowerShell andere Codeteile integrieren kann.

Kriterien zur Bewertung

PowerShell bietet nun eine ganze Menge von Optionen an, um ein Skript in kleine Häppchen zu teilen und wieder zu verwenden. Ich versuche hier eine kurze Übersicht zu liefern, die natürlich veralten kann und ich bin auch kein 100% Programmierer. Korrekturen werden gerne angenommen. Zuerst die Kriterien:

  • Performance
    Was glauben sie, was einfacher ist? Ein Skripte lädt ein anderes Skript mit einer Funktion nach, die dann mehrfach aufgerufen wird oder ein Skript lädt zu jedem Aufruf einer Funktion das andere Skript nach?. Performance kann wichtig sein, wenn große Mengen an Elementen verarbeiten werden sollen. Allerdings sollten Sie auch überlegen. ob sich der Aufwand lohnt um ein paar Sekunden pro Stunde zu gewinnen. Wenn ein Skript ein anderes Skript jedes Mal nachlädt, ist es trotz Cache etwas langsamer aber flexibler. Wird das Modul geändert, müssen alle "Vorlader" neu gestartet werden. Dynamische Funktionen sind also besser durch Nachladen möglich.
  • Scope
    Wenn ein Skript ein anderes Skript oder eine Funktion in einem Modul aufruft, dann ist es wichtig zu wissen, auf welche Variablen das aufgerufene Skript zugreifen kann und wie es Werte zurück übergeben kann. Oft erfolgt die Rückgabe über STDOUT, manchmal kann das Skript aber auch die Variablen des aufrufenden Programms direkt ändern. Wägen sie Vorteile und Nachteile ab bezüglich Sicherheit, Speicherbedarf, Performance und Portabilität.
    "Lokal" bedeutet, dass der Code des untermoduls seinen eigenen Variablenbereich hat und in der Regel nicht die Variablen im aufrufenden Prozess ändern.
  • Remoting
    Um Funktion "Remote" auf einem anderen System auszuführen, müssen bestimmte Voraussetzungen erfüllt sein. Nicht alle Optionen erlauben eine "entfernte" Ausführung.
  • Parallelisierung
    Ähnliches gilt für die Parallelisierung. Nur ein Teil der Möglichkeiten erlaubt eine einfache Parallelisierung von Aufgaben.

Viele Wege führen nach ..

Neben den Modulen in Form von PS1M-Dateien sind weiterhin auch einfache PS1-Dateien natürlich nutzbar., dazu gibt es mehrere Varianten diese einzubinden

Einbindung Ladeverhalten Performance Scope Remote Parallelisierbar

PowerShell SnapIn

Vorher

Hoch

Entfällt

Nein

Nein

Powershell Modul

Vorher

Hoch

Lokal

Nein

Nein

Einbinden von DLLs

Vorher

Hoch

Lokal

Nein

Nein

Invoke-Command oder "&"-Aufruf

Inline

Mittel

Lokal

Ja

Ja

Invoke-Expression

Inline

Mittel

Lokal

Nein

Ja

DOT-Sourcing

Inline

Niedrig

Gleich

Nein

Nein

Ich versuche mal einige dieser Varianten gegenüber zu stellen.

Modularisierung mit "DOT Sourceing"

Wer auf der Suche nach "Modularen Skripten" ist, stößt unweigerlich auf die "DOT-Source"-Methode. Der Name kommt daher, weil das Script mit einem "Punkte" aufgerufen wird. Hier ein Beispiel:

write-host "Modul Start"

function test-dotsource {
   write-host "Dot Source funktioniert"
}
write-host "Modul End"

Speichern Sie diese paar Zeilen z.B.: als "test.ps1 Sie können es nun auf verschiedene Arten aufrufen:

# Einfacher Aufruf. Start und End werden ausgegeben, aber nichts verbleibt
.\test.ps1

# Aufruf mit Ampersand. Start und End werden ausgegeben, aber nichts verbleibt.
# Mit dem Ampersand kann man beliebige Strings, auch aus Variablen, ausführen.
& .\test.ps1

# Aufruf mit Punkt. Start und End werden ausgegeben und die Funktion bleibt vorhanden
. .\test.ps1

# d.h. nun können Sie einfach "test-dotsource"  eingeben

Nur der dritte Aufruf legt die im Skript hinterlegten Funktionen in der aktuellen Shell ab, so dass sie auch nachträglich genutzt werden können.

Achtung:
Allerdings gibt es das Problem, wenn man mehrere andere PS1-Dateien per Dot-Sourcing einbindet, die aber die gleichen Funktionen definieren. Dann gewinnt das letzte eingebundene Skript.

Achtung: alle Änderungen eines solchen Programms landen auch in der aufrufenden Umgebung. Das ist besonders knifflig, wenn Variablennamen übereinstimmen.

Diese beiden Fakten (Funktionen werden überschrieben und kein lokaler Scope der Variablen sind der Grund, warum ich die Dot-Source-Logik nur in ganz wenigen Fällen nutze, z.B.: um Konfigurationen oder Skripte einzubinden, die wirklich nur einmal vorkommen.

Es eignet sich aber durchaus zur Einbindung von PS1-Skripten, die eine Sammlung nützlicher Funktionen bereitstellen und ist zu Powershell-Modulen (Siehe PS Module) vergleichbar. Module können natürlich einfacher von Produkten im Rahmen einer Installation an den "richtigen" Platz gelegt werden und sind dann von jeder Shell unabhängig vom aktuellen Verzeichnis einbindbar. Ein Skript, welches per DOT-Sourcing eingebunden wird, muss mit dem Pfad spezifiziert werden. Das ist aber durchaus interessant, wenn diese Modularität eben nur für die aktuelle Skriptumgebung gefordert wird.

Unterroutinen als Powershell-Skript

Als klassisches Modul würde ich das nicht bezeichnen aber ich habe Anforderungen, dass ein Hauptskript dynamisch andere Code-teile einbindet und ausführt. Das kann das Skript natürlich dynamisch als Hintergrund-Job machen (Siehe PS Job). Oft reicht aber auch ein anderen Skript, welche die Ergebnisse wieder per Parameter erhält und per Pipeline retour gibt oder über globale Variablen arbeitet. Hier eine einfache nicht modulare Version:

Das Ganze habe ich dann in ein Hauptprogramm und eine Sub-Routine ausgelagert. Mit Hilfe einer "$global"-Variabel kann auch das aufgerufene Skript auf die Variable zugreifen. Bei umfangreicheren Skripten sollte man aber schon die Eingaben als Parameter übergeben und z.B. über STDOUT die Rückgaben einsammeln.

Beachten Sie, dass Diagnose-Ausgaben per "Write-Host" die Laufzeit solcher kurzen Skripte extrem verfälschen. Daher habe ich das Untermodul einfach die Variable aufaddieren und nur am Ende ausgeben lassen und folgende Testreihe gebaut.

  • Direkt
    Ich habe die "Aufaddierung" direkt im gleichen Skript ohne Modularisierung gemacht
  • PS1-Aufruf
    Ich habe das Script "sub1" einfach 1000 mal ohne Übergabe von Parametern aufgerufen. Mich hat am Ergebnis einfach interessiert, wie viel mehr Zeit es dauert, wenn ein Powershell-Script ein anderes Script aufruft und dafür eine eigene Umgebung bereitstellen muss
  • PS1-Aufruf mit Parameter
    Realistischer ist aber natürlich, dass so eine Unterroutine auch einen Wert übergeben bekommt und die Ergebnisse per STDOUT an den aufrufenden Prozess übergibt. Diese Messung erlaubt eine Abschätzung, was zusätzlich die Übergabe und Verarbeitung von Ergebnissen "kostet"

Hier die Ergebnisse für unterschiedliche Wiederholungszahlen:

  Direkt PS1-Aufruf PS1-Aufruf mit Parameter

Code

Write-host "Main: start"
$a=1

foreach ($count in (1..1000)) {
   $a++
}
Write-host "Ergebnis" $a
Write-host "Main: End"
Write-host "Main: start"

$global:a=1

foreach ($count in (1..1000)) {
   .\sub1.ps1
}
Write-host "Ergebnis" $a

Write-host "Main: End"
# sub1
# write-host "sub1:" $a
$global:a++
Write-host "Main: start"

$a=1

foreach ($count in (1..1000)) {
   $a= .\sub1.ps1 -var $a
}
Write-host "Ergebnis" $a

Write-host "Main: End"
# sub1
# write-host "sub1:" $a
param (
   $b
)
$b++

1 mal

9ms

14ms

14ms

1000 mal

10ms

2,1 sec

2,6 sec

2000 mal

20ms

4,1 sec

4,24 sec

10000 mal

30ms

20 Sek

22 Sek

Variable

Lokal

Global erforderlich, da jedes Skript seine eigene Umgebung hat

Übergabe als Parameter mit Rückgabe per STDOUT ist etwas langsamer als eine globale Variable

Sessions

Lokal

Die PowerShell Sessions werden, anders als beim PS Job, übergeben.

Die PowerShell Sessions werden, anders als beim PS Job, übergeben.

Bemerkung

Kein

Das Instanzieren eines PS1-Skript dauert bei mir ca. 2ms

Das Instanzieren eines PS1-Skript dauert bei mir ca. 2ms.

Gemessen wurde mit "Measure-Command {.\main.ps1}" mit mehreren Durchläufen. Meist war dabei 1 CPU-Kern komplett belegt. (25% auf einem QuadCore). Auf der einen Seite ist der Unterschied zwischen 30ms und 20Sek schon immens. 10.000 mal ein PowerShell-Skript starten ist aber auch nicht unbedingt der Regelfall. Für meine Anwendungsfälle ist dies vermutlich der am einfachsten zu verstehende und flexibelste Ansatz das PowerShell-Skript "erweiterbar" zu machen, z.B. in dem ein Hauptmodul die Objekte ermittelt und dann verschiedene Skripte zur Überprüfung oder Verarbeitung startet.

Eine produktive Subroutine wird sicher länger als wenige Nanosekunden dauern. Damit relativiert sich der Aufwand für den Aufruf.

Die erste Zeile eines einmaligen Aufrufs lässt diese Art der Modularität natürlich erst einmal noch gut aussehen. Erst viele Wiederholungen zeigen dann ein anderes Bild. Wer also 10.000 AD-Objekte mit vielen Einzelskripten bearbeiten will, kann dennoch mit den 20 Sekunden "Overhead" leben, wenn der Code damit besser pflegbar ist. Wer 1 Mio mal eine Funktion aufrufen will, sollte vermutlich gar nicht mit Powershell anfangen.

PowerShell und "Includes" mit "Invoke-Expression"

Es gibt noch eine Option, Code in anderen Dateien auszuöagern und dann einfach einzubinden. Sie müssen nur darauf achten ,dass Sie die Funktionen VOR dem Aufruf einbindet. Dabei hilft einem das Commandlet "Invoke-Expression", welches einen String als Befehl ausfuhrt. Damit man sicher ist, dass der String auch als PowerShell Skript verwendet wird können Sie bei der Deklaration mit "[scriptblock]" arbeiten:

$Script = "get-Process"
Invoke-Expression $Script

So können sogar ganze Dateien eingebunden werden. Hier ein "Hauptprogramm", welches Code einer Subroutine einbindet und aufruft bzw. nutzt.

# include-main.ps1
Write-Host "Include-main started"
$test1="test1"
write-host "Include-main Test1=$test1"
get-content -Path ".\include-part.ps1" | Invoke-Expression
write-host "Include-main Test2=$test2"
Write-Host "Include-main ended"
# include-part.ps1
Write-Host "Include-part started"
write-host "Include-part Test1=$test1"
$test2 = "test2"
write-host "Include-part Test2=$test2"
Write-Host "Include-part ended"

Achtung:
Der Include-Block wird ausgeführt, als wäre er im Hauptprogramm selbst enthalten. Es ist ein echter Include zur Laufzeit.

Leistungsfähig wird diese Funktion, wenn man den Code sogar auf einer Kommandozeile angeben kann. Insofern können viele Dinge so für Anwender geöffnet werden, z.B. indem der Anwender selbst Code "einbinden" kann. Analog gibt es noch Invoke-Item.

Klassen und Commandlets

Als alter VBScript-Programmierer habe ich natürlich die Funktion von "Klassen" schätzen gelernt. Leider steht hierzu in der PowerShell Hilfe wörtlich.

Although it is possible to create a class in Windows PowerShell, it's not a very straightforward process and definitely goes beyond the scope of this introductory manual. So, für the time being, forget we even mentioned it.
Quelle: Converting VBScript's Class Statement http://technet.microsoft.com/en-us/library/ee156807.aspx

Klassen in Powershell sind aber dennoch schon mit PowerShell 2 über den Umweg von C# Code möglich gewesen, die im Skript kompiliert wurden und mit Powerhell 5 gibt es endlich auch "[class] als direktes Schlüsselwort.

Vielleicht soll man Klassen einfach als Commandlet mit Visual Studio entwickeln oder gleiche Commandlets daraus machen.

Allerdings fordert so ein Vorgehen dann schon wieder eine sinnvoll Sourcecode-Verwaltung (GitHub?) und Compiler und ist eher nicht für ein "AdHoc"-Script geeignet.

Weitere Links