George Mauer George Mauer - 3 months ago 35
PowerShell Question

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:

gci | ? {$_.PsIsContainer} | gci -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.

Answer

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.

UPDATE: 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, vote for it here.

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

# IMPORTANT: NEVER USE THIS INSIDE A LOOP, except 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 1 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. Effectively, you cannot distinguish between $null | Test-Any and Test-Any.
  • $true, written to the output stream, signals that at least 1 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.
  • 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.

In the absence of proper support for exiting a pipeline, there are workarounds:

  • Enclose any pipeline in which you use the above Test-Any function in a run-once-only dummy loop - obviously, this is inconvenient and easy to forget:

    # The dummy loop ensures that the `break` inside Test-Any()
    # only breaks out of *it*.
    do { ... | Test-Any } while ($false)
    
  • PowerShell v3+: Employ a more elaborate, but robust and encapsulated workaround; note that the following function also incorporates a cleaned-up version of Paolo Tedesco's extension, which accepts an optional filter parameter to test whether any input object matches the filter:

#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 (PS v3+) uses internally to stop the pipeline, namely [System.Management.Automation.StopUpstreamCommandsException], which as of PS 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:

  • > @() | Test-Any false

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

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