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?
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 I 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 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
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.
ps.AddScript("Write-Output '" + 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
# To REDIRECT TO THE STANDARD OUTPUT STREAM (1) in order 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: [$_]" }
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.