2026-01-17, Matěj Kafka (#powershell, #windows, #csharp, #logging)
A common recommendation for writing binary PowerShell cmdlets is to keep your core logic outside the cmdlets themselves. The PowerShell .NET API supports calling .NET from PowerShell through cmdlets and calling back into PowerShell through scriptblocks, but calling cmdlets from other cmdlets is not directly supported – once you cross the boundary from PowerShell to C# (or F#), you're mostly limited to non-cmdlet .NET types. If your core logic lives inside a cmdlet, reusing it from another cmdlet is quite difficult.
One of the great features of PowerShell is its logging system with caller-controlled verbosity (.WriteWarning(), .WriteVerbose(),...). These logs help users understand failures and help you debug issues. However, while attempting to keep core logic separate from cmdlets, you'll eventually hit a snag: you're several method calls deep, outside the cmdlet context, and you want to log something.
Sometimes you can restructure your code to keep logging in the cmdlet, but that's not always feasible. For example, a library for parsing a specific file format might want to log debug information about the loading process. An obvious workaround is passing a logging callback to library methods, but this often requires threading the callback through multiple layers, frequently resulting in just skipping the log instead.
I've used both approaches in my PowerShell projects, but always felt there must be a better way. Recently, I had an idea: all PowerShell code runs in the context of a runspace, and the current thread's runspace is always available via Runspace.DefaultRunspace. The executing cmdlet must be stored somewhere for features like cancellation (.StopProcessing()) to work, so we should be able to extract the current cmdlet from the runspace and call its logging methods from anywhere in the call stack below the cmdlet.
Looking through the Runspace type, I found that through the ExecutionContext property, we can access CurrentCommandProcessor, which contains a CommandRuntime — the object that Cmdlet delegates to for logging. Based on this, I put together the following utility class with logging methods you can call from anywhere, as long as you were invoked from PowerShell.
Note that this relies on reflection over internal PowerShell APIs, so it might break in future versions. However, the failure mode is just silent log loss, and these properties have been stable across PowerShell versions so far. If you'd prefer to reliably show the log, even if not through the PowerShell log API, you can add a fallback to Console.WriteLine().
using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
public static class Log {
public static void Warn(string message) => GetCommandRuntime()?.WriteWarning(message);
public static void Verbose(string message) => GetCommandRuntime()?.WriteVerbose(message);
public static void Debug(string message) => GetCommandRuntime()?.WriteDebug(message);
public static void Info(object messageData, string[]? tags = null) {
// for cmdlets, `source` is typically the cmdlet path/name, but null is also a valid value
var info = new InformationRecord(messageData, null);
if (tags != null) {
info.Tags.AddRange(tags);
}
GetCommandRuntime()?.WriteInformation(info);
}
public static void Host(string message, bool noNewline = false,
ConsoleColor? foregroundColor = null, ConsoleColor? backgroundColor = null) {
// information messages tagged PSHOST are treated specially by the host
// would be nice if this was documented somewhere, eh?
Info(new HostInformationMessage {
Message = message, NoNewLine = noNewline, ForegroundColor = foregroundColor, BackgroundColor = backgroundColor,
}, ["PSHOST"]);
}
// uses a bunch of reflection to pull out the executing command runtime from the current runspace; I don't think these
// properties are likely to change, but even if they do, the result will just be a silent loss of the logs
private static ICommandRuntime2? GetCommandRuntime() {
// this thread-local property always reflects the runspace of the currently executing cmdlet
var runspace = Runspace.DefaultRunspace;
var ec = ExecutionContextProp?.GetValue(runspace);
var cmdProcessor = CurrentCommandProcessorProp?.GetValue(ec);
// `.CommandRuntime` is set for both cmdlets and non-advanced functions/scripts, unlike `.Cmdlet`
var cmdRuntime = CommandRuntimeProp?.GetValue(cmdProcessor);
return cmdRuntime as ICommandRuntime2;
}
private const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
private static readonly PropertyInfo? ExecutionContextProp =
typeof(Runspace).GetProperty("ExecutionContext", Flags);
private static readonly PropertyInfo? CurrentCommandProcessorProp =
ExecutionContextProp?.PropertyType.GetProperty("CurrentCommandProcessor", Flags);
private static readonly PropertyInfo? CommandRuntimeProp =
CurrentCommandProcessorProp?.PropertyType.GetProperty("CommandRuntime", Flags);
}