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

时间:2012-05-19 14:36:14

标签: windows powershell command-line heisenbug

为什么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的{​​{1}})。我检查变量echo$?。正如预期的那样,它们分别等于True和0。

但是,如果我要求PowerShell通过第一个命令将标准错误重定向到标准输出,我会得到一个NativeCommandError:

$LastExitCode

我的第一个问题,为什么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 成功运行且$?为0时,为什么cmd为假? PowerShell的文档about automatic variables未明确定义$LastExitCode。当且仅当$?为0时,我总是认为它是真的,但我的例子与之相矛盾。


以下是我在现实世界(简化)中遇到此行为的方式。真的是FUBAR。我从另一个调用了一个PowerShell脚本。内部脚本:

$LastExitCode

简单地将其作为cmd /c "echo Hello from standard error 1>&2" if (! $?) { echo "Job failed. Sending email.." exit 1 } # Do something else 运行,它运行正常,并且不会发送任何电子邮件。但是,我从另一个PowerShell脚本调用它,记录到文件.\job.ps1。在这种情况下,会发送一封电子邮件!使用错误流在脚本外部执行的操作会影响脚本的内部行为。 观察现象会改变结果。这就像量子物理而不是脚本!

[有趣的是:.\job.ps1 2>&1 > log.txt可能会或不会爆炸,具体取决于您运行它的位置]

6 个答案:

答案 0 :(得分:65)

(我正在使用PowerShell v2。)

$?”变量记录在about_Automatic_Variables

$?
  Contains the execution status of the last operation

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

在您的示例中,$LastExitCode为0,因为上一个外部命令是cmd,它在回显某些文本时成功。但是2>&1导致将stderr的消息转换为输出流中的错误记录,这告诉PowerShell在上一次操作期间出现错误,导致{{1成为$?

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

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

False为1,因为这是java.exe的退出代码。 $LastExitCode是假的,因为shell最后一次失败了。

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

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

...然后$?为True,因为shell做的最后一件事是向主机打印$?,这是成功的。

最后:

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

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


返回$?重定向....导致错误记录进入输出流,这就是2>&1的长卷blob。 shell正在转储整个错误记录。

当你想要做的就是将NativeCommandError stderr组合在一起时,这可能会特别烦人,因此它们可以组合在一个日志文件中。谁想要PowerShell对接他们的日志文件???如果我stdout,则ant build 2>&1 >build.log的任何错误都会增加PowerShell的 nosy $ 0.02,而不是在我的日志文件中收到干净的错误消息。

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

自:

> 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

答案 1 :(得分:16)

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

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

False

疑难杂症!仍然,经过一些痛苦的刮擦,你永远不会忘记这一课。

使用($LastExitCode -eq 0)代替$?

答案 2 :(得分:10)

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

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

  1. 如果PowerShell选择了标准错误流上的内容,则会发出错误并抛出NativeCommandError
  2. 如果监视标准错误流,PowerShell只能选择它。
  3. PowerShell ISE 必须监视它,因为它不是控制台应用程序,因此本机控制台应用程序没有可写入的控制台。这就是为什么在PowerShell ISE中,无论2>&1重定向运算符如何都会失败。
  4. 如果使用2>&1重定向操作符,控制台主机监视标准错误流,因为标准错误流上的输出必须重定向并因此被读取。
  5. 我的猜测是,控制台PowerShell主机是懒惰的,如果不需要对其输出进行任何处理,只需将本机控制台命令控制台。

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

答案 3 :(得分:1)

Powershell 7.1

有一个new experimental feature可以启用“理智”的行为:

onclick

不幸的是,此功能不能仅在当前范围内启用,它将在整个用户帐户(所有参数为None的用户)上启用,并且需要启动新的PS会话才能生效。

现在此示例代码...

fn_convertLanguage('kor')
fn_convertLanguage('eng')
fn_convertLanguage('chn')
fn_convertLanguage('rus')
fn_convertLanguage('spn')
fn_convertLanguage('jpn')
GotoLogin()
register()
evalSearch()
...

...输出期望的结果:

Enable-ExperimentalFeature PSNotApplyErrorActionToStderr

答案 4 :(得分:0)

对我来说,这是ErrorActionPreference的一个问题。 当从ISE运行时,我在第一行设置了$ ErrorActionPreference =“Stop”,并且拦截了所有事件,并将*&gt;&amp; 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 }
    }
}

答案 5 :(得分:0)

自v7.0起的问题的摘要

PowerShell引擎在将2>重定向应用于外部程序调用方面仍然存在错误

根本原因是使用2>导致stderr(标准错误)输出通过PowerShell的错误流路由 (请参阅about_Redirection ),则有以下不良后果:

  • 如果$ErrorActionPreference = 'Stop'生效,则使用2>会意外触发脚本终止错误,即中止 (甚至采用2>$null的形式,其目的显然是忽略 stderr行)。参见this GitHub issue

    • 解决方法:(临时)设置$ErrorActionPreference = 'Continue'
  • 由于2>当前接触错误流$?,因此,如果发出了至少一条stderr行,则自动将成功状态变量始终设置为$False,然后不再反映命令的真实成功状态。参见this GitHub issue

    • your answer中所建议的解决方法:仅在调用外部程序后使用$LASTEXITCODE -eq 0来测试成功。
  • 对于2>,stderr行意外地记录在自动$Error变量(该变量记录会话中发生的所有错误的变量)中,即使您使用{{ 1}}。参见this GitHub issue

    • 解决方法:缺少跟踪添加了多少错误记录并用2>$null逐一删除它们的记录,没有任何记录。

通常,不幸的是,某些PowerShell主机默认情况下 通过PowerShell的错误流将来自外部程序的stderr输出路由,即将其作为错误处理 em>输出,这是不恰当的,因为许多外部程序也将stderr用作状态信息,或更普遍地,它用于任何内容就是不是数据$Error.RemoveAt()是一个很好的例子):不能假定每条stderr行都代表一个错误,并且stderr输出的存在确实< em> not 暗示失败。

受影响的主机

在非远程,非后台调用中表现符合预期的主机(它们将stderr行传递到显示器并正常打印):

this GitHub issue中讨论了主机之间的不一致问题。