3

I have a C# class library that provides a number of interfaces that can be called from PowerShell scripts (.PS1) and Advanced Modules (.PSM1). I have a static method to write verbose and debug messages to the console using the System.Console class:

public class Write
{

        public static void Verbose(string msg, string source)
        {

            if (Config.EnableVerbose)
            {

                ConsoleColor originalForeGroundColor = Console.ForegroundColor;
                ConsoleColor originalBackGroundColor = Console.BackgroundColor;
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.BackgroundColor = ConsoleColor.Black;
                Console.Write("VERBOSE: {0} {1}{2}", source, msg, Environment.NewLine);
                Console.ForegroundColor = originalForeGroundColor;
                Console.BackgroundColor = originalBackGroundColor;

            }

       }

}

However, when those messages are displayed in a PowerShell console, they cannot be captured using redirection, like with Out-File, >&0 or even with Start-Transcript.

I have read about_Redirection, and using the redirect modifiers does not capture the console output. For instance, using a PowerShell Advanced Function (aka Cmdlet) I have written:

Get-CommandTrace -ScriptBlock { Get-Resource } *> C:\Temp\capture.log

The Get-CommandTrace Cmdlet sets the $VerbosePreference = 'Continue' during the ScriptBlock execution, and does capture the verbose from Get-Resource output there. But does not capture the Console output from my C# library.

So, my question is simply: Can a C# class that is not a Cmdlet class, nor inherited class, be able to write output to the existing PowerShell runspace it is being called from?

2 Answers 2

2

Note:

  • This is not a complete answer, because it has severe limitations - though it may work for specific use cases.

  • The original form of this answer used since-deprecated PowerShell SDK method .CreateNestedPipeline(), which cannot be used anymore if you're writing your code against the PowerShellStandard library for cross-platform and cross-edition compatibility (code that should run in both Windows PowerShell and PowerShell Core, on all supported platforms).
    Chris (the OP) himself found a compatible alternative, which the current form of this answer is based on.


The challenge is to write to the invoking pipeline's output streams (as you've observed writing via Console is unrelated to PowerShell's output streams and prints directly to the console, with no ability to capture or redirect such output in PowerShell).

While you can obtain a reference to the invoking runspace, there is no way I know of to obtain a reference to the running pipeline.

Using the invoking runspace you can write to PowerShell's output streams via a new pipeline, but that comes with severe limitations:

  • You cannot directly write to the caller's success output stream (1) that way; that is, while you can call cmdlets that target the other streams, such as Write-Verbose, Write-Output / implicit output does not work.

  • Capturing the output from this nested pipeline in a variable or sending it through the pipeline requires enclosing the method call in (...) (or $(...) or @(...)) in addition to applying the appropriate redirection to the success output stream (e.g., 4>&1 for the verbose stream).

See the code comments for details.

Add-Type -TypeDefinition @'
  using System.Management.Automation;

  public class Write
  {

      public static void Verbose(string msg)
      {
        using (PowerShell ps = PowerShell.Create(RunspaceMode.CurrentRunspace)) {
          // IMPORTANT: Use .AddScript(), not .AddCommand().
          //            Even though .AddCommand() + .AddParameter() is arguably a cleaner way to
          //            formulate the command, it results in output that cannot be captured.
          //            As noted, Write-Output / success-stream output does NOT work.
          ps.AddScript("Write-Verbose '" + msg.Replace("'", "''") + "'").Invoke();
        }
      }

  }
'@

#"

$VerbosePreference = 'Continue'
    
# Regular output to the verbose stream.
Write-Verbose 'msg1'

# Verbose output via the custom type.
[Write]::Verbose('msg2')

# SUPPRESSING and REDIRECTING TO A FILE work.
[Write]::Verbose('msg3') 4> $null
[Write]::Verbose('msg4') 4> t.txt

# By default, REDIRECTING TO THE STANDARD OUTPUT STREAM (1) 
# works only for the OUTSIDE, i.e. for CALLERS of this script.
[Write]::Verbose('msg5') 4>&1

# Redirecting to the standard output stream (1) can also be used to:
#  * CAPTURE the result INSIDE your script
#  * SEND THE RESULT THROUGH THE PIPELINE, 
# additionally invoke the method call enclosed in (...) or $(...) or @(...)
$out = ([Write]::Verbose('msg6') 4>&1); "captured: [$out]"
([Write]::Verbose('msg7') 4>&1) | ForEach-Object { "piped: [$_]" }
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks. I stumbled across some code that actually showed me nested pipelines, as the Runspace class didn't have a method to attach to an existing one. Only to create a new or new nested pipeline. Attempting to use CreateNewPipeline method resulted in an error, which was expected. I'll have to experiment with your guidance.
Even though you stated your answer is incomplete, it does work for my needs. I do wish there was a way or method to attach to an existing PowerShell session/pipeline. But I marked your response as an acceptable answer. Thank you for your guidance.
0

While the answer above from mklement0 is a good one, it will not work if you attempt to use it with PowerShellCore or targeting .NetStandard 2.0 as the CreateNestedPipeline API is deprecated. (See this thread on the PowerShellStandard GitHub repo.)

So, instead, I have this working code:

Add-Type -TypeDefinition @'
using System.Management.Automation;
using System.Management.Automation.Runspaces;

    public class Write
    {

        public static void Verbose(string msg)
        {

            using (PowerShell initialPowerShell = PowerShell.Create(RunspaceMode.CurrentRunspace))
            {

                initialPowerShell.Commands.AddScript("Write-Verbose " + msg.Replace("\"", "\"\"") + "\" -v");
                initialPowerShell.Invoke();

            }

        }

    }
'@

$VerbosePreference = 'Continue'

# Regular output to the verbose stream.
Write-Verbose 'msg1'

# Verbose output via the custom type.
# !! This can NOT be redirected from the outside.
[Write]::Verbose('msg2')

# !! SUPPRESSING or REDIRECTING TO A FILE only works
# !! when DIRECTLY APPLIED to the method call.
[Write]::Verbose('msg3') 4> $null
[Write]::Verbose('msg4') 4> t.txt

# !! REDIRECTING TO THE STANDARD OUTPUT STREAM (1) for the OUTSIDE works,
# !! but obviously it then merges with success output.
[Write]::Verbose('msg5') 4>&1

# !! To REDIRECT TO THE STANDARD OUTPUT STREAM (1) and capture the result
# !! INSIDE your script, invoke the method call in  (...) or $(...) or @(...)
$out = ([Write]::Verbose('msg6') 4>&1)
"[$out]"

Which works with PowerShell 5.1 for Windows, PowerShell Core 6.2.2 for Windows and Linux (Ubuntu/Debian).

I will still leave mklement0 reply marked as the answer to the original question. I'm just adding another one based on research I had compiled over the past few days on migrating my class library to .NetStandard 2.0.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.