Is there a native PowerShell way to test, if IEnumerable
is empty?
I know I can call Linq.Enumerable.Any
like this:
[Linq.Enumerable]::Any($enumeration)
But I hope that there's a more native way.
Unfortunately, there is no PowerShell-native way in Windows PowerShell / as of PowerShell (Core) v7.2, and while [Linq.Enumerable]::Any()
is at least concise, it can break in various scenarios and doesn't operate lazily in PowerShell:
# These invocations with various *PowerShell* enumerables all FAIL with
# 'Cannot find an overload for "Any" and the argument count: "1"'
foreach ($enumerable in 1.5, $null, (& {}), (1,2,3)) {
[Linq.Enumerable]::Any($enumerable) # !! BREAKS
}
# However, it does work with arrays that are *explicitly typed*.
[Linq.Enumerable]::Any([int[]] (1, 2)) # -> $true
Now, 1.5
, $null
and & {}
do not implement [IEnumerable]
([System.Collections.IEnumerable]
or a generic counterpart), but in the world of PowerShell everything is enumerable, even scalars and $null
, but the latter only in the pipeline, not with foreach
. The notable exception is the "collection null" value, a.k.a. "AutomationNull" whose sole purpose is to signal that there's nothing to enumerate (and as such you could argue that, as a special enumeration case, it should implement [IEnumerable]
) - see this answer.
However, 1, 2, 3
does implement [IEnumerable]
: it is a regular PowerShell array of type [object[]]
; while you can fix this particular case with - curiously - an explicit [object[]]
cast, it is obviously not a general solution, as potential conversion to an array forces full enumeration - see the bottom section for more information.
Bringing better LINQ integration to PowerShell in the future is the subject of GitHub issue #2226.
A robust - but obscure and non-lazy - PowerShell-native-features-only solution that handles all of the above cases would be (PSv4+):
# Returns $true if $enumerable results in enumeration of at least 1 element.
# Note that $null is NOT considered enumerable in this case, unlike
# when you use `$null | ...`
$haveAny = $enumerable.Where({ $true }, 'First').Count -ne 0
# Variant with an example of a *filter*
# (Find the first element that matches a criterion; $_ is the object at hand).
$haveAny = $enumerable.Where({ $_ -gt 1000 }, 'First').Count -ne 0
The above is:
[Linq.Enumerable]::Any()
.That is, the enumerable - because a method is being called on it - must be an expression, and when PowerShell uses a command as an expression, including assigning to a variable, it runs the command to completion and implicitly collects all output in an [object[]]
-typed array.
Therefore, a lazy native PowerShell solution requires use of the pipeline, in the form of a hypothetical Test-Any
cmdlet, which:
$true
if at least one input object is received.Note: The examples use $enumerable
as pipeline input for brevity, but only with actual calls to PowerShell commands, say , Get-ChildItem
, would you get streaming (lazy) behavior.
# WISHFUL THINKING
$haveAny = $enumerable | Test-Any
# Variant with filter.
$haveAny = $enumerable | Test-Any { $_ -gt 100 }
The unconditional (filter-free) variant of Test-Any
can be emulated efficiently via a Select-Object
-First 1
$haveAny = 1 -eq ($enumerable | Select-Object -First 1).Count
Select-Object
can short-circuit pipeline input, taking advantage of an exception type that is private in Windows PowerShell and as of PowerShell (Core) v7.2. That is, there is currently no way for user code to stop a pipeline on demand, which the Test-Any
cmdlet would need to do in order to work efficiently (in order to prevent full enumeration):
A long-standing feature request can be found in GitHub issue #3821.
An efficient, lazy Test-Any
implementation that works around the limitation through use of a private PowerShell type can be found in this answer, courtesy of PetSerAl; aside from relying on a private type, you also incur an on-demand compilation performance penalty on first use in a session.
In a similar vein, GitHub issue #13834 asks that the .Where()
array method's advanced features be brought to its pipeline-based equivalent, the Where-Object
cmdlet (whose built-in alias is where
), which would then allow a solution such as:
# WISHFUL THINKING
$haveAny = 1 -eq ($enumerable | where { $_ -gt 1000 } -First).Count
Optional reading: Use of [Linq.Enumerable]::Any($enumerable)
with PowerShell arrays
1, 2, 3
(often represented as @(1, 2, 3)
, which is unnecessary, however) is an instance of an [object[]]
array, PowerShell's default array type.
It is unclear to me why you cannot pass such arrays to .Any()
as-is, given that it does work with an explicit cast to the same type: [Linq.Enumerable]::Any([object[]] (1,2,3)) # OK
Finally, an array constructed with a specific type can be passed as-is too:
$intArr = [int[]] (1, 2, 3); [Linq.Enumerable]::Any($intArr) # OK
If anyone knows why you cannot use an [object[]]
array in this context without an explicit cast and whether there's a good reason for that, please let us know.