【问题标题】:PowerShell Streaming OutputPowerShell 流输出
【发布时间】:2017-11-13 06:23:58
【问题描述】:

我想在 PowerShell 中捕获一些流输出。例如

cmd /c "echo hi && foo"

这个命令应该先打印 hi 然后再炸弹。我知道我可以使用 -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] 的输出流是不可接受的,因为有许多“不喜欢”直接从 PowerShell 进程执行的程序

.NET System.Diagnostics.Process API 让我可以做到这一点......但我不能从流处理程序内部流式输出(因为线程和阻塞 - 虽然我想我可以使用 while 循环和流/清除收集到的输出)

【问题讨论】:

    标签: powershell stream powershell-4.0 piping


    【解决方案1】:

    - 所描述的行为适用于在常规控制台/终端窗口中运行 PowerShell,不涉及远程处理使用远程处理并在ISE,行为与 PSv5.1 不同 - 见底部。
    - Burt's answer 所依赖的 2&gt;$null 行为 - 2&gt;$null 仍在秘密写入 PowerShell 的错误流,因此,在 $ErrorActionPreference Stop 生效的情况下,正在中止 外部实用程序将 anything 写入 stderr 后立即执行该脚本 - 已为 classified as a bug 并且可能会消失。

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

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

    • 相比之下,如果你使用2&gt;&amp;1作为PowerShell重定向,你可以根据输入对象的类型过滤成功流

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

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

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

      Invoke-CommandLine 'cmd /c "echo hi && foo"'
      
    • 传递cmd 命令行的调用和直接调用外部实用程序(如git.exe)之间没有根本区别,但请注意,只能通过@987654340 调用@ 允许通过运算符 &amp;&amp;&amp;|| 使用多个命令,并且只有 cmd 解释 %...% 样式的环境变量引用,除非 你使用--%,停止解析符号。

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

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

      • 功能很容易适配:

        • 在收到第一条 stderr 行后采取行动。
        • 收集单个变量中的所有标准错误行
        • 和/或在终止后,如果收到任何 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/函数调用或表达式中的错误和.NET 异常的反应

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

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

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

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

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

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

        • 2&gt;&amp;1 作为 PowerShell 重定向 也将 stderr 行发送到 PowerShell 的成功流,但是 您可以通过它们的 DATA 区分源自 stdout 和 stderr 的行TYPE[string]-typed 行是源自 stdout 的行,而[System.Management.Automation.ErrorRecord]-typed 行是源自 stderr 的行。

    【讨论】:

    • 哦,关于在 PowerShell w/2&gt;&amp;1 中重定向和过滤 [System.Management.Automation.ErrorRecord] 类型的部分很有趣。太糟糕了,它很大程度上取决于重定向发生的位置。这让人很难向人们解释,但你已经完成了令人钦佩的工作。
    【解决方案2】:

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

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

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

    stdout 被传送到 set-variable,而 std 错误仍然出现在您的屏幕上。如果你想隐藏和捕获stderr 输出,试试这个:

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

    2&gt;$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
    

    如果您在启用调试输出的情况下调用,您还将看到抛出的 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&gt;$null 技巧使 cmdlet 和外部命令的行为非常相似。

    【讨论】:

    • try catch 通常或似乎不适用于其他程序内部抛出的错误。它只是设置 lasterrorcode
    • 写入 stderr 的外部程序不会抛出错误,但如果 $errorActionPreference 设置为“stop”,则 PowerShell 会将其转换为 ErrorRecord 并抛出错误。它与捕获内部抛出给其他程序的错误无关。
    • 更新了示例代码,使 PowerShell 如何处理标准错误流更加清晰。
    • 那为什么我会遇到需要检查 lastexitcode 的情况,尽管我的 erroractionpreference 为 true
    • 我猜是因为应用程序可能不会写入 stderr 但仍返回不成功的退出代码。