Search code examples
powershellstreampowershell-4.0piping

PowerShell Streaming Output


I'd like to capture some streaming output in PowerShell. For example

cmd /c "echo hi && foo"

This command should print hi and then bomb. I know that I can use -ErrorVariable:

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

however there is an issue: in the case of long running commands, I want to stream the output, not capture it and only get the stderr/stdout output at the end of the command

Ideally, I'd like to be able to split stderr and stdout and pipe to two different streams - and pipe the stdout back to the caller, but be prepared to throw stderr in the event of an error. Something like

$stdErr
Invoke-Command "cmd" "/c `"echo hi && foo`"" `
  -OutStream (Get-Command Write-Output) `
  -ErrorAction {
    $stdErr += "`n$_"
    Write-Error $_
  }

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

the closest I can get is using piping, but that doesn't let me discriminate between stdout and stderr so I end up throwing the entire output stream

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())" 
    }
}

Common usage:

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

Note that an analogous version that just uses a ScriptBlock (and checking the output stream for [ErrorRecord]s isn't acceptable because there are many programs that "don't like" being executed directly from the PowerShell process

The .NET System.Diagnostics.Process API lets me do this...but I can't stream output from inside the stream handlers (because of the threading and blocking - though I guess I could use a while loop and stream/clear the collected output as it comes in)


Solution

  • Note: updated sample below should now work across PowerShell hosts. GitHub issue Inconsistent handling of native command stderr has been opened to track the discrepancy in previous example. Note however that as it depends on undocumented behavior, the behavior may change in the future. Take this into consideration before using it in a solution that must be durable.

    You are on the right track with using pipes, you probably don't need Invoke-Command, almost ever. Powershell DOES distinguish between stdout and stderr. Try this for example:

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

    The stdout is piped on to set-variable, while std error still appears on your screen. If you want to hide and capture the stderr output, try this:

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

    The 2>$null part is an undocumented trick that results in the error output getting appended to the PowerShell $Error variable as an ErrorRecord.

    Here's an example that displays stdout, while trapping stderr with an catch block:

    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
    

    Generates the message:

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

    If you invoke with debug output enabled, you'll alsow see the details of the thrown 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      : 
    

    Setting $ErrorActionPreference="stop" causes PowerShell to throw an exception when the child process writes to stderr, which sounds like it's the core of what you want. This 2>$null trick makes the cmdlet and external command behavior very similar.