如何在PowerShell中向标准错误输出写入信息?

55

我有困难弄清楚如何同时将内容回显到标准错误流和重定向可执行文件的错误流。

我来自Bourne shellKorn shell背景,我通常会使用以下方法:

# Write to stderr
echo "Error Message!" >&2

# Redirect stderr to file
/do/error 2>/tmp/err.msg

你是在谈论将外部可执行文件的输出重定向,该可执行文件本身是从PowerShell中运行的吗? - x0n
值得注意的是,我发现 Write-Error 会导致我的脚本终止,因为我使用了 $ErrorActionPreference = "Stop",我认为这更有价值。 - Peter L
3个回答

48

使用Write-Error将内容写入标准错误流(stderr),如果想要将标准错误流重定向到文件,请使用:

 Write-Error "oops" 2> /temp/err.msg
或者
 exe_that_writes_to_stderr.exe bogus_arg 2> /temp/err.msg

请注意,PowerShell以错误记录的形式编写错误。如果你想避免冗长的错误记录输出,你可以按如下方式自己编写错误信息:

PS> Write-Error "oops" -ev ev 2>$null
PS> $ev[0].exception
oops

-EV-ErrorVariable的简称,任何错误都将存储在此参数命名的变量中。 除非我们将错误重定向到$null,否则PowerShell仍将向控制台报告错误。


3
很抱歉,这不是我想要的。我不想让堆栈跟踪出现,我只想用单个错误消息写入错误流。 - Brett Ryan
1
在您更新的版本中仍然没有帮助,我想要能够仅将一段文本写入stderr,以便我可以有两个重定向cmd >stdout.log 2>stderr.log。请考虑能够输入内容并将其管道传输到stderr,目前看来似乎无法实现。 - Brett Ryan
1
似乎 Write-Error "oops" -ev ev 2>&1 > $null 不会将任何内容重定向到标准错误输出。Write-Error "oops" 很好用,但你会卡在冗长的输出上。 - Ohad Schneider
我的意思是,不仅可以避免显示stderr输出,而且oops实际上根本没有被重定向到stderr(我尝试在外部执行它,结果oops只出现在标准输出中)。 - Ohad Schneider
尽管已经发送到StdOut和$null,但我仍然能在我的程序中看到"oops"被输出到了StdErr。 - Steven Vachon
显示剩余10条评论

46

注意:

  • 本回答讨论的是在从外部调用PowerShell脚本时从“外部世界”的角度写入stderr;尽管该答案是从Windows shell(cmd.exe)的角度编写的,但与PowerShell Core结合使用时,同样适用于Unix shell(如bash)。

  • 相比之下,从Powershell内部,您应该使用Write-Error,如Keith Hill的答案所述。

  • 不幸的是,没有一种统一的方法可以同时从PowerShell内部和外部工作-请参见我的这个答案进行讨论。


补充Chris Sear的出色答案

虽然$host.ui.WriteErrorLine应该在所有主机上都能工作,但如果通过批处理文件等方式从cmd.exe调用,则默认情况下它不会将内容写入stderr。 相比之下,[Console]::Error.WriteLine始终会将内容写入stderr

因此,如果想要编写一个在从cmd.exe调用时在输出流方面正常工作的PowerShell脚本,请使用以下函数Write-StdErr,该函数在常规的PS/cmd.exe主机(控制台窗口)中使用[Console]::Error.WriteLine, 否则使用$host.ui.WriteErrorLine

<#
.SYNOPSIS
Writes text to stderr when running in a regular console window,
to the host''s error stream otherwise.

.DESCRIPTION
Writing to true stderr allows you to write a well-behaved CLI
as a PS script that can be invoked from a batch file, for instance.

Note that PS by default sends ALL its streams to *stdout* when invoked from
cmd.exe.

This function acts similarly to Write-Host in that it simply calls
.ToString() on its input; to get the default output format, invoke
it via a pipeline and precede with Out-String.

#>
function Write-StdErr {
  param ([PSObject] $InputObject)
  $outFunc = if ($Host.Name -eq 'ConsoleHost') {
    [Console]::Error.WriteLine
  } else {
    $host.ui.WriteErrorLine
  }
  if ($InputObject) {
    [void] $outFunc.Invoke($InputObject.ToString())
  } else {
    [string[]] $lines = @()
    $Input | % { $lines += $_.ToString() }
    [void] $outFunc.Invoke($lines -join "`r`n")
  }
}

可选背景信息:PowerShell CLI 的输出流如何被外部调用者看到:

在内部,PowerShell 拥有比传统的输出流(stdout 和 stderr)更多的输出流,并且它们的数量随着时间的推移而增加(尝试 Write-Warning "I'll go unheard." 3> $null 作为示例,并在 Get-Help about_Redirection 中进一步了解)。

与外部交互时,PowerShell 必须将非传统输出流映射到 stdout 和 stderr。

然而奇怪的是,默认情况下 PowerShell 将所有的输出流(包括 Write-Host$host.ui.WriteErrorLine() 输出)发送到 stdout,即使将 PowerShell 的错误流映射到 stderr 是逻辑上的选择。这个行为从至少 v2 开始生效,到 v5.1 时仍然有效(可能出于向后兼容性的原因,不会改变 - 参见 GitHub 问题 #7989)。

您可以使用以下命令进行验证,如果你从 cmd.exe 中调用它:

powershell -noprofile -command "'out'; Write-Error 'err'; Write-Warning 'warn'; Write-Verbose -Verbose 'verbose'; $DebugPreference='Continue'; write-debug 'debug'; $InformationPreference='Continue'; Write-Information 'info'; Write-Host 'host'; $host.ui.WriteErrorLine('uierr'); [Console]::Error.WriteLine('cerr')" >NUL
命令会写入所有 PowerShell 输出流(在运行旧版 PowerShell-v5 时,您将看到有关 Write-Information(引入于 PowerShell v5)的其他错误消息),并使 cmd.exe 仅将 stdout 重定向到 NUL(即抑制 stdout 输出)。除了 cerr(来自 Console.Error.WriteLine(),直接写入 stderr 的内容)之外,您将看不到任何输出 - 所有 PowerShell 流都发送到 stdout。更奇怪的是,实际上可以捕获 PowerShell 的错误流,但只能通过重定向才能做到:如果您将上面的“>NUL”更改为“2>NUL”,则会抑制专属于 PowerShell 的错误流和 $host.ui.WriteErrorLine() 输出;当然,与任何重定向一样,您也可以将其发送到文件。(如上所述,[Console].Error.WriteLine()始终将内容输出到stderr,无论后者是否被重定向)。为了举一个更具体的例子(同样是从cmd.exe运行):
powershell -noprofile -command "'out'; Write-Error 'err'" 2>NUL

上述代码只输出 outWrite-Error 的输出被抑制。

总结:

  • 在没有任何 (cmd.exe) 重定向或仅存在 stdout 重定向 (>...1>...) 的情况下,PowerShell 将其所有输出流发送到 stdout

  • stderr 重定向 (2>...) 的情况下,PowerShell 会选择性地将其 错误流 发送到 stderr (无论 stdout 是否也被重定向)。

  • 因此,以下常见用法不会按预期工作:
    powershell ... >data-output.txt 这不会像人们期望的那样只将 stdout 发送到文件 data-output.txt,同时将 stderr 输出打印到终端;相反,您必须使用
    powershell ... >data-output.txt 2>err-output.tmp; type err-output.tmp >&2; del err-output.tmp

由此可见,PowerShell 意识到 cmd.exe 的重定向并有意调整其行为。(这也可以从 PowerShell 在 cmd.exe控制台 中生成 彩色 输出,而在将输出重定向到文件时剥离颜色代码的情况下看出。)


我发现PS7将$Host.UI.WriteErrorLine的输出发送到stdout而不是stderr,即使在pwsh shell中调用。 - Marc
@Marc:在 PowerShell 中,不存在 stdout 和 stderr,只有 PowerShell 的 6 个流和 to-host 输出。在 PowerShell 外部,$Host.UI.WriteErrorLine - 以及 Write-Error - 默认情况下写入 stdout,但是你可以使用 2> 进行重定向,如答案中所述。要默认写入外部世界的 stderr,请使用 [Console]::Error.WriteLine(),如答案中所推荐的那样,但代价是无法在 PowerShell 会话内捕获此类输出。 - mklement0
我感觉自己在逆流中游泳:PowerShell打算如何让开发人员在脚本之间捕获错误输出?我想调用script.ps1并捕获错误流/对象。我应该只是抛出异常吗?设置“-ErrorVariable $foo”似乎对“$foo”没有任何影响,无论是否抛出或写入任何错误。 - Marc
1
@Marc:只有在高级脚本或函数、编译的 cmdlet 中,您才能使用 -ErrorVariable,而且必须使用 -ErrorVariable foo - 也就是说,您不能使用 $ 符号。 - mklement0
好的,但是很奇怪的是 ./scriptlet.ps1 2> 没有捕获错误,而 run-cmdlet 2> 却可以。 - Marc
@Marc:我不理解这个区别。在PowerShell中,任何写入错误流的内容都可以使用“2>”进行重定向,无论是简单函数/脚本还是高级函数/脚本、命令或者外部程序写入stderr。PowerShell命令通过使用“Write-Error”显式地写入错误流,或者通过触发非终止或语句终止错误来隐式地写入错误流。 - mklement0

43

你可能想要这个:

$host.ui.WriteErrorLine('I work in any PowerShell host')

你可能也会看到以下内容,但它假定你的PowerShell主机是一个控制台窗口/设备,因此我认为它的用处较小:

[Console]::Error.WriteLine('I will not work in the PowerShell ISE GUI')

3
WriteErrorLine 不是好的选择。查看文档(https://msdn.microsoft.com/en-us/library/system.management.automation.host.pshostuserinterface.writeerrorline%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396):*此方法将一行写入主机错误显示。* 换句话说,这是一个UI方法,有时按照您的期望工作并重定向到stderr,而有时(大多数情况下?)不会(请参阅mklement0的答案:https://dev59.com/52445IYBdhLWcg3wLXMK#15669365)。正如Keith Hill所说,Write-Error 是最佳选项。 - Ohad Schneider

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接