web-dev-qa-db-ja.com

$ LastExitCode = 0、ただしPowerShellでは$?= False。 stderrをstdoutにリダイレクトするとNativeCommandErrorが発生します

下の2番目の例で、PowerShellが驚くべき動作を示すのはなぜですか?

まず、正気な振る舞いの例:

PS C:\> & cmd /c "echo Hello from standard error 1>&2"; echo "`$LastExitCode=$LastExitCode and `$?=$?"
Hello from standard error
$LastExitCode=0 and $?=True

驚く様な事じゃない。メッセージを標準エラーに出力します(cmdechoを使用)。変数$?および$LastExitCodeを調べます。期待どおり、それぞれTrueと0に等しくなります。

ただし、PowerShellに最初のコマンドで標準エラーを標準出力にリダイレクトするように依頼すると、NativeCommandErrorが発生します。

PS C:\> & cmd /c "echo Hello from standard error 1>&2" 2>&1; echo "`$LastExitCode=$LastExitCode and `$?=$?"
cmd.exe : Hello from standard error
At line:1 char:4
+ cmd <<<<  /c "echo Hello from standard error 1>&2" 2>&1; echo "`$LastExitCode=$LastExitCode and `$?=$?"
    + CategoryInfo          : NotSpecified: (Hello from standard error :String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

$LastExitCode=0 and $?=False

私の最初の質問、なぜNativeCommandErrorなのですか?

第二に、cmdが正常に実行され、$?が0であるときに$LastExitCode Falseになるのはなぜですか? PowerShellのドキュメント 自動変数について$?を明示的に定義していません。 $LastExitCodeが0の場合にのみTrueであると常に思っていましたが、私の例はそれと矛盾しています。


現実世界でこの動作に遭遇した方法を以下に示します(簡略化)。本当にFUBARです。別のPowerShellスクリプトを呼び出していました。内部スクリプト:

cmd /c "echo Hello from standard error 1>&2"
if (! $?)
{
    echo "Job failed. Sending email.."
    exit 1
}
# Do something else

これを単に.\job.ps1として実行すると、正常に動作し、電子メールは送信されません。ただし、別のPowerShellスクリプトから呼び出して、ファイル.\job.ps1 2>&1 > log.txtに記録していました。この場合、メールが送信されます!エラーストリームを使用してスクリプトの外部で行うことは、スクリプトの内部動作に影響します。 現象を観察すると、結果が変わります。これは、スクリプトではなく量子物理学のように感じます!

[興味深いことに、.\job.ps1 2>&1は実行場所に応じて爆発する場合としない場合があります]

42
Colonel Panic

このバグは、PowerShellのエラー処理に関する規範的な設計の予期しない結果であるため、ほとんど修正されることはありません。スクリプトが他のPowerShellスクリプトでのみ再生される場合、安全です。ただし、スクリプトが広大な世界のアプリケーションと対話する場合、このバグが噛み付く可能性があります。

PS> nslookup Microsoft.com 2>&1 ; echo $?

False

ガッチャ!それでも、いくつかの痛みを伴うスクラッチの後、レッスンを決して忘れないでしょう。

使用する ($LastExitCode -eq 0) の代わりに $?

16
Colonel Panic

(PowerShell v2を使用しています。)

'$?'変数はabout_Automatic_Variablesで文書化されています:

 $?
最後の操作の実行ステータスが含まれます

これは、$LastExitCodeで取得する最後の外部コマンドとは対照的に、最新のPowerShell操作を指します。

あなたの例では、$LastExitCodeは0です。これは、最後の外部コマンドがcmdであり、一部のテキストをエコーする際に成功であったためです。ただし、2>&1により、stderrへのメッセージが出力ストリームのエラーレコードに変換され、PowerShellに最後のoperationでエラーが発生したことが通知され、$?Falseになります。

これをもう少し説明するために、これを考慮してください:

> Java -jar foo; $ ?; $ LastExitCode 
 jarfile foo 
 False 
 1 [._______にアクセスできません] 。]

$LastExitCodeは1です。これはJava.exeの終了コードだったためです。 $?はFalseです。これは、シェルが最後に失敗したためです。

しかし、私がやるのは、それらを切り替えることだけです:

> Java -jar foo; $ LastExitCode; $?
 jarfile foo 
 1 
 True [._______にアクセスできません] 。]

... $?はTrueです。これは、シェルが最後にしたことは$LastExitCodeをホストに出力し、成功したためです。

最後に:

>&{Java -jar foo}; $ ?; $ LastExitCode 
 jarfile foo 
 True 
 1にアクセスできません] 

...これは少し直感に反しているように見えますが、$?Trueです。これは、スクリプトブロックの実行が成功であったためです。その中にはありませんでした。


2>&1 redirect ....に戻ると、エラーレコードが出力ストリームに送られます。これにより、NativeCommandErrorについての長蛇の塊が得られます。シェルはエラーレコード全体をダンプしています。

パイプstderrandstdoutをまとめてログファイルなどにまとめることができる場合、これは特に面倒です。 PowerShellをログファイルに追加するのは誰ですか? ant build 2>&1 >build.logを実行すると、stderrに向かうエラーには、ログファイルにクリーンなエラーメッセージが表示される代わりに、PowerShellのnosey $ 0.02が追加されます。

しかし、出力ストリームはtextストリームではありません!リダイレクトはobjectパイプラインの別の構文です。エラーレコードはオブジェクトなので、リダイレクトする前に、そのストリーム上のオブジェクトをstringsに変換するだけです。

から:

> cmd/c "標準エラー1>&2からエコーHello" 2>&1 
 cmd.exe:標準エラーからHello 
 1行目:char:4 
 + cmd&2 "2>&1 
 + CategoryInfo:NotSpecified:(標準エラーからのこんにちは:String)[]、RemoteException 
 + FullyQualifiedErrorId:NativeCommandError 

に:

> cmd/c "標準エラーからのHello Hello 1>&2" 2>&1 | %{"$ _"} 
標準エラーからのこんにちは

...そしてファイルへのリダイレクト付き:

> cmd/c "標準エラーからのHello Hello 1>&2" 2>&1 | %{"$ _"} | tee out.txt 
こんにちは、標準エラーから

...あるいは単に:

> cmd/c "標準エラーからのHello Hello 1>&2" 2>&1 | %{"$ _"}> out.txt 
72
Droj

(注:これはほとんど推測です。PowerShellで多くのネイティブコマンドを使用することはほとんどありません。他の人はおそらく私よりもPowerShell内部について詳しく知っています)

PowerShellコンソールホストで矛盾が見つかったと思います。

  1. PowerShellが標準エラーストリームの内容を取得すると、エラーを想定してNativeCommandErrorをスローします。
  2. PowerShellは、monitorsが標準エラーストリームの場合にのみこれを選択できます。
  3. PowerShell ISE has toコンソールアプリケーションではないため、ネイティブコンソールアプリケーションには書き込み先のコンソールがないため、監視します。これが、2>&1リダイレクト演算子に関係なくPowerShell ISEでこれが失敗する理由です。
  4. コンソールホストwillは、2>&1リダイレクト演算子を使用する場合、標準エラーストリームの出力をリダイレクトする必要があるため、標準エラーストリームを監視します。

ここでの私の推測は、コンソールPowerShellホストは怠zyであり、出力で処理を行う必要がない場合、ネイティブコンソールコマンドをコンソールに渡すだけです。

PowerShellはホストアプリケーションによって異なる動作をするため、これはバグだと本当に信じています。

10
Joey

私にとっては、ErrorActionPreferenceの問題でした。 ISEから実行する場合、最初の行に$ ErrorActionPreference = "Stop"を設定しました。これは、呼び出しにパラメーターとして*>&1を追加してすべてのイベントをインターセプトしていました。

だから最初にこの行がありました:

& $exe $parameters *>&1

ファイルで以前に$ ErrorActionPreference = "Stop"があったため(または、スクリプトを起動するユーザーのプロファイルでグローバルに設定できるため)、これは機能しませんでした。

だから私はそれをInvoke-ExpressionでラップしてErrorActionを強制しようとしました:

Invoke-Expression -Command "& `"$exe`" $parameters *>&1" -ErrorAction Continue

そして、これも機能しません。

そのため、一時的にErrorActionPreferenceをオーバーライドして、ハックするためにフォールバックする必要がありました。

$old_error_action_preference = $ErrorActionPreference

try
{
    $ErrorActionPreference = "Continue"
    & $exe $parameters *>&1
}
finally
{
    $ErrorActionPreference = $old_error_action_preference
}

それは私のために働いています。

そして、それを関数にラップしました:

<#
    .SYNOPSIS

    Executes native executable in specified directory (if specified)
    and optionally overriding global $ErrorActionPreference.
#>
function Start-NativeExecutable
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    Param
    (
        [Parameter (Mandatory = $true, Position = 0, ValueFromPipelinebyPropertyName=$True)]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [Parameter (Mandatory = $false, Position = 1, ValueFromPipelinebyPropertyName=$True)]
        [string] $Parameters,

        [Parameter (Mandatory = $false, Position = 2, ValueFromPipelinebyPropertyName=$True)]
        [string] $WorkingDirectory,

        [Parameter (Mandatory = $false, Position = 3, ValueFromPipelinebyPropertyName=$True)]
        [string] $GlobalErrorActionPreference,

        [Parameter (Mandatory = $false, Position = 4, ValueFromPipelinebyPropertyName=$True)]
        [switch] $RedirectAllOutput
    )

    if ($WorkingDirectory)
    {
        $old_work_dir = Resolve-Path .
        cd $WorkingDirectory
    }

    if ($GlobalErrorActionPreference)
    {
        $old_error_action_preference = $ErrorActionPreference
        $ErrorActionPreference = $GlobalErrorActionPreference
    }

    try
    {
        Write-Verbose "& $Path $Parameters"

        if ($RedirectAllOutput)
            { & $Path $Parameters *>&1 }
        else
            { & $Path $Parameters }
    }
    finally
    {
        if ($WorkingDirectory)
            { cd $old_work_dir }

        if ($GlobalErrorActionPreference)
            { $ErrorActionPreference = $old_error_action_preference }
    }
}
0
Michael Logutov