Search code examples
.netpowershellienumerable

Check if IEnumerable is empty in PowerShell


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.


Solution

  • 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:

    • not exactly obvious and therefore hard to remember.
    • more importantly, this approach cannot take advantage of the pipeline, which is how PowerShell implements lazy (on-demand) enumeration, and this limitation applies to all .NET method calls, including [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:

    • receives input via the pipeline, in a streaming fashion (object by object).
    • outputs $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.