Is it possible to pipe conditionally in Powershell, i.e. execute an element of a pipeline only if a condition is met?

Viewed 14213

I want to do something like this:

<statement> | <filter1> | <filter2> if <condition> | <filter3> | <filter4> | <filter5>

The results of <statement> run through <filter1>, then they run through <filter2> only if <condition> is met, then through the remaining filters regardless of whether <filter2> was applied. This is the equivalent of:

if (<condition>) {
  <statement> | <filter1> | <filter2> | <filter3> | <filter4> | <filter5>
} else {
  <statement> | <filter1> | <filter3> | <filter4> | <filter5>
}

This would be useful in functions where a given filter is applied to the result set only if a certain switch was invoked. If the conditional filter occurs early in a long pipeline, writing it with an outer if-block results in a lot of repetition of code, especially if there is more than one conditional filter.

Here's an example. The following function shows the permissions a given account has in a given directory subtree (e.g. Show-AccountPerms \\SERVERX\Marketing DOMAIN\jdoe gives a report of permissions that the user DOMAIN\jdoe has in the directory tree under \SERVERX\Marketing).

function Show-AccountPerms {
    param (
        [parameter(mandatory = $true)]$rootdir,
        [parameter(mandatory = $true)]$account,
        [switch]$files,
        [switch]$inherited
    )
    gci -r $rootdir `
    |where {$_.psiscontainer} `
    |foreach {
        $dir = $_.fullname
        (get-acl $_.pspath).access `
        | where {$_.isinherited -eq 'False'} `
        |foreach {
            if ($_.identityreference -eq $account) {
                "{0,-25}{1,-35}{2}" -f $_.identityreference, $_.filesystemrights, $dir
            }
        }
    }
}

By default, it only shows explicit permissions (enforced by the | where {$_.isinherited -eq 'False'} filter), and only on directories (enforced by the |where {$_.psiscontainer} filter).

However, I want to ignore |where {$_.psiscontainer} if the -files switch is invoked, and ignore | where {$_.isinherited -eq 'False'} if the -inherited switch is invoked. Accomplishing this with outer if blocks would quadruple the code, and almost 75% of it would be repetition. Is there a way to keep these filters in-line but instruct powershell to only apply them of the corresponding switch is false?

Please note that this is just an example, so I'm not interested in any workarounds specific to this function. I'm looking for an answer to my general question regarding piping conditionally, not a solution for how to accomplish this particular task.

6 Answers

I think you mean something like the following, which I just concocted:

function Pipe-If([ScriptBlock]$decider, [ScriptBlock]$pipeElement)
{
    if (&$decider) {
        $pipeElement
    } else {
        {$input}
    }
}

@(1,2,3) | &(Pipe-If {$doDouble} {$input | % { $_ * 2} })

results in 2, 4, 6 if $doDouble is $true, and on $false it results in 1, 2, 3.

The key here is that an arbitrary pipe element like % { $_ * 2} can be encapsulated as a ScriptBlock as {$input | % { $_ * 2 } }, and that it can be converted back to a pipe element by prepending &.

I used https://devblogs.microsoft.com/powershell/diy-ternary-operator for inspiration.


Important note. Don't use something like this:

filter Incorrect-Pipe-If([ScriptBlock]$decider, [ScriptBlock]$pipeElement) {
    if (&$decider) {
        $_ | &$pipeElement
    } else {
        $_
    }
}

@(1,2,3) | Incorrect-Pipe-If {$doDouble} {$_ | % { $_ * 2} }

This causes % to be executed multiple times, once for each object in the pipeline. Pipe-If correctly executes the % command just once, and sends it the entire stream of objects.

In the example above that is not a problem. But if the command is tee bla.txt then the difference is important.

This is similar to Marnix Klooster's answer but simpler to work with. The advantage of this formulation over that one is in the syntax of the code to be executed. It's far closer to a normal pipeline block. Basically you just need to enclose whatever you want in {} (braces).

Note that, Like Marnix's script this is a blocking function. The pipeline results are collected into $Input and the function itself executes only once. Only $pipeElement code ever executes more than once and then only if -Execute is true.

function Conditional_Block([bool]$Execute,[ScriptBlock]$PipeElement)
{
    if ($Execute) 
        {$ExecutionContext.InvokeCommand.NewScriptBlock("`$(`$Input|$PipeElement)").invoke()}
    else
        {$input}
}

Using this function does not require it be defined already! You really can actually define it within the function where you want to use it and let it disappear when your function completes.

The -execute parameter enables/disables step execution. Any expression with a boolean result works. For example $(1 -eq 0) works just like $false.

@(1,2,3)|Conditional_Block $true  {?{$_ -le 2}}|%{"'$_'"}
@(1,2,3)|Conditional_Block $false {?{$_ -le 2}}|%{"'$_'"}
Related