Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

powershell

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?

like image 880
Jeremy Fortune Avatar asked Nov 01 '18 19:11

Jeremy Fortune


People also ask

Which one of the following is a comparison operator in PowerShell?

Use comparison operators ( -eq , -ne , -gt , -lt , -le , -ge ) to compare values and test conditions. For example, you can compare two string values to determine whether they are equal. The comparison operators also include operators that find or replace patterns in text.

How do I compare two values in a PowerShell script?

To check to see if one object is equal to another object in PowerShell is done using the eq operator. The eq operator compares simple objects of many types such as strings, boolean values, integers and so on. When used, the eq operator will either return a boolean True or False value depending on the result.

What comparison operator does Windows PowerShell use for wildcard string comparisons?

The Like operator is a Powershell comparison operator that uses wildcards.

What is not equal in PowerShell?

PowerShell includes the following comparison operators: Equality. -eq , -ieq , -ceq - equals. -ne , -ine , -cne - not equals. -gt , -igt , -cgt - greater than.


2 Answers

tl;dr

In PowerShell conditionals / implicit Boolean contexts:

  • 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:
    • The following are implicitly $false:

      • ''/"" (empty string)

      • 0 (of any numeric type).

      • $null

        • Pitfall: Comparing $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 -eq $null # !! $false - unlike `$false -eq [bool] $null`
          
    • Pitfall: Any non-empty string evaluates to $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 $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, as is the special "null collection" value indicating the absence of output from a command, [System.Management.Automation.Internal.AutomationNull]::Value.

    • Pitfall: Single-element collections evaluate to:

      • If the one and only element is a scalar: its Boolean value
      • If that element is itself a collection: $true if it has at least 1 element (irrespective of what that element is).
    • 2+-element collections are always $true.

like image 174
mklement0 Avatar answered Sep 20 '22 19:09

mklement0


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.

like image 34
Maximilian Burszley Avatar answered Sep 21 '22 19:09

Maximilian Burszley