54

为什么 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?

其次,为什么运行成功$?时为 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

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

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

4

6 回答 6

89

(我使用的是 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 的退出代码。$?是 False,因为 shell 做的最后一件事失败了。

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

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

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

最后:

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

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


返回到2>&1重定向......这会导致输出流中出现错误记录,这就是关于NativeCommandError. shell 正在转储整个错误记录。

当您想要做的只是管道stderr stdout在一起时,这可能特别烦人,因此它们可以组合在一个日志文件或其他东西中。谁想要 PowerShell 插入他们的日志文件???如果我这样做ant build 2>&1 >build.log了,那么任何错误都会stderr导致 PowerShell 的多管闲事$0.02 被附加,而不是在我的日志文件中获得干净的错误消息。

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

从:

> 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
于 2012-10-12T20:32:09.590 回答
22

这个错误是 PowerShell 对错误处理的规范设计的一个无法预料的结果,因此很可能它永远不会被修复。如果您的脚本仅与其他 PowerShell 脚本一起使用,那么您是安全的。但是,如果您的脚本与来自广阔世界的应用程序交互,则此错误可能会咬人。

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

False

明白了!尽管如此,在一些痛苦的抓挠之后,你永远不会忘记这一课。

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

于 2012-10-01T18:51:50.953 回答
11

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

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

  1. 如果 PowerShell 在标准错误流中提取内容,它将假定一个错误并抛出一个NativeCommandError.
  2. PowerShell 只能在监视标准错误流时才能够接收到它。
  3. PowerShell ISE必须对其进行监控,因为它不是控制台应用程序,因此本机控制台应用程序没有可写入的控制台。这就是为什么在 PowerShell ISE 中无论2>&1重定向运算符如何都会失败的原因。
  4. 如果您使用重定向运算符,控制台主机2>&1监视标准错误流,因为标准错误流上的输出必须被重定向并因此被读取。

我的猜测是控制台 PowerShell 主机很懒惰,如果不需要对其输出进行任何处理,它只会将本机控制台命令交给控制台。

我真的认为这是一个错误,因为 PowerShell 的行为因主机应用程序而异。

于 2012-05-19T14:52:08.130 回答
11

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


从 v7.1 开始的问题总结:

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

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

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

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

  • 使用2>,stderr 行会意外记录在自动$Error变量(记录会话中发生的所有错误的变量)中 - 即使您使用2>$null. 请参阅此 GitHub 问题

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

通常,不幸的是,默认情况下,某些 PowerShell 主机通过 PowerShell 的错误流从外部程序路由 stderr 输出,即将其视为错误输出,这是不合适的,因为许多外部程序也使用 stderr 来获取状态信息,或者更一般地,用于任何不是数据git作为一个主要示例):并非每个 stderr 行都可以假定代表一个错误,并且 stderr 输出的存在并不意味着失败。

受影响的主机

  • 过时的Windows PowerShell ISE和可能的其他基于 GUI 的旧 IDE,而不是Visual Studio Code。

  • 通过PowerShell 远程处理或在后台作业中执行外部程序时(这两种调用机制共享相同的基础架构并使用ServerRemoteHostPowerShell 附带的主机)。

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

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

于 2019-12-17T14:33:43.873 回答
1

电源外壳 7.1

有一个新的实验功能可以启用“理智”的行为:

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
于 2020-12-06T23:19:21.827 回答
0

对我来说,这是 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 }
    }
}
于 2015-07-22T09:47:13.087 回答