PowerShell collection operators -in and -contain not acting as expected

39 Views Asked by At

I have a calculated PowerShell variable that may be set to $True (Boolean), or 'True' (String), or $False (Boolean), or 'False' (String), and I want to test if it's "True" (Boolean or string).

While I can easily frame the successfully test as below:

$MyVar = $false
If (($MyVar -eq $True) -or ($MyVar -eq 'True')) {1} Else {0}

0

If I change this to the more succinct format below, I get the unexpected result:

If ($MyVar -in @($True,'True')) {1} Else {0} 1

Can anyone explain why the collection operators -in and -contains behave this way? I've observed this behavior with PowerShell 5.1 and PWSH 7.4.1

I've tried numerous variations of this, and the same unexpected behavior is observed.

If ('False' -in @($true)) {1} else {0} 1

If (@($true) -contains 'False') {1} else {0} 1

This stackoverflow article: https://stackoverflow.com/questions/38002509/powershell-why-does-truth-contain-lies says: 'What's happening is that it is converting the right hand side of the operation to match the type of the left hand side';e.g. $true -eq "Lies" ~> $true -eq [Bool]"Lies" True

But that doesn't seem to be happening when I reverse the comparison elements here:

If ('False' -in @($true)) {1} else {0} 1

1

There are 1 best solutions below

0
mklement0 On

tl;dr

To avoid the pitfall described in the next section, use:

if ("$MyVar" -eq 'True') { 1 } Else { 0 }

That is, stringify your variable using an expandable (interpolating), double-quoted string ("...") and perform string comparison:

  • If $MyVar is a [bool] instance, it stringifies to either 'True' or 'False' irrespective of the current culture
  • If $MyVar already is a string, the enclosure in "..." is in effect a no-op.

Note that the -eq operator is case-insensitive by default; if you want to case-exact matching, use its case-sensitive variant, -ceq.


Type coercion in -in / -contains operations:

'False' -in @($true) is equivalent to @($true) -contains 'False'.

That is, the two operators involved, -in and -contains, are equivalent and differ only in which operand is the scalar vs. the collection to test containment in.

In both cases, it is the elements of the collection operand that effectively act as the LHS of the implied -eq comparison performed for each collection element against the scalar operand.

Therefore, both of the above are the effective equivalent of:

@($true).Where({ $_ -eq 'False' }, 'First').Count -eq 1

Given that it is generally[1] the LHS of operations that drives implicit type coercions in PowerShell, with -in and -contains it is therefore each collection element that drives type coercions.

With a [bool] instance as the LHS, as in the case at hand, the RHS is coerced to that type based on the rules described in about_Booleans. These rules notably include considering any nonempty string to be $true, irrespective of the string's content.

Thus, $true -eq 'False' is - perhaps surprisingly - $true, given that 'False' is a nonempty string.


[1] There are exceptions: - and / invariably interpret coerce both operands to numbers; e.g., '10' - '2' yields [int] 8. The reason is that with these specific operators interpretation as numbers is the only sensible interpretation - unlike with polymorphic operators such as + and *.