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.
- PS Module
- How to Reuse Windows
PowerShell Functions in Scripts
http://blogs.technet.com/b/heyscriptingguy/archive/2010/08/10/how-to-reuse-windows-PowerShell-functions-in-scripts.aspx
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.
- about_Scopes
https://technet.microsoft.com/de-de/library/hh847849.aspx - How to Reuse Windows PowerShell
Functions in Scripts
https://blogs.technet.microsoft.com/heyscriptingguy/2010/08/10/how-to-reuse-windows-powershell-functions-in-scripts/
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.
- Protecting Against Malicious Code Injection
http://blogs.msdn.com/PowerShell/archive/2006/11/23/protecting-against-malicious-code-injection.aspx
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.
- PowerShell-Komponente im
Eigenbau
http://www.codingfreaks.de/2009/05/31/powershell-komponente-im-eigenbau/
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
- PS Module
- PS Parallel
- PS Remote
- PS Klassen
- How to dot-source a
PowerShell script
http://www.miru.ch/2009/03/how-to-dot-source-a-PowerShell-script/ - Why would I want to call a
function that exists in a
separate script?
http://www.PowerShellpro.com/function-calling/144/ - Organizing Script Code –
Calling Scripts from Other
Script Files
http://www.PowerShellpro.com/organizing-PowerShell-script-code-is-a-snap-in/102/ - PowerShell: Running
Executables
http://social.technet.microsoft.com/wiki/contents/articles/7703.powershell-running-executables.aspx#The_Call_Operator_amp - “PowerShell Private Gallery”
– Your internal
PowerShell Repository
https://kurtroggen.wordpress.com/2016/06/06/powershell-private-gallery-your-internal-powershell-repository/
Ansatz eines eigenen privaten Repository für interne Skripte - Install-Module : A parameter
cannot be found that matches
parameter name AllowPrerelease
https://evotec.xyz/install-module-a-parameter-cannot-be-found-that-matches-parameter-name-allowprerelease/ - Installing PowerShell
modules behind corporate proxy
https://daveshap.github.io/DavidShapiroBlog/powershell/kb/2021/03/12/install-powershell-modules.html