下の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なのですか?
第二に、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
は実行場所に応じて爆発する場合としない場合があります]
このバグは、PowerShellのエラー処理に関する規範的な設計の予期しない結果であるため、ほとんど修正されることはありません。スクリプトが他のPowerShellスクリプトでのみ再生される場合、安全です。ただし、スクリプトが広大な世界のアプリケーションと対話する場合、このバグが噛み付く可能性があります。
PS> nslookup Microsoft.com 2>&1 ; echo $?
False
ガッチャ!それでも、いくつかの痛みを伴うスクラッチの後、レッスンを決して忘れないでしょう。
($LastExitCode -eq 0)
の代わりに $?
(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
についての長蛇の塊が得られます。シェルはエラーレコード全体をダンプしています。
パイプstderr
andstdout
をまとめてログファイルなどにまとめることができる場合、これは特に面倒です。 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
(注:これはほとんど推測です。PowerShellで多くのネイティブコマンドを使用することはほとんどありません。他の人はおそらく私よりもPowerShell内部について詳しく知っています)
PowerShellコンソールホストで矛盾が見つかったと思います。
NativeCommandError
をスローします。2>&1
リダイレクト演算子に関係なくPowerShell ISEでこれが失敗する理由です。2>&1
リダイレクト演算子を使用する場合、標準エラーストリームの出力をリダイレクトする必要があるため、標準エラーストリームを監視します。ここでの私の推測は、コンソールPowerShellホストは怠zyであり、出力で処理を行う必要がない場合、ネイティブコンソールコマンドをコンソールに渡すだけです。
PowerShellはホストアプリケーションによって異なる動作をするため、これはバグだと本当に信じています。
私にとっては、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 }
}
}