Discrepancy in PowerShell type checking operators between function and console (PS7)?

64 views Asked by At

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.

2

There are 2 answers

1
mklement0 On BEST ANSWER

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:

1
Santiago Squarzon On

Your function's name has actually the interface you should be using for your comparison, ICollection:

function IsCollection {
    [CmdletBinding()]
    param (
        [Parameter( Mandatory = $true )]
        [object] $InputObject
    )

    $InputObject -is [System.Collections.ICollection]
}

IsCollection @{}    # true
IsCollection @()    # true
IsCollection string # false

If you want to include lists and arrays but exclude dictionary types, then change to IList:

function IsIList {
    [CmdletBinding()]
    param (
        [Parameter( Mandatory = $true )]
        [object] $InputObject
    )

    $InputObject -is [System.Collections.IList]
}

IsIList @{}    # false
IsIList @()    # true
IsIList string # false