| | 0

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ß! 🙂