Powershell kennt zwei Kategorien von Fehlern: terminierende und behandelte Fehler. Ein terminierender Fehler tritt unvermittelt auf. Powershell kann den Fehler nicht abfangen, wirft einen Fehler und beendet das Programm oder Skript, in dem der Fehler aufgetreten ist. Das muss nicht zwingend heißen, dass gleich Ihr ganzes Skript abbricht, aber zumindest der Befehl, der den Fehler verursacht hat, wird abgebrochen.
Ein behandelter Fehler ist ein Fehler, den der Programmierer vorhergesehen und für den er einen spezifischen Fehlerbehandlungscode geschrieben hat. Das Cmdlet kann beim Auftreten eines behandelten Fehlers eine Fehlermeldung anzeigen (was aber nicht passieren muss, prinzipiell kann ein behandelter Fehler auch völlig unbemerkt bleiben), läuft dann aber einfach weiter. Eine Zugriffsverweigerung von Get-Childitem ist z.B. ein behandelter Fehler.
Get-ChildItem -Path 'C:\Windows\CSC','C:\Windows\addins'
> Get-ChildItem : Der Zugriff auf den Pfad "C:\Windows\CSC" wurde verweigert.
Verzeichnis: C:\Windows\addins
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 29.09.2017 15:43 802 FXSEXT.ecf
Im Beispiel werden zwei Ordner abgerufen, wobei der Zugriff auf C:\Windows\CSC\ einen Fehler wirft. Trotzdem wird das zweite Verzeichnis angezeigt. Wäre der Fehler unbehandelt gewesen, hätte Get-Childitem direkt nach dem Fehler abgebrochen und der zweite Ordner wäre nicht angezeigt worden. Das kann man simulieren, denn man kann Powershell über den Parameter -Erroraction (oder kurz -EA) zwingen, alle Fehler wie unbehandelte Fehler zu verarbeiten.
Get-ChildItem -Path 'C:\Windows\CSC','C:\Windows\addins' -ErrorAction Stop
> Get-ChildItem : Der Zugriff auf den Pfad "C:\Windows\CSC" wurde verweigert.
In diesem Beispiel bricht die Verarbeitung nach dem Fehler sofort ab. Über -ErrorAction SilentlyContinue kann man die Fehlerausgabe auch unterdrücken. Der Parameter -Erroraction überschreibt für die Ausführung des jeweiligen Kommandos einfach die Einstellung der Standardvariable $Erroractionpreference, die in der Konsole festgelegt ist. Alle Einstellungen für $Erroraction finden Sie in der Tabelle ActionPreference-Einstellungen.
Als Debugging bezeichnet man den Prozess der Fehlersuche in Programmen. Ein Debugger ist dabei das Schweizer Taschenmesser, dass Dir bei der Fehlersuche hilft - und Powershell liefert natürlich bereits einen in die Sprache integrierten Debugger mit. Den Umgang mit dem Debugger zu lernen, ist die vielleicht wichtigste Fählgkeit, die man lernen kann, um Powershell-Code besser zu verstehen und lange Skripte in den Griff zu bekommen.
Das Hauptfeature eines Debuggers sind sogenannte Haltepunkte oder Breakpoints. Sie erlauben es, den Code während der Ausführung kontrolliert zu bestimmten Ereignissen anzuhalten, um den Zustand der Laufzeitumgebung zu überprüfen – beispielsweise den Wert, der in einer Variablen gerade gespeichert ist. Haltepunkte setzt man mit dem Cmdlet Set-PSBreakPoint. Wenn ein Haltepunkt gesetzt ist, merkt sich Powershell im Arbeitsspeicher, in welcher Zeile die Skriptausführung angehalten werden soll. Da das Skript selbst nicht verändert wird, kann man Haltepunkte nur für gespeicherte Skripte setzen.
Um einen Haltepunkt zu entfernen, brauchen Sie nur den Powershell-Prozess zu beenden. Alternativ können Sie mit Disable-PSBreakpoint einen Haltepunkt deaktiviert, oder ihn mit Remove-PSBreakpoint entfernen.
Das erste Beispiel in Zeile 1deaktiviert alle Haltepunkte, ohne sie zu löschen. Das zweite Beispiel reaktiviert die Haltepunkte wieder, und das letzte Beispiel entfernt alle Haltepunkte.
Sie können Haltepunkte auch direkt im Editor konfigurieren. In der ISE setzen Sie hierfür den Cursor in die Zeile, in der der Haltepunkt gesetzt werden soll, wechseln in das Menü "Debuggen" und wählen "Haltepunkt umschalten". Alternativ und viel schneller geht es, wenn Sie die F9-Taste drücken. Wenn Sie den Haltepunkt nicht setzen können, prüfen Sie, ob die Datei schon gespeichert wurde. Geben Sie anschließend Get-PSBreakPoint in der Konsole ein, werden Sie feststellen, dass die ISE nichts anderes gemacht hat, als für Sie das Cmdlet Set-PSBreakPoint im Hintergrund aufzurufen.
In der Abbildung der ISE sehen Sie einen braun markierten Haltepunkt. Er ist innerhalb einer Funktion gesetzt. Die Ausführung des Skriptes mit F5 führt noch nicht zum Aktivieren des Haltepunktes. Erst wenn Sie die Funktion aufrufen, wird der Haltepunkt ausgelöst und das Skript pausiert. In der Konsole wird der Text "Treffer Zeilenhaltepunkt bei <Skript>:Codezeile" angezeigt, und der Prompt zeigt ein führendes [DBG]: an. Powershell hat jetzt den kompletten Code bis Zeile 13 ausgeführt. Sie können in der Debugkonsole den Zustand der Shell jetzt prüfen, indem Sie sich Variablen ausgeben lassen. Sie können auch Skriptcode ausführen.
[DBG]: PS C:\Users\Holger>> $FolderData
> Count : 65819
> Average :
> Sum : 15270776814
> Maximum : 2524096521
> Minimum : 0 Property : Length
Im Visual Studio Code gibt es einen eigenen Button in der Seitenleiste, der einen Käfer auf einem Play-Button darstellt.
Wählen Sie den Button (1), um die Debug-Leiste zu öffnen. Die Haltepunkte können Sie in der aktiven Zeile wie in der ISE mit der F9 Taste aktivieren oder deaktivieren, oder alternativ mit der Maus, wenn Sie links neben die Zeilennummern klicken (3). Im Debugmenü werden die gesetzen Haltepunkte aufgelistet (4), aber irritierenderweise sind sie noch gar nicht in der Powershell gesetzt. Wenn Sie Ihr Skript starten, wird der Debugger nicht aktiviert. Wenn Sie Get-PSBreakpoint eingeben, werden keine Haltepunkte aufgelistet. Erst wenn Sie den Button "Ausführen und Debuggen" (2) klicken, werden die Haltepunkt tatsächlich auch in der Powershell gesetzt. Das Gleiche gilt im übrigen auch für das Deaktivieren. Ändern Sie Haltepunkt, müssen Sie in VSCode immer erste einmal mit "Ausführen und debuggen" die Haltepunkte aktualisieren. Denken Sie außerdem daran, dass Haltepunkte in Funktionen nicht beim Ausführen des Skriptes, sondern immer erst beim Aufrufen der Funktion greifen.
Nachdem Sie Haltepunkte gesetzt haben, können Sie den Code ausführen. Wenn Powershell auf den Haltepunkt trifft, stoppt die Ausführung.
Sie können das Skript ab jetzt zeilenweise durchgehen, indem Sie in der Konsole "s" für step eingeben, oder (in der ISE) die Funktionstaste F11 betätigen. So können Sie das Verhalten Ihres Skriptes Zeile für Zeile prüfen. Dabei werden Sie feststellen, dass auch Schleifen schrittweise verarbeitet werden:
Die Ockerfarbene Markierung zeigt die Zeile, in der Powershell gerade wartet, hier also in der runden Klammer des Foreach-Statements.
Der Debugger springt sogar in Unterfunktionen, die gar nicht Teil des Skriptes sind, wie folgende Beispiel zeigt:
In Zeile 3 des Skripts ist ein Haltepunkt gesetzt. Sobald Sie im Debug-Modus mit "s" die nächste Code-zeile ausführen, wird ein neues Skriptfenster in der ISE geöffnet. Sie debuggen jetzt nämlich die Funk-tion Test-NetConnection, da dieses Powershell-eigene Kommando von Microsoft auch als Funktion im-plementiert wurde.
Mit den zwei zusätzlichen Debugger-Befehlen StepO(v)er und Step(O)ut können Sie Ihr Skript dort weiter fortführen, wo es Sie interessiert. Wenn Sie im Beispielskript in Abbildung statt "s" "v" eingeben oder in der ISE F10 drücken, überspringt Powershell die Funktion Test-Netconnection und bleibt in der nächsten Zeile Ihres eigenen Skriptes stehen. Ist das Kind bereits in den Brunnen gefallen wie in der Abbildung, können Sie mit "o" bzw. Shift+F11 die Unterfunktion wieder verlassen.
In einer Schleife oder in der Pipeline können Sie mit StepOver auch die Verarbeitung von Klammern überspringen. Wenn Ihr Haltepunkt vor einer Schleife liegt, können Sie so also die gesamte Schleife übergehen und direkt nach der Schleife weiter debuggen. Achten Sie aber darauf, dass das bei einem Haltepunkt innerhalb der Schleife nicht funktioniert, da der Haltepunkt mit jedem Schleifendurchlauf neu ausgelöst wird.
Wenn Sie Ihren Fehler gefunden haben, können Sie die komplette Debug-Verarbeitung des Skripts mit "q" einfach beenden. Der Debugger bricht dann ab, und das Skript wird beendet. Alternativ können Sie auch "c" für Continue wählen, um Ihr Skript weiter auszuführen. Das ist vor allem dann sinnvoll, wenn Sie weitere Haltepunkte aktiviert haben oder Sie das (hoffentlich jetzt korrekte) Ergebnis Ihres Skriptes sehen wollen.
Um sich alle Kommandozeilenkürzel anzeigen zu lassen, können Sie im Debugger auch einfach "?" eingeben. Powershell gibt Ihnen dann eine kurze Hilfe mit allen verfügbaren Debug-Kommandos aus:
[DBG]: PS C:\Users\Holger>> ?
> s, stepInto Einzelschritt (in Funktionen, Skripts usw.)
> v, stepOver Prozedurschritt zur nächsten Anweisung
> o, stepOut Rücksprung aus der aktuellen Funktion, dem Skript usw.
c, continue : Vorgang fortsetzen
q, quit : Vorgang und Debugger beenden
d, detach : Setzen Sie den Vorgang fort, und trennen Sie den Debugger.
k, Get-PSCallStack - Aufrufliste anzeigen
Im VSCode ist der Debugger deutlich funktionaler.
Ist der Debugger gestartet, kann man mit einer Leiste am oberen Bildschirmrand das Skript fortführen (2). Der Linke Button mit dem Play-Symbol führt das Skript weiter aus, bis der nächste Haltepunkt erreicht ist. Der Prozedurschritt (step over) springt in die nächste Zeile des Codes, überspringt aber Unterprozeduren, verlässt das Skript also nicht, im Gegensatz zum nächsten Button, dem Einzelschritt. Ist man in einer Unterprozedur gelandet, kann man Sie mit dem nächsten Button (Punkt mit Pfeil nach oben) wieder verlassen. Der gründe, runde Pfeil startet die Prozedur von vorne, der Stop-Button beendet die Ausführung sofort. Alle Optionen lassen sich auch bequem von der der Tastatur aus Steuern. Die zugehören Tastenkürzel werden angezeigt, wenn man mit der Maus über die Buttons fährt.
Auch im VSCode wird die aktuelle Zeile mit einem farbigen Balkem im Code markiert (1). Sehr hilfreich ist die Anzeige der Debugging-Leiste links, denn hier kann man sich den Inhalt aller Variablen anzeigen lassen (3). Wenn man eine oder mehrere bestimmte Variablen beobachten möchte, kann man das mit dem Wachtlist (Überwachen) machen (4). Hier kann man einzelne Variablen über den +-Button eintragen, oder Sie aus der Liste unter (3) hinzufügen, indem man aus dem Kontextmenü der Variablen "Zur Überwachung hinzufügen" auswählt.
Der Debugger von Powershell kann aber noch deutlich mehr. So kann man z.B. auch Haltepunkte auf Variablen setzen, die das Skript immer dann anhalten, wenn eine Variable geschrieben oder gelesen wird.
Denjenigen, die echte Powershell Debugging-Ninjas werden wollen empfehle ich den Talk von Kirk Munro auf der Powershell + Devops Global Summit 2018:
[DBG]: PS C:\Users\Holger>> ?
> s, stepInto Einzelschritt (in Funktionen, Skripts usw.)
> v, stepOver Prozedurschritt zur nächsten Anweisung
> o, stepOut Rücksprung aus der aktuellen Funktion, dem Skript usw.
c, continue : Vorgang fortsetzen
q, quit : Vorgang und Debugger beenden
d, detach :Setzen Sie den Vorgang fort, und trennen Sie den Debugger.
k, Get-PSCallStack - Aufrufliste anzeigen
Wenn Sie in Powershell eine Fehlermeldung ausgeben wollen, haben Sie zwei Möglichkeiten - Write-Error und Throw.
Write-Error ist im Prinzip gar kein richtiger Fehler. Das Cmdlet schreibt tatsächlich einfach nur eine Ausgabe in den Fehler-Datenstrom, der dann von Powershell als roter Text in der Konsole ausgeben wird. Die Ausgabe von Write-Error kann beim Aufrufen eines Cmdlets mit dem Standard-Parameter -ErrorAction und dem Argument SilentlyContinue unterdrückt werden. Alle Ausgaben von Write-Error werden auch in der Standardvariablen $Error gespeichert.
Write-Error "Hier ist was schief gelaufen"
Man spricht bei Write-Error auch von einem Nicht-terminierenden Fehler, weil er keine Auswirkungen auf die Code-Ausführung hat.
Um einen "echten" Fehler auszulösen, der die Code-Ausführung sofort beendet, können Sie Throw verwenden. Hinter dem Throw-Statement geben Sie in den String an, der als Fehlermeldung ausgegeben werden soll.
Throw 'Das ist jetzt aber wirklich mal ein schlimmer Fehler!!!'
> Das ist jetzt aber wirklich mal ein schlimmer Fehler!!!
> In Zeile:1 Zeichen:1
> + Throw 'Das ist jetzt aber wirklich mal ein schlimmer Fehler!!!'
Throw löst Terminierende Fehler aus. Wenn Sie terminierende Fehler abfangen und behandeln wollen, können Sie das mit dem Try-Catch-Konstrukt tun.
Im vorigen Abschnitt wurde der Unterschied zwischen terminierenden und behandelten Fehlern besprochen. Alle behandelten Fehler gehen ursprünglich auf einen unbehandelten Fehler zurück. Auch in Powershell ist es möglich, einen terminierenden Fehler aufzufangen und zu behandeln. Man spricht von Auffangen, weil ein Fehler normalerweise mit dem Befehl Throw geworfen wird. Um einen Fehler aufzufangen, muß der Befehl, der einen terminierenden Fehler auslösen kann, mit einem Try-Block umschlossen werden.
Try {
TuWas # Der Befehl, der fehlschlagen kann
}
Catch {
'Es ist ein Fehler aufgetreten!' # Die Fehlerbehandlung
$_.Exception.Message
}
Finally {
'Einen Moment, ich räume auf'
1..100 | Foreach-object {
Write-Progress -Activity 'Cleanup' -PercentComplete $_
Start-Sleep -Milliseconds 10
}
}
Der Try-Block ist verpflichtend. In seinem Skriptblock geben Sie den Code an, der einen Fehler erzeugen kann. Hinter dem Try-Block kann ein Catch-Block folgen, der aber nur dann aufgerufen wird, wenn im Try-Block ein terminierender Fehler aufgetreten ist. Behandelte Fehler lösen den Catch-Block nicht aus! Als letztes kann ein Finally-Block angegeben werden. Der Code im Finally-Block wird immer ausgeführt, unabhängig davon, ob ein Fehler aufgetreten ist oder nicht. Der Finally-Block wird sogar dann ausgeführt, wenn Sie die Skript-Ausführung mit Strg-C abbrechen! Sie können einen Try-Block auch ausschließlich mit einem Finally-Block kombinieren.
Wenn Sie den obenstehenden Code ausführen, wird ein Fehler ausgelöst, da es sich bei TuWas weder um einen ausführbaren Befehl noch um einen String handelt - der Text ist ohne Anführungszeichen angegeben. Sobald ein Fehler ausgelöst wird, wird der Fehler in der $Error-Variablen gespeichert, der Try-Block abgebrochen und die Ausführung des Catch-Blocks beginnt – sofern ein Catch-Block existiert, denn Catch-Blöcke sind optional.
Innerhalb des Catch-Blocks kann beliebiger Code ausgeführt werden. Sie können hier also eine Routine einbauen, die den Fehler behebt, oder den Fehler ausgeben oder protokollieren. Powershell stellt das Fehler-Objekt dafür in der anonymen Variable $_ bereit. Wenn Sie nur den Text der Fehlermeldung ausgeben wollen, können Sie auf $_.Exception.Message zugreifen. Der Fehlertext wird dann nur noch als normaler Text ausgegeben, nicht als rote Fehlermeldung.
Der Finally-Block ist ebenfalls optional. Wenn er vorhanden ist, wird er immer ausgeführt, unabhängig davon, ob ein Fehler aufgetreten ist oder nicht. Er kann z.B. dazu genutzt werden, Dateien wieder zu entfernen, die im Try- oder Catch-Block erzeugt worden sind, oder um Netzwerkverbindungen wieder zu beenden. Beachten Sie, dass der Finally-Block selbst dann ausgeführt wird, wenn Sie mit Strg+C das Abbrechen der Skriptverarbeitung erzwingen. Allerdings werden nach dem Abbruch keine Daten mehr in die Standardausgabe geschrieben, so dass Textausgaben aus dem Finally-Block nicht mehr ausgegeben werden.
Try
{
get-date | Out-File C:\temp\Try.txt
Read-Host -Prompt 'Bitte geben Sie etwas ein'
}
Finally
{
Write-Host 'Did Something'
Remove-Item -Path C:\temp\try.txt -Force
}
> Bitte geben Sie etwas ein: Did Something
Im diesem Beispiel sehen Sie die Ausgabe des Finally-Blocks nach Abbruch durch Strg+C. Sie bekommen dank des Write-Host die Ausgabe "Did Something" angezeigt. Wird Write-Host entfernt, und nur der Text 'Did something' ausgegeben, scheint Finally nichts zu tun. Sie können allerdings an der Datei Try.txt erkennen, dass die Datei im Finally-Block wieder gelöscht wird. Die Ausgabe mit Write-Host dient nur zu Demonstrationszwecken und macht aus technischer Sicht keinen Sinn!
Prinzipiell können Sie Try mit Catch und Finally beliebig kombinieren, also den Finally wie im vorigen Beispiel nur mit Try verwenden, oder den Try nur mit einem Catch-Block nutzen, oder alle drei Blöcke zusammen zum Einsatz bringen. Der Catch-Block kann aber noch mehr, denn er kann sogar mehrfach verwendet werden, um auf spezifische Fehler mit einem eigenen Catch-Block zu reagieren. Alles, was Sie dafür tun müssen, ist den Fehlertyp hinter den Catch-Befehl zu schreiben.
Im nächsten Beispiel wird ein Kopiervorgang in einen Try-Block eingeschlossen. Wenn der Zielordner nicht existiert, wird eine DirectoryNotFoundException ausgelöst, die direkt vom ersten Catch-Block abgefangen wird. Handelt es sich um einen anderen Fehler, wird der zweite, unspezifische Catch-Block aufgerufen. Wurde ein Catch-Block ausgelöst, werden die übrigen Catch-Blöcke ignoriert.
Wenn in Powershell ein Fehler auftritt, wird normalerweise eine Fehlermeldung in Form eines ErrorRecord-Objekts ausgegeben. Alle Fehler werden dabei in einem Array namens $Error protokolliert. Das erste Element des Arrays enthält immer die letzte Fehlermeldung. Geben Sie z.B. 1/0 ein, wird ein Division durch Null-Fehler geworfen.
Tatsächlich können je nach Fehler unterschiedliche Eigenschaften im Fehlerobjekt auftauchen. Die Exception (Ausnahme) ist selbst ein Objekt, das weitere Informationen zur Verfügung stellt. Exception.Message beinhaltet z.B. die reine Textmeldung, die ausgegeben wurde.
$Error[0].Exception.Message
> Es wurde versucht, durch 0 (null) zu teilen.
Die $Error-Variable
Die Variable $Error ist ein Array, das alle Fehler enthält, die von Powershell ausgelöst ("ge-worfen") wurden. Immer, wenn ein neuer Fehler auftritt, wird das ErrorRecord-Objekt in das Listenelement mit dem Index 0 eingefügt, und alle anderen Objekte rutschen im Array einen Index weiter. Dadurch beinhaltet $Error[0] immer den letzten ausgelösten Fehler.
Die Anzahl der Fehler, die in $Error vorgehalten werden, ist endlich. Sie wird bestimmt durch die Standardvariable $MaximumErrorCount. Standardmäßig steht $Maximumerrorcount auf 256, kann aber bis 32768 hochgesetzt werden.
Um $Error zurückzusetzen, muss die Methode $Error.Clear() verwendet werden. Um die Anzahl der Fehler im Array zu bestimmen, rufen Sie $Error.Count auf.
Über den Typ von Exception oder InnerException können Sie ermitteln, was für ein Typ von Fehler ausgelöst wurde.