Last active
August 17, 2026 04:25
-
-
Save jborean93/4c48bc95a3d9df26f796ef094b2cf56e to your computer and use it in GitHub Desktop.
Check to see if the output is being captured.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright: (c) 2026, Jordan Borean (@jborean93) <jborean93@gmail.com> | |
| # MIT License (see LICENSE or https://opensource.org/licenses/MIT) | |
| function Test-IsOutputCaptured { | |
| <# | |
| .SYNOPSIS | |
| Tests if output is being captured by PowerShell or not. | |
| .DESCRIPTION | |
| This is used to see if the output is being captured or piped in any way | |
| other than the default '| Out-Default' that PowerShell has. It replicates | |
| the same logic that the NativeCommandProcessor does to determine whether to | |
| redirect the stdout pipe of the subprocess it spawns or not. | |
| .EXAMPLE | |
| Test-IsOutputCaptured | |
| # False | |
| & { Test-IsOutputCaptured } | |
| # False | |
| This shows how the output is not considered to be captured when there is no | |
| subsequent pipelines, variables, or redirection involved in the statement. | |
| .EXAMPLE | |
| Test-IsOutputCaptured | Out-String | |
| # True | |
| $var = Test-IsOutputCaptured | |
| $var | |
| # True | |
| $var = & { Test-IsOutputCaptured } | |
| $var | |
| # True | |
| & { Test-IsOutputCaptured } | Out-String | |
| # True | |
| This shows the various ways in which the output is considered to be | |
| captured. Note it takes into account any downstream pipelining or | |
| variables and not just the current statement. | |
| .NOTES | |
| This uses reflection to access non-public APIs. PowerShell does not expose | |
| a public API to determine this. This may break in future PowerShell | |
| versions if the API ever changes | |
| #> | |
| [OutputType([bool])] | |
| [CmdletBinding()] | |
| param () | |
| # If you truly wish to use this logic, extract this out into your function | |
| # and use as you wish. | |
| try { | |
| $instancePrivate = [System.Reflection.BindingFlags]'Instance, NonPublic' | |
| $staticPrivate = [System.Reflection.BindingFlags]'NonPublic, Static' | |
| # First check can use public APIs, we check whether we are the last | |
| # command in the current pipeline. If we aren't then the output is | |
| # being piped/captured into the downstream command. | |
| $pipelinePosition = $MyInvocation.PipelinePosition | |
| $pipelineLength = $MyInvocation.PipelineLength | |
| Write-Verbose "PipelinePosition [$pipelinePosition] - PipelineLength [$pipelineLength]" | |
| if ($pipelinePosition -ne $pipelineLength) { | |
| return $true | |
| } | |
| # Check if we are running in a PSRemoting session, this is treated as | |
| # always being captured due to output going over the PSRemoting output | |
| # payload and handled client side. | |
| $nativeCommandProcessorType = [PSObject].Assembly.GetType( | |
| 'System.Management.Automation.NativeCommandProcessor') | |
| $isServerSideProp = $nativeCommandProcessorType.GetProperty( | |
| 'IsServerSide', | |
| $staticPrivate) | |
| if (-not $isServerSideProp) { | |
| throw "Failed to find NativeCommandProcessor.IsServerSide property" | |
| } | |
| $isServerSide = $isServerSideProp.GetValue($null) | |
| Write-Verbose "NativeCommandProcess.IsServerSide [$isServerSide]" | |
| if ($isServerSide) { | |
| return $true | |
| } | |
| # Next check sees what the DownstreamCmdlet of the OutputPipe is set | |
| # to. This is not a public API and is based on the logic in | |
| # NativeCommandProcessor.IsDownstreamOutDefault. | |
| $cmdRuntime = $PSCmdlet.CommandRuntime | |
| $outputPipeProp = $cmdRuntime.GetType().GetProperty('OutputPipe', $instancePrivate) | |
| if (-not $outputPipeProp) { | |
| throw "Failed to find `$PSCmdlet.CommandRuntime OutputPipe property" | |
| } | |
| $outputPipe = $outputPipeProp.GetValue($cmdRuntime) | |
| $downstreamCmdletProp = $outputPipe.GetType().GetProperty('DownstreamCmdlet', $instancePrivate) | |
| if (-not $downstreamCmdletProp) { | |
| throw "Failed to find OutputPipe DownstreamCmdlet property" | |
| } | |
| $downstreamCmdlet = $downstreamCmdletProp.GetValue($outputPipe) | |
| # Now we have the DownstreamCmdlet we do a few checks. First sees if | |
| # there is a downstream cmdlet. No downstream cmdlet means something | |
| # is either capturing the output or it is redirected elsewhere. | |
| if (-not $downstreamCmdlet) { | |
| Write-Verbose "No downstream cmdlet detected, output is being stored in var or redirected" | |
| return $true | |
| } | |
| # If there is a downstream cmdlet it means that somewhere down the | |
| # chain this is being piped to something, | |
| # e.g. & { Test-IsOutputCaptured } | ... | |
| # We expect the implicit '| Out-Default' done by PowerShell that is | |
| # always present, otherwise anything else is treated as being captured. | |
| $cmdInfoProp = $downstreamCmdlet.GetType().GetProperty('CommandInfo', $instancePrivate) | |
| if (-not $cmdInfoProp) { | |
| throw "Failed to find DownstreamCmdlet CommandInfo property" | |
| } | |
| $cmdProp = $downstreamCmdlet.GetType().GetProperty('Command', $instancePrivate) | |
| if (-not $cmdProp) { | |
| throw "Failed to find DownstreamCmdlet Command property" | |
| } | |
| # PowerShell also checks whether transcription is enabled and treats | |
| # that as output being captured. | |
| $cmdInfo = $cmdInfoProp.GetValue($downstreamCmdlet) | |
| $cmd = $cmdProp.GetValue($downstreamCmdlet) | |
| Write-Verbose "Downstream Cmdlet Name [$($cmdInfo.Name)]" | |
| Write-Verbose "Downstream Cmdlet Parameters Set [$($cmd.MyInvocation.BoundParameters.Keys -join ", ")]" | |
| return $cmdInfo.Name -ne 'Out-Default' -or | |
| $cmd.MyInvocation.BoundParameters.ContainsKey('Transcript') | |
| } | |
| catch { | |
| $PSCmdlet.ThrowTerminatingError($_) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment