PowerShell 流式输出

3

我希望能够在PowerShell中捕获一些流式输出。例如:

cmd /c "echo hi && foo"

这个命令应该会打印“hi”,然后是“bomb”。我知道可以使用-ErrorVariable:

Invoke-Command { cmd /c "echo hi && foo" } -ErrorVariable ev

然而存在一个问题:对于长时间运行的命令,我希望能够流式传输输出,而不是捕获并仅在命令结束时获取stderr/stdout输出。
理想情况下,我希望能够将stderr和stdout分开并连接到两个不同的流中,并将stdout管道返回给调用者,但准备在发生错误时丢弃stderr。类似以下内容:
$stdErr
Invoke-Command "cmd" "/c `"echo hi && foo`"" `
  -OutStream (Get-Command Write-Output) `
  -ErrorAction {
    $stdErr += "`n$_"
    Write-Error $_
  }

if ($lastexitcode -ne 0) { throw $stdErr}

我能做的最接近的方法是使用管道,但这不能让我区分标准输出和标准错误输出,因此我最终只能抛弃整个输出流

function Invoke-Cmd {
<# 
.SYNOPSIS 
Executes a command using cmd /c, throws on errors.
#>
    param([string]$Cmd)
    )
    $out = New-Object System.Text.StringBuilder
    # I need the 2>&1 to capture stderr at all
    cmd /c $Cmd '2>&1' |% {
        $out.AppendLine($_) | Out-Null
        $_
    }

    if ($lastexitcode -ne 0) {
        # I really just want to include the error stream here
        throw "An error occurred running the command:`n$($out.ToString())" 
    }
}

常见用法:

Invoke-Cmd "GitVersion.exe" | ConvertFrom-Json

请注意,只使用ScriptBlock(并检查输出流中的[ErrorRecord]是不可接受的,因为有许多程序“不喜欢”直接从PowerShell进程执行。.NET System.Diagnostics.Process API可以让我做到这一点...但是,我无法从流处理程序内部流式传输输出(由于线程和阻塞-尽管我想我可以使用while循环,并在数据进入时流/清除收集的输出)。
2个回答

3
- 所描述的行为仅适用于在没有远程操作的情况下在常规控制台/终端窗口中运行 PowerShell。使用远程操作和 ISE,自 PSv5.1 起行为不同 - 请参见底部。 - 2>$null 行为是 Burt's answer 所依赖的内容 - 2>$null 秘密地仍然写入 PowerShell 的错误流,因此,在生效的 $ErrorActionPreference Stop 下,只要外部实用程序向 stderr 写入任何内容,脚本就会被中止 - 已被归类为一个 bug,并有可能消失。
  • 当PowerShell调用外部实用程序(例如cmd)时,默认情况下会直接传递其stderr输出。也就是说,stderr输出会直接打印到控制台上,而不会包含在捕获的输出中(无论是通过分配给变量还是重定向到文件)。

  • 虽然您可以将2>&1作为cmd命令行的一部分使用,但在PowerShell中,您将无法区分stdout和stderr输出。

  • 相比之下,如果您将2>&1用作PowerShell重定向,您可以根据输入对象类型过滤成功流:

    • [string]实例是stdout
    • [System.Management.Automation.ErrorRecord]实例是stderr行。

以下函数Invoke-CommandLine利用了这一点:

  • Note that the cmd /c part isn't built in, so you would invoke it as follows, for instance:

    Invoke-CommandLine 'cmd /c "echo hi && foo"'
    
  • There is no fundamental difference between passing invocation of a cmd command line and direct invocation of an external utility such as git.exe, but do note that only invocation via cmd allows use of multiple commands via operators &, &&, and ||, and that only cmd interprets %...%-style environment-variable references, unless you use --%, the stop-parsing symbol.

  • Invoke-CommandLine outputs both stdout and stderr line as they're being received, so you can use the function in a pipeline.

    • As written, stderr lines are written to PowerShell's error stream using Write-Error as they're being received, with a single, generic exception being thrown after the external command terminates, should it report a nonzero $LASTEXITCODE.

    • It's easy to adapt the function:

      • to take action once the first stderr line is received.
      • to collect all stderr lines in a single variable
      • and/or, after termination, to take action if any stderr input was received, even with $LASTEXITCODE reporting 0.
  • Invoke-CommandLine uses Invoke-Expression, so the usual caveat applies: be sure you know what command line you're passing, because it will be executed as-is, no matter what it contains.


function Invoke-CommandLine {
<# 
.SYNOPSIS 
Executes an external utility with stderr output sent to PowerShell's error           '
stream, and an exception thrown if the utility reports a nonzero exit code.   
#>

  param([parameter(Mandatory)][string] $CommandLine)

  # Note that using . { ... } is required around the Invoke-Expression
  # call to ensure that the 2>&1 redirection works as intended.
  . { Invoke-Expression $CommandLine } 2>&1 | ForEach-Object {
    if ($_ -is [System.Management.Automation.ErrorRecord]) { # stderr line
      Write-Error $_  # send stderr line to PowerShell's error stream
    } else { # stdout line
      $_              # pass stdout line through
    } 
  }

  # If the command line signaled failure, throw an exception.
  if ($LASTEXITCODE) {
    Throw "Command failed with exit code ${LASTEXITCODE}: $CommandLine"
  }

}

可选阅读:如何将对外部实用程序的调用纳入PowerShell的错误处理中

截至当前版本:Windows PowerShell v5.1,PowerShell Core v6-beta.2

  • 偏好变量$ErrorActionPreference的值仅控制对发生于PowerShell cmdlet/function调用或表达式中的错误和.NET异常的反应。

  • Try/Catch用于捕获PowerShell的终止错误和.NET异常。

  • 在没有远程连接的常规控制台窗口中,诸如cmd之类的外部实用程序目前从不生成任何错误 - 它们所做的只是报告一个退出代码,PowerShell会将其反映在自动变量$LASTEXITCODE中,而自动变量$?在退出代码为非零时反映$False

    • 注意:事实行为在控制台主机以外的主机上存在根本差异(包括Windows ISE和涉及远程连接时)是有问题的:在那里,对外部实用程序的调用会导致将stderr输出视为已报告非终止错误;具体而言:

      • 每个stderr输出行都作为错误记录输出,并记录在自动$Error集合中。
      • 除了将$?设置为非零退出代码的$false之外,任何stderr输出的存在也将将其设置为$False
      • 这种行为是有问题的,因为仅stderr输出本身并不一定表示错误 - 只有非零退出代码才表示。
      • Burt在PowerShell GitHub存储库中创建了一个问题以讨论此不一致性
    • 默认情况下,由外部实用程序生成的stderr输出会直接传递到控制台 - 它们不会被PowerShell变量赋值或(成功流)输出重定向所捕获。

    • 如上所述,可以更改此设置:

      • 2>&1 作为传递给cmd的命令行的一部分将stdout和stderr组合发送到PowerShell的成功流中,作为字符串,无法区分给定行是stdout还是stderr行。

      • 2>&1作为PowerShell重定向将stderr行也发送到PowerShell的成功流中,但您可以通过它们的数据类型区分stdout和stderr源行:一个[string]类型的行是stdout源行,而[System.Management.Automation.ErrorRecord]类型的行是stderr源行。


1
哦,关于在PowerShell中使用2>&1重定向并过滤类型为[System.Management.Automation.ErrorRecord]的部分很有趣。可惜它非常依赖于重定向发生的位置。这使得向人们解释变得困难,但你已经做得很好了。 - Burt_Harris

1
注意:下面的更新示例应该可以在各种PowerShell主机上使用。GitHub问题原生命令stderr的不一致处理已经被打开以跟踪之前示例中的差异。请注意,由于它依赖于未记录的行为,因此该行为可能会在将来发生更改。在使用必须耐久的解决方案之前,请考虑这一点。

使用管道是正确的做法,你几乎永远不需要使用Invoke-Command。Powershell确实区分stdout和stderr。例如,尝试这个:

cmd /c "echo hi && foo" | set-variable output

"

stdout被连接到set-variable,而标准错误仍会显示在屏幕上。如果您想隐藏并捕获stderr输出,请尝试以下方法:

"
cmd /c "echo hi && foo" 2>$null | set-variable output

"

2>$null 部分是一种未记录的技巧,它会将错误输出附加到 PowerShell $Error 变量作为 ErrorRecord

以下是一个示例,它显示标准输出,同时使用 catch 块捕获 stderr

"
function test-cmd {
    [CmdletBinding()]
    param()

    $ErrorActionPreference = "stop"
    try {
        cmd /c foo 2>$null
    } catch {
        $errorMessage = $_.TargetObject
        Write-warning "`"cmd /c foo`" stderr: $errorMessage"
        Format-list -InputObject $_ -Force | Out-String | Write-Debug
    }
}

test-cmd

生成消息:
WARNING: "cmd /c foo" stderr: 'foo' is not recognized as an internal or external command

如果您启用了调试输出,还将看到抛出的ErrorRecord的详细信息:
DEBUG: 

Exception             : System.Management.Automation.RemoteException: 'foo' is not recognized as an internal or external command,
TargetObject          : 'foo' is not recognized as an internal or external command,
CategoryInfo          : NotSpecified: ('foo' is not re...ternal command,:String) [], RemoteException
FullyQualifiedErrorId : NativeCommandError
ErrorDetails          : 
InvocationInfo        : System.Management.Automation.InvocationInfo
ScriptStackTrace      : at test-cmd, <No file>: line 7
                        at <ScriptBlock>, <No file>: line 1
PipelineIterationInfo : {}
PSMessageDetails      : 

设置$ErrorActionPreference="stop"会导致PowerShell在子进程写入stderr时抛出异常,这似乎是您想要的核心。使用2>$null技巧可以使cmdlet和外部命令的行为非常相似。

一个写入到stderr的外部程序并没有抛出错误,但如果$errorActionPreference被设置为“stop”,那么PowerShell就会将其转换为ErrorRecord并抛出它。这与捕获其他程序内部抛出的错误无关。 - Burt_Harris
更新了示例代码,以更清晰地展示PowerShell如何处理stderr流。 - Burt_Harris
那么为什么我会遇到需要检查lastexitcode的情况,即使我已经将erroractionpreference设置为true呢? - Jeff
2
我猜这是因为应用程序有可能不向stderr写入内容,但仍然返回非成功的退出代码。 - Burt_Harris
@Jeff:你说得对:$ErrorActionPreference 只控制对 非终止错误 的反应,这是 PowerShell 内部的概念。 相比之下,Try / Catch 用于捕获 PowerShell 终止错误 和 _.NET 异常_。 据我所知,像 cmd 这样的外部实用程序 从不 生成 任何一个 错误 - 它们只报告一个 _退出代码_,该代码反映在 $LASTEXITCODE 中。将 stderr 行转换为 PS 错误记录的唯一方法是使用 2>&1 作为 PowerShell 重定向。 换句话说:我认为这个解决方案行不通。如果它确实有效,请告诉我们原因。 - mklement0
显示剩余7条评论

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