PowerShell: Scripts, Funktionen und Module einbinden
In diesem Artikel geht es um das Einbinden von PowerShell Scripts, Funktionen und Modulen in die aktuelle Session oder ein anderes Script. Zunächst benötigen wir ein einfaches Script, welches wir unter Hello-World.ps1 speichern.
Ein PowerShell Script aufrufen
Führen wir das Script einmal aus:
PS D:\Scripts\> .\Hello-World.ps1 Hello World [Date] 2013-11-17 [Time] 17:05 PS D:\Scripts\>
Das Ergebnis ist wenig überraschend – das Script wird einfach ausgeführt! Auf diesem Weg können wir das Script natürlich auch aus einem anderen Script aufrufen.
Dot-Sourcing
Rufen wir das Script ein weiteres Mal auf, diesmal jedoch per “Dot-Sourcing”, also mit einem zusätzlichen vorangestellten Punkt:
PS D:\Scripts\> . .\Hello-World.ps1 Hello World [Date] 2013-11-17 [Time] 17:05 PS D:\Scripts\>
Das sieht zunächst nicht anders aus als beim ersten Aufruf. Den Unterschied erkennen wir erst wenn wir Folgendes eingeben:
PS D:\Scripts\> Get-Variable D* Name Value ---- ----- Date 2013-11-17 DebugPreference SilentlyContinue PS D:\Scripts\> Get-Variable T* Name Value ---- ----- Time 17:05 true True PS D:\Scripts\>
Die in dem Script definierten Variablen $Date und $Time sind diesmal erhalten geblieben und können weiter von uns genutzt werden! Tatsächlich gilt das nicht nur für die Variablen, sondern auch für die in dem Script definierten Funktionen.
Wenn wir also eine coole Funktion geschrieben haben, die wir in der Console oder aus anderen Scripts aufrufen möchten, speichern wir die Funktion einfach als Script und binden dieses dann per Dot-Sourcing in unsere aktuelle PowerShell Session ein.
Funktionen einbinden
Probieren wir das gleich einmal aus. Unser Script Hello-World.ps1 ändert sich wie folgt:
Function Write-HelloWorld
{
$Date = Get-Date -Format yyyy-MM-dd
$Time = Get-Date -Format HH:mm
Write-Output "Hello World"
Write-Output "[Date] $Date"
Write-Output "[Time] $Time"
}
Jetzt binden wir das Script per Dot-Sourcing in unsere Session ein und rufen die Funktion Write-HelloWorld auf:
PS D:\Scripts\> . .\Hello-World.ps1 PS D:\Scripts\> Write-HelloWorld Hello World [Date] 2013-11-17 [Time] 17:42 PS D:\Scripts\>
Unsere Funktion “Write-HelloWorld” kann nun also wie ein ganz normales Cmdlet aufgerufen werden. Auch in der Liste der PowerShell-Commands ist sie jetzt verzeichnet:
PS D:\Scripts\Include_Scripts> Get-Command Write-* CommandType Name ModuleName ----------- ---- ---------- Function Write-HelloWorld Cmdlet Write-Debug Microsoft.PowerShell.Utility Cmdlet Write-Error Microsoft.PowerShell.Utility Cmdlet Write-EventLog Microsoft.PowerShell.Management Cmdlet Write-Host Microsoft.PowerShell.Utility Cmdlet Write-Output Microsoft.PowerShell.Utility Cmdlet Write-Progress Microsoft.PowerShell.Utility Cmdlet Write-Verbose Microsoft.PowerShell.Utility Cmdlet Write-Warning Microsoft.PowerShell.Utility PS D:\Scripts\>
Soweit – so gut!
Wenn wir größere Scripte schreiben, werden diese wahrscheinlich aus mehr als einer Funktion bestehen. Möglicherweise sollen aber gar nicht alle Funktionen nach außen sichtbar sein.
“Private” Funktionen
Damit wir uns das näher anschauen können, bauen wir unser Script ein wenig um.
Function Write-HelloWorld
{
$Date = GetDateString
$Time = GetTimeString
Write-Output "Hello World"
Write-Output $Date
Write-Output $Time
}
Function GetDateString
{
$Date = Get-Date -Format yyyy-MM-dd
Write-Output "[Date] $Date"
}
Function GetTimeString
{
$Time = Get-Date -Format HH:mm
Write-Output "[Time] $Time"
}
GetDateString und GetTimeString sollen dabei Unterfunktionen darstellen, die nicht für die Allgemeinheit gedacht sind und ausschließlich durch die Hauptfunktion aufgerufen werden sollen. Mit dem o. g. Beispiel ist das jedoch nicht der Fall:
PS D:\Scripts\> . .\Hello-World.ps1 PS D:\Scripts\> Get-Command Get* CommandType Name ModuleName ----------- ---- ---------- Function GetDateString Function Get-DscConfiguration PSDesiredStateConfiguration Function Get-DscLocalConfigurationManager PSDesiredStateConfiguration Function Get-DscResource PSDesiredStateConfiguration Function Get-FileHash Microsoft.PowerShell.Utility Function Get-IseSnippet ISE Function Get-LogProperties PSDiagnostics Function GetTimeString PS D:\Scripts\>
Doch wie können wir unsere Unterfunktionen verstecken? Am Einfachsten geht das mit Verschachtelung. Die Unterfunktionen werden innerhalb der Hauptfunktion definiert und stehen dann auch nur dort zur Verfügung.
Generell ist bei Funktionen zu beachten, dass die Definition der Funktion VOR ihrem ersten Aufruf erfolgen muss, anderenfalls läuft das Script auf einen Fehler. Wenn wir die Unterfunktionen jetzt noch zwecks Übersichtlichkeit einrücken, sieht unser Script so aus:
Function Write-HelloWorld
{
Function GetDateString
{
$Date = Get-Date -Format yyyy-MM-dd
Write-Output "[Date] $Date"
}
Function GetTimeString
{
$Time = Get-Date -Format HH:mm
Write-Output "[Time] $Time"
}
$Date = GetDateString
$Time = GetTimeString
Write-Output "Hello World"
Write-Output $Date
Write-Output $Time
}
Binden wir das Script jetzt wie gehabt per Dot-Sourcing ein, steht uns lediglich die Funktion Write-HelloWorld zur Verfügung. Die Unterfunktionen hingegen sind versteckt und können damit nicht unbefugt verwendet werden.
Das Wichtigste zuerst
Mit nur 21 Zeilen ist unser Script noch recht übersichtlich. In der Praxis kann ein Script aber leicht auf hunderte oder tausende Zeilen anwachsen. Ich persönlich mag es, wenn ich in einem Script schnell erkennen kann was es eigentlich tut. Das geht am einfachsten, indem das eigentliche Script am Anfang steht und nicht erst nach 1274 Zeilen und der Definition aller benötigten Funktionen beginnt.
Glücklicherweise lässt sich das relativ einfach umsetzen – wir definieren auch das “Hauptprogramm” als Funktion, und rufen dieses nach der Deklaration der anderen Funktionen auf. Diese Hauptfunktion nenne ich in Anlehnung an diverse andere Programmier- und Scriptsprachen stets “Main”.
Passen wir also unser Script entsprechend an:
Function Write-HelloWorld
{
Function Main
{
$Date = GetDateString
$Time = GetTimeString
Write-Output "Hello World"
Write-Output $Date
Write-Output $Time
}
Function GetDateString
{
$Date = Get-Date -Format yyyy-MM-dd
Write-Output "[Date] $Date"
}
Function GetTimeString
{
$Time = Get-Date -Format HH:mm
Write-Output "[Time] $Time"
}
Main
}
Angenommen, Sie haben eine oder mehrere Funktionen geschrieben, die Sie sehr häufig verwenden. In diesem Fall möchten Sie vielleicht, dass die Funktionen dauerhaft in der PowerShell zur Verfügung stehen und nicht ständig neu eingebunden werden müssen.
Funktionen ins Profil einbinden
Eine Möglichkeit ist die Verwendung eines PowerShell-Profils. Wie das funktioniert ist z. B. hier beschrieben.
In dem Profilscript wird die Funktion dann wie gehabt per Dot-Sourcing eingebunden. Da ich mein Profilscript nicht ständig anpassen möchte, habe ich einen generischen Ansatz gewählt:
Meine Scripte entwickle ich unter “D:\Scripts”. Dort habe ich den Unterordner “include” angelegt. Meinem Profilscript habe ich folgenden Code hinzugefügt, der somit beim Start einer neuen PowerShell-Session oder eines Scripts ausgeführt wird.
Set-Location "D:\Scripts"
Get-ChildItem ".\include" | Where {$_.Name -like "*.ps1"} | ForEach {
Write-Host "[Including $_]" -ForegroundColor Green
. .\include\$_
}
Dieser Code ermittelt alle PowerShell-Scripts im Include-Ordner und bindet diese in die aktuelle Session ein. Möchte ich ein neues Script oder eine Funktion hinzufügen, kopiere ich die entsprechende Datei in den Include-Ordner und öffne eine neue PowerShell-Console – und schon steht mir die neue Funktionalität zur Verfügung! Möchte ich sie nicht mehr nutzen, lösche ich die Datei wieder aus dem Include-Ordner, und starte eine neue Session.
Das geht schnell, ist unkompliziert, und ich muss nicht dauernd mein Profil bearbeiten! Und damit ich dabei nicht die Übersicht verliere, werden die jeweils inkludierten Scripts namentlich aufgelistet.
PowerShell Module erstellen
Eine andere Möglichkeit ein Script einzubinden ist die Erstellung eines PowerShell Moduls. Das ist relativ einfach: wir benennen unser Script “Hello-World.ps1” in “Hello-World.psm1” um. Fertig ist unser Script-Modul!
Jetzt müssen wir das Modul nur noch in unsere PowerShell-Session einbinden. Die von der PowerShell genutzten Modulpfade sind in der Umgebungsvariablen $PSModulePath gespeichert:
PS D:\Scripts> Get-Content Env:PSModulePath D:\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules\ PS D:\Scripts>
Der Pfad “C:\Windows\system32\WindowsPowerShell\v1.0\Modules\” ist für die Module von Microsoft vorgesehen und sollte nicht für eigene Module genutzt werden.
Der zweite Pfad liegt im Benutzerprofil (auf meinem Rechner sind die Documents nach D:\ umgeleitet). Falls wir einen anderen Pfad verwenden möchten, können wir diesen einfach der Variable $PSModulePath hinzufügen.
[Update: Änderungen der Variablen $PSModulePath gelten nur für die aktuelle Session. Zusätzliche Pfade füge ich daher wie oben beschrieben über ein Profilscript ein. Verschiedene Möglichkeiten zur dauerhaften Änderung der Variablen finden Sie hier.]
Das Modules-Unterverzeichnis ist standardmäßig noch nicht vorhanden und muss von uns angelegt werden. Anschließend erstellen wir ein weiteres Unterverzeichnis, welches zwingend den selben Namen verwenden muss wie unser Modul. Und schließlich legen wir noch das Modul-Script in diesem Verzeichnis ab. In meinem Fall lautet der vollständige Pfad zu dem Modul demnach:
“D:\WindowsPowerShell\Modules\Hello-World\Hello-World.psm1”
Wenn wir nun eine neue PowerShell Console öffnen, steht uns unsere Funktion bereits zur Verfügung:
PS D:\Scripts> Get-Command Write-* CommandType Name ModuleName ----------- ---- ---------- Function Write-HelloWorld Hello-World Cmdlet Write-Debug Microsoft.PowerShell.Utility Cmdlet Write-Error Microsoft.PowerShell.Utility Cmdlet Write-EventLog Microsoft.PowerShell.Management ...
Das liegt daran, dass ab PowerShell Version 3.0 Module automatisch geladen werden. Falls wir noch die PowerShell 2.0 verwenden, muss das Modul vorab importiert werden:
PS D:\Scripts> Import-Module Hello-World
Nun sind wir in der Lage Scripts, Funktionen und Module zu entwickeln, einzubinden, zu verwenden, und an Kunden oder andere Anwender weiter zu geben. Dabei wünsche ich viel Spaß! 🙂