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:
# !! The attempt use of $input as a custom *parameter variable*
# !! is preempted by the *built in* $input value, representing *pipeline input*
# !! -> 'Object[]'
& { [CmdletBinding()] param([object] $input) $input.GetType().Name } 42
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.
function Test-Enumerability {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[object] $InputObject
)
($InputObject -is [System.Collections.IEnumerable] -and
$InputObject -isnot [System.Collections.IDictionary] -and
$InputObject -isnot [string] -and
$InputObject -isnot [System.Xml.XmlNode]
) -or
$InputObject -is [System.Data.DataTable] -or
$InputObject -is [System.Collections.IEnumerator]
}
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.