Eingabefehler bei Powershell-Skripts abfangen
3. Dezember 2009Mit Hilfe von Wrapper-Skripts lassen sich Eingabefehler bei Powershell-Skripts abfangen, die sich aufgrund von falschen Aufrufparametern ergeben. Der Einsatz von verschiedenen Lösungen mit dieser Technik bringt für die Powershell-Umgebung eine höhere Sicherheit. Dieser Beitrag enthält dazu auch passende Skripts, die der Administrator für sein Arbeiten mit der Powershell adaptieren kann.
Die Sicherheitseinstellungen sehen auf Windows-Plattformen vor, dass die Powershell keine spezielle Verbindung zwischen der Skript-Dateierweiterung ps1 und der Kommando-Shell der Powershell herstellt. Das hat zur Folge, dass ein Doppelklick auf eine Datei mit der Endung ps1 nicht die Ausführung dieser Datei veranlasst. Auch das Drag&Drop einer Datei mit dieser Endung auf das Programmsymbol für die Powershell startet dieses Skript nicht.
Um eine ps1-Datei möglichst komfortabel zu starten, muss der Administrator eine Cmd.exe-Batch-Datei anlegen, das den kompletten Aufrufvorgang der Powershell explizit für ein Skript übernimmt. Diese nicht sonderlich elegante Lösung stellt zwar an sich kein Sicherheitsproblem dar und löst auch die Schwachstellen, die Dateitypen-Zuordnung besitzt.
Da aber die Übergabe-Argumente von Windows weiter gereicht werden, müssen sie unter Umständen in Hochkomma eingeschlossen werden (wenn sie zum Beispiel Leerzeichen enthalten). Und diese Argumente werden dann nicht korrekt an die Powershell übergeben.
Es stehen einige Möglichkeiten zur Verfügung, um dieses Problem zu umgehen. Die übliche Vorgehensweise besteht darin, eine Batch-Datei um das Skript zu legen – das wird auch als Wrapper-Skript bezeichnet. Im Folgenden werden einige weitere Probleme dazu angesprochen und Lösungsmöglichkeiten dafür gezeigt.
Erstes Problem: Entfernen der doppelten Anführungszeichen
Ohne die theoretischen Ausführungen zu vertiefen, zeigt ein kurzes Beispiel – in Form des Skripts echodemo.ps1 – am besten, wie sich eine Lösung ergibt. Dieses kurze Skript sieht wie folgt aus:
echodemo.ps1
$i = 0;
foreach($arg in $args)
{$i++; "$i $arg"}
Die Aufgaben dieses Programms sind schnell skizziert: Es geht in Form einer Schleife durch die Argumente, die an das Skript übergeben wurden. Dabei gibt es für jeden Schleifendurchlauf den Argumente-Zähler und das jeweilige Argument aus.
Um das Testen zu vereinfachen, hat der Autor das Skript im selben Verzeichnis gespeichert als die Dateien, die als Aufruf-Parameter an das Skript übergeben wurde, und die Leerzeichen in ihrem Dateinamen aufweisen. Die Ausgabe dazu zeigt das Bild 1. Das Skript behandelt jede in Hochkomma eingebettete Zeichenkette als ein einzelnes Argument.
Die Powershell erlaubt es, Kommandozeilen-Optionen anzugeben, wenn die Powershell gestartet wird. Dazu gibt es die Option Command. Damit kann der Skripter der Powershell explizit mitgeben, ob sie ein Skript oder ein Powershell-internes Kommando ausführen soll. Die detaillierte Hilfe-Information für die Kommandozeilen-Optionen der Powershell lassen sich durch die Eingabe des folgenden Kommandos (entweder in einer Cmd.exe-Datei oder am Powershell.exe-Prompt) anzeigen:
powershell -?
Möchte man nun Kommando aufrufen, bei denen auf der Kommandozeile Parameter an die Powershell übergeben werden, muss man gemäß der Powershell-Dokumentation folgenden Ansatz nehmen:
powershell -Command
Danach schließt sich dann der Skript-Block an. Dieses Vorgehen funktioniert bestens –allerdings nur so lange keine Anführungszeichen für die Dateinamen oder für weitere Parameter nötig werden.
Die Kommando-Shell-Skripts mit Cmd.exe besitzen eine spezielle, generische Platzhalter-Variable, %*, die alle unverwendeten Argumente ersetzt. Damit kann der Skripter Hochkomma um sie legen, wie er es für nötig hält. Allerdings lassen sich damit nicht alle Kommandos in der Powershell starten.
Listing 1: Cmd.exe Wrapper Scripts that Demonstrate %* Variable Expansion
::wrapper0.cmd
:: just expands and echoes arguments to standard output
echo %*
::wrapper1.cmd
:: Everything inside {} is echoed as a string
powershell -Command { .\echodemo.ps1 %* }
::wrapper2.cmd
:: Doublequotes are stripped out completely
powershell -Command "& {.\echodemo.ps1 %* }"
::wrapper3.cmd
:: Expanded arguments become one string in ‚%*‘
powershell -Command "& {.\echodemo.ps1 ‚%*‘ }"
::wrapper4.cmd
:: Works properly – PowerShell 2 only
powershell -File .\echodemo.ps1 %*
::wrapper5.cmd
:: Input processed as a command; works in PowerShell 1 and 2
echo .\echodemo.ps1 %* | powershell -Command –
::wrapper6.cmd
:: As wrapper5.cmd, but -NoExit keeps PowerShell running.
echo .\echodemo.ps1 %* | powershell -NoExit -Command –
Das Listing 1 zeigt einige Cmd.exe-Wrapper-Skripts, die verdeutlichen, wie die Erweiterung mit Hilfe der Variablen %* arbeitet. Diese Beispiele helfen bei der Untersuchung, was passiert, wenn man ein Cmd.exe-Wrapper-Skript verwendet und darin ein Powershell-Skript einbettet, das über mehrere in Anführungszeichen gesetzte Argumente enthält.
Das Bild 2 zeigt die Ausgabe für das erste der vier Skripts aus Listing 1. Die Ausgabe von wrapper0 verdeutlicht, dass die %*-Erweiterung die Dateinamen sauber ersetzt.
Das Skript wrapper1 ruft führt nicht dazu, dass echodemo.ps1 startet. Dagegen sieht es aus, als würde die Powershell alles innerhalb der Skript-Block-Klammern als einen String interpretieren und die Powershell gibt einfach zurück, was sie empfangen hat.
Beim wrapper2 wurde der Aufruf-Operator der Powershell, das &, eingebaut und der gesamte String in Anführungszeichen genommen. Nun läuft echodemo.ps1 zwar, doch die Anführungszeichen werden von den Argumenten weg genommen. Das Ergebnis: Jedes Wort im Dateinamen wird als eigenes Argument angesehen.
Beim wrapper3 wurde ein weiterer Versuch unternommen, der fehlschlagen musste. Dabei wurde der Ausdruck die die Erweiterung /also das %*, in Anführungszeichen genommen. Bei diesem Ansatz werden alle Kommandozeilen-Argumente in ein einziges Argument zusammengeführt.
Damit bleibt die Frage offen, wie man Daten, die Leerzeichen enthalten, an die Powershell übergeben kann. Und hierzu gibt es im Folgenden mehrere Lösungen.
Lösung 1: Die File Parameter der Powershell 2.0
Mit der Version 2.0 der Powershell steht eine alternative Technik für das Zerteilen – Parsen – der Strings zur Verfügung. Anstelle des Parameters -Command kann man nun den Parametzer -File übergeben und muss dann noch den Pfad zum Skript sowie die Argumente angeben, die man zusammen mit dem Skript verwenden möchte. Diese Argumente werden dann auch von der Powershell korrekt behandelt. Daher kann das Wrapper-Skript für die Powershell 2.0 wie folgt aussehen (es entspricht auch dem wrapper4.cmd im Listing 1):
PowerShell -File echodemo.ps1 %*
Dabei übergibt die Powershell die Argumente, die nach dem Skript-Pfad kommen, direkt an das Skript und zwar in Form des bereits vorbearbeiteten Arrays $args. Damit erscheint ein jedes in Anführungszeichen gepacktes Objekt als ein eigenes Argument. Doch das funktioniert nur, wenn man Powershell 2.0 installiert hat. Die Community Technology Preview (CTP) in der Version 3 (in englischer Sprache) von Powershell 2.0 steht bei Microsoft zum Download bereit.
Microsoft verweist allerdings darauf, diese Software nicht in einer Produktivumgebung ei8nzusetzen. Zudem muss man auf dem System, das die Powershell 2.0 CTP Version 3.0 beherbergen soll, die früheren Powershell-Versionen deinstallieren. Daher konzentrieren sich die weiteren Lösungen für das anfangs gezeigte Problem auf die Powershell Version 1.0.
Lösung 2: Argumente werden bei Powershell 1.0 zur Eingabe
Um die Flexibilität der Powershell zu erhöhen, lässt sich der Bindestrich (-) nach dem Parameter Command verwenden. Damit teilt man der Powershell mit, dass sie die Eingabe als Kommando-Code interpretieren soll, den sie auszuführen hat. In einer Batch-Datei bedeutet das, dass mit Hilfe dieser Technik man alles in eine Pipe geben – auch die Ausgabe des Echo-Kommandos – und das an die Powershell leiten kann.
Es wird dort in Empfang genommen, ohne dass ein weiteres Parsen oder Entfernen der Anführungszeichen durch Windows oder Cmd.exe erfolgt. Die Batch-Datei wrapper5.cmd (sie ist im Listing 1 enthalten) verwendet diese Technik. Damit interpretiert die Powershell dann die in Anführungszeichen eingeschlossenen Argumente korrekt.
Die Powershell beendet beim Ansatz mit wrapper5.cmd ihre Arbeit automatisch, nachdem sie das Skript abgearbeitet hat. Dieses Verhalten ist wünschenswert, wenn mit einem Skript eine Aufgabe direkt erledigt werden soll. Möchte man aber zum Beispiel die Ausgabe des Powershell-Skripts weiter untersuchen, muss man den Parameter NoExit der Powershell einsetzen. Ein Beispiel dazu zeigt wrapper6.cmd aus Listing 1.
Diese Technik funktioniert auch mit den verschiedenen Versionen der Powershell 2.0 CTP. Damit sollte dieser Ansatz auch aufwärtskompatibel sein, wenn die endgültige Version der Powershell 2.0 freigegeben wird. Allerdings gibt es noch einige Problembereiche.
Viele Sonderzeichen der Powershell werden auch in Dateinamen verwendet. Namen, die Klammerzeichen enthalten, sind dabei besonders zu erwähnen. Denn bei ihnen kann es vorkommen, dass die Powershell versucht, den Inhalt in den Klammern als ein eingebettetes Kommando zu interpretieren.
Um derartige Situationen zu vermeiden, ist eine bessere Lösung für das Übergeben von Kommandos an die Powershell nötig. Es muss ein Vorgehen gewählt werden, damit die Powershell zweifelsfrei bestimmen kann, ob es sich um Datei- oder Verzeichnisnamen handelt.
Daher ist eine mächtigere Lösung nötig, als sie mit dem Ansatz von Wrappern in Form von Batch-Dateien zur Verfügung steht. Die ultimative Lösung verwendet daher VBScript, um die Argumente korrekt an die Powershell zu übergeben.
Lösung 3: Ein WSH-Wrapper-Skript kommt zum Einsatz
Das Listing 2 zeigt ein generisches Wrapper-Skript, das mit Hilfe des Windows Script Host (WSH) arbeitet. Es lässt sich als ein Drag&Drop-Wrapper für jedes Powershell-Skript verwenden.
Listing 2: Generic WSH Wrapper Script for Any PowerShell Script
‚ BEGIN WSH wrapper script
‚ Should have same basename as PS script it will run,
‚ and must be in same folder as PS script.
‚ ex: if c:\tmp\fred.ps1, this must be c:\tmp\fred.vbs
dim fso: Set fso = CreateObject("Scripting.FileSystemObject")
Dim WshScript: WshScript = WScript.ScriptFullName
Dim PsScript
PsScript = fso.BuildPath( _
fso.GetFile(WshScript).ParentFolder.Path, _
fso.GetBaseName(WshScript) & ".ps1")
‚ Escape spaces embedded in script path, if any.
PsScript = Replace(PsScript, " ", "` ")
‚ Escape single quotes by doubling.
PsScript = Replace(PsScript, "’", "’")
Dim i, arg
i = 0
Dim ArgSet: Set ArgSet = CreateObject("Scripting.Dictionary")
Argset(i) = PsScript
For each arg in WScript.Arguments
‚ EXPLICITLY ensure these resolve to file/folder paths
if fso.FileExists(arg) or fso.FolderExists(arg) then
i = i + 1
‚ Include escapes for singlequotes in paths, if any
Argset(i) = "’" & Replace(arg, "’", "’") & "’"
End If
Next
Dim base
‚ BEGIN Callout A
base = "PowerShell -Command ""& {"
‚ END Callout A
‚ Use the following base instead to keep the window open.
‚ BEGIN Callout B
‚base = "powershell -NoExit -Command ""& {"
‚ END Callout B
Dim Command
Command = base & Join(ArgSet.Items) & "}"""
‚ WScript.Echo "command as passed to PowerShell:", Command
Dim WshShell: Set WshShell = CreateObject("WScript.Shell")
‚ Now run the command
‚ BEGIN Callout C
WshShell.Run Command, 2
‚ END Callout C
‚ END WSH Wrapper Script
Um das Wrapper-Skript vorzubereiten, müssen lediglich die folgenden Schritte ausgeführt werden:
1. Es ist eine Kopie des VBScript-Templates im selben Verzeichnis abzuspeichern, in dem auf das Powershell-Skript liegt, das man starten möchte. Dabei ist sicher zu stellen, dass der Basisname – also der reine Dateinamen für das Skript – der VBScript-Datei identisch mit dem reinen Dateinamen des Powershell-Skripts ist. Wenn zum Beispiel der Name für das Powershell-Skript
C:\apps\Scan-File.ps1
lautet, so ist der reine Dateiname Scan-File. Daher müsste das VBScript-Template als
C:\apps\Scan-File.vbs
abgespeichert werden. Dann kann das VBScript-Wrapper-Skript herausfinden, wie der Name des entsprechenden Powershell-Skripts lautet.
2. Das VBScript-Wrapper-Skript startet das Powershell-Skript. Damit beendet es automatisch wenn die Aktionen ausgeführt sind. Soll das Powershell-Skript weiter laufen, muss der Administrator in den Bereich des Wrapper-Skripts gehen, in dem das Basis-Kommando definiert ist (siehe den Bereich des Callout A) und muss es dort auskommentieren. Dazu ist ein einfaches Anführungszeichen einzufügen. Danach muss er das einfache Anführungszeichen bei der Definition im Bereich des Callout B entfernen.
3. Das VBScript-Wrapper-Skript startet das Powershell-Skript in einem minimierten Fenster. Soll das Powershell-Fenster anders aussehen, kann man das im Bereich des Callout C ändern. Dabei handelt es sich um die Zeile, die das WshShellRun-Kommando liest. Die abschließende Zahl bestimmt den Fensterstil. Um das Fenster mit der Standardeinstellung in Bezug auf Position und Größe zu bekommen, ist die 2 in eine 1 zu ändern. Wenn das Skript dem Anwender keine Eingabe abverlangt, kann man auch eine 0 angeben. Damit läuft es komplett im Hintergrund. Diese Änderungen sollte man aber nur dann machen, wenn die Powershell nicht den Code aus dem Callout B verwendet.
4. Wenn man es möchte, kann man auch noch eine Verknüpfung zum WSH-Skript auf dem Desktop erstellen. Damit kann man noch einfacher darauf zugreifen.
Um das Powershell-Skript zu verwenden, braucht der Administrator dann lediglich die Dateien und Ordner mit Drag&Drop auf das WSH-Skript (oder die zugehörigen Verknüpfung) zu ziehen. Das Skript bestimmt dann das Powershell-Skript – basierend auf der Vorgabe, dass für das Powershell-Skript als auch für das WSH-Skript derselbe Basisdateiname genommen wird und dass beide Objekte im selben Ordner liegen. Dann wird die Kommandozeile für die Ausführung im Powershell-Skript aufbereitet.
Zuerst stellt VBScript sicher, dass eventuelle Leerzeichen im Pfad mit den entsprechenden Escape-Zeichen versehen werden. Damit interpretiert die Powershell die Bestandteile des Namens nicht als einzelne Argumente. Danach geht VBScript mit einer Schleife durch die Namen, die darauf abgelegt wurden. VBScript prüft dabei, ob es sich bei jedem Objekt um eine echte Datei oder aber um einen Ordner handelt. Andere Objekte werden weggelassen.
Nachdem fest steht, dass es sich nur um Objekte aus dem Dateisystem handelt, prüft VBScript die einzelne Hochkomma, die eventuell als Bestandteil des Dateinamens Verwendung finden. Auch diese Zeichen bekommen dann ein Escape-Symbol vorangestellt, damit die Powershell damit keine Probleme hat. Als nächstes wird der Name mit einfachen Hochkommas umgeben und in der Collection von vorbereiteten Argumenten abgelegt. Wenn alle Argumente bearbeitet wurden, fügt sie das Skript in einem „Powershell-sicheren“ Kommando ab und startet es.
Diese Vorgehensweise wird für viele zunächst als ein enormer Aufwand für das Problem angesehen. Doch in der Praxis erweist es sich als eine vernünftige Lösung. Andere Skriptsprachen benötigen von Fall zu Fall ähnliche Vorkehrungen. Ein Beispiel dazu ist Perl – auch diese Skriptsprache braucht Batch-Datei-Wrapper, um Befehle von der Kommandozeile aus zu starten. Allerdings sind diese Wrapper bis zu Hunderte Zeile von Code lang. Zudem kann der Administrator die hier gezeigten Lösungen nehmen und sie in Form eines eigenen Templates ablegen. Dann muss er es nur für den jeweiligen Einsatzfall anpassen.
Die richtige Lösung wählen
Es wird daher für viele Anwender das größere Problem sein, die passende Lösung aus den hier gezeigten Ansätzen zu wählen. Wenn die Powershell 2.0 reif für den Einsatz im Produktivbetrieb ist, wird dies sicher die angesagte Lösung sein.
Bis es aber soweit ist, kann man sich mit den anderen Lösungen behelfen. In der Download-Datei zu diesem Beitrag sind daher neben den verschiedenen vorgestellten Wrappern auch Templates angelegt, die zu den Wrapper-Skripts 2, 5 und 6 passen.