Why do PowerShell comparison operators not enumerate collections of size 1?
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).
-
-
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
.
-
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.