powershellpowershell-7.3

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


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.


Solution

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

    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: