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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/10666101/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-09 09:32:10  来源:igfitidea点击:

$LastExitCode=0, but $?=False in PowerShell. Redirecting stderr to stdout gives NativeCommandError

windowspowershellcommand-line

提问by Colonel Panic

Why does PowerShell show the surprising behaviour in the second example below?

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

First, an example of sane behaviour:

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

PS C:\> & cmd /c "echo Hello from standard error 1>&2"; echo "`$LastExitCode=$LastExitCode and `$?=$?"
Hello from standard error
$LastExitCode=0 and $?=True

No surprises. I print a message to standard error (using cmd's echo). I inspect the variables $?and $LastExitCode. They equal to True and 0 respectively, as expected.

没有惊喜。我将消息打印到标准错误(使用cmd's echo)。我检查了变量$?$LastExitCode. 正如预期的那样,它们分别等于 True 和 0。

However, if I ask PowerShell to redirect standard error to standard output over the first command, I get a NativeCommandError:

但是,如果我要求 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

My first question, why the NativeCommandError?

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

Secondly, why is $?False when cmdran successfully and $LastExitCodeis 0? PowerShell's documentation about automatic variablesdoesn't explicitly define $?. I always supposed it is True if and only if $LastExitCodeis 0, but my example contradicts that.

其次,为什么运行成功$?时为Falsecmd$LastExitCode为 0?PowerShell关于自动变量的文档没有明确定义$?. 我一直认为它是 True 当且仅当$LastExitCode为 0,但我的例子与此相矛盾。



Here's how I came across this behaviour in the real-world (simplified). It really is FUBAR. I was calling one PowerShell script from another. The inner script:

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

cmd /c "echo Hello from standard error 1>&2"
if (! $?)
{
    echo "Job failed. Sending email.."
    exit 1
}
# Do something else

Running this simply as .\job.ps1, it works fine, and no email is sent. However, I was calling it from another PowerShell script, logging to a file .\job.ps1 2>&1 > log.txt. In this case, an email is sent! What you do outside the script with the error stream affects the internal behaviour of the script. Observing a phenomenon changes the outcome.This feels like quantum physics rather than scripting!

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

[Interestingly: .\job.ps1 2>&1may or not blow up depending on where you run it]

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

采纳答案by Colonel Panic

This bug is an unforeseen consequence of PowerShell's prescriptive design for error handling, so most likely it will never be fixed. If your script plays only with other PowerShell scripts, you're safe. However if your script interacts with applications from the big wide world, this bug may bite.

这个错误是 PowerShell 的错误处理规范设计的不可预见的后果,因此很可能永远不会被修复。如果您的脚本只与其他 PowerShell 脚本一起运行,那么您就安全了。但是,如果您的脚本与来自广阔世界的应用程序交互,则此错误可能会咬人。

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

False

Gotcha! Still, after some painful scratching, you'll never forget the lesson.

知道了!尽管如此,经过一些痛苦的抓挠,你永远不会忘记这个教训。

Use ($LastExitCode -eq 0)instead of $?

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

回答by Droj

(I am using PowerShell v2.)

(我正在使用 PowerShell v2。)

The '$?' variable is documented in about_Automatic_Variables:

' $?' 变量记录在about_Automatic_Variables

$?
  Contains the execution status of the last operation

This is referring to the most recent PowerShell operation, as opposed to the last external command, which is what you get in $LastExitCode.

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

In your example, $LastExitCodeis 0, because the last external command was cmd, which was successfulin echoing some text. But the 2>&1causes messages to stderrto be converted to error records in the output stream, which tells PowerShell that there was an error during the last operation, causing $?to be False.

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

To illustrate this a bit more, consider this:

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

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

$LastExitCodeis 1, because that was the exit code of java.exe. $?is False, because the very last thing the shell did failed.

$LastExitCode是 1,因为那是 java.exe 的退出代码。$?是 False,因为 shell 做的最后一件事失败了。

But if all I do is switch them around:

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

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

... then $?is True, because the last thing the shell did was print $LastExitCodeto the host, which was successful.

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

Finally:

最后:

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

...which seems a bit counter-intuitive, but $?is Truenow, because the execution of the script block was successful, even if the command run inside of it was not.

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



Returning to the 2>&1redirect.... that causes an error record to go in the output stream, which is what gives that long-winded blob about the NativeCommandError. The shell is dumping the whole error record.

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

This can be especially annoying when all you want to do is pipe stderrandstdouttogether so they can be combined in a log file or something. Who wants PowerShell butting in to their log file??? If I do ant build 2>&1 >build.log, then any errors that go to stderrhave PowerShell's nosey$0.02 tacked on, instead of getting clean error messages in my log file.

这可能是特别恼人时,所有你需要做的就是管stderrstdout在一起,这样他们就可以在日志文件或东西相结合。谁想要 PowerShell 插入他们的日志文件???如果我这样做了ant build 2>&1 >build.log,那么任何stderr导致 PowerShell 多管闲事的$0.02 的错误都会被添加进来,而不是在我的日志文件中获取干净的错误消息。

But, the output stream is not a textstream! Redirects are just another syntax for the objectpipeline. The error records are objects, so all you have to do is convert the objects on that stream to stringsbefore redirecting:

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

From:

从:

> 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

To:

到:

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

...and with a redirect to a file:

...并重定向到文件:

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

...or just:

...要不就:

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

回答by Joey

(Note: This is mostly speculation; I rarely use many native commands in PowerShell and others probably know more about PowerShell internals than me)

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

I guess you found a discrepancy in the PowerShell console host.

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

  1. If PowerShell picks up stuff on the standard error stream it will assume an error and throw a NativeCommandError.
  2. PowerShell can only pick this up if it monitorsthe standard error stream.
  3. PowerShell ISE has tomonitor it, because it is no console application and thus a native console application has no console to write to. This is why in the PowerShell ISE this fails regardless of the 2>&1redirection operator.
  4. The console host willmonitor the standard error stream if you use the 2>&1redirection operator because output on the standard error stream has to be redirected and thus read.
  1. 如果 PowerShell 在标准错误流中获取内容,它将假定一个错误并抛出一个NativeCommandError.
  2. PowerShell 只能在它监视标准错误流时才能选择它。
  3. PowerShell ISE必须对其进行监控,因为它不是控制台应用程序,因此本机控制台应用程序没有可写入的控制台。这就是为什么在 PowerShell ISE 中无论2>&1重定向运算符如何都失败的原因。
  4. 如果您使用重定向运算符,控制台主机监视标准错误流,2>&1因为标准错误流上的输出必须被重定向并因此被读取。

My guess here is that the console PowerShell host is lazy and just hands native console commands the console if it doesn't need to do any processing on their output.

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

I would really believe this to be a bug, because PowerShell behaves differently depending on the host application.

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

回答by mklement0

A summary of the problems as of v7.0:

v7.0问题总结:

The PowerShell engine still has bugs with respect to 2>redirections applied to external-program calls:

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

The root cause is that using 2>causes the stderr (standard error) output to be routed via PowerShell's error stream(see about_Redirection), which has the following undesired consequences:

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

  • If $ErrorActionPreference = 'Stop'happens to be in effect, using 2>unexpectedly triggers a script-terminating error, i.e. abortsthe script (even in the form 2>$null, where the intent is clearly to ignorestderr lines). See this GitHub issue.

    • Workaround: (Temporarily) set $ErrorActionPreference = 'Continue'
  • Since 2>currently touches the error stream, $?, the automatic success-status variable is invariably set to $Falseif at least one stderr line was emitted, and then no longer reflects the true success status of the command. See this GitHub issue.

    • Workaround, as recommended in your answer: only ever use $LASTEXITCODE -eq 0to test for success after calls to external programs.
  • With 2>, stderr lines are unexpectedly recorded in the automatic $Errorvariable (the variable that keeps a log of all errors that occurred in the session) - even if you use 2>$null. See this GitHub issue.

    • Workaround: Short of keeping track how many error records were added and removing them with $Error.RemoveAt()one by one, there is none.
  • 如果$ErrorActionPreference = 'Stop'碰巧生效,使用2>意外触发脚本终止错误,即中止脚本(即使在形式中2>$null,其意图显然是忽略stderr 行)。请参阅此 GitHub 问题

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

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

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


Generally, unfortunately, some PowerShell hosts by defaultroute stderr output from external programs via PowerShell's error stream, i.e. treat it as erroroutput, which is inappropriate, because many external programs use stderr also for statusinformation, or more generally, for anything that is not data(gitbeing a prime example): Not every stderr line can be assumed to represent an error, and the presence of stderr output does notimply failure.

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

Affected hosts:

受影响的主机

Hosts that DO behave as expectedin non-remoting, non-background invocations (they pass stderr lines through to the display and print them normally):

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

This inconsistency across hosts is discussed in this GitHub issue.

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

回答by Michael Logutov

For me it was an issue with ErrorActionPreference. When running from ISE I've set $ErrorActionPreference = "Stop" in the first lines and that was intercepting everything event with *>&1 added as parameters to the call.

对我来说,这是 ErrorActionPreference 的问题。当从 ISE 运行时,我在第一行设置了 $ErrorActionPreference = "Stop" 并且它拦截了所有事件,*>&1 作为参数添加到调用中。

So first I had this line:

所以首先我有这条线:

& $exe $parameters *>&1

Which like I've said didn't work because I had $ErrorActionPreference = "Stop" earlier in file (or it can be set globally in profile for user launching the script).

就像我说的那样不起作用,因为我在文件中早些时候有 $ErrorActionPreference = "Stop" (或者它可以在用户启动脚本的配置文件中全局设置)。

So I've tried to wrap it in Invoke-Expression to force ErrorAction:

所以我试图将它包装在 Invoke-Expression 中以强制 ErrorAction:

Invoke-Expression -Command "& `"$exe`" $parameters *>&1" -ErrorAction Continue

And this doesn't work either.

这也不起作用。

So I had to fallback to hack with temporary overriding ErrorActionPreference:

所以我不得不回退以临时覆盖 ErrorActionPreference 来破解:

$old_error_action_preference = $ErrorActionPreference

try
{
    $ErrorActionPreference = "Continue"
    & $exe $parameters *>&1
}
finally
{
    $ErrorActionPreference = $old_error_action_preference
}

Which is working for me.

这对我有用。

And I've wrapped that into a function:

我把它包装成一个函数:

<#
    .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 }
    }
}