Why do PowerShell comparison operators not enumerate collections of size 1?
Asked Answered
P

2

5

When checking variables and collections of variables for nullity, comparison operators seem to enumerate collections of size 2 or more:

> if ( @( $null, $null ) -eq $null ) { $True } else { $False }
True

But they do not for collections of size 1:

> if ( @( $null ) -eq $null ) { $True } else { $False }
False

I'm aware that it's best practice to null-compare using the left-hand side ($null -eq @( $null )), but can someone explain what's happening here? I suspect there's something more subtle happening that impacts other code that I write.

Why are these two results different?

Piled answered 1/11, 2018 at 19:11 Comment(0)
H
4

The following items evaluate to $false:

@()
0
$null
$false
''

In your first example:

@($null, $null) -eq $null

This evaluates to $null, $null which is a non-zero collection, so it is $true. You can observe this with the following:

[bool]($null, $null)

In your second example, what you're observing is filtering of an array like the first case, but returning a scalar (instead of an array) since only one item of the array matched the filter:

@($null) -eq $null

This evaluates to @($null) but powershell is evaluating it as a scalar in a boolean context, so it returns $false, observed by:

[bool]@($null)

Footnote: in powershell v2, there was a bug with $null filtering which spawned the left-hand $null comparison. This bug caused if/else blocks to be skipped entirely.

Heulandite answered 1/11, 2018 at 19:26 Comment(0)
F
8

tl;dr

In PowerShell conditionals / implicit Boolean contexts and [bool] casts:

  • Single-element arrays are treated like scalars: that is, their one and only element itself is interpreted as a Boolean.[1]

  • 2+-element arrays are always $true, irrespective of their content.


With an array as the LHS, array-aware operators such as -eq invariably also output an array.

Since your array elements are all $null and you compare to $null, your comparison is an effective no-op - e.g., @( $null ) -eq $null results in @( $null ) - and your conditionals are equivalent to:

[bool] @( $null, $null ) # -> $true - array with 2+ elements is always $True
[bool] @( $null )        # -> $false(!) - treated like: [bool] $null

Perhaps surprisingly, the implicit Boolean logic applies pipeline logic to an array:

That is, a single-element array is (conceptually) unwrapped and its element is interpreted as a Boolean.

Therefore, [bool] @( $null ) is treated the same as [bool] $null, which is $false.

Generally, @( <one-and-only-element> ) (or , <one-and-only-element>) is treated the same as <one-and-only-element> in a Boolean context.

By contrast, if an array has 2 or more elements, it is always $true in a Boolean context, even if all its elements would individually be considered $false.


Workaround for testing whether an arbitrary array is empty:

Base your conditional on the .Count property:

if ( (<array>).Count ) { $true } else { $false }

You could append -gt 0, but that's not strictly necessary, because any nonzero value is implicitly $true.

Applied to your example:

PS> if ( ( @($null) -eq $null ).Count ) { $true } else { $false }
True

Testing an arbitrary value for being a (scalar) $null:

if ($null -eq <value>) { $true } else { $false }

Note how $null must be used as the LHS in order to prevent the array-filtering logic from taking effect, should <value> be an array.

That's also the reason why Visual Studio Code with the PowerShell extension advises "$null should be on the left side of comparisons" if you write something like $var -eq $null.


[1] To-Boolean conversion summary:

  • Among scalars (non-collections):

    • The following are implicitly $false:

      • ''/"" (empty string)

      • 0 (of any numeric type).

      • $null, including the "enumerable null" value indicating the absence of output from a command (see this answer for background information); e.g. both
        [bool] $null and [bool] (Get-ChildItem NoSuchFiles*) yield $false.

        • Pitfall: Comparing $null or the "enumerable null" to a Boolean explicitly with -eq is always $false, even with $null as the RHS (despite the RHS normally getting coerced to the type of the LHS):

          # !! -> $false
          # By contrast, `$false -eq [bool] $null` is $true
          $false -eq $null
          
          # Ditto for the "enumerable null"
          # !! -> $false
          $false -eq (Get-ChildItem NoSuchFiles*)
          
    • Pitfall: Any non-empty string is always $true:

      • e.g., [bool] 'False' is $true

      • Note that this differs from explicit string parsing: [bool]::Parse('false') does return$false (and $true for 'true', but recognizes nothing else).

    • Instances of any other non-collection type are implicitly always $true, including of type [pscustomobject] and [hashtable] (which PowerShell treats as a single object, not as a collection of entries).

      • Unfortunately, this includes types that define explicit [bool] .NET conversion operators, meaning that these operators are - mostly - not honored; see this answer.
  • Among collections such as arrays (more accurately, collection-like types that implement the IList interface - see the source code):
    • Empty collections are always $false.

    • Pitfall: Single-element collections are evaluated as that element:

      • If the one and only element is a scalar (non-collection): its Boolean value.

        • E.g., [bool] @(0) is $false, whereas [bool] @(42) is $true
      • If that element is itself a collection: $true if it has at least 1 element - irrespective of what that element is.

        • E.g., [bool] (,, $false) is $true (a jagged array: a single-element array whose only element is itself a (single-element) array)
    • 2+-element collections are always $true, irrespective of their element values.

      • E.g., [bool] @($null, $false) is $true.
Flabby answered 1/11, 2018 at 19:29 Comment(0)
H
4

The following items evaluate to $false:

@()
0
$null
$false
''

In your first example:

@($null, $null) -eq $null

This evaluates to $null, $null which is a non-zero collection, so it is $true. You can observe this with the following:

[bool]($null, $null)

In your second example, what you're observing is filtering of an array like the first case, but returning a scalar (instead of an array) since only one item of the array matched the filter:

@($null) -eq $null

This evaluates to @($null) but powershell is evaluating it as a scalar in a boolean context, so it returns $false, observed by:

[bool]@($null)

Footnote: in powershell v2, there was a bug with $null filtering which spawned the left-hand $null comparison. This bug caused if/else blocks to be skipped entirely.

Heulandite answered 1/11, 2018 at 19:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.