PowerShell では外部プログラムの標準エラー (stderr) を error stream に流すために大きな落とし穴があるので、まとめておく。

  • stderr を redirect して外部プログラムを起動し、 stderr に出力すると一行ごとに ErrorRecord でラップされる
  • stdout への redirect 2>&1 、ファイルへの redirect 2> a.txt$null への redirect 2>$null で ErrorRecord でのラップがされる
  • 外部プログラムの stderr を stdout やファイルに redirect すると、 NativeCommandError と出力される
  • stderr に出力があるとエラーとみなされ $?$false になり、エラーは $Error に記録される。
  • $ErrorActionPreference = "stop" していると、stderr に一行目が出力されると処理が止まる
  • PowerShell スクリプトで 外部プログラムの起動において stderr を redirect していなくても、その PowerShell スクリプトの stderr が redirect されると ErrorRecord でのラップがされる
  • $null への redirect では stderr は捨てられるが、エラーが発生したことは $Error に記録される。

対策としては、$? ではなく $LastExitCode をチェックする。 stderr の元の文字列が見たいときは stdout に redirect して ToString() する 2>&1 |% { "$_" }

The Big Book of PowerShell Error Handling という本にも書かれている。

If an external executable writes anything to the StdErr stream, PowerShell sometimes sees this and wraps the text in an ErrorRecord, but this behavior doesn’t seem to be consistent. I’m not sure yet under what conditions these errors will be produced, so I tend to stick with $LASTEXITCODE when I need to tell whether an external command worked or not.

本家でもたくさん議論されていて状況がよくわからない。

Invoke-NativeApplication

Invoke-NativeApplication を使うと良さそう。 特に $ErrorActionPreference = "stop" しているスクリプトで有効と思われる。

  • $ErrorActionPreferenceContinue にしてコマンドを実行する
  • 外部コマンドを実行し stdout と stderr の各行を文字列のリストで返す
  • 文字列には IsError プロパティがセットされる
  • $LastExitCode に基づき例外を投げる (例外よりも Write-Error で error stream に出力したほうがよいかも)
  • -IgnoreExitCode-AllowedExitCodes 引数あり
  • あいにく stderr に出力があると エラーとして $Error に記録が残る

Start-Process

Start-Process を使うと、PowerShell が stdout や stderr を一行ずつ処理するのを回避でき、処理されずにコンソールに出力されるようである。

function Invoke-NativeCommand($command) {
    $process = Start-Process $command -ArgumentList $args -Wait -NoNewWindow -PassThru
    if ($process.ExitCode -ne 0) {
       Write-Error -Category InvalidResult -Message "$command exits with $($process.ExitCode)"
    }
}

私がやりたかったことはこれで十分だった。 欲を言えば、 親プロセスである PowerShell の stdout と stderr を継承して欲しいのだが。 この方法では PowerShell スクリプトの stdout と stderr をファイルにリダイレクトしても、 Start-Process で起動したプロセスはコンソールに出力されるようである。

PowerShell 7.1

PowerShell 7.1 で 改修された

  • Fix $? to not be $false when native command writes to stderr (#13395)
  • Make $ErrorActionPreference not affect stderr output of native commands (#13361)