PowerShell equivalent of LINQ Any()?

linq, powershell

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.

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

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

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.

Problem

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.

Original source