Zum Hauptinhalt springen

Debugging und Fehlerbehandlung

Um Fehler in Powershell von vornherein zu vermeiden, sollten einige Dinge beherzigt werden.

  • Verwenden Sie in Skripten immer die vollständigen Cmdlet-Namen, keine Aliase.
  • Verwenden Sie immer den vollständigen Parameternamen, keine Positionsparameter.
  • Verwenden Sie sprechende Variablennamen, so dass Sie zu jeder Zeit wissen, welchen Inhalt eine Variable hat.
  • Wenn Sie Scriptblöcke verwenden, versuchen Sie den Code immer so einzurücken, dass die öffnende und schließende Klammer in derselben Spalte stehen.
  • Achten Sie darauf, bei Benutzereingaben die Eingabe direkt in den korrekten Datentyp zu konvertieren.
  • Wenn eine Variable ein Array beinhalten soll, konvertieren Sie den zugewiesenen Wert immer erst in ein Array. Das kann durch den Operator @() geschehen, oder durch den Konvertierungsoperator [Array] bzw. [Datentyp[]].
  • Versuchen Sie, komplexe Skripte in Funktionen aufzuteilen. Eine einzelne Funktion sollte nach Möglichkeit nicht länger als 100 Zeilen werden, um den Code übersichtlich und verständlich zu halten. 

 

Autodiscovery und Export-ModuleMember

Beim Starten des Powershell-Prozesses ist Autodiscovery dafür verantwortlich zu ermitteln, welche Funktionen, Variablen und Aliase in einem Skript vorliegen und diese in Powershell sichtbar zu machen, ohne das Modul-Skript sofort auszuführen. Es gibt allerdings einige Unterschiede im Verhalten zwi-schen Modulen mit und ohne Modulmanifest, die ziemlich viel Verwirrung stiften können.

Autodiscovery funktioniert automatisch und macht, wie wir bereits gesehen haben, alle Funktionen aus dem Skriptmodul sichtbar. Wenn Sie in Ihrem Skriptmodul aber Variablen oder Aliase anlegen, die glo-bal in der Powershell genutzt werden sollen, werden Sie feststellen, dass Aliase erst in der Konsole sichtbar sind und benutzt werden können, nachdem Sie eine Funktion aus Ihrem Skript aufgerufen ha-ben. Variablen sind selbst nach dem Aufrufen des Skriptes nicht sichtbar! Zur Demonstration speichern Sie dazu folgendes Skript in Ihrem persönlichen Profilordner als DemoModule.psm1 und starten Sie einen neuen Powershell-Prozess.

Wenn die Funktion Test-Modulemember nicht aufgerufen werden kann, und Dateiname, Modulordernname und Modulpfad korrekt sind, prüfen Sie Ihre Ausführungsrichtlinie. Wenn die Ausführung von Powershell-Skripten verboten ist, können auch keine Skriptmodule geladen werden.

Function Test-ModuleMember { 
   [Parameter()]
   param(
      $UserName = "Holger"
   )

"$Username ist da!"
}

New-Alias -Name TMM -Value Test-ModuleMember
$DefaultPassword = "Passw0rd"

Wenn Sie versuchen, Test-ModuleMember über das Alias TMM zu starten, werden Sie feststellen, dass das Alias nicht vorhanden ist, genauso wenig wie die Variable $DefaultPassword. TestModuleMember lässt sich dagegen ausführen. Sobald Test-Modulemember einmal gestartet wurde, ist plötzlich auch das Alias verfügbar, aber die Variable bleibt weiterhin verschwunden.

Dieses Verhalten hängt damit zusammen, dass Autodiscovery nur Elemente aus Ihrem Modul an Pow-ershell weitergibt, die explizit exportiert werden, wobei Funktionen aus einem Skriptmodul standardmä-ßig exportiert werden. Aliase werden nach dem Aufrufen der ersten Funktion verfügbar, weil Powershell dann das komplette Skriptmodul zum ersten Mal wirklich ausführt und alle Aliase global erstellt. Variablen werden explizit nicht sichtbar, weil Variablen in einem Skriptmodul weiterhin als lokale Variablen ausgeführt werden.

Wenn Sie Aliase oder Variablen exportieren lassen wollen, müssen Sie das explizit angeben, indem Sie in Ihrem Skriptmodul den Befehl Export-ModuleMember aufrufen.

<# Hier steht der Skriptcode #> 
Export-ModuleMember -Function * 
Export-ModuleMember -Alias * 
Export-Modulemember -Variable *

Ohne Export-Modulemember sind Elemente durch Autodiscovery nicht ermittelbar. Die Ausnahme sind Funktionen, die Powershell automatisch exportiert, solange Export-ModuleMember im Skript nicht auf-gerufen wird. Daher sind die Funktionen, die im Skriptmodul Helperfunctions.psm1 angelegt sind, trotzdem sichtbar gewesen. Aliase werden erst dann sichtbar, wenn sie explizit exportiert werden.

Function Write-ReturnArrayFromScriptblock
{
   # hier steht der Funktionscode
}

Function Test-ParameterValueFromPipeline
{
   # hier steht der Funktionscode
}

$ExecutionTime = Get-Date

New-Alias -Name WRA -Value Write-ReturnArrayFromScriptblock 
New-Alias -Name PVF -Value Test-ParameterValueFromPipeline

Export-ModuleMember -Function * 
Export-ModuleMember -Alias * 
Export-ModuleMember -Variable *

Sie können auch Variablen exportieren, allerdings verhalten sich diese noch einmal anders. Exportierte Variablen werden nämlich durch den Autodiscovery nicht sichtbar, sondern erst bei der ersten Ausfüh-rung des Skriptmoduls als globale Variable angelegt. Das heißt, dass exportierte Variablen trotz des Ex-ports erst nach dem ersten Ausführen Ihres Skriptmoduls erzeugt werden. Das ist leider alles andere als konsistent.

Achten Sie darauf, dass Sie, sobald Sie Export-ModuleMember verwenden, auch Ihre Funktionen ma-nuell exportieren müssen, da der automatische Funktionsexport nur solange funktioniert, wie Sie Ex-port-ModuleMember nicht verwenden.

Wenn Sie eine Manifest-Datei verwenden, können Sie in der Manifest-Datei ebenfalls angeben, welche Elemente exportiert werden sollen. Hier noch einmal der Ausschnitt aus der Manifest-Datei:

# Aus diesem Modul zu exportierende Funktionen. FunctionsToExport = '*'
# Aus diesem Modul zu exportierende Cmdlets. CmdletsToExport = '*'
# Die aus diesem Modul zu exportierenden Variablen VariablesToExport = '*'
# Aus diesem Modul zu exportierende Aliase. AliasesToExport = '*'

Die Manifest-Datei kann aber nur Elemente exportieren, die über Export-Modulemember bereits sichtbar sind. Die Cmdlets, Funktionen, Variablen und Aliase müssen im Skriptmodul exportiert sein, damit sie über das Manifest gesteuert werden können!

Das heißt, dass alle nach außen sichtbaren Funktionen, Aliase und Variablen im Skriptmodul und, sobald Sie eine Manifest-Datei benutzen, auch in dieser explizit zum Export angegeben werden müssen. Ver-gessen Sie Export-Modulemember im Skript anzugeben, können Sie Aliase und Variablen im Manifest nicht mehr exportieren! Aus Performance-Gründen sollten Sie außerdem alle Funktionen und Aliase namentlich exportieren, selbst wenn Sie alle Funktionen Ihres Skriptes exportieren wollen. Das be-schleunigt den Autodiscovery-Prozess deutlich!

Helperfunctions.psm1 könnte also z.B. so aussehen:

Function Write-ReturnArrayFromScriptblock
{
   # hier steht der Funktionscode
}

Function Test-ParameterValueFromPipeline
{
   # hier steht der Funktionscode
}

$ExecutionTime = Get-Date

New-Alias -Name WRA -Value Write-ReturnArrayFromScriptblock 
New-Alias -Name PVF -Value Test-ParameterValueFromPipeline

Export-ModuleMember -Function * 
Export-ModuleMember -Alias * 
Export-ModuleMember -Variable *

Wie Sie hier sehen, können Sie auch einzelne Funktionen, Aliase oder Variablen namentlich angeben. Sie können sogar Wildcards in Namen für den Import verwenden. Dadurch wird es möglich, z.B. eine ganze Kategorie von Daten zu exportieren:

Export-ModuleMember -Function Get-*

 

Modul-Manifest

Wie man in der Ausgabe von Get-Command -Module Helperfunctions sehen kann, gibt Powershell zu jedem Cmdlet eine Versionsnummer aus.

Um Meta-Informationen wie die Version oder den Autoren eines Modules mitzugeben, brauchen Sie eine Manifest-Datei. Es handelt sich um eine Datei mit der Endung .psd1, die aus einer großen Hash-Table besteht, in der alle relevanten Informationen über ein Modul gespeichert werden können.

Um eine Manifest-Datei zu erstellen, können Sie das Cmdlet New-Modulemanifest verwenden.


$Module = Get-Module -Name Helperfunctions

$Psd1 = ( Join-Path -Path $Module.ModuleBase -ChildPath $Module.Name ) + '.psd1' New-ModuleManifest -Path $Psd1 -Author 'Holger' FunctionsToExport * `

ModuleVersion '1.0' RootModule $Module.Path

In der ersten Zeile des Beispiels werden die Informationen über das aktuell installierte Modul abgefragt. Das ist nicht notwendig, erspart es uns aber, die Pfade für die Dateien manuell herauszusuchen und ein-zugeben.

In der zweiten Zeile wird aus dem Ordner ($Module.Modulebase) und dem Namen des Moduls ($Mo-dule.Name) ein neuer Pfad erstellt. Hierfür wird das Kommando Join-Path verwendet, das einfach zwei Strings zu einem gültigen Pfad zusammenfügt. Anschließend wird die Dateiendung .psd1 angehängt.

In der dritten Zeile wird mit dienste New-Modulemanifest eine Manifestdatei generiert. Für alle Meta-daten in der Datei hat New-Modulemanifest einen Parameter. Im Beispiel wird der Pfad der Manifestda-tei, der Autor, die zu exportierenden Funktionen (s. weiter unten), die Version des Moduls und das Root-modul angegeben. Das Rootmodul ist besonders wichtig, denn wenn in einem Modulordner eine <Mo-dulname>.psd1-Datei liegt, wird die .psm1-Datei nicht mehr standardmäßig geladen. Stattdessen gibt die Eigenschaft Rootmodul das Skript vor, das gestartet werden soll.

Die Helperfunctions.psd1 sollte nun ungefähr so aussehen:


@{


# Die diesem Manifest zugeordnete Skript- oder Binärmoduldatei. RootModule = '.\Helperfunctions.psm1'


# Die Versionsnummer dieses Moduls ModuleVersion = '1.0'


# Unterstützte PSEditions

# CompatiblePSEditions = @()


# ID zur eindeutigen Kennzeichnung dieses Moduls GUID = 'bcc731c2-0db3-4e8a-b452-3b5300cf4fd3'


# Autor dieses Moduls Author = 'Me'


# Die für dieses Modul mindestens erforderliche Version des Windows PowerShell-Mo-duls

# PowerShellVersion = "


# Die Module, die vor dem Importieren dieses Moduls in die globale Umgebung geladen werden müssen

# RequiredModules = @()

# Die Skriptdateien (PS1-Dateien), die vor dem Importieren dieses Moduls in der Um-gebung des Aufrufers ausgeführt werden.

# ScriptsToProcess = @()

# Aus diesem Modul zu exportierende Funktionen. FunctionsToExport = '*'


# Aus diesem Modul zu exportierende Cmdlets. CmdletsToExport = '*'


# Die aus diesem Modul zu exportierenden Variablen VariablesToExport = '*'

# Aus diesem Modul zu exportierende Aliase. AliasesToExport = '*'


}

Die Datei ist nur in Auszügen abgebildet, aber man kann erkennen, dass es sich hier um eine Hash-Table handelt. Nicht konfigurierte Parameter sind auskommentiert. Die mit New-Modulemanifest angegebe-nen Parameter sind bereits eingetragen, genauso wie Benutzerinformationen, die Powershell direkt aus dem Host auslesen konnte, und eine eindeutige Modulkennung in Form einer GUID. Wenn Sie eine Manifestdatei von einem bestehenden Modul kopieren, anstatt sie neu anzulegen, müssen Sie auf jeden Fall eine neue GUID erstellen. Das können Sie ab Powershell 5 mit dem Cmdlet New-Guid erledigen, und in älteren Versionen einer .NET-Methode:


New-Guid [guid]::NewGuid()

Skriptmodule erstellen

Module können als Binärmodule mit einer .NET-Sprache oder in Powershell selbst erstellt werden. In Powershell geschriebene Module werden auch als Skriptmodule bezeichnet und sind letztlich nichts weiter als Skripte, die nur Funktionen enthalten.

Prinzipiell kann jedes Skript über den Befehl Import-Module als Modul geladen werden. Bei einem klassischen Skript macht das aber nur wenig Sinn. Stattdessen sollte ein Skriptmodul nur aus Funktio-nen oder Variablen bestehen, die auch nach dem Aufruf des Skriptmoduls noch verwendet werden kön-nen.

Mit der Einführung von Modulen in Powershell 2 musste tatsächlich jedes zusätzliche Modul, das man verwenden wollte, mit Import-Module nachgeladen werden. Ab Powershell 3.0 erledigt die Powershell das für uns automatisch, wenn man sich bei der Erstellung an eine Reihe von Regeln hält:


• Die Module müssen in einem der Module-Ordner abgelegt sein, die in der Umgebungsvariablen PSModulePath hinterlegt sind. Sie können sie über $ENV:PSModulePath abfragen

• Skriptmodule müssen für das Autoloading sta� der Dateiendung .ps1 mit der Endung .psm1 gespeichert werden. Dadurch erkennt Powershell beim Laden, welche Datei gestartet werden muss und versucht nicht, alle Skriptdateien im Modulordner zu starten.

• Die .psm1-Datei muss den gleichen Namen tragen wie der Ordner, in dem sie gespeichert ist.

Hier ein Beispiel:


C:\Program Files\WindowsPowerShell\Modules\BCDStore\BCDStore.psm1

• "C:\Program Files\WindowsPowerShell\Modules\" ist der Modulordner, den Sie zum Speichern von Systemweit verfügbaren Modulen verwenden sollten.

‌• "BCDStore" ist der Name des Modul-Ordners

• "BCDStore.psm1" ist der Name der Skriptdatei. Der Name der Datei (ohne Endung) muss dem Namen des Ordners entsprechen.

Dieses Feature wird als Autodiscovery bezeichnet, weil Powershell das Skriptmodul erst dann ausführt, wenn eine Ihrer Funktionen zum ersten Mal aufgerufen wird. Beim Start der Powershell werden die Funktionen in Ihrem Skript nur ermittelt und in der Konsole sichtbar gemacht. Dadurch müssen Sie beim Öffnen eines Powershell-Prozesses nicht warten, bis alle Skriptmodule ausgeführt wurden.

Im Folgenden werden die Funktionen Write-ReturnArrayFromScriptblock sowie Test-ParameterValue-FromPipeline in ein Skriptmodul ausgelagert.


Function Write-ReturnArrayFromScriptblock

{

Param( [int]$Wert1, [int]$Wert2

)


'Summe: ' + ( $Wert1 + $Wert2 ) 'Produkt: ' + ( $Wert1 * $Wert2 )

}


Function Test-ParameterValueFromPipeline

{

param(

[Parameter(ValueFromPipelineByPropertyName=$true)] [Alias('Fullname','FilePath')]

[string]$Path

)


Process {

$Path

}

}

image

Get-Module -name NWHelperfunctions -Listavailable

ModuleType ExportedCommands

Script {Write-ReturnArrayFromScriptblock, Test-ParameterValueFromPipeline}

Speichern Sie die Funktionen als Skript mit dem Namen NWHelperfunctions.psm1 im Ordner NWHel-perfunctions. Achten Sie darauf, dass der Name des Ordners und der Datei identisch ist. Nun kopieren Sie den Ordner in einen der Modulordner und starten eine neue Powershell-Konsole. Ihr Modul wird angezeigt, wenn Sie Get-Module -Listavailable eingeben.


Wie sie sehen können, hat Autodiscovery die Funktionen aus Ihrem Skriptmodul erkannt und als Expor-tedCommands aufgelistet. (Die Spalten Version und Name sind im Beispiel entfernt worden). Sie kön-nen die Funktionen auch in der Konsole per Tabulator-Ergänzung auflösen lassen. Wenn Sie Get-Modu-le -Name NWHelperfunctions aufrufen, wird Ihr Module aber nicht angezeigt. Das liegt daran, dass Sie noch keine der Funktionen des Moduls gestartet haben – das Skriptmodul wurde noch gar nicht ausge-führt.

Mit Get-Command -Module <IhrModul> können Sie sich die Funktionen Ihres Moduls auflisten lassen.


Get-Command -Module NWHelperfunctions

CommandType

Name

Version …

-----------

----

-------

Function

Test-ParameterValueFromPipeline

0.0

Function

Write-ReturnArrayFromScriptblock

0.0

Mit Aufrufen von Get-Command hat Powershell das Skriptmodul ausgeführt. Ein erneuter Aufruf von Get-Module -Name Helperfunctions zeigt das Modul jetzt an. Das gleiche passiert, wenn Sie eins der Kommandos aus dem Modul aufrufen.