Search code examples
powershellproxycommandtee

Proxy command that tees


For a PowerShell feature request (whether this is a good idea or not), I tried to create a prototype using a proxy command named Write-Table based on the Format-Table cmdlet that directly writes to the host and depending on the PassThru switch should pass the current input object:

function Write-Table {
    [CmdletBinding(HelpUri='https://go.microsoft.com/fwlink/?LinkID=2096703')]
    param(
        [switch]
        ${AutoSize},
    
        [switch]
        ${RepeatHeader},
    
        [switch]
        ${HideTableHeaders},
    
        [switch]
        ${Wrap},
    
        [Parameter(Position=0)]
        [System.Object[]]
        ${Property},
    
        [System.Object]
        ${GroupBy},
    
        [string]
        ${View},
    
        [switch]
        ${ShowError},
    
        [switch]
        ${DisplayError},
    
        [switch]
        ${Force},
    
        [ValidateSet('CoreOnly','EnumOnly','Both')]
        [string]
        ${Expand},
    
        [Parameter(ValueFromPipeline=$true)]
        [psobject]
        ${InputObject},
    
        [switch]
        ${PassThru})
    
    begin
    {
        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
            {
                $PSBoundParameters['OutBuffer'] = 1
            }
    
            if ($PSBoundParameters.TryGetValue('PassThru', [ref]$outBuffer))
            {
                $Null = $PSBoundParameters.Remove('PassThru')
            }
    
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Format-Table', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = {& $wrappedCmd @PSBoundParameters }
    
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        } catch {
            throw
        }
    }
    
    process
    {
        try {
            if ($PassThru) { $_ }
            $steppablePipeline.Process($_)
        } catch {
            throw
        }
    }
    
    end
    {
        try {
            $steppablePipeline.End()
        } catch {
            throw
        }
    }
    
    clean
    {
        if ($null -ne $steppablePipeline) {
            $steppablePipeline.Clean()
        }
    }
    <#
    
    .ForwardHelpTargetName Microsoft.PowerShell.Utility\Format-Table
    .ForwardHelpCategory Cmdlet
    
    #>
}

But somehow it appears that the current object is removed from the pipeline before it is also passed to the host (I have no clue how to prevent that)

gci *.txt | Write-Table -PassThru | Get-Item # Final intention: ... | Remove-Item

    Directory: C:\Users\Gebruiker\Downloads\temp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---           7/20/2023  3:44 PM              0 file1.txt
Get-Item: Cannot find path 'Microsoft.PowerShell.Commands.Internal.Format.FormatStartData' because it does not exist.
Get-Item: Cannot find path 'Microsoft.PowerShell.Commands.Internal.Format.GroupStartData' because it does not exist.
Get-Item: Cannot find path 'Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData' because it does not exist.
-a---           7/20/2023  3:44 PM              0 file2.txt
Get-Item: Cannot find path 'Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData' because it does not exist.
-a---           7/20/2023  3:44 PM              0 file3.txt
Get-Item: Cannot find path 'Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData' because it does not exist.
Get-Item: Cannot find path 'Microsoft.PowerShell.Commands.Internal.Format.GroupEndData' because it does not exist.
Get-Item: Cannot find path 'Microsoft.PowerShell.Commands.Internal.Format.FormatEndData' because it does not exist.

Does anyone have an explanation where this goes sideways and how I might resolve this?


Solution

  • It seems a little update to the wrapped command should solve your problem if I'm understanding correctly, the output generated by Format-Table should go directly to the host, thus adding Out-Host. Then if -PassThru is present you more than likely want to send the original object thru the pipeline so:

    function Write-Table {
        [CmdletBinding(HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=2096703')]
        param(
            [switch]
            ${AutoSize},
    
            [switch]
            ${RepeatHeader},
    
            [switch]
            ${HideTableHeaders},
    
            [switch]
            ${Wrap},
    
            [Parameter(Position = 0)]
            [System.Object[]]
            ${Property},
    
            [System.Object]
            ${GroupBy},
    
            [string]
            ${View},
    
            [switch]
            ${ShowError},
    
            [switch]
            ${DisplayError},
    
            [switch]
            ${Force},
    
            [ValidateSet('CoreOnly', 'EnumOnly', 'Both')]
            [string]
            ${Expand},
    
            [Parameter(ValueFromPipeline = $true)]
            [psobject]
            ${InputObject},
    
            [switch]
            ${PassThru})
    
        begin {
            $null = $PSBoundParameters.Remove('PassThru')
            $scriptCmd = { Microsoft.PowerShell.Utility\Format-Table @PSBoundParameters | Out-Host }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        }
    
        process {
            if ($PassThru.IsPresent) {
                $InputObject
            }
    
            $steppablePipeline.Process($InputObject)
        }
    
        end {
            $steppablePipeline.End()
        }
    
        clean {
            if ($null -ne $steppablePipeline) {
                $steppablePipeline.Clean()
            }
        }
        <#
    
        .ForwardHelpTargetName Microsoft.PowerShell.Utility\Format-Table
        .ForwardHelpCategory Cmdlet
    
        #>
    }
    

    Then something like this:

    $capture = 0..5 | ForEach-Object { [pscustomobject]@{ Item = $_ } } |
        Write-Table -PassThru |
        ForEach-Object Item
    

    Works just fine:

    PS ..\pwsh> $capture = 0..10 | ForEach-Object { [pscustom....
    
    # Output sent to the host:
    Item
    ----
       0
       1
       2
       3
       4
       5
    
    PS ..\pwsh> $capture
       
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10