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.
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+ .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 use of
-
The
(Add-Type ...
statement uses an ad-hoc type created with C# code that uses reflection to throw the same exception thatSelect-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 callingTest-Any
outside of a pipeline unhelpfully returns$true
. To distinguish between between$null | Test-Any
andTest-Any
, check$MyInvocation.ExpectingInput
, which is$true
only in a pipeline: Thanks, PetSerAlfunction Test-Any() { process { $MyInvocation.ExpectingInput; break } end { $false } }
- Small caveat: By design, if there's no pipeline at all, the
-
$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 executesbreak
, theend
block is only reached if theprocess
block was never entered, which implies an empty pipeline, so$false
is written to the output stream to signal that.
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?
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
You can use the original LINQ Any
:
[Linq.Enumerable]::Any($list)
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") }
}