Search code examples
powershellerror-handlingstderr

How can I redirect stdout and stderr without polluting PowerShell error output


Problem:

I am trying to run a command via PowerShell and capture its stdout and stderr without printing them on screen (command is incredibly noisy and pollutes the console).

I want to capture the stdout and stderr in a variable and then throw an exception if particular strings are found.

My logic seems to be working and I can make the cmdlet fail/pass when I expect it to, however the output does not match what I expect, instead of returning the error message that I am specifying I get what I believe is the stderr from the command instead?

My code:

(Simplified for easier reading)
First cmdlet:

function Test-Validation
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 0)]
        [Array]
        $ValidExitCodes = @(0),

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 1)]
        [bool]
        $FailOnWarning = $true
    )
    $Validation = . pdk validate 2>&1

    if ($LASTEXITCODE -notin $ValidExitCodes)
    {
        throw "Module validation has failed. Exit code: $($LASTEXITCODE)."
    }
    $Warnings = $Validation -match 'pdk \(WARNING\)'
    if (($Warnings) -and ($FailOnWarning -eq $true))
    {
        throw "Module validation contains warnings.`n$Warnings"
    }
    Write-Verbose "Module successfully passed validation"
}

Which is called by a second cmdlet:

function Test-Module
{
    [CmdletBinding()]
    param
    ()

    try
    {
      Test-Validation
    }
    catch
    {
      throw "$($_.Exception.Message)"
    }
    try
    {
      Test-Unit
    }
    catch
    {
      throw "$($_.Exception.Message)"
    }
}

Expected output

PS /opt/script/test/> Test-Module
Exception: /opt/script/test/Test-Module.ps1:13:9
Line |
  13 |          throw "$($_.Exception.Message)"
     |          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Module validation has failed. Exit code: 1.

Actual output

PS /opt/script/test/> Test-Module
Exception: /opt/script/test/Test-Module.ps1:13:9
Line |
  13 |          throw "$($_.Exception.Message)"
     |          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | pdk (INFO): Using Ruby 2.5.8 

As you can see it seems to be returning the output from the command I'm running (pdk) instead of the "Module validation has failed. Exit code: $($LASTEXITCODE)." message I am defining in the Test-Validation cmdlet.

Why am I not getting the error message I want and is there any way for me the achieve what I'm looking for? Alongside any code suggestions I would very much appreciate any further readings around my issues to so I can better understand these things.

N.B.: This is being run by PoSh Core on MacOS


Solution

  • Preface:

    • The answer below applies to console / terminals, including Visual Studio Code' integrated terminal. In these PowerShell hosts, the problem at hand is fixed in PowerShell v7.2+.

    • However, the behavior persists in the context of remoting and background jobs - see GitHub issue #3996 for a discussion.


    Your symptom implies that $ErrorActionPreference = 'Stop' is in effect at the time function
    Test-Validation executes. (Temporarily) set it to 'Continue' to fix your problem - which in PowerShell 7.2+ is no longer required (see below).

    The reason for the observed behavior is that, in Windows PowerShell and in PowerShell (Core) 7 up to v7.1.x, using an error-stream redirection (2>) makes PowerShell route an external program's stderr output through PowerShell's error stream (see about_Redirection), and $ErrorActionPreference = 'Stop' therefore throws a script-terminating error once the first stderr line is received.

    This behavior is unfortunate, because stderr output from external programs cannot be assumed to represent an error condition, given that external programs in effect use stderr, the standard error stream, for anything that other than data, which includes status information, for instance.

    PowerShell 7.2 and above change this behavior for the better: stderr output is no longer routed through PowerShell's error stream, which means that: