2

My application should write it's errors as literal JSON objects on stderr. This is proving difficult with PowerShell (5, 6 or 7) since PowerShell seems to want to prevent you from writing to stderr and, if you do succeed, it changes what you write.

In all examples we are running the following from within a powershell/pwsh console:

./test.ps1 2> out.json

test.ps1

Write-Error '{"code": "foo"}'

out.json


[91mWrite-Error: [91m{"code": "foo"}[0m

PowerShell is changing my stderr output. Bad PowerShell.

test.ps1

$Host.UI.WriteErrorLine('{"code": "foo"}')

out.json

PowerShell not writing to stderr (or >2 is not capturing it). Bad PowerShell.

test.ps1

[Console]::Error.WriteLine('{"code": "foo"}')

out.json

PowerShell not writing to stderr (or >2 is not capturing it). Bad PowerShell.

Update

I now understand that PowerShell does not have a stderr but rather numbered streams of which 2 corresponds to Write-Error and [Console]::Error.WriteLine() output and is sent to stderr of the pwsh/powershell.exe process IFF that output is redirected.

In short, stderr only exists outside of powershell and you can only access it via redirection: pwsh ./test.ps1 2> out.json

Inside of powershell you can only redirect 2> the output from Write-Error. [Console]::Error.WriteLine() is not captured internally but sent to the console.

Marc
  • 13,011
  • 11
  • 78
  • 98

1 Answers1

3

Problem

When you write to the error stream, Powershell creates an ErrorRecord object for each message. When you redirect the error stream and output it, Powershell formats it like an error message by default. The sub strings like [91m are ANSI escape sequences that colorize the message when written to the console.

Solution

To output plain text messages, convert the error records to strings before redirecting them to the file:

./test.ps1 2>&1 | ForEach-Object { 
    if( $_ -is [System.Management.Automation.ErrorRecord] ) { 
        # Message from the error stream 
        # -> convert error message to plain text and redirect (append) to file

        "$_" >> out.json
    }
    else {
        # Message from the success stream
        # -> already a String, so output it directly
        
        $_    # Shortcut for Write-Output $_
    }
}

Remarks:

  • 2>&1 merges the error stream with the success stream, so we can process both by the pipeline.
  • $_ is the current object processed by ForEach-Object. It is of type ErrorRecord, when the message is from the error stream of "test.ps1". It is of type String, when the message is from the success stream of "test.ps1".
  • Using the -is operator we check the type of the message to handle messages originating from the error stream differently than those from the success stream.
  • "$_" uses string interpolation to convert the ErrorRecord to the plain text message.
  • The >> operator redirects to the given file, but appends instead of overwriting.

Bonus code - a reusable cmdlet

If we regularly need to redirect error streams as plain text to a file, it makes sense to wrap the whole thing in a reusable cmdlet:

Function Out-ErrorMessageToFile {
    [CmdletBinding()]
    param (
        [Parameter( Mandatory                    )] [String]   $FilePath,
        [Parameter( Mandatory, ValueFromPipeline )] [PSObject] $InputObject,
        [Parameter(                              )] [Switch]   $Append
    )

    begin {
        if( ! $Append ) {
            $null > $FilePath   # Create / clear the file
        }
    }

    process {
        if( $InputObject -is [System.Management.Automation.ErrorRecord] ) { 
            # Message from the error stream 
            # -> convert error message to plain text and redirect (append) to file
            
            "$InputObject" >> $FilePath
        }
        else {
            # Message from the success stream
            # -> already a String, so output it directly
            
            $InputObject  # Shortcut for Write-Output $InputObject
        }
    }
}

Usage examples:

# Overwrite "out.json"
./test.ps1 2>&1 | Out-ErrorMessageToFile out.json

# Append to "out.json"
./test.ps1 2>&1 | Out-ErrorMessageToFile out.json -Append
zett42
  • 25,437
  • 3
  • 35
  • 72
  • Unfortunately this solution captures stdout to out.json as well. – Marc Dec 28 '20 at 18:42
  • @Marc Fixed. I've also changed `"$_"` to `Write-Output "$_"` just to make the code a little bit more readable. They are synonymous. – zett42 Dec 28 '20 at 19:34
  • Good but again stdout is suppressed. – Marc Dec 29 '20 at 09:45
  • @Marc I've added an `else` branch so the success stream is no longer discarded. – zett42 Dec 29 '20 at 11:04
  • @mklement0 Good point. Personally I prefer to use `Write-Output` for variables as it documents intent and can be easily "scanned" for by the human mind. When I have to debug complex scripts from other people (or my own half a year later), it can be hard to know if a single variable is really meant to be output or if the author just forgot an assignment, for example. For succinct one-liners or if the context already makes the intention clear (e. g. `$var | cmdlet`), I'm Ok with the implicit output behaviour. – zett42 Dec 29 '20 at 13:35
  • 1
    @zett42. Good point re complex scripts. A compromise that doesn't introduce processing overhead (though that will often not matter in practice) is to accompany each intentional implicit output statement with a short comment, similar to what you've done, only more concise; e.g., `$InputObject # output`. Implicit output is a powerful concept, especially in statements such as `$collected = foreach ($i in 1..10) { $i }`, hence my personal preference to showcase it here on SO. – mklement0 Dec 29 '20 at 13:48
  • `{"foo";write-error "bar"}.Invoke() 2>&1 | Out-ErrorMessageToFile out.json` leaves out.json empty. – Marc Dec 30 '20 at 10:28
  • 1
    @Marc This doesn't work, because `Invoke()` only returns the output (success stream). It works when done in the PowerShell way: `& {"foo";write-error "bar"} 2>&1 | Out-ErrorMessageToFile out.json` – zett42 Dec 30 '20 at 18:52
  • PowerShell is Weird – Marc Jan 01 '21 at 17:46
  • This answer almost works - I created a basic python file in which I just print to stdout and stderr 10 times in a loop. I would expect alternating outputs, but as a matter of fact I get stderr output to the console in real time and then stdout is placed in one step at the end. I would expect both streams to be output simultaneously to both console and the file. – ashrasmun Dec 24 '21 at 10:26
  • @ashrasmun This is propably due to buffering. Many programs output immediately when writing directly to the console but buffer output when redirected. Try to explicitly flush stdout stream in Python after each line or disable buffering (I don't know the exact possibilities in Python). – zett42 Dec 24 '21 at 21:47
  • 1
    @zett42 You are right. This can be done with python by using `python -u`. Although this feature seems to be a bit bugged, as the messages do not always appear as they are printed in the program. I created a program with 1. print to stdout 2. print to stderr 3. 5x loop of 1 and 2. Then, the output was err out out err out err out err... Which was slightly unexpected, but at least everything was redirected in real time. Quick reminder - python's StreamHandler uses stderr by default. – ashrasmun Dec 25 '21 at 22:37