Home > Enterprise >  How to invoke Powershell filter from another Powershell filter?
How to invoke Powershell filter from another Powershell filter?

Time:10-14

Suppose we have a PS filter X:

filter X([Parameter(ValueFromPipeline)]$o)
{
    begin { Write-Host "Begin X" }
    process { "|$o|" }
    end { Write-Host "End X" }
}

Here is the result of running it:

C:\> 0..3 | X
Begin X
|0|
|1|
|2|
|3|
End X
C:\>

I want to write another filter Y which must invoke X internally. The desired output of running 0..3 | Y should be:

Begin Y
Begin X
|0|
|1|
|2|
|3|
End X
End Y

Here is my current implementation:

filter Y([Parameter(ValueFromPipeline)]$o)
{
    begin { Write-Host "Begin Y" }
    process { $o | X }
    end { Write-Host "End Y" }
}

Unfortunately, it does not work as I need:

C:\> 0..3 | Y
Begin Y
Begin X
|0|
End X
Begin X
|1|
End X
Begin X
|2|
End X
Begin X
|3|
End X
End Y
C:\>

Which is perfectly expected, because the correct implementation should be something like this:

filter Y([Parameter(ValueFromPipeline)]$o)
{
    begin { Write-Host "Begin Y" ; InvokeBeginBlockOf X }
    process { $o | InvokeProcessBlockOf X }
    end { Write-Host "End Y" ; InvokeEndBlockOf X }
}

This is pseudo code, of course, but the point is - I must be able to invoke the begin/process/end blocks of the X filter individually and that would be the correct way to invoke filter from a filter.

So my question is - how can we invoke filter from a filter by individually invoking the begin/process/end blocks from the respective blocks in the outer filter?

CodePudding user response:

You would need a SteppablePipeline to accomplish this, as far as I know. I'm also unclear on why you're using the filter keyword when what you're defining are actually functions.

A filter is supposed to be a named Script Block which implicitly acts as a process block. See this helpful answer for details.

I would advise you to change them for actual functions:

function Y {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [object] $InputObject
    )

    begin {
        Write-Host "Begin Y"
        $cmd     = Get-Command X
        $wrapped = { & $cmd @PSBoundParameters }

        $steppablepipe = $wrapped.GetSteppablePipeline($myInvocation.CommandOrigin)
        $steppablepipe.Begin($PSCmdlet)
    }
    process { $steppablepipe.Process($InputObject) }
    end {
        $steppablepipe.End()
        Write-Host "End Y"
    }
}

0..10 | Y

Outputs:

Begin Y
Begin X
|0|
|1|
|2|
|3|
|4|
|5|
|6|
|7|
|8|
|9|
|10|
End X
End Y

CodePudding user response:

Edit: It look like Santiago's use of steppable pipeline is better for what you want to do. I was not aware that it existed.

I am leaving my answer anyway as it provide a different take on this anyway.


A filter is a kind of function. Functions are stored in the $Function automatic variable. You could use that to get the begin block from the AST and execute it.

Something like ([Scriptblock]::Create($function:X.Ast.Body.ProcessBlock.Extent.Text).invoke($o))

Complete example


filter X([Parameter(ValueFromPipeline)]$o) {
    begin { Write-Host "Begin X" }
    process { "|$o|" }
    end { Write-Host "End X" }
}

filter Y([Parameter(ValueFromPipeline)]$o) {
    begin { 
        Write-Host "Begin Y"
        & ([Scriptblock]::Create($function:X.Ast.Body.BeginBlock.Extent.Text))
    }
    process { ([Scriptblock]::Create($function:X.Ast.Body.ProcessBlock.Extent.Text).invoke($o)) }
    end {
        Write-Host "End Y"
        & ([Scriptblock]::Create($function:X.Ast.Body.EndBlock.Extent.Text))
    }
}

Output for 0..3 | y

Begin Y
Begin X
|0|
|1|
|2|
|3|
End Y
End X

Note, when testing, I had some error for the begin block with the use of

[Scriptblock]::Create($function:X.Ast.Body.BeginBlock.Extent.Text).invoke()
# The script block cannot be invoked because it contains more than one 
clause

so I switched it up with this instead.

& ([Scriptblock]::Create($function:X.Ast.Body.BeginBlock.Extent.Text))

(Not sure why yet, I'll have to investigate but in any case, this work)

  • Related