I am trying to write a PowerShell function that can take an input object and return whether or not it is a collection. For my purposes, I do NOT wish to treat a string as a collection even though "stringLiteral" -is [System.Collections.IEnumerable]
returns True
in PowerShell, so I wrote my function like so:
function IsCollection {
[CmdletBinding()]
param (
[Parameter( Mandatory = $true )]
[object] $Input
)
if ( $Input -is [System.Collections.IEnumerable] -AND $Input -isnot [string] ) {
return $true
}
else {
return $false
}
}
However, when trying to test this function directly in a fresh PowerShell console session, I am getting what appears to be inconsistent behavior between the function and commands issued directly in the console. Here is what I am seeing, copied directly from the session:
PowerShell 7.3.9
Version: 7.3.9
Loading personal and system profiles took 567ms.
PS C:\Users\ornsio> function IsCollection {
>> [CmdletBinding()]
>> param (
>> [Parameter( Mandatory = $true )]
>> [object] $Input
>> )
>>
>> if ( $Input -is [System.Collections.IEnumerable] -AND $Input -isnot [string] ) {
>> return $true
>> }
>> else {
>> return $false
>> }
>> }
PS C:\Users\ornsio> IsCollection "test"
True
PS C:\Users\ornsio> IsCollection 23
True
PS C:\Users\ornsio> $lump = [object]::new()
PS C:\Users\ornsio> IsCollection $lump
True
PS C:\Users\ornsio> $lump -is [System.Collections.IEnumerable]
False
PS C:\Users\ornsio> $lump -is [string]
False
Why does the function always return True
?
My first thought was that maybe it is treating the $Input
argument as a true [object]
type without being able to "see" its runtime type (which wouldn't really make a lot of sense), but as you can see above, when creating an [object]
type variable directly in the console and then running the type checks against it, the results still don't match up.
I have tried Googling this, but this is a hard one to make a search engine "understand", and I have not had any luck.
Leaving aside what interface you should be testing for, the sole problem with your code is the accidental use of the automatic
$input
variable, which should never be used for custom purposes:It is unfortunate that PowerShell doesn't prevent such custom use, which leads to subtle bugs, as in your case:
That is, the attempt to use
$Input
as a parameter (variable) was effectively ignored.The customary name for a non-type-constrained parameter is
$InputObject
(though it is typically also declared with[Parameter(ValueFromPipeline)]
to allow binding via the pipeline, which in your case wouldn't be useful).The following function,
Test-Enumerability
:shows the use of an
$InputObject
parameter.follows PowerShell's naming conventions
is an enhanced implementation that indicates for a given object whether its use in PowerShell's pipeline would result in its enumeration.
Note:
The logic has been gleaned from PowerShell's source code.
There is a subtlety with respect to
System.Collections.IDictionary
, i.e. with dictionary types such as (ordered) hashtables:As of PowerShell (Core) 7.3.x, instances of a type that only implements the generic version of that interface,
System.Collections.Generic.IDictionary`2
, are enumerated - unexpectedly.While most types that implement the latter also implement the former, there are some that don't: see GitHub issue #15204.
System.Collections.IEnumerable
/System.Collections.Generic.IEnumerable`1
interface pair, because the latter derives from the former.