PowerShell equivalent of LINQ Any()?

前端 未结 11 626
灰色年华
灰色年华 2020-12-01 03:29

I would like to find all directories at the top level from the location of the script that are stored in subversion.

In C# it would be something like this

         


        
相关标签:
11条回答
  • 2020-12-01 03:54

    This is the best method that I found so far (does not iterate over all elements if already found a true, and does not break the pipeline):

    From LINQ Any() equivalent in PowerShell

    It’s possible to use a built-in $input variable that contains the whole pipeline in a scope of function.

    So, the desired code could look like the following:

    function Test-Any([scriptBlock] $scriptBlock = {$true}, [scriptBlock] $debugOut = $null)
    {
        if ($debugOut)
        {
            Write-Host(“{0} | % {{{1}}}” -f $input, $scriptBlock)
        }
    
        $_ret = $false;
        $_input = ($input -as [Collections.IEnumerator])
    
        if ($_input)
        {
            while ($_input.MoveNext())
            {
                $_ = $_input.Current;
    
                Write-Host $_
    
                if ($debugOut)
                {
                    Write-Host(“Tested: [{0}]” -f (&$debugOut))
                }
    
                if (&$scriptBlock)
                {
                    if ($debugOut)
                    {
                        Write-Host(“Matched: [{0}]” -f (&$debugOut))
                    }
    
                    $_ret = $true
                    break
                }
            }
        }
    
        $_ret
    }
    
    0 讨论(0)
  • 2020-12-01 03:55

    You can use the original LINQ Any:

    [Linq.Enumerable]::Any($list)
    
    0 讨论(0)
  • 2020-12-01 03:57

    To answer the immediate question with a PowerShell v3+ solution:

    (Get-ChildItem -Force -Directory -Recurse -Depth 2 -Include '_svn', '.svn').Parent.FullName
    

    -Directory limits the matches to directories, -Recurse -Depth 2 recurses up to three levels (children, grandchildren, and great-grandchildren), Include allows specifying multiple (filename-component) filters, and .Parent.FullName returns the full path of the parent dirs. of the matching dirs., using member enumeration (implicitly accessing a collection's elements' properties).

    As for the bonus question: select-object {$_.Directory} does not work, because the \[System.IO.DirectoryInfo\] instances returned by Get-ChildItem have no .Directory property, only a .Parent property; Select-Object -ExpandProperty Parent should have been used.

    In addition to only returning the property value of interest, -ExpandProperty also enforces the existence of the property. By contrast, Select-Object {$_.Directory} returns a custom object with a property literally named $_.Directory, whose value is $null, given that the input objects have no .Directory property; these $null values print as empty lines in the console.


    As for the more general question about a PowerShell equivalent to LINQ's .Any() method, which indicates [with a Boolean result] whether a given enumerable (collection) has any elements at all / any elements satisfying a given condition:

    Natively, PowerShell offers no such equivalent, but the behavior can be emulated:


    Using the PowerShell v4+ .Where() collection method:

    Caveat: This requires collecting the entire input collection in memory first, which can be problematic with large collections and/or long-running input commands.

    (...).Where({ $_ ... }, 'First').Count -gt 0
    

    ... represents the command of interest, and $_ ... the condition of interest, applied to each input object, where PowerShell's automatic $_ variable refers to the input object at hand; argument 'First' ensures that the method returns once the first match has been found.

    For example:

    # See if there's at least one value > 1
    PS> (1, 2, 3).Where({ $_ -gt 1 }, 'First').Count -gt 0
    True
    

    Using the pipeline: Testing whether a command produced at least one output object [matching a condition]:

    The advantage of a pipeline-based solution is that it can act on a command's output one by one, as it is being produced, without needing to collect the entire output in memory first.

    • If you don't mind that all objects are enumerated - even if you only care if there is at least one - use Paolo Tedesco's helpful extension to JaredPar's helpful answer. The down-side of this approach is that you always have to wait for a (potentially long-running) command to finish producing all output objects, even though - logically - the determination whether there are any output objects can be made as soon as the first object is received.

    • If you want to exit the pipeline as soon as one [matching] object has been encountered, you have two options:

      • [Ad-hoc: Easy to understand, but cumbersome to implement] Enclose the pipeline in a dummy loop and use break to break out of the pipeline and that loop (... represents the command whose output to test, and $_ ... match the condition):

         # Exit on first input object.
         [bool] $haveAny = do { ... | % { $true; break } } while ($false)
        
         # Exit on first input object that matches a condition.
         [bool] $haveAny = do { ... | % { if ($_ ...) { $true ; break } } } while ($false)
        
      • [Use a PowerShell v3+ self-contained utility function that is nontrivial to implement] See the implementation of function Test-Any below. It can be added to scripts or, for use in interactive sessions, to your $PROFILE file.


    PowerShell v3+: Optimized utility function Test-Any

    The function is nontrivial, because as of Windows PowerShell v5.1, PowerShell Core v6, there is no direct way to exit a pipeline prematurely, so a workaround based on .NET reflection and a private type is currently necessary.

    If you agree that there should be such a feature, take part in the conversation on GitHub.

    #requires -version 3
    Function Test-Any {
    
        [CmdletBinding()]
        param(
            [ScriptBlock] $Filter,
            [Parameter(ValueFromPipeline = $true)] $InputObject
        )
    
        process {
          if (-not $Filter -or (Foreach-Object $Filter -InputObject $InputObject)) {
              $true # Signal that at least 1 [matching] object was found
              # Now that we have our result, stop the upstream commands in the
              # pipeline so that they don't create more, no-longer-needed input.
              (Add-Type -Passthru -TypeDefinition '
                using System.Management.Automation;
                namespace net.same2u.PowerShell {
                  public static class CustomPipelineStopper {
                    public static void Stop(Cmdlet cmdlet) {
                      throw (System.Exception) System.Activator.CreateInstance(typeof(Cmdlet).Assembly.GetType("System.Management.Automation.StopUpstreamCommandsException"), cmdlet);
                    }
                  }
                }')::Stop($PSCmdlet)
          }
        }
        end { $false }
    }
    
    • if (-not $Filter -or (Foreach-Object $Filter -InputObject $InputObject)) defaults to true if $Filter wasn't specified, and otherwise evaluates the filter (script block) with the object at hand.

      • The use of ForEach-Object to evaluate the filter script block ensures that $_ binds to the current pipeline object in all scenarios, as demonstrated in PetSerAl's helpful answer here.
    • The (Add-Type ... statement uses an ad-hoc type created with C# code that uses reflection to throw the same exception that Select-Object -First (PowerShell v3+) uses internally to stop the pipeline, namely [System.Management.Automation.StopUpstreamCommandsException], which as of PowerShell v5 is still a private type. Background here: http://powershell.com/cs/blogs/tobias/archive/2010/01/01/cancelling-a-pipeline.aspx A big thank-you to PetSerAl for contributing this code in the comments.

    Examples:

    • PS> @() | Test-Any false

    • PS> Get-EventLog Application | Test-Any # should return *right away* true

    • PS> 1, 2, 3 | Test-Any { $_ -gt 1 } # see if any object is > 1 true


    Background information

    JaredPar's helpful answer and Paolo Tedesco's helpful extension fall short in one respect: they don't exit the pipeline once a match has been found, which can be an important optimization.

    Sadly, even as of PowerShell v5, there is no direct way to exit a pipeline prematurely. If you agree that there should be such a feature, take part in the conversation on GitHub.

    A naïve optimization of JaredPar's answer actually shortens the code:

    # IMPORTANT: ONLY EVER USE THIS INSIDE A PURPOSE-BUILT DUMMY LOOP (see below)
    function Test-Any() { process { $true; break } end { $false } }
    
    • The process block is only entered if there's at least one element in the pipeline.
      • Small caveat: By design, if there's no pipeline at all, the process block is still entered, with $_ set to $null, so calling Test-Any outside of a pipeline unhelpfully returns $true. To distinguish between between $null | Test-Any and Test-Any, check $MyInvocation.ExpectingInput, which is $true only in a pipeline: Thanks, PetSerAl function Test-Any() { process { $MyInvocation.ExpectingInput; break } end { $false } }
    • $true, written to the output stream, signals that at least one object was found.
    • break then terminates the pipeline and thus prevents superfluous processing of additional objects. HOWEVER, IT ALSO EXITS ANY ENCLOSING LOOP - break is NOT designed to exit a PIPELINEThanks, PetSerAl .

      • If there were a command to exit the pipeline, this is where it would go.
      • Note that return would simply move on to the next input object.
    • Since the process block unconditionally executes break, the end block is only reached if the process block was never entered, which implies an empty pipeline, so $false is written to the output stream to signal that.

    0 讨论(0)
  • 2020-12-01 03:57

    A variation on @JaredPar's answer, to incorporate the test in the Test-Any filter:

    function Test-Any {
        [CmdletBinding()]
        param($EvaluateCondition,
            [Parameter(ValueFromPipeline = $true)] $ObjectToTest)
        begin {
            $any = $false
        }
        process {
            if (-not $any -and (& $EvaluateCondition $ObjectToTest)) {
                $any = $true
            }
        }
        end {
            $any
        }
    }
    

    Now I can write "any" tests like

    > 1..4 | Test-Any { $_ -gt 3 }
    True
    
    > 1..4 | Test-Any { $_ -gt 5 }
    False
    
    0 讨论(0)
  • 2020-12-01 03:57

    My approach now was:

    gci -r -force `
        | ? { $_.PSIsContainer -and $_.Name -match "^[._]svn$" } `
        | select Parent -Unique
    

    The reason why

    select-object {$_.Directory}
    

    doesn't return anything useful is that there is no such property on a DirectoryInfo object. At least not in my PowerShell.


    To elaborate on your own answer: PowerShell can treat most non-empty collections as $true, so you can simply do:

    $svnDirs = gci `
        | ? {$_.PsIsContainer} `
        | ? {
            gci $_.Name -Force `
                | ? {$_.PSIsContainer -and ($_.Name -eq "_svn" -or $_.Name -eq ".svn") }
            }
    
    0 讨论(0)
  • 2020-12-01 04:00

    Unfortunately there is no equivalent in PowerShell. I wrote a blog post about this with a suggestion for a general purpose Test-Any function / filter.

    function Test-Any() {
        begin {
            $any = $false
        }
        process {
            $any = $true
        }
        end {
            $any
        }
    }
    

    Blog post: Is there anything in that pipeline?

    0 讨论(0)
提交回复
热议问题