54

以下の 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

驚く様な事じゃない。メッセージを標準エラーに出力します ( を使用cmd) echo$?変数とを検査します$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 なのですか?

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


これが、現実世界でこの動作に遭遇した方法です(簡略化)。まさに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、実行する場所によって爆発する場合としない場合があります]

4

6 に答える 6

89

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

' $?' 変数は次のドキュメントに記載されていabout_Automatic_Variablesます。

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

これは、最後の外部コマンドではなく、最新の PowerShell 操作を参照しています$LastExitCode

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

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

> java -jar foo; $?; $LastExitCode
jarfile foo にアクセスできません
間違い
1

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

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

> java -jar foo; $LastExitCode; $?
jarfile foo にアクセスできません
1
真実

...$?シェルが最後に行った$LastExitCodeのはホストへの出力であり、成功したため、True です。

ついに:

> &{ java -jar foo }; $?; $LastExitCode
jarfile foo にアクセスできません
真実
1

...これは少し直感に反しているように見えますが、スクリプト ブロックの実行が成功したため、内部で実行されたコマンドが成功しなかったとしても、現在$?Trueです。


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

これは、ログ ファイルなどに結合できるように、パイプstderr て結合するだけの場合に特に厄介です。stdout自分のログ ファイルに PowerShell を突っ込みたいのは誰ですか? 私がそうant build 2>&1 >build.logすると、ログファイルにきれいなエラーメッセージが表示される代わりに、 stderrPowerShellの厄介な$ 0.02が追加されるエラーが発生します。

ただし、出力ストリームはテキストストリームではありません。リダイレクトは、オブジェクトパイプラインのもう 1 つの構文です。エラー レコードはオブジェクトなので、リダイレクトする前にそのストリームのオブジェクトを文字列に変換するだけです。

から:

> cmd /c "標準エラーから Hello をエコー 1>&2" 2>&1
cmd.exe : 標準エラーからこんにちは
行:1 文字:4
+ コマンド &2" 2>&1
    + CategoryInfo : NotSpecified: (標準エラーからこんにちは:文字列) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

に:

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

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

> cmd /c "標準エラーから Hello をエコー 1>&2" 2>&1 | %{ "$_" } | ティーアウト.txt
標準エラーからこんにちは

...あるいは単に:

> cmd /c "標準エラーから Hello をエコー 1>&2" 2>&1 | %{ "$_" } >out.txt
于 2012-10-12T20:32:09.590 に答える
22

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

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

False

ガッチャ!それでも、いくつかの痛みを伴う引っかき傷の後、あなたはレッスンを決して忘れません。

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

于 2012-10-01T18:51:50.953 に答える
11

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

PowerShell コンソール ホストに不一致が見つかったと思います。

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

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

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

于 2012-05-19T14:52:08.130 に答える
0

私にとっては、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 }
    }
}
于 2015-07-22T09:47:13.087 に答える