Externe EXEs aus PowerShell korrekt aufrufen — Die Fallstricke bei Argument-Quoting, Exitcodes und Zeichensalat
· Go Komura · PowerShell, Windows, Automatisierung, Skript, Prozess, Zeichenkodierung, Betriebsoptimierung, Stapelverarbeitung
„In meiner Eingabeaufforderung funktioniert es einwandfrei, aber sobald ich es in ein PowerShell-Skript übertrage, gibt das externe Werkzeug einen Fehler zurück“ — das werden Sie fast sicher erleben, wenn Sie Batchdateien in PowerShell umwandeln oder eine hauseigene EXE oder ein Open-Source-CLI automatisieren. Meistens liegt die Ursache nicht in der Logik, sondern in dem Weg, auf dem die Argumente das Programm erreichen. Ein Pfad mit Leerzeichen, ein Argument mit einem doppelten Anführungszeichen, eine Zeichenfolge mit % oder ( ). Jedes davon kann der Auslöser sein, der das Argument, von dem Sie glaubten, es übergeben zu haben, in etwas anderes verwandelt.
Erschwerend kommt hinzu, dass sich dieses Verhalten in PowerShell 7.3 geändert hat. Ein Workaround, der für 5.1 geschrieben wurde, wird in 7 zu einem doppelten Escaping, und ein für 7 geschriebenes Skript zerbricht in 5.1. In deutschsprachigen Umgebungen kommt oben drauf noch Zeichensalat in der Ausgabe.
Dieser Artikel behandelt, wie Argumente tatsächlich reisen, wenn Sie ein externes Programm (einen nativen Befehl) aus PowerShell aufrufen, ausgehend von der Spezifikation: die korrekte Verwendung von --%, ProcessStartInfo für den Fall, dass Sie Sicherheit brauchen, den Umgang mit Exitcodes und stderr sowie den Umgang mit Zeichensalat — alles aus der Perspektive der Praxis. Das Design der Fehlerbehandlung selbst wird in „PowerShell-Fehlerbehandlung und Wiederholungsdesign“ behandelt, dieser Artikel bleibt daher auf die Grenze zu externen Prozessen fokussiert.
1. Das Wichtigste zuerst
- PowerShell parst auch die Argumente externer Programme selbst. Alles nach einem Befehlsaufruf wird im Argumentmodus geparst, und Werte mit Leerzeichen müssen in Anführungszeichen eingeschlossen werden. Zeichen wie
,(){}|&<>@#sind Metazeichen; um sie wörtlich zu übergeben, entwerten Sie sie mit einem Backtick.1 - PowerShell 7.3 hat geändert, wie Argumente übergeben werden (ein Breaking Change). Eingebettete Anführungszeichen und leere Zeichenfolgen als Argumente bleiben nun erhalten, und Sie können das Verhalten mit
$PSNativeCommandArgumentPassingwählen. Der Standard unter Windows istWindows.12 - Im Modus
Windowsfallen nur cmd.exe, cscript.exe, wscript.exe und die Erweiterungen.bat.cmd.js.vbs.wsfauf die alte (Legacy) Übergabeart zurück. Das ist eine Ausnahme zur Kompatibilität mit vorhandenen Batch-Assets.1 - Das Stop-Parsing-Token
--%ist eine letzte Möglichkeit, die alles danach wörtlich behandelt. Umgebungsvariablen in der Form%VAR%werden jedoch weiterhin expandiert, PowerShell-Variablen lassen sich überhaupt nicht verwenden, und die Wirkung endet bei einem Zeilenumbruch oder einer Pipe. Auch Umleitungen lassen sich nicht schreiben.1 - Möchten Sie den Wert einer Variablen zuverlässig übergeben, ist ein Array die erste Wahl. Splatting mit
& $exe @argArraymacht jedes Element zu einem eigenständigen Argument. MitProcessStartInfo.ArgumentListkönnen Sie das Quoting .NET überlassen, aber das ist eine API ab .NET Core 2.1 und in 5.1 nicht verfügbar.13 - Übergeben Sie nie nicht vertrauenswürdige Eingaben an eine Batchdatei. Unter Windows werden Argumente an eine Batchdatei als rohe Befehlszeilenzeichenfolge an cmd.exe weitergereicht, weshalb die offizielle Dokumentation davor warnt, nicht vertrauenswürdige Eingaben auf anderem Wege zu übergeben.1
- Beurteilen Sie Erfolg oder Misserfolg mit
$LASTEXITCODE. Standardmäßig ist ein von null verschiedener Exitcode kein PowerShell-Fehler und landet nicht in try/catch.4 - Beheben Sie Zeichensalat auf Sende- und Empfangsseite getrennt. Das Decodieren der Ausgabe eines externen Befehls übernimmt
[Console]::OutputEncoding; Zeichenfolgen, die von PowerShell an einen externen Befehl weitergeleitet werden, übernimmt$OutputEncoding.2 - Im Zweifel
Trace-Command -Name ParameterBindingverwenden, um zu sehen, welche Argumente tatsächlich übergeben wurden. Ab PowerShell 7.3 lässt sich auch die Argumentbindung nativer Befehle nachverfolgen.15
2. Warum Argumente verstümmelt werden — Die Prämisse des Argumentmodus
PowerShell zerlegt eine Befehlszeile in Token und interpretiert jedes entweder im Ausdrucksmodus oder im Argumentmodus. Sobald ein Befehlsaufruf erscheint, wird alles Nachfolgende im Argumentmodus geparst. Im Argumentmodus wird eine Eingabe grundsätzlich als expandierbare Zeichenfolge behandelt: Etwas, das mit $ beginnt, ist ein Variablenverweis, ein Anführungszeichen beginnt eine Zeichenfolge, ( ) beginnen einen Ausdruck, und so weiter — mit anderen Worten, die Symbole tragen syntaktische Bedeutung.1
Das bedeutet, dass eine Zeichenfolge, die Sie direkt in eine Eingabeaufforderung einfügen und ausführen könnten, in PowerShell völlig anders interpretiert werden kann. Das klassische Beispiel ist icacls.1
# Funktioniert in cmd.exe, aber in der PowerShell-2.0-Ära wurden die Klammern als Ausdruck interpretiert und verursachten einen Fehler
icacls X:\VMS /grant Dom\HVAdmin:(CI)(OI)F
# Die Metazeichen mit Backticks entwerten (schwer zu lesen)
icacls X:\VMS /grant Dom\HVAdmin:`(CI`)`(OI`)F
# Mit dem Stop-Parsing-Token erklären, dass "alles ab hier wörtlich ist" (PowerShell 3.0+)
icacls X:\VMS --% /grant Dom\HVAdmin:(CI)(OI)F
Die andere Prämisse ist der Weg, auf dem geparste Argumente das Programm erreichen. In Windows PowerShell 5.1 werden die geparsten Argumente zu einer einzigen, durch Leerzeichen getrennten Zeichenfolge neu zusammengesetzt, bevor sie an den Prozess übergeben werden. Bei dieser Neuzusammensetzung gehen Anführungszeichen innerhalb eines Arguments verloren, und leere Zeichenfolgen als Argumente verschwinden — der klassische Unfall der 5.1-Ära.1
3. Der Breaking Change in PowerShell 7.3 — $PSNativeCommandArgumentPassing
PowerShell 7.3 hat geändert, wie diese Zusammensetzung funktioniert. Das ist eine Stelle, an der die offizielle Dokumentation ausdrücklich sagt, dass es sich um einen Breaking Change gegenüber dem Verhalten von Windows PowerShell 5.1 handelt.1
Das neue Verhalten lässt sich über die Präferenzvariable $PSNativeCommandArgumentPassing umschalten, deren Werte Legacy (das alte Verhalten), Standard und Windows lauten. Der Standard auf der Windows-Plattform ist Windows; unter Nicht-Windows ist es Standard.12
Es gibt genau einen Unterschied zwischen Windows und Standard: Im Modus Windows verwenden die folgenden Aufrufe automatisch die Legacy-Übergabeart.1
| Aufrufe, die automatisch den Legacy-Stil erhalten |
|---|
cmd.exe / cscript.exe / wscript.exe |
Dateien mit den Erweiterungen .bat .cmd .js .vbs .wsf |
Es ist eine Ausnahme, die entwickelt wurde, um den Unfall zu verhindern, dass alte Batchdateien oder WSH-Skripte „genau dann zerbrechen, wenn man PowerShell auf 7 aktualisiert, weil sich die Argumentbehandlung geändert hat“. Andersherum betrachtet: Setzen Sie $PSNativeCommandArgumentPassing ausdrücklich auf Standard oder Legacy, wird diese Feststellung nicht mehr getroffen.1
Der neue Stil verbessert zwei Dinge.1
# (1) In eine Zeichenfolge eingebettete Anführungszeichen bleiben erhalten
$a = 'a" "b'
TestExe -echoargs $a 'c" "d' e" "f
# Arg 0 is <a" "b>
# Arg 1 is <c" "d>
# Arg 2 is <e f>
# (2) Leere Zeichenfolgen als Argumente überleben, statt zu verschwinden
TestExe -echoargs '' a b ''
# Arg 0 is <>
# Arg 1 is <a>
# Arg 2 is <b>
# Arg 3 is <>
Möchten Sie eine in Anführungszeichen gesetzte Pfadzeichenfolge wie "C:\Program Files (x86)\Microsoft\" unverändert übergeben, können Sie sie im Modus Windows / Standard direkt so schreiben.1
# 7.3 und später (Modus Windows / Standard)
TestExe -echoargs '"C:\Program Files (x86)\Microsoft\"'
# Dasselbe Ergebnis im Legacy-Modus (das Äquivalent von 5.1) erfordert doppeltes Escaping der Anführungszeichen
TestExe -echoargs "\""C:\Program Files (x86)\Microsoft\\"""
Ein Hinweis dazu. Der Backslash (\) ist nicht das Escape-Zeichen von PowerShell. \" erscheint im obigen Beispiel, weil die zugrunde liegende .NET-API (ProcessStartInfo.ArgumentList) den Backslash als Escape-Zeichen behandelt; das Escaping in der PowerShell-Syntax erfolgt mit einem Backtick (`). Die Vermischung dieser beiden ist der größte Grund, warum dieser Bereich schwierig wirkt.13
Die praktische Beurteilung ist einfach. In einer Umgebung, in der 5.1 und 7 nebeneinander bestehen, hören Sie auf, Argumente mit Anführungszeichen als Literale zu schreiben. Verlassen Sie sich auf „ein Array übergeben“ und „ProcessStartInfo verwenden“ aus den nächsten Abschnitten, und Sie sind fast vollständig gegen Versionsunterschiede isoliert. Zur grundsätzlichen Haltung zur Koexistenz von 5.1/7 siehe „Die Unterschiede zwischen Windows PowerShell 5.1 und PowerShell 7“.
4. Die korrekte Verwendung und die Grenzen von –% (dem Stop-Parsing-Token)
--% ist ein Token, das PowerShell veranlasst, die Zeichen danach unverändert durchzureichen, ohne sie zu interpretieren (PowerShell 3.0+). Die offizielle Dokumentation stellt ausdrücklich fest, dass es nur zur Verwendung mit nativen Befehlen auf der Windows-Plattform gedacht ist.1
PS> cmd /c echo "a|b"
'b' is not recognized as an internal or external command,
operable program or batch file.
PS> cmd /c --% echo "a|b"
"a|b"
Es ist mächtig, aber Sie müssen seine Einschränkungen genau kennen.1
| Einschränkung | Details |
|---|---|
| Nur Umgebungsvariablen werden expandiert | Formen wie %USERPROFILE% in der Form %<Name>% werden immer expandiert. Escaping mit %% ist nicht möglich. Undefinierte Namen bleiben unverändert |
| PowerShell-Variablen können nicht verwendet werden | $path und Ähnliches werden nicht expandiert; sie werden als literale Zeichenfolgen übergeben |
| Umfang der Wirkung | Bis zum nächsten Zeilenumbruch oder Pipe (|). Er lässt sich nicht mit Backtick-Zeilenfortsetzung erweitern und nicht mit ; beenden |
| Keine Umleitung | Dinge wie >datei.txt werden unverändert als Argumente an den Zielbefehl weitergereicht |
Mit anderen Worten: --% ist nur dann verwendbar, wenn das, was Sie übergeben, vollständig eine feste Zeichenfolge ist und kein % enthält. Im typischen Automatisierungsszenario, in dem ein Skript einen Wert in eine Variable zusammensetzt und übergibt, ist diese Bedingung praktisch nie erfüllt. Eine Gewohnheit von „einfach --% hinzufügen“ bricht zusammen, sobald Sie ein Kennwort oder ein Platzhalterzeichen mit % übergeben.
5. Variablen zuverlässig übergeben — Array-Splatting und ProcessStartInfo
Wenn Sie Argumente übergeben müssen, die Variablenwerte enthalten, ist die erste Wahl, ein Argument pro Array-Element zu setzen und das Array zu splatten. Jedes Array-Element wird als eigenständiges Argument übergeben, sodass Sie selbst für Pfade mit Leerzeichen keine eigenen Anführungszeichen schreiben müssen.
$exe = 'C:\Program Files\MyTool\convert.exe'
$args = @(
'--input', 'D:\Order Data\2026-07.csv' # Leerzeichen und Nicht-ASCII-Zeichen sind kein Problem
'--output', 'D:\Output\result.json'
'--mode', 'strict'
)
& $exe @args # Array-Splatting. Jedes Element wird zu einem Argument
if ($LASTEXITCODE -ne 0) { throw "Conversion failed (ExitCode=$LASTEXITCODE)" }
Der Aufrufoperator & ist außerdem erforderlich, wenn Sie eine exe mit Leerzeichen im Pfad ausführen ('C:\Program Files\...' allein wird lediglich als String-Literal ausgewertet und nie ausgeführt).
Wenn Sie mehr Sicherheit brauchen — vollständige Kontrolle über jedes einzelne Argument, oder die Möglichkeit, die Ausgabekodierung pro Prozess anzugeben — verwenden Sie direkt .NETs ProcessStartInfo. Zu ArgumentList hinzugefügte Werte werden von .NET angemessen mit Anführungszeichen versehen, sodass sie weder von PowerShells Parsing noch von der erneuten Analyse der Befehlszeile betroffen sind.3
Allerdings ist ArgumentList eine API ab .NET Core 2.1 und existiert nicht bei ProcessStartInfo von Windows PowerShell 5.1, das auf .NET Framework läuft.3 Setzen Sie unter 5.1 entweder die Arguments-Zeichenfolge wie weiter unten in diesem Abschnitt selbst zusammen, oder verwenden Sie das oben beschriebene Array-Splatting.
# [PowerShell 7+] Die Argumentzusammensetzung .NET überlassen
$psi = [System.Diagnostics.ProcessStartInfo]::new()
$psi.FileName = 'C:\Program Files\MyTool\convert.exe'
foreach ($a in '--input', $inputPath, '--output', $outputPath) {
$psi.ArgumentList.Add($a) # 1 Element = 1 Argument. Keine eigenen Anführungszeichen schreiben (nur 7)
}
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.UseShellExecute = $false
# Die Ausgabekodierung ausdrücklich angeben zu können, ist ein weiterer Vorteil von ProcessStartInfo (Abschnitt 6)
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
$proc = [System.Diagnostics.Process]::Start($psi)
# Standardausgabe und Standardfehler "gleichzeitig" lesen. Liest man einen der beiden
# synchron vollständig aus und erst danach den anderen, füllt sich der Puffer der anderen
# Pipe, während man wartet, der Kindprozess blockiert beim Schreiben, und man landet
# genau dort in einem Deadlock
$stdoutTask = $proc.StandardOutput.ReadToEndAsync()
$stderrTask = $proc.StandardError.ReadToEndAsync()
$proc.WaitForExit()
$stdout = $stdoutTask.GetAwaiter().GetResult()
$stderr = $stderrTask.GetAwaiter().GetResult()
if ($proc.ExitCode -ne 0) {
throw "Conversion failed (ExitCode=$($proc.ExitCode)): $stderr"
}
Der häufigste Unfall bei der Ausgabeumleitung ist ein Deadlock. Der Puffer einer Pipe hat eine Obergrenze, und ist sie erreicht, blockiert der Kindprozess beim Schreiben. Es ist also nicht nur gefährlich, erst WaitForExit() aufzurufen und danach zu lesen — einen Stream mit ReadToEnd() synchron vollständig zu lesen, bevor Sie den anderen lesen, ist ebenfalls gefährlich (füllt sich der Puffer des Streams, den Sie nicht lesen, zuerst, stockt der Kindprozess, und ReadToEnd() kehrt nie zurück). Beginnen Sie entweder, wie oben, beide asynchron zu lesen und warten Sie dann, oder entwerfen Sie es so, dass nur ein Stream umgeleitet wird. Zum allgemeinen Umgang mit Kindprozessen siehe auch „Eine Checkliste zum sicheren Umgang mit Kindprozessen in Windows-Apps“.
Verwenden Sie ProcessStartInfo unter Windows PowerShell 5.1, landen Sie dabei, Arguments eine einzige, von Ihnen selbst mit Anführungszeichen versehene Zeichenfolge zu übergeben. Diese Zusammensetzung von Hand zu schreiben ist eine Unfallquelle, machen Sie also unter 5.1 Array-Splatting (& $exe @args) zu Ihrer ersten Wahl.
# [5.1] Ohne ArgumentList eine einzige Zeichenfolge mit den Anführungszeichen selbst zusammensetzen
$quote = {
param([string] $s)
if ($s -eq '') { return '""' } # Eine leere Zeichenfolge muss zu "" werden, sonst verschwindet das Argument vollständig
if ($s -notmatch '[\s"]') { return $s } # Ist kein Einschließen nötig, unverändert lassen
# Den Parsing-Regeln der Windows-Befehlszeile folgen:
# (1) Die Folge von Backslashes unmittelbar vor einem " verdoppeln und das " selbst zu \" machen
# (2) Eine abschließende Folge von Backslashes ebenfalls verdoppeln. Da sie unmittelbar vor
# dem schließenden Anführungszeichen steht, würde sie unverändert als \" geparst, das
# Anführungszeichen würde nie schließen, und sogar nachfolgende Argumente wären kaputt
# (z. B. 'C:\Program Files\input\' -> "C:\Program Files\input\\")
$e = $s -replace '(\\*)"', '$1$1\"'
$e = $e -replace '(\\+)$', '$1$1'
'"' + $e + '"'
}
$psi.Arguments = (@('--input', $inputPath, '--output', $outputPath) |
ForEach-Object { & $quote $_ }) -join ' '
Die Kompliziertheit dieser Regeln ist genau der Grund, weshalb Sie die manuelle Zusammensetzung unter 5.1 vermeiden sollten. Allerdings ist Array-Splatting auch unter Windows PowerShell 5.1 kein Allheilmittel. Die übergebenen Werte werden letztlich im alten Stil zu einer Befehlszeilenzeichenfolge neu zusammengesetzt, sodass leere Zeichenfolgen als Argumente verschwinden und Werte mit Anführungszeichen verändert werden.1 Verwenden Sie daher unter 5.1 folgende Arbeitsteilung.
| Übergebenes Argument unter 5.1 | Methode |
|---|---|
| Gewöhnliche Werte mit Leerzeichen oder Nicht-ASCII-Zeichen | Array-Splatting (& $exe @args) genügt |
| Leere Zeichenfolgen und Werte mit Anführungszeichen | ProcessStartInfo + das obige Escaping, oder --% (nur feste Zeichenfolgen) |
PowerShell 7 löst beide Probleme, sodass diese Arbeitsteilung überflüssig wird.
Beachten Sie, dass die offizielle Dokumentation davor warnt, nicht vertrauenswürdige Eingaben an Batchdateien zu übergeben, weil Argumente an eine Batchdatei als rohe Befehlszeilenzeichenfolge an cmd.exe weitergereicht werden.1 Ein Design, das Benutzereingaben oder Dateinamen direkt in einen Batchaufruf verkettet, ist ein Nährboden für Command Injection. Übergeben Sie die Werte über eine temporäre Datei oder Umgebungsvariablen, oder migrieren Sie die Batchdatei selbst zu PowerShell (siehe „Sollte diese Batchdatei zu PowerShell migriert werden?“).
6. Zeichensalat beheben — [Console]::OutputEncoding und $OutputEncoding
In einer nicht-englischsprachigen Umgebung ist Zeichensalat unvermeidlich. Der entscheidende Punkt ist, dass die verwendete Einstellung von der Richtung abhängt.
| Richtung | Verwendete Einstellung | Symptom |
|---|---|---|
| PowerShell empfängt die Ausgabe eines externen Befehls | [Console]::OutputEncoding |
Die Ausgabe eines UTF-8-Werkzeugs kommt verstümmelt zurück |
| PowerShell leitet eine Zeichenfolge an einen externen Befehl weiter | $OutputEncoding |
Der gesendete nicht-ASCII-Text ist auf der anderen Seite verstümmelt |
$OutputEncoding ist die Präferenzvariable, die die Kodierung bestimmt, die PowerShell beim Senden von Zeichenfolgen an einen nativen Befehl verwendet.2 Das Decodieren der Bytefolge, die ein externer Befehl ausgibt, zurück in eine Zeichenfolge, ist hingegen die Aufgabe von [Console]::OutputEncoding. Behebt jemand eines der beiden und sagt „es ist immer noch verstümmelt“, liegt fast immer eine Vermischung dieser beiden vor.
# Die Standardbehebung beim Aufruf eines externen Werkzeugs, das UTF-8 ausgibt, aus Windows PowerShell 5.1
$prevOut = [Console]::OutputEncoding
$prevPs = $OutputEncoding
try {
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) # UTF-8 ohne BOM
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$result = & $exe --list
}
finally {
# Dies wirkt sich auf die gesamte Sitzung aus, also immer zurücksetzen
[Console]::OutputEncoding = $prevOut
$OutputEncoding = $prevPs
}
Verwenden Sie ProcessStartInfo, können Sie StandardOutputEncoding / StandardErrorEncoding wie im vorherigen Abschnitt beschrieben pro Prozess angeben, sodass Sie die Einstellungen der Sitzung nie anfassen müssen. Das ist die Option mit weniger Nebenwirkungen. Zeichenkodierung unter Windows allgemein wird in „Windows-Zeichenkodierungen und Zeilenenden“ behandelt.
7. Exitcodes und stderr — „Es war erfolgreich, wurde aber als Fehlschlag behandelt“ verhindern
Sie beurteilen Erfolg oder Misserfolg eines externen Programms mit $LASTEXITCODE. Standardmäßig erzeugt ein von null verschiedener Exitcode keinen ErrorRecord und landet nicht in try/catch.4 Diese Grundlagen wurden ausführlich in „PowerShell-Fehlerbehandlung und Wiederholungsdesign“ behandelt, hier ergänze ich nur zwei Punkte, die spezifisch für externe Prozesse sind.
(1) Das Schreiben nach stderr ist kein „Fehlschlag“. Viele CLI-Werkzeuge schreiben Fortschritt und Protokolle nach stderr. PowerShell leitet die stderr-Ausgabe eines nativen Befehls in den Fehlerstrom, sodass der Bildschirm rot wird und es wie ein Fehlschlag aussieht — ist der Exitcode jedoch 0, war es erfolgreich. In Windows PowerShell 5.1 konnte $? allein durch etwas, das nach stderr geschrieben wurde, zu $false werden; in PowerShell 7 wurde das behoben, sodass es nur bei einem von null verschiedenen Exitcode zu $false wird.6
(2) Der Typ nach dem Zusammenführen mit 2>&1 unterscheidet sich je nach Version. In Windows PowerShell 5.1 wird jede stderr-Zeile als ErrorRecord eingemischt, aber ab PowerShell 7.4 wird die umgeleitete Ausgabe eines nativen Befehls als Bytestrom behandelt, und nach dem Zusammenführen wird sie zu Zeichenfolgendaten.78 Mit anderen Worten: Code, der „nach ErrorRecord oder nicht sortiert“, funktioniert unter 7.4 und später nicht mehr. Brauchen Sie beide Ausgaben, ist der zuverlässige Ansatz, sie getrennt zu empfangen, statt sie zusammenzuführen.
# [Empfohlen] Stdout und stderr getrennt empfangen (unabhängig von Versionsunterschieden)
$errFile = [System.IO.Path]::GetTempFileName()
try {
$stdout = & $exe --import $csvPath 2> $errFile
$stderr = Get-Content -Path $errFile -Raw
# Beide im Protokoll behalten
$stdout | Add-Content -Path $logPath -Encoding utf8
if ($stderr) { $stderr | Add-Content -Path $logPath -Encoding utf8 }
if ($LASTEXITCODE -ne 0) {
throw "Import failed (ExitCode=$LASTEXITCODE): $stderr"
}
# Kommen Sie hierher, bedeutet das Erfolg. Eine Ausgabe auf stderr gilt nicht als Fehlschlag
}
finally {
Remove-Item $errFile -ErrorAction SilentlyContinue
}
# [Wenn Sie nicht unterscheiden müssen] Zusammenführen ist unproblematisch, wenn Sie nur alles ins Protokoll kippen
(& $exe --import $csvPath 2>&1) | ForEach-Object { $_.ToString() } |
Add-Content -Path $logPath -Encoding utf8
8. Prüfen, was tatsächlich übergeben wurde
Escaping auf Verdacht hinzuzufügen macht die Dinge schlimmer. Der schnellste Weg ist, sich anzusehen, welche Argumente tatsächlich übergeben wurden. Ab PowerShell 7.3 können Sie die Argumentbindung nativer Befehle mit Trace-Command nachverfolgen.15
Trace-Command -Name ParameterBinding -PSHost -Expression {
& $exe --input 'D:\Order Data\2026-07.csv' --mode strict
}
# DEBUG: ... BIND cmd line arg [--input] to position [0]
# DEBUG: ... BIND cmd line arg [D:\Order Data\2026-07.csv] to position [1]
Ist das, was Sie aufrufen, Ihr eigenes Werkzeug, macht das Hinzufügen eines einzigen Prüfmodus, der lediglich die empfangenen args ausgibt, diese Art der Untersuchung augenblicklich (dieselbe Idee wie TestExe -echoargs in PowerShells eigenem Testwerkzeug).1 Möchten Sie dasselbe in einer aktuellen 5.1-Umgebung tun, genügt ein kleines .ps1, das lediglich $args auflistet, oder eine kleine EXE, die lediglich ihre Argumente ausgibt.
9. Praktische Faustregeln (Entscheidungstabelle)
| Situation | Optionen | Richtlinie |
|---|---|---|
Argumente mit fester Zeichenfolge (ohne %) |
--% / normaler Aufruf |
Wird das Escaping unübersichtlich, ist --% der kürzeste Weg. Variablen lassen sich jedoch nicht verwenden1 |
| Den Wert einer Variablen übergeben | Array-Splatting & $exe @args |
Erste Wahl. Keine eigenen Anführungszeichen nötig, selbst bei Leerzeichen, Nicht-ASCII-Zeichen oder Symbolen |
| Derselbe Code soll unter 5.1 und 7 funktionieren | Array-Splatting | Der Code ist derselbe, aber unter 5.1 brechen leere Zeichenfolgen und Argumente mit Anführungszeichen (siehe Hinweis unten)1 |
| Leere Zeichenfolgen oder Argumente mit Anführungszeichen unter 5.1 übergeben | ProcessStartInfo + eigenes Escaping | Zuverlässig, weil es nicht durch die Befehlszeilen-Rekonstruktion von 5.1 läuft (Abschnitt 5) |
| Vollständige Kontrolle über jedes Argument gewünscht (nur 7) | ProcessStartInfo + ArgumentList |
Erlaubt es, das Quoting .NET zu überlassen. Eine API ab .NET Core 2.13 |
| Separates Fenster, anderer Benutzer oder Erhöhung nötig | Start-Process | ExitCode mit -Wait -PassThru erhalten. Ausgabe in eine Datei umleiten9 |
| Einen Wert an eine Batchdatei (.bat) übergeben | Über Umgebungsvariablen oder eine temporäre Datei | Nicht vertrauenswürdige Eingaben nicht als Argumente übergeben (offizielle Warnung)1 |
| Ausgabe kommt als Zeichensalat zurück | [Console]::OutputEncoding (Empfang) / $OutputEncoding (Senden) |
Die Einstellung hängt von der Richtung ab. Pro Prozess StandardOutputEncoding verwenden2 |
| Erfolg oder Misserfolg beurteilen | $LASTEXITCODE |
Ein von null verschiedener Code landet standardmäßig nicht in catch. Ausgabe auf stderr bedeutet nicht Fehlschlag46 |
| Stdout von stderr unterscheiden wollen | Umleiten und getrennt empfangen | Der Typ nach dem Zusammenführen mit 2>&1 unterscheidet sich zwischen 5.1 und 7.4+78 |
| Keine Ahnung, was tatsächlich übergeben wurde | Trace-Command -Name ParameterBinding |
Messen, bevor Sie auf Verdacht Escaping hinzufügen5 |
10. Zusammenfassung
- PowerShell parst auch die Argumente externer Programme im Argumentmodus. Symbole tragen syntaktische Bedeutung, sodass eine in cmd.exe funktionierende Zeichenfolge nicht zwangsläufig unverändert durchgereicht wird.
- PowerShell 7.3 hat die Argumentübergabe geändert (ein Breaking Change), sodass eingebettete Anführungszeichen und leere Zeichenfolgen erhalten bleiben. Der Standard unter Windows ist der Modus
Windows, in dem nur cmd.exe, Batchdateien und Ähnliches auf den Legacy-Stil zurückfallen. --%ist eine letzte Möglichkeit nur für feste Zeichenfolgen. Verwenden Sie es im Bewusstsein seiner Einschränkungen:%VAR%wird immer expandiert, PowerShell-Variablen können nicht verwendet werden, und die Wirkung endet bei einem Zeilenumbruch oder einer Pipe.- Zum Übergeben von Variablen ist Array-Splatting die erste Wahl. Unter PowerShell 7 können Sie auch
ArgumentListvonProcessStartInfoverwenden (existiert in 5.1 nicht). Die Tatsache, dass der Backslash nicht das Escape-Zeichen von PowerShell ist, ist eine Hauptquelle der Verwirrung. - Leiten Sie sowohl Standardausgabe als auch Standardfehler um, lesen Sie immer beide gleichzeitig. Einen Stream synchron vollständig zu lesen, führt zum Deadlock.
- Zeichensalat wird je nach Richtung unterschiedlich behandelt. Empfang:
[Console]::OutputEncoding. Senden:$OutputEncoding. Pro Prozess:StandardOutputEncoding. - Beurteilen Sie Erfolg oder Misserfolg mit
$LASTEXITCODE. Eine Ausgabe auf stderr ist kein Fehlschlag. Möchten Sie stdout von stderr unterscheiden, führen Sie sie nicht mit2>&1zusammen — empfangen Sie sie getrennt (der Typ nach dem Zusammenführen unterscheidet sich zwischen 5.1 und 7.4+).
Beispielcode zum Download
Der in diesem Artikel behandelte Code liegt als direkt ausführbares Paket vor. Es enthält das Modul zum Argument-Quoting und die Ausführung über ProcessStartInfo.
Beispielcode herunterladen (zip)
Die Beispiele in diesem Artikel wurden tatsächlich unter PowerShell 7.6 ausgeführt und verifiziert (22 Pester-Tests). Führen Sie das im ZIP enthaltene Invoke-SampleTests.ps1 aus, um dieselbe Verifizierung auf Ihrem eigenen Rechner zu reproduzieren.
# Syntaxanalyse + statische Analyse + Pester-Tests
./Invoke-SampleTests.ps1
Konfigurationswerte (Pfade, Servernamen, Mandanten-IDs und so weiter) sind Beispiele. Führen Sie sie nicht unverändert in einer Produktivumgebung aus — passen Sie sie an Ihre eigene Umgebung an.
Verwandte Artikel
- PowerShell-Fehlerbehandlung und Wiederholungsdesign — Von der try/catch-Falle bis zu Exitcodes und Best Practices für Wiederholungen
- Die Unterschiede zwischen Windows PowerShell 5.1 und PowerShell 7 — Ein praktischer Leitfaden zur Migration hauseigener Skripte
- Sollte diese Batchdatei zu PowerShell migriert werden? — cmd/bat-Bestände erfassen und über die Migration entscheiden
- Eine Checkliste zum sicheren Umgang mit Kindprozessen in Windows-Apps
- Windows-Zeichenkodierungen und Zeilenenden - Die Grundlagen von Zeichensalat und CRLF/LF
- PowerShell-Parameterdesign und Modularisierung — Von „einem funktionierenden Skript“ zu „einem übergabefähigen Skript“
Verwandte Beratungsbereiche
KomuraSoft LLC übernimmt die Umwandlung von Batch-Beständen in PowerShell, das Design von Automatisierung, die externe Werkzeuge und hauseigene EXEs kombiniert, sowie die Untersuchung der Ursachen von Skripten, die „in manchen Umgebungen funktionieren und in anderen nicht“.
- Technische Beratung und Design-Review
- Migration und Weiterverwendung von Legacy-Assets
- Fehleranalyse und Ursachenermittlung
- Kontakt
Referenzlinks
</content>
-
Microsoft Learn, about_Parsing. Zur Unterscheidung zwischen Ausdrucksmodus und Argumentmodus, den Metazeichen des Argumentmodus, dem Escaping mit Backticks, der Tatsache, dass an native Befehle übergebene Argumente nach dem Parsing zu einer einzigen, durch Leerzeichen getrennten Zeichenfolge zusammengefügt werden, der Spezifikation des Stop-Parsing-Tokens
--%ab PowerShell 3.0 (nur Umgebungsvariablen werden expandiert, Escaping mit%%ist nicht möglich, die Wirkung hält bis zu einem Zeilenumbruch oder einer Pipe an, und Umleitung ist nicht möglich), dem Breaking Change in PowerShell 7.3 beim Parsen von Befehlszeilen nativer Befehle, den Werten von$PSNativeCommandArgumentPassing(Legacy/Standard/Windows) und dem Standard unter Windows, der Tatsache, dass im Windows-Modus cmd.exe, cscript.exe, wscript.exe und .bat/.cmd/.js/.vbs/.wsf den Legacy-Stil verwenden, der Tatsache, dass der Backslash nicht das Escape-Zeichen von PowerShell ist, der Warnung, nicht vertrauenswürdige Eingaben nicht an Batchdateien zu übergeben, sowie der ab 7.3 möglichen Nachverfolgung der Argumentbindung nativer Befehle. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25 ↩26 -
Microsoft Learn, about_Preference_Variables. Dazu, dass
$PSNativeCommandArgumentPassingeine Präferenzvariable mit plattformabhängigem Standard ist, sowie dazu, dass$OutputEncodingdie beim Senden von Zeichenfolgen von PowerShell an andere Anwendungen verwendete Kodierung bestimmt. ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn, ProcessStartInfo.ArgumentList Property. Dazu, dass sich Argumente einzeln als Sammlung angeben lassen, wobei die Laufzeit das nötige Quoting und Escaping übernimmt, dazu, dass der Backslash als Escape-Zeichen behandelt wird, sowie dazu, dass die API ab .NET Core 2.1 gilt (sie existiert nicht in .NET Framework), weshalb sie aus Windows PowerShell 5.1, das auf .NET Framework läuft, nicht verwendet werden kann. Siehe auch den Hinweis zu Process.StandardOutput Property zum Deadlock, der auftreten kann, wenn sowohl Standardausgabe als auch Standardfehler umgeleitet und synchron gelesen werden, sowie zur Vermeidung (einen der beiden asynchron lesen). ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, about_Error_Handling. Dazu, wie ein von null verschiedener Exitcode eines nativen Befehls
$?auf$falsesetzt und in$LASTEXITCODEgespeichert wird, während kein ErrorRecord erzeugt wird und es nicht in try/catch landet. ↩ ↩2 ↩3 -
Microsoft Learn, Trace-Command. Zur Nachverfolgung der Parameterbindung mit
-Name ParameterBinding, zur Ausgabe an den Host mit-PSHost, sowie zur Angabe des Trace-Ziels mit-Expression. ↩ ↩2 ↩3 -
Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. Zur Änderung in PowerShell 7, wonach
$?nicht mehr allein dadurch zu$falsewird, dass ein nativer Befehl nach stderr geschrieben hat, sondern nur bei einem von null verschiedenen Exitcode zu$falsewird. ↩ ↩2 -
Microsoft Learn, about_Redirection. Zum Nummerierungsschema der PowerShell-Ausgabeströme, zum Zusammenführen des Fehlerstroms mit dem Erfolgsstrom über
2>&1, sowie zur Behandlung der stderr-Ausgabe nativer Befehle. ↩ ↩2 -
Microsoft Learn, What’s New in PowerShell 7.4. Dazu, dass die Umleitungsoperatoren die Ausgabe nativer Befehle nun als Bytestrom bewahren, wobei PowerShell den Inhalt nicht mehr interpretiert oder formatiert (ein Breaking Change), sowie zur daraus folgenden Tatsache, dass mit 2>&1 zusammengeführtes stderr als Zeichenfolgendaten behandelt wird. ↩ ↩2
-
Microsoft Learn, Start-Process. Dazu, dass standardmäßig nicht auf den Abschluss des neuen Prozesses gewartet wird, zum Warten mit
-Wait, zum Erhalten des Process-Objekts mit-PassThruund Lesen vonExitCode, zur Umleitung in Dateien mit-RedirectStandardOutput/-RedirectStandardError, zur Erhöhung mit-Verb RunAs, sowie zur Ausführung als anderer Benutzer mit-Credential. ↩
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Wo Sie nachsehen sollten, wenn ein PowerShell-Skript langsam ist — Arrays, Pipelines und Abgleich
Die klassischen Ursachen langsamer PowerShell-Skripte im Überblick. Warum += bei einem Array O(n²) ist, der Unterschied zwischen Pipeline...
Schluss mit Write-Host — PowerShells Ausgabeströme und Log-Design
Wie Sie zwischen den sechs Ausgabeströmen von PowerShell wählen, welche Probleme Write-Host mit sich bringt und wo es tatsächlich hingehö...
Parallelverarbeitung in PowerShell — Die Wahl zwischen ForEach-Object -Parallel und Jobs
Ein praxisnaher Überblick über die Unterschiede zwischen ForEach-Object -Parallel, Start-ThreadJob und Start-Job und wann welches zum Ein...
Zugangsdaten in PowerShell sicher handhaben — Klartext-Passwörter aus Ihren Skripten verbannen
Ein praktischer Leitfaden, um Klartext-Passwörter aus PowerShell-Skripten zu entfernen und sicher zu speichern: was SecureString wirklich...
Parameterdesign und Modularisierung für PowerShell-Skripte — Von der „funktionierenden“ zur „übergabefähigen“ Skript
Eine schrittweise Vorgehensweise, um ein PowerShell-Skript auf eine Qualität zu heben, die Sie an andere übergeben können. Behandelt den ...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Wenn ich eine EXE aus PowerShell aufrufe, verschwinden die doppelten Anführungszeichen in meinen Argumenten. Warum?
- Weil PowerShell die Argumente selbst parst, bevor es sie an das externe Programm weiterreicht. In Windows PowerShell 5.1 werden die geparsten Argumente zu einer einzigen, durch Leerzeichen getrennten Zeichenfolge neu zusammengesetzt, wobei eingebettete Anführungszeichen verloren gehen und leere Zeichenfolgen als Argumente verschwinden. PowerShell 7.3 hat dieses Verhalten geändert, sodass eingebettete Anführungszeichen und leere Zeichenfolgen als Argumente erhalten bleiben. Leicht zu übersehen ist, dass in 5.1 selbst Array-Splatting (& $exe @args) durch dieselbe Neuzusammensetzung läuft. Müssen Sie zuverlässig einen Wert mit Anführungszeichen oder eine leere Zeichenfolge übergeben, ist Splatting nicht die Antwort. Für eine feste Zeichenfolge können Sie das Stop-Parsing-Token --% verwenden, es lässt sich jedoch nicht verwenden, wenn Variablen im Spiel sind. Der zuverlässige Weg ist, die Anführungszeichen selbst nach den Regeln der Windows-Befehlszeile hinzuzufügen und das Ergebnis als Arguments-Zeichenfolge von ProcessStartInfo zu übergeben (ArgumentList ist eine API ab .NET Core 2.1 und in 5.1 nicht verfügbar). Ein Implementierungsbeispiel findet sich im Artikeltext.
- Kann ich mit --% (dem Stop-Parsing-Token) jedes Argument sicher übergeben?
- Nein — es hat zu viele Einschränkungen, um ein Allheilmittel zu sein. Alles nach --% wird wörtlich behandelt, außer dass Umgebungsvariablenverweise wie %USERPROFILE% weiterhin expandiert werden, sodass jede Zeichenfolge mit % unbeabsichtigt ersetzt werden kann (und Escaping mit %% funktioniert ebenfalls nicht). PowerShell-Variablen lassen sich überhaupt nicht expandieren, die Wirkung endet beim nächsten Zeilenumbruch oder Pipe-Zeichen, und Umleitungen können Sie nicht schreiben. Sobald Sie den Wert einer Variablen übergeben müssen, scheidet --% aus; erwägen Sie dann ProcessStartInfo oder Start-Process.
- Ist es sicher, eine von außen empfangene Zeichenfolge an eine Batchdatei (.bat) zu übergeben?
- Vermeiden Sie es, nicht vertrauenswürdige Eingaben an eine Batchdatei zu übergeben. Unter Windows werden Argumente an eine Batchdatei als rohe Befehlszeilenzeichenfolge an cmd.exe weitergereicht, weshalb die offizielle Dokumentation ausdrücklich feststellt, dass nicht vertrauenswürdige Eingaben auf anderem Wege übergeben werden sollten. Einen Dateinamen oder eine Benutzereingabe direkt zu verketten lässt Raum für Command Injection. Es ist sicherer, Werte über eine temporäre Datei oder Umgebungsvariablen zu übergeben, oder die Batchdatei durch ein PowerShell-Skript zu ersetzen.
- Die Ausgabe eines externen Befehls kommt bei nicht-ASCII-Text als Zeichensalat zurück. Was muss ich beheben?
- Gleichen Sie [Console]::OutputEncoding — womit PowerShell die Standardausgabe eines externen Befehls decodiert — an die Kodierung an, die der Befehl tatsächlich ausgibt. Bei einem Werkzeug, das UTF-8 schreibt, setzen Sie vor dem Aufruf [Console]::OutputEncoding = [System.Text.Encoding]::UTF8. Umgekehrt wird beim Weiterleiten einer Zeichenfolge von PowerShell an einen externen Befehl $OutputEncoding verwendet, der Trick besteht also darin, Sende- und Empfangsseite getrennt zu betrachten. Diese Einstellungen gelten pro Sitzung; ändern Sie sie also temporär innerhalb eines Skripts, setzen Sie sie wieder zurück.
- Wie entscheide ich zwischen Start-Process und direktem Aufruf (&)?
- Für die üblichen Fälle — Sie möchten die Ausgabe über die Pipeline erhalten, oder Ihnen genügt der Exitcode — ist der direkte Aufruf (& oder einfach der Befehlsname) die Standardwahl. Start-Process ist dafür da, wenn Sie steuern möchten, wie der Prozess gestartet wird: in einem separaten Fenster starten, als anderer Benutzer ausführen, auf Administrator erhöhen (-Verb RunAs), oder die Standardausgabe in eine Datei umleiten. Beachten Sie, dass Start-Process standardmäßig nicht auf den Abschluss wartet; brauchen Sie also den Exitcode, müssen Sie -Wait und -PassThru kombinieren und die Eigenschaft ExitCode auslesen.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.