linqpowershell

PowerShell equivalent of LINQ Any()?


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

Directory.GetDirectories(".")
  .Where(d=>Directories.GetDirectories(d)
     .Any(x => x == "_svn" || ".svn"));

I'm having a bit of difficulty finding the equivalent of "Any()" in PowerShell, and I don't want to go through the awkwardness of calling the extension method.

So far I've got this:

 Get-ChildItem | ? {$_.PsIsContainer} | Get-ChildItem -force | ? {$_.PsIsContainer -and $_.Name -eq "_svn" -or $_.Name -eq ".svn"

This finds me the svn directories themselves, but not their parent directories - which is what I want. Bonus points if you can tell me why adding

 | Select-Object {$_.Directory}

to the end of that command list simply displays a sequence of blank lines.


Solution

  • 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-access 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+ intrinsic.Where() 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.


    PowerShell v3+: Optimized utility function Test-Any

    The function is nontrivial, because as of PowerShell (Core) v7.2.x, 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 in GitHub issue #3821.

    #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 }
    }
    

    Examples:


    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 in GitHub issue #3821.

    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 } }