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

丶灬走出姿态 提交于 2019-11-29 17:23:18

问题


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.


回答1:


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>



回答2:


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.




回答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 http://blogs.msdn.com/b/powershell/archive/2006/12/29/dyi-ternary-operator.aspx 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.




回答4:


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.




回答5:


Another option is to use a global preference flag (of type System.Management.Automation.ActionPreference) to allow the user to determine whether the pipeline filter does something.

For example, the $progressPreference can be set to the following values:

  • SilentlyContinue
  • Stop
  • Continue
  • Inquire
  • Ignore

This preference flag is then used by Write-Progress to determine the desired behavior.

for example, if you have a pipeline filter Show-Progress, that counts items and displays a progress bar, then it will only display this progress bar when $progressPreference is set to Continue.

You can use simular construction in your own pipeline filters.




回答6:


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}}|%{"'$_'"}


来源:https://stackoverflow.com/questions/11621365/is-it-possible-to-pipe-conditionally-in-powershell-i-e-execute-an-element-of-a

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!