PowerShell 流式输出

PowerShell Streaming Output

我想在 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}

我能得到的最接近的是使用管道,但这并不能让我区分 stdout 和 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] 的输出流是不可接受的,因为有许多程序 "don't like" 直接从 PowerShell 进程执行

.NET System.Diagnostics.Process API 允许我这样做...但我无法从流处理程序内部流式传输输出(因为线程和阻塞 - 虽然我想我可以使用 while 循环和 stream/clear 收集的输出)

注意:下面的更新示例 应该 现在可以跨 PowerShell 主机工作。 GitHub 问题 Inconsistent handling of native command stderr 已经打开以跟踪前面示例中的差异。但是请注意,由于它取决于未记录的行为,因此该行为将来可能会发生变化。在必须耐用的解决方案中使用它之前,请考虑到这一点。

您在使用管道方面走在正确的轨道上,您可能几乎不需要 Invoke-Command。 Powershell 确实区分标准输出和标准错误。例如试试这个:

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

stdout 被传送到设置变量,而标准错误仍然出现在您的屏幕上。如果你想隐藏和捕获 stderr 输出,试试这个:

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

2>$null 部分是一个未记录的技巧,导致错误输出作为 ErrorRecord.

附加到 PowerShell $Error 变量

这是一个显示标准输出的示例,同时用 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

如果您在启用调试输出的情况下调用,您还会看到抛出的错误记录的详细信息:

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 和外部命令的行为非常相似。

- 描述的行为适用于 运行 PowerShell 常规 console/terminal windows 不涉及远程处理使用远程处理和在 ISE 中,行为与 PSv5.1 不同 - 见底部。
- 2>$null 行为 relies on - 2>$null secretly still writing to PowerShell's error stream and therefore, with $ErrorActionPreference Stop in effect, aborting the script as soon as an external utility writes anything to stderr - has been classified as a bug 并且可能会消失。

  • 当 PowerShell 调用诸如 cmd 之类的外部实用程序时,其 stderr 输出默认直接通过。也就是说,stderr 输出直接打印到控制台,而不包含在捕获的输出中(无论是通过分配给变量还是重定向到文件)。

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

  • 相比之下,如果您使用 2>&1 作为 PowerShell 重定向,您可以根据输入对象的类型:

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

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

  • 请注意,cmd /c 部分不是内置的,因此您可以按如下方式调用它,例如:

    Invoke-CommandLine 'cmd /c "echo hi && foo"'
    
  • 通过调用 cmd 命令行和直接调用外部实用程序(例如 git.exe,但请注意,只有通过 cmd 调用才允许通过运算符 &&&|| 使用 多个 命令,并且只有cmd解释%...%风格的环境变量引用,除非你使用--%,停止解析符号。

  • Invoke-CommandLine 输出接收到的 stdout 和 stderr 行,因此您可以在管道中使用该函数。

    • 如所写,stderr 行在接收时使用 Write-Error 写入 PowerShell 的错误流,带有 单个通用 异常在外部命令终止后被抛出,它应该报告一个非零 $LASTEXITCODE.

    • 适配函数很简单:

      • 在收到第一行 stderr 后采取行动。
      • 收集单个变量中的所有stderr行
      • and/or,终止后,如果收到 any stderr 输入,即使 $LASTEXITCODE 报告 0.
  • Invoke-CommandLine 使用 Invoke-Expression,所以通常的警告适用:确保你知道你传递的是什么命令行,因为它会按原样执行,不不管它包含什么。


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异常.

  • 在不涉及远程处理的常规控制台 window 中 cmd 等外部实用程序当前 从不生成任一个错误 - 他们所做的只是报告一个退出代码,PowerShell 在 自动变量 $LASTEXITCODE 中反映,自动变量 $? 在退出代码非零时反映 $False .

    • 注意:在控制台主机以外的主机中行为根本不同这一事实 - 包括 Windows ISE 和涉及远程处理时 - 是有问题的:调用外部实用程序导致 stderr 输出被视为已报告 非终止错误;具体来说:

      • 每个 stderr 输出行都作为错误记录输出,并记录在自动 $Error 集合中。
      • addition$? 中设置为 $false 且退出代码为非零,任何 stderr 输出的存在 also 将其设置为 $False.
      • 这种行为是有问题的,因为 stderr 输出本身并不一定表示 错误 - 只有 非零退出代码 是。
      • Burt has created an issue in the PowerShell GitHub repository to discuss this inconsistency.
    • 默认情况下,外部实用程序生成的标准错误输出直接传递到控制台 - 它们 不是 由 PowerShell 变量赋值或(成功流)输出重定向捕获。

    • 如上所述,可以更改为:

      • 2>&1 作为命令行的一部分传递给 cmd 发送标准输出和 stderr combined 到 PowerShell 的成功流,作为 strings,无法区分给定行是 stdout 还是 stderr 行。

      • 2>&1 作为 PowerShell 重定向 也将标准错误行发送到 PowerShell 的成功流,但是 你可以通过数据类型 来区分源自 stdout 和 stderr 的行:[string] 类型的行是源自 stdout 的行,而 [System.Management.Automation.ErrorRecord] 类型的行line 是一个 stderr 起源的。