$ LastExitCode = 0,但在PowerShell中$?= False.将stderr重定向到stdout会产生NativeCommandError

Col*_*nic 43 windows powershell command-line

为什么PowerShell在下面的第二个例子中显示出令人惊讶的行为?

首先,一个理智行为的例子:

PS C:\> & cmd /c "echo Hello from standard error 1>&2"; echo "`$LastExitCode=$LastExitCode and `$?=$?"
Hello from standard error
$LastExitCode=0 and $?=True
Run Code Online (Sandbox Code Playgroud)

没有惊喜.我打印一条消息到标准错误(使用cmd's 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
Run Code Online (Sandbox Code Playgroud)

我的第一个问题,为什么NativeCommandError?

其次,为什么在成功运行$?时为False 为0?PowerShell 关于自动变量的文档没有明确定义.我总是认为它是真的,当且仅当它是0,但我的例子与之相矛盾.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
Run Code Online (Sandbox Code Playgroud)

简单地运行.\job.ps1它,它工作正常,并且不发送电子邮件.但是,我从另一个PowerShell脚本调用它,记录到一个文件.\job.ps1 2>&1 > log.txt.在这种情况下,会发送一封电子邮件!使用错误流在脚本外部执行的操作会影响脚本的内部行为.观察现象会改变结果.这感觉像量子物理而不是脚本!

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

Dro*_*roj 74

(我使用的是PowerShell v2.)

' $?'变量记录在about_Automatic_Variables:

$?
  Contains the execution status of the last operation

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

在您的示例中,$LastExitCode为0,因为最后一个外部命令是cmd,它成功地回显了一些文本.但是,2>&1导致邮件stderr要转换的输出流,它告诉PowerShell中,有过去的过程中出现错误的错误记录的操作,导致$?False.

为了进一步说明这一点,请考虑以下事项:

> java -jar foo; $?; $LastExitCode
Unable to access jarfile foo
False
1

$LastExitCode是1,因为这是java.exe的退出代码.$?是假的,因为shell失败的最后一件事.

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

> java -jar foo; $LastExitCode; $?
Unable to access jarfile foo
1
True

...然后$?是真的,因为shell做的最后一件事是打印$LastExitCode到主机,这是成功的.

最后:

> &{ java -jar foo }; $?; $LastExitCode
Unable to access jarfile foo
True
1

...这似乎有点反直觉,但现在$?True,因为脚本块的执行是成功的,即使在其中运行的命令不是.


返回2>&1重定向....导致错误记录进入输出流,这就是那个长篇大论的blob NativeCommandError.shell正在转储整个错误记录.

这可能是特别恼人时,所有你需要做的就是管stderr stdout在一起,这样他们就可以在日志文件或东西相结合.谁想要PowerShell对接他们的日志文件??? 如果我这样做ant build 2>&1 >build.log,那么任何错误都会stderr导致PowerShell的nosy $ 0.02,而不是在我的日志文件中获得干净的错误消息.

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

从:

> cmd /c "echo Hello from standard error 1>&2" 2>&1
cmd.exe : Hello from standard error
At line:1 char:4
+ cmd &2" 2>&1
    + CategoryInfo          : NotSpecified: (Hello from standard error :String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

至:

> cmd /c "echo Hello from standard error 1>&2" 2>&1 | %{ "$_" }
Hello from standard error

...并重定向到文件:

> cmd /c "echo Hello from standard error 1>&2" 2>&1 | %{ "$_" } | tee out.txt
Hello from standard error

...要不就:

> cmd /c "echo Hello from standard error 1>&2" 2>&1 | %{ "$_" } >out.txt

  • 嘿,喜欢那个'2>&1 | %{"$ _"}`解决方法,我将使用它,谢谢. (5认同)
  • 这是正确的。将流对象放在引号 ("$_") 中会转换为字符串。 (2认同)
  • 在V2.0`2&gt;&1中| 如果ErrorActionPref设置为“停止”,则%{“ $ _”}`不起作用。有没有一种方法可以运行命令并覆盖全局EA值? (2认同)

Col*_*nic 16

这个错误是PowerShell针对错误处理的规范性设计无法预料的结果,因此很可能永远不会修复它.如果您的脚本仅与其他PowerShell脚本一起播放,那么您就是安全的.但是,如果您的脚本与来自广阔世界的应用程序进行交互,则此错误可能会受到影响.

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

False
Run Code Online (Sandbox Code Playgroud)

疑难杂症!尽管如此,经过一些痛苦的刮擦,你永远不会忘记这一课.

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

  • 另外,建议应该是*Use($ LastExitCode -eq 0)而不是$?**用于本机命令***.在调用PowerShell脚本/ Cmdlet时,`$?`是要走的路 - 如果脚本不包含显式的`Exit-PSSession`调用,则甚至不会设置`$ LastExitCode`. (5认同)
  • 反过来当然也适用 - 您可以获得写入 stdErr 但*不*设置退出代码的应用程序(尤其是控制台脚本),在这种情况下,您*必须*使用 $? 检测这些错误。所以答案是......这取决于。 (4认同)

Joe*_*oey 10

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

我猜您在PowerShell控制台主机中发现了差异.

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

我的猜测是,控制台PowerShell主机是懒惰的,如果它不需要对其输出进行任何处理,只需手动本机控制台命令控制台.

我真的相信这是一个错误,因为PowerShell的行为会有所不同,具体取决于主机应用程序.


mkl*_*nt0 10

更新:v7.2 的预览版本现在包含一个实验性功能,可修复以下问题 - 请参阅此答案;但是请注意,实验性功能不能保证成为官方功能。


v7.1问题总结:

PowerShell 引擎仍然存在2>应用于外部程序调用的重定向相关的错误

根本原因是using2>导致 stderr(标准错误)输出通过 PowerShell 的错误流(请参阅about_Redirection进行路由,这会产生以下不良后果:

  • 如果$ErrorActionPreference = 'Stop'碰巧生效,使用2>意外触发脚本终止错误,即中止脚本(即使在形式中2>$null,其意图显然是忽略stderr 行)。请参阅此 GitHub 问题

    • 解决方法:(临时)设置 $ErrorActionPreference = 'Continue'
  • 由于2>当前涉及错误流,如果至少发出一个 stderr 行$?,则自动成功状态变量总是设置为$False,然后不再反映命令的真实成功状态。请参阅此 GitHub 问题

    • 解决方法,如您的回答中所建议的:仅$LASTEXITCODE -eq 0用于在调用外部程序后测试是否成功。
  • 使用2>,stderr 行意外地记录在自动$Error变量中(该变量保存会话中发生的所有错误的日志) - 即使您使用2>$null. 请参阅此 GitHub 问题

    • 解决方法:除了跟踪添加了多少错误记录并$Error.RemoveAt()一一删除它们之外,没有。

一般情况下,不幸的是,一些PowerShell的主机默认情况下,通过PowerShell的错误流从外部程序输出的路线标准错误,即把它当作错误输出,这是不恰当的,因为很多外部程序使用标准错误也为状态信息,或者更一般地,这通常就是不是数据git作为主要示例):并非每个 stderr 行都可以假设表示错误,并且 stderr 输出的存在并不意味着失败。

受影响的主机

确实在非远程、非后台调用中按预期运行的主机(它们将 stderr 行传递到显示器并正常打印它们):

GitHub 问题 中讨论了主机间的这种不一致。

  • @zett42,v7.2 已将此功能正式化,顶部的注释暗示了这一点,即问题在 v7.2+ 中不再存在,所以我认为实验性的 v7.1 功能不需要成为不再回答,但感谢您在评论中指出。 (2认同)