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

13,330

Solution 1

Sorry, I didn't mean to abandon this question. The answers that were posted weren't what I was driving at, but I figured out a way to do it shortly after posting, and didn't come back to the site for a long time. Since a solution hasn't been posted, here's what I came up with. It's not quite what I had in mind when I asked the question and it isn't too pretty, but apparently it's the only way to do it:

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

So, in the example, the line

|where {$_.psiscontainer} `

would be changed to

|foreach {if (-not $files) {$_ | where {$_.psiscontainer}} else {$_}} `

and

|where {$_.isinherited -eq 'False'} `

would be changed to

|foreach {if (-not $inherited) {$_ | where {$_.isinherited -eq 'False'}} else {$_}} `

(Yes, normally I'd write that as |foreach {if ($files) {$_} else {$_ | where {$_.psiscontainer}}}, and |foreach {if ($inherited) {$_} else {$_ | where {$_.isinherited -eq 'False'}}} but I did it this way for clarity.)

I was hoping there might be something more elegant, that would evaluate a condition in front of the filter once to determine whether to execute or skip a stage of the pipeline. Something like this:

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

(a special case of if, not the usual meaning; a different keyword could be used), or maybe

<statement> | <filter1> | (<condition>) ? <filter2> | <filter3>

$_ would be invalid in the condition, unless it's defined outside the current pipeline, for example if the pipeline is contained within a switch statement, $_ in the <condition> would refer the switch statement's $_.

I think I'll make a feature suggestion to Microsoft. This would not only make the code more elegant, it would be more efficient as well, because if it's a built-in feature, <condition> could be evaluated once for the entire pipeline, rather then testing the same independent condition in each iteration.

Solution 2

You can test for both conditions in your filter allowing the object down the pipeline if either one is true. If your "condition" is on the left side of the -or operator, make it result to $true if you don't want your filter condition tested.

To use your example:

| where {$_.psiscontainer}

becomes:

| where {$files -or $_.psiscontainer}

and

| where {$_.isinherited -eq 'False'}

becomes

| where {$inherited -or $_.isinherited -eq 'False'}

To generalise:

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

becomes:

<statement> | <filter1> | <-not condition -or filter2> | <filter3> | <filter4> | <filter5>

Solution 3

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.

Solution 4

I think the other answer to this question misunderstands what is being asked.

The solution lies in the following:

... | %{if($_ -match "Something"){DoSomethingWith $_ }else{$_}} | ...

What this will do, is pass all elements through to the next filter, EXCEPT those that match "Something", in which case it does different logic. The logic can be changed to make it pass an altered version of the pipeline element instead of a function.

Solution 5

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}}|%{"'$_'"}
Share:
13,330
Adi Inbar
Author by

Adi Inbar

Okay, I figure that now that I'm an "established user", I should have a profile and an avatar. It's a stack with an overflow, get it??? I used to script almost exclusively in Perl, but a couple of years ago I discovered the wonders of PowerShell, and have since fallen in love with it. Why am I here? Because I greatly enjoy problem-solving, and I find that I vastly increase my knowledge and well-roundedness by figuring out other people's problems. I started answering questions mainly because I wanted to gain 50 rep points so I could post comments, and, well, as you see on the left I kind of overshot my goal a little bit...okay, I got addicted. I really should see someone about this... [If you're asking yourself, "WTF? This all sounds strangely out of context for this site. Is this guy on crack??", the answer is no. This profile was written for Stack Overflow, but apparently it automatically gets copied when a new account is created at some other Stack Exchange, but not all. I'm not sure why. BTW, if you change the question to "...is this guy on a mushroom trip??", then the answer changes to "Possibly, but that's still not why this profile sounds strangely out of context."]

Updated on June 05, 2022

Comments

  • Adi Inbar
    Adi Inbar almost 2 years

    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.

  • Adi Inbar
    Adi Inbar almost 11 years
    That works fine if the filter is a where clause, but not if the filter itself doesn't evaluate a condition, e.g. if you want to pass the objects through Select-String <pattern> only if the condition is true. | -not <condition> -or <filter2> | doesn't work, e.g. | -not $switch -or select-string 'sometext' |. Keep in mind what "filter" means - a command that takes pipeline input and sends the results down the pipeline. The condition test would be a part of <filter2>, and therefore only works if the filter command evaluates a condition (i.e., is a where).
  • Adi Inbar
    Adi Inbar almost 11 years
    The other answerer understood correctly. The idea isn't to apply a condition to each object and process it differently based on whether it meets the condition; it's to test a condition that's independent of the objects in the pipeline and apply the filter to all objects but only if the condition is true - equivalent to using if to execute the pipeline with or without one stage, but without repeating a lot of code. In my example, $files isn't something that's compared with each object, it's a simple switch that determines whether one filter will be applied to all objects or none.
  • Adi Inbar
    Adi Inbar almost 11 years
    However, this answer was helpful in that it introduced the idea of having the else block simply echo all the objects if a condition isn't met. The idea is if (<condition that has nothing to do with $_>) {<pipe $_ through this filter>}, otherwise skip this filter and move on to the next stage of the pipeline. The else {$_} is effectively a workaround for that last part, skipping the stage of the pipeline and moving on to the next.
  • Adi Inbar
    Adi Inbar over 9 years
    I'm not quite sure what you're trying to say, but it sounds like you misread the question, because this doesn't appear to be at all related. This isn't about interactive user decisions, progress bars, or error handling, it's about executing a section of the pipeline if a given independent condition is true and skipping that section if the condition is false. By "independent" I mean not based on the content of the pipelined objects, i.e. if the condition is true, that section is executed for all objects; if false, it's not executed for any object.
  • oɔɯǝɹ
    oɔɯǝɹ over 9 years
    @AdiInbar The original question needed a solution to skip pipelines from executing, this describes a scenario to do exactly that, and without resorting to complex code constructions. What is your confusion?
  • Adi Inbar
    Adi Inbar over 9 years
    I was hoping there was a way to do it "inline", without the need to keep reevaluating, at each iteration, a condition that's immutable for the duration of the pipeline. That doesn't appear to be possible. I might submit it as a feature request.
  • Adi Inbar
    Adi Inbar over 9 years
    If you think this addresses the question, please give an example.
  • oɔɯǝɹ
    oɔɯǝɹ over 9 years
    This solution works without reevaluating, but relies upon a switch within the filter that responds to a preference flag. Not all filter may provide such an option, but it can work for your own filters.
  • Adi Inbar
    Adi Inbar over 9 years
    I still don't see how this addresses the question. AFAIK preference flags only apply to handling errors and displaying informational messages (warning, progress, debugging, error, verbose). This seems to be narrowly applicable to one particular type of filter that happens to be controlled by an associated preference flag.
  • Adi Inbar
    Adi Inbar over 9 years
    I don't see how this could possibly be used to control the execution of a Where-Object, ForEach-Object, Select-String, or Tee-Object filter (just to give a few examples). Heck, I can't think of a single pipeline filter that can be turned on or off by a preference flag other than the one you mentioned.
  • Adi Inbar
    Adi Inbar over 9 years
    In any case, verbal descriptions only go so far. We could discuss this all night long and keep talking past each other, but an example would clear up whether you have a solution or misunderstood the question. Forget about Show-Progress; can you show how to use the method you're proposing for the example in my question and answer--skipping a Where-Object filter and continuing to the rest of the pipeline if a given boolean variable whose value was determined prior to the pipeline is true?
  • Adi Inbar
    Adi Inbar over 9 years
    This looks to me like essentially the same solution (workaround, really, since it's not quite what I wanted) as in my answer, except it's wrapped in a function. I see that as a step in the opposite direction, because not only does it rely on the availability of an external function without significantly compacting the code, it actually makes it less efficient, because the condition is still evaluated for each iteration, but now it makes an external functional call to execute the same logic.
  • oɔɯǝɹ
    oɔɯǝɹ over 9 years
    @AdiInbar This may not work in those cases, i just wanted to show that it is possible to use another approach to this. I agree with you that it would be nice to be able to apply a pipeline step conditionally. I have a simular problem where i would like to group function results (by calling Group-Object), but only when a -Summarize switch is provided by the user.
  • oɔɯǝɹ
    oɔɯǝɹ over 9 years
    did you make the feature request? If you post a link other people may support this request.
  • MarnixKlooster ReinstateMonica
    MarnixKlooster ReinstateMonica over 9 years
    @AdiInbar You were asking, "Is there a way to keep these filters in-line but instruct powershell to only apply them of the corresponding switch is false?" And you said that you're looking for "something more elegant, that would evaluate a condition in front of the filter once to determine whether to execute or skip a stage of the pipeline." My &(Pipe-If {<condition>} {$_| <filter> }) is exactly such a syntax. Whether or not to use a function (with likely no performance impact) depends on context, and whether one prefers foreach {if (<condition>) {$_ | <filter>} else {$_} is preference.
  • bielawski
    bielawski almost 6 years
    I'm pretty sure the OP either misunderstood your answer or doesn't understand how, for example, for-each actually works. It's nothing more than a function, like yours, and anything MS might provide would necessarily work in one of the 2 ways presented in this thread. That is, the function must either be blocking (like this one) or require that the condition be re-evaluated for each element in the pipeline. I'm rather certain that no other way is possible, even in theory, without dynamic code.