$LastExitCode=0,但在 PowerShell 中 $?=False。将标准错误重定向到标准输出会产生 NativeCommandError
Posted
技术标签:
【中文标题】$LastExitCode=0,但在 PowerShell 中 $?=False。将标准错误重定向到标准输出会产生 NativeCommandError【英文标题】:$LastExitCode=0, but $?=False in PowerShell. Redirecting stderr to stdout gives NativeCommandError 【发布时间】:2012-05-26 19:45:23 【问题描述】:为什么 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
'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
我的第一个问题,为什么是 NativeCommandError?
其次,为什么cmd
运行成功而$LastExitCode
为0时$?
为False? PowerShell 的文档about automatic variables 没有明确定义$?
。我一直认为当且仅当 $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 difference between $? and $LastExitCode的可能重复 雷蒙德,可能是相关的,但不是真正的复制品。不过,如果 Jeffrey Snover 在这里用Word of God 插话就好了 :-) 我问了另一个问题,以确保我在表现出意外之前了解预期的行为。 看起来解决方法是转义重定向运算符:& cmd /c "echo Hello from standard error 1>&2" 2`>`&1
Matt:他们当然应该把它当作参数来使用。在引用的情况下,它是 shell cmd
的参数。如果你想要重定向,你需要一个能够理解它的 shell。 nslookup
只是一个执行其任务的命令,但它不是 shell。
【参考方案1】:
更新:预览版 v7.2 现在包含一个实验性功能,可以修复以下问题 - 请参阅 this answer;但是请注意,实验性功能并不能保证成为官方功能。
从 v7.1 开始的问题总结:
PowerShell 引擎仍然存在关于应用于外部程序调用的2>
重定向的错误:
根本原因是使用2>
导致stderr(标准错误)输出通过PowerShell的错误流路由(参见about_Redirection),它有以下不良后果:
如果$ErrorActionPreference = 'Stop'
恰好生效,使用2>
会意外触发脚本终止错误,即中止脚本(即使是@987654338 形式) @,其意图显然是 ignore stderr 行)。见this GitHub issue。
$ErrorActionPreference = 'Continue'
由于2>
当前触及错误流$?
,如果发出了至少一条stderr 行,自动成功状态变量总是设置为$False
,然后不再反映真正的成功状态命令。见this GitHub issue。
$LASTEXITCODE -eq 0
来测试是否成功。
使用2>
,stderr 行会意外记录在自动$Error
变量(记录会话中发生的所有错误的变量)中——即使您使用2>$null
。见this GitHub issue。
$Error.RemoveAt()
将它们一一删除,没有。
不幸的是,一些 PowerShell 主机默认通常通过 PowerShell 的错误流路由来自外部程序的 stderr 输出,即将其视为错误 em> 输出,这是不适当的,因为许多外部程序也使用stderr 来获取状态信息,或者更一般地说,用于任何事情那是不是数据(git
是一个很好的例子):不是每个标准错误行都可以被认为代表一个错误,并且标准错误输出的存在不意味着失败。
受影响的主机:
obsolescent Windows PowerShell ISE 和可能的其他基于 GUI 的较旧 IDEVisual Studio Code 除外。
通过PowerShell remoting 或background job 执行外部程序时(这两种调用机制共享相同的基础架构并使用PowerShell 附带的ServerRemoteHost
主机)。
在非远程、非后台调用中运行正常的主机(它们将 stderr 行传递到显示器并正常打印它们):
终端(控制台),包括Windows Terminal。
Visual Studio Code 与PowerShell extension;此跨平台编辑器 (IDE) 旨在取代 Windows PowerShell ISE。
this GitHub issue 中讨论了主机之间的这种不一致。
【讨论】:
【参考方案2】:Powershell 7.1
有一个new experimental feature 可以启用“理智”的行为:
Enable-ExperimentalFeature PSNotApplyErrorActionToStderr
很遗憾,此功能不能仅在当前范围内启用,它将为整个用户帐户(参数为 -Scope AllUsers
的所有用户)启用,并且需要启动新的 PS 会话才能生效。
现在这个示例代码...
& cmd /c "echo Hello from standard error 1>&2" 2>&1
echo "`$LastExitCode=$LastExitCode and `$?=$?"
echo "`$Error.Count=$($Error.Count)"
...输出预期的结果:
Hello from standard error
$LastExitCode=0 and $?=True
$Error.Count=0
【讨论】:
【参考方案3】:(注意:这主要是推测;我很少在 PowerShell 中使用很多本机命令,其他人可能比我更了解 PowerShell 内部)
我猜你在 PowerShell 控制台主机中发现了一个差异。
-
如果 PowerShell 拾取标准错误流中的内容,它将假定一个错误并抛出
NativeCommandError
。
只有在监控标准错误流时,PowerShell 才能检测到这一点。
PowerShell ISE 必须对其进行监控,因为它不是控制台应用程序,因此本机控制台应用程序没有可写入的控制台。这就是为什么在 PowerShell ISE 中无论2>&1
重定向运算符如何都会失败。
如果您使用2>&1
重定向运算符,控制台主机将监控标准错误流,因为标准错误流上的输出必须被重定向并因此被读取。
我的猜测是控制台 PowerShell 主机是惰性的,如果不需要对其输出进行任何处理,它只会将本地控制台命令交给控制台。
我真的认为这是一个错误,因为 PowerShell 的行为因宿主应用程序而异。
【讨论】:
我同意 Joey 的评估,我认为 PowerShell.exe 的行为应该改进。 你在 Powershell ISE 中告诉我,我的两个命令都崩溃了。现在我更困惑了! 是否有关于第 1 点的 Powershell 文档?恕我直言,这是一个糟糕的设计决定,许多程序将调试信息打印到标准错误,这并不意味着它们失败了。 刚刚搜索了该规范。没有什么可以解释你看到的行为。对于$?
的错误究竟是什么也有点模糊。
我制作了一个快速的 python 脚本,它只是将一条消息写入控制台的 stderr 并使用powershell.exe
执行它,$?
是真的。 python 脚本只是 sys 的导入,然后是 sys.stderr.write("Hi There\n")
。所以似乎$?
只有在本机命令退出代码不为零时才为假。【参考方案4】:
(我使用的是 PowerShell v2。)
“$?
”变量记录在about_Automatic_Variables
:
这是指最近的 PowerShell 操作,而不是最后一个外部命令,即您在 $LastExitCode
中获得的内容。
在您的示例中,$LastExitCode
为 0,因为最后一个外部命令是 cmd
,它成功回显了一些文本。但是2>&1
导致发送到stderr
的消息被转换为输出流中的错误记录,这告诉PowerShell 在最后一次操作 期间出现错误,导致$?
变为@987654329 @。
为了更详细地说明这一点,请考虑以下内容:
> java -jar foo; $?; $LastExitCode 无法访问 jarfile foo 错误的 1$LastExitCode
是 1,因为那是 java.exe 的退出代码。 $?
是 False,因为 shell 所做的最后一件事失败了。
但如果我所做的只是切换它们:
> java -jar foo; $LastExitCode;美元? 无法访问 jarfile foo 1 真的...那么$?
为True,因为shell 做的最后一件事是向主机打印$LastExitCode
,这是成功的。
最后:
> & java -jar foo ; $?; $LastExitCode 无法访问 jarfile foo 真的 1...这似乎有点违反直觉,但$?
现在是True,因为脚本块的执行成功,即使命令里面跑的不是。
返回到 2>&1
重定向.... 这会导致错误记录进入输出流,这就是关于 NativeCommandError
的冗长 blob 的原因。 shell 正在转储整个错误记录。
当您只想将stderr
和 stdout
连接在一起时,这可能会特别烦人,这样它们就可以组合到一个日志文件或其他东西中。谁想要 PowerShell 插入他们的日志文件???如果我执行ant build 2>&1 >build.log
,那么任何转到stderr
的错误都会附加PowerShell 的nosey $0.02,而不是在我的日志文件中获得干净的错误消息。
但是,输出流不是 text 流!重定向只是 object 管道的另一种语法。错误记录是对象,所以您所要做的就是在重定向之前将该流上的对象转换为 strings:
发件人:
> cmd /c "从标准错误中回显你好 1>&2" 2>&1 cmd.exe:来自标准错误的问候 在行:1 字符:4 + cmd &2" 2>&1 + CategoryInfo : NotSpecified: (Hello from standard error :String) [], RemoteException + FullyQualifiedErrorId : NativeCommandError收件人:
> cmd /c "从标准错误回显你好 1>&2" 2>&1 | % "$_" 来自标准错误的你好...并重定向到文件:
> cmd /c "从标准错误回显你好 1>&2" 2>&1 | % "$_" |开球.txt 来自标准错误的你好...或者只是:
> cmd /c "从标准错误回显你好 1>&2" 2>&1 | % "$_" >out.txt【讨论】:
嘿,喜欢2>&1 | % "$_"
解决方法,我会使用它,谢谢。
But, the output stream is not a text stream! Redirects are just another syntax for the object pipeline. The error records are objects, so all you have to do is convert the objects on that stream to strings before redirecting
+1
没错。将流对象放在引号 ("$_") 中会转换为字符串。
如果 ErrorActionPref 设置为“停止”,在 V2.0 中 2>&1 | % "$_"
不起作用。有没有办法运行命令并覆盖全局 EA 值?
我将此与 $LASTEXITCODE 检查相结合,否则命令被认为失败:command 2>&1 | % "$_" ; if ($LASTEXITCODE -ne 0) throw "Command returned exit code $LASTEXITCODE" else Write-Host "Command finished successfully"
【参考方案5】:
对我来说,这是 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
【讨论】:
自 2011 年以来,Microsoft 存在一个关于此行为的活动错误:connect.microsoft.com/PowerShell/feedback/details/645954。重定向标准错误并防止错误的更好解决方案是使用 cmd /c "command 2>&1"【参考方案6】:此错误是 PowerShell 错误处理规范设计的意外后果,因此很可能永远无法修复。如果您的脚本仅与其他 PowerShell 脚本一起使用,那么您是安全的。但是,如果您的脚本与来自广阔世界的应用程序进行交互,则此错误可能会咬人。
PS> nslookup microsoft.com 2>&1 ; echo $?
False
明白了!不过,经过一些痛苦的抓挠,你永远不会忘记这一课。
使用($LastExitCode -eq 0)
而不是$?
【讨论】:
当然反之亦然 - 您可以获得写入 stdErr 但 dont 设置退出代码的应用程序(尤其是控制台脚本),在这种情况下您必须 使用 $? 检测这些错误。所以答案是......这取决于。 @piers7 或者您可以自己将 stderr 捕获到变量 (***.com/questions/24222088/…) 中,然后同时测试$LastExitCode
和 $stderr
。
另外,建议应该是 使用 ($LastExitCode -eq 0) 而不是 $? 用于本机命令。在调用 PowerShell 脚本/Cmdlet 时,$?
是可行的方法 - 如果脚本不包含显式的 Exit-PSSession
调用,则甚至不会设置 $LastExitCode
。
我几乎对此投了反对票,因为这不是一个错误。 @OhadSchneider 推荐的是更好的答案:使用 $LastExitCode 而不是 $? 仅适用于本机命令。以上是关于$LastExitCode=0,但在 PowerShell 中 $?=False。将标准错误重定向到标准输出会产生 NativeCommandError的主要内容,如果未能解决你的问题,请参考以下文章
powershell $ lastexitcode在运行批处理文件时不起作用
Codeforces ECR 83 C. Adding Powers (位运算)