2

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?

mark
  • 59,016
  • 79
  • 296
  • 580
  • It is implied Santiago's helpful answer, but just to state it explicitly: you're not using `filter`s the way they're intended: a `filter` is primarily meant to be a _parameter-less_ function whose body is an _implied_ `process` block; it is syntactic sugar for a special-purpose `function`, namely one that processes _pipeline input only_. Example: `filter foo { "[$_]" }; 1..5 | foo` – mklement0 Oct 13 '22 at 16:37
  • 1
    Thanks, I got it and changed my code. I will leave it as is in the question so that your comments stay relevant. – mark Oct 13 '22 at 18:13

2 Answers2

3

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
mklement0
  • 382,024
  • 64
  • 607
  • 775
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
1

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)

Sage Pourpre
  • 9,932
  • 3
  • 27
  • 39
  • It's an interesting approach, but steppable pipelines are indeed the right and _robust_ solution. Using the `.Invoke()` method to execute a script block in PowerShell code is best avoided, because it changes the semantics of the call in several respects - see [this answer](https://stackoverflow.com/a/59220711/45375) . A closer (but not complete) approximation would require you to call the created script blocks with `. ` (dot-sourcing), so that state created in the `begin` block is available in the calls to the `process` block. – mklement0 Oct 13 '22 at 17:41
  • Also, it's worth noting that `filter`s shouldn't be used this way and that what the code in the question defines should really be `function`s. Finally, a quibble: `$function` isn't an automatic variable: Prefix `$function:` is a _namespace_ qualifier (a scope modifier of sorts), and `function` refers to a PowerShell _drive_ by that name, which allows access to all defined functions. `$function:X` - which returns the body of function `X` as a script block - is an instance of [_namespace variable notation_](https://stackoverflow.com/a/55036515/45375). The docs don't use that term, unfortunately. – mklement0 Oct 13 '22 at 17:54