Search code examples
powershellhttp-redirectstdoutstderr

PowerShell: Why I can't redirect stdout and stderr of external methods sometimes?


I have issues with redirecting output from non .NET assemblies method calls:

In the code below, you see one successful redirection with .NET class System.Net.Dns and two failed redirections.

One with an inline C# type and the other is a VS compiled .dll, which contains only the same content as the $cs_code codeblock.

My only workaround so far is to catch their output with [Console]::SetOut and [Console]::SetError.

But why do they fail and how can I redirect/capture those stream outputs ?

# .NET Version                   4.7.2
# PSVersion                      5.1.16299.431                                                                                                                                                                                                                     
# PSEdition                      Desktop                                                                                                                                                                                                                           
# PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}                                                                                                                                                                                                           
# BuildVersion                   10.0.16299.431                                                                                                                                                                                                                    
# CLRVersion                     4.0.30319.42000                                                                                                                                                                                                                   
# WSManStackVersion              3.0                                                                                                                                                                                                                               
# PSRemotingProtocolVersion      2.3                                                                                                                                                                                                                               
# SerializationVersion           1.1.0.1    


if ($psISE) { cls }

$cs_code = @"
using System;
static public class demo
{
    static public void go()
    {       
        Console.WriteLine("***Console.WriteLine***");

        Console.Out.WriteLine("***Console.Out.WriteLine***");
        //Console.Out.Flush(); // no effect here

        Console.Error.WriteLine("***Console.Error.WriteLine***"); // no output in ISE !
        //Console.Error.Flush(); // no effect here
    }
}
"@
Add-Type -TypeDefinition $cs_code -Language CSharp

#[Console]::SetOut((New-Object IO.StringWriter))   # this would catch all stdout            
#[Console]::SetError((New-Object IO.StringWriter)) # this would catch all stderr

&{ [demo]::go() } 1> $null 2> $NULL              # no redirection, why ?

# &{ [System.Net.Dns]::Resolve('bla') } 2> $NULL # works as expected

exit 0


Add-Type -AssemblyName 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' `
         -ErrorAction Stop

Add-Type -Path "c:\_ClassLibraryDemo.dll" `
         -ErrorAction Stop

&{ [MyLib.Demo]::Go() } 1> $null 2> $null // no effect here

Solution

  • tl;dr:

    • If you want to capture output from in-process code that produces output via the [Console] API, you must use explicit redirections via [Console]::SetOut() and [Console]::SetError(), the technique you mention in the question.

    • See below for why that is necessary.


    PowerShell only allows capturing / redirecting the standard stdout and stderr streams of external (console) programs, inside which, in the case of .NET-based programs, Console.WriteLine() and Console.Out.WriteLine() write to stdout, and Console.Error.WriteLine() writes to stderr.

    When running in a console window, PowerShell passes an external program's stdout and stderr streams through to the console (screen) by default; by contrast, the ISE sends stderr output to PowerShell's error stream[1].

    > or 1> redirects an external program's stdout (either to a file or to $null to suppress it), 2> redirects stderr[2].
    Additionally, assigning output from an external program to a variable captures its stdout output, and sending an external program's output through the pipeline redirects its stdout output to PowerShell's success output stream.


    By contrast, you're using the [Console] type's output methods in-process, where no such capturing is possible, because such method calls simply output to the same console that PowerShell itself runs in, without PowerShell knowing about it.[3]

    You can cut out the middleman to verify this behavior:

    PS> [Console]::WriteLine('hi')  *> $null # Try to suppress ALL output streams.
    hi  # !! Still prints to the console - PowerShell streams were bypassed.
    

    The only way to (temporarily) redirect [Console] output in-process is to explicitly call .SetOut() and .SetError(), as mentioned in the question.

    The reason that 2> $NULL in &{ [System.Net.Dns]::Resolve('bla') } 2> $NULL does work is that the method throws an exception, which PowerShell outputs to its error stream (stream number 2), whose output 2> $NULL effectively suppresses.
    Note that, because an exception is thrown, 2> $NULL is only effective because the method call is enclosed in & { ... }; otherwise, the exception would terminate the redirection itself too.
    However, with respect to in-process [Console] behavior with no exceptions involved, whether & { ... } is involved or not makes no difference.


    Therefore, in order for your custom C# method to integrate with PowerShell's streams - short of using PowerShell APIs directly in your C# code - do the following:

    • use return for what should go to PowerShell's success stream (stream number 1)

    • throw an exception for what should go to PowerShell's error stream (stream number 2), but note that an unhandled exception will by default abort the enclosing statement as a whole.


    Alternatively, compile your C# code to an external program, say, godemo.exe:

    # With an external executable, redirections work as expected.
    godemo.exe 1> $null 2> $null 
    

    [1] This divergent behavior in the ISE is problematic; it is discussed in this GitHub issue.

    [2] If $ErrorActionPreference = 'Stop' happens to be in effect, any 2> redirection unexpectedly causes a script-terminating error; this problematic behavior is discussed in this GitHub issue.

    [3] The methods write to the current console's stdout and stderr streams, which, in the absence of an external redirection, print to the screen.