$ LastExitCode = 0,但在PowerShell中$?= False。 将stderrredirect到stdout会导致NativeCommandError

为什么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 )。 我检查variables$?$LastExitCode 。 如预期的那样,它们分别等于“真”和“0”。

但是,如果我要求PowerShell通过第一个命令将标准错误redirect到标准输出,我得到一个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运行成功并且$LastExitCode为0时$LastExitCode ? PowerShell 有关自动variables的文档没有明确定义$? 。 我总是认为这是真的当且仅当$LastExitCode是0,但我的例子与此相矛盾。


以下是我在现实世界中遇到的这种行为(简化)。 这真的是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脚本调用它,logging到文件.\job.ps1 2>&1 > log.txt 。 在这种情况下,电子邮件发送! 您在脚本外部使用错误stream进行的操作会影响脚本的内部行为。 观察现象会改变结果。 这感觉就像量子物理而不是脚本!

[有趣的是: .\job.ps1 2>&1可能会爆炸或不爆炸,取决于你在哪里运行]

(我正在使用PowerShell v2。)

' $? '变量记录在about_Automatic_Variables

 $?
  包含上次操作的执行状态

这是指最近的PowerShell操作,而不是最后一个外部命令,这是你在$LastExitCode得到的。

在你的例子中, $LastExitCode是0,因为最后一个外部命令是cmd ,在回显一些文本方面是成功的 2>&1导致消息stderr被转换为输出流中的错误记录,这告诉PowerShell在上次操作过程中出现错误,导致$? False

为了说明这一点,请考虑这一点:

 > java -jar foo;  $ ?;  $ LastExitCode
无法访问jarfile foo

 1

$LastExitCode是1,因为那是java.exe的退出码。 $? 是错误的,因为壳的最后一件事失败了。

但是,如果我所做的只是切换它们:

 > java -jar foo;  $ LastExitCode;  $?
无法访问jarfile foo
 1
真正

…那么$? 是真的,因为shell的最后一件事是打印$LastExitCode到主机,这是成功的。

最后:

 >&{java -jar foo};  $ ?;  $ LastExitCode
无法访问jarfile foo
真正
 1

…这似乎有点反直觉,但$? 现在是真的 ,因为脚本块的执行是成功的 ,即使它里面运行的命令不是。


返回到2>&1重定向….导致一个错误记录在输出流,这是什么给了那个关于NativeCommandErrorNativeCommandError shell正在倾倒整个错误记录。

当你所要做的只是将stderr stdout管道在一起,以便它们可以被合并到一个日志文件中时,这可能会特别恼人。 谁想要PowerShell对接他们的日志文件? 如果我ant build 2>&1 >build.log ,那么任何发送到stderr让PowerShell的臭名昭着 $ 0.02,而不是在我的日志文件中得到干净的错误信息。

但是,输出流不是文本流! 重定向只是对象管道的另一种语法。 错误记录是对象,所以您只需在重定向之前将该流上的对象转换为字符串

从:

 > cmd / c“echo标准错误1>&2”2>&1
 cmd.exe:从标准错误你好
在行:1 char:4
 + cmd&2“2>&1
     + CategoryInfo:NotSpecified :(从标准错误:字符串)[],RemoteException
     + FullyQualifiedErrorId:NativeCommandError

至:

 > cmd / c“echo标准错误1>&2”2>&1 |您好  %{“$ _”}
你好,从标准错误

…并重定向到一个文件:

 > cmd / c“echo标准错误1>&2”2>&1 |您好  %{“$ _”} | 发球out.txt
你好,从标准错误

…要不就:

 > cmd / c“echo标准错误1>&2”2>&1 |您好  %{“$ _”}> out.txt

这个错误是PowerShell的错误处理规定性设计的一个无法预料的后果,所以它很可能永远不会被修复。 如果您的脚本只能与其他PowerShell脚本一起玩,那么您是安全的。 但是,如果你的脚本与来自广阔世界的应用程序交互,这个bug可能会咬人。

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

疑难杂症! 不过,经过一些痛苦的抓挠,你永远不会忘记教训。

使用($LastExitCode -eq 0)而不是$?

(注意:这主要是猜测;我很少在PowerShell中使用许多本机命令,其他人可能比我更了解PowerShell内部)

我想你在PowerShell控制台主机上发现了一个差异。

  1. 如果PowerShell会在标准错误流中拾取一些东西,如果会出现错误并抛出一个NativeCommandError
  2. 如果监控标准错误流,PowerShell只能选择它。
  3. PowerShell ISE 必须监视它,因为它不是控制台应用程序,因此本机控制台应用程序没有控制台可以写入。 这就是为什么在PowerShell ISE中,无论2>&1重定向操作符如何都会失败。
  4. 如果使用2>&1重定向操作符,控制台主机监视标准错误流,因为标准错误流上的输出必须重定向并读取。

我的猜测是,控制台PowerShell主机是懒惰的,只要交出本地控制台命令控制台,如果它不需要做任何处理他们的输出。

我真的相信这是一个错误,因为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 } } }