为什么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
的echo
)。 我检查variables$?
和$LastExitCode
。 如预期的那样,它们分别等于“真”和“0”。
但是,如果我要求PowerShell通过第一个命令将标准错误redirect到标准输出,我得到一个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时$LastExitCode
? PowerShell 有关自动variables的文档没有明确定义$?
。 我总是认为这是真的当且仅当$LastExitCode
是0,但我的例子与此相矛盾。
以下是我在现实世界中遇到的这种行为(简化)。 这真的是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脚本调用它,logging到文件.\job.ps1 2>&1 > log.txt
。 在这种情况下,电子邮件发送! 您在脚本外部使用错误stream进行的操作会影响脚本的内部行为。 观察现象会改变结果。 这感觉就像量子物理而不是脚本!
[有趣的是: .\job.ps1 2>&1
可能会爆炸或不爆炸,取决于你在哪里运行]
(我正在使用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的退出码。 $?
是错误的,因为壳的最后一件事失败了。
但是,如果我所做的只是切换它们:
> java -jar foo; $ LastExitCode; $? 无法访问jarfile foo 1 真正
…那么$?
是真的,因为shell的最后一件事是打印$LastExitCode
到主机,这是成功的。
最后:
>&{java -jar foo}; $ ?; $ LastExitCode 无法访问jarfile foo 真正 1
…这似乎有点反直觉,但$?
现在是真的 ,因为脚本块的执行是成功的 ,即使它里面运行的命令不是。
返回到2>&1
重定向….导致一个错误记录在输出流,这是什么给了那个关于NativeCommandError
长NativeCommandError
。 shell正在倾倒整个错误记录。
当你所要做的只是将stderr
和 stdout
管道在一起,以便它们可以被合并到一个日志文件中时,这可能会特别恼人。 谁想要PowerShell对接他们的日志文件? 如果我ant build 2>&1 >build.log
,那么任何发送到stderr
让PowerShell的臭名昭着 $ 0.02,而不是在我的日志文件中得到干净的错误信息。
但是,输出流不是文本流! 重定向只是对象管道的另一种语法。 错误记录是对象,所以您只需在重定向之前将该流上的对象转换为字符串 :
从:
> cmd / c“echo标准错误1>&2”2>&1 cmd.exe:从标准错误你好 在行:1 char:4 + cmd&2“2>&1 + CategoryInfo:NotSpecified :(从标准错误:字符串)[],RemoteException + FullyQualifiedErrorId:NativeCommandError
至:
> cmd / c“echo标准错误1>&2”2>&1 |您好 %{“$ _”} 你好,从标准错误
…并重定向到一个文件:
> cmd / c“echo标准错误1>&2”2>&1 |您好 %{“$ _”} | 发球out.txt 你好,从标准错误
…要不就:
> cmd / c“echo标准错误1>&2”2>&1 |您好 %{“$ _”}> out.txt
这个错误是PowerShell的错误处理规定性设计的一个无法预料的后果,所以它很可能永远不会被修复。 如果您的脚本只能与其他PowerShell脚本一起玩,那么您是安全的。 但是,如果你的脚本与来自广阔世界的应用程序交互,这个bug可能会咬人。
PS> nslookup microsoft.com 2>&1 ; echo $? False
疑难杂症! 不过,经过一些痛苦的抓挠,你永远不会忘记教训。
($LastExitCode -eq 0)
而不是$?
(注意:这主要是猜测;我很少在PowerShell中使用许多本机命令,其他人可能比我更了解PowerShell内部)
我想你在PowerShell控制台主机上发现了一个差异。
NativeCommandError
。 2>&1
重定向操作符如何都会失败。 2>&1
重定向操作符,控制台主机将监视标准错误流,因为标准错误流上的输出必须重定向并读取。 我的猜测是,控制台PowerShell主机是懒惰的,只要交出本地控制台命令控制台,如果它不需要做任何处理他们的输出。
我真的相信这是一个错误,因为PowerShell的行为有所不同,具体取决于主机应用程序。
对我来说这是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 } } }