Schrödinger's -ArgumentList
Last night on my way home I was feeling slightly mischievous, and so I dropped a #PowerShell brain teaser into the PowerShell Slack/Discord bridge, with a challenge - let's have a look at it!
The puzzle
function Get-True {
$args[0] -eq $null -and $args[0] -ne $null
}
The challenge is simple - the name of the function is Get-True
, so all I'm asking is that you provide an argument for which that will be the case - it should return $true
- simple, right? ;-)
Described as a pester test, it might look something like this:
function Get-True {
$args[0] -eq $null -and $args[0] -ne $null
}
Describe 'Get-True' {
It 'Returns $true' {
$MagicSauce = $( <# what needs to go here...? #> )
Get-True $MagicSauce | Should BeExactly $true
}
}
If you like these sorts of puzzles, I encourage you to have a go at solving this, then come back and read the rest :-)
Cracking the code
A number of people immediately started throwing $null
-like values at it - DbNull
, NullString
, AutomationNull
etc, presumably in the hopes that the $null
-likeness of them would confuse PowerShell into accepting them as being both $null
and not $null
at the same time, except...
Someone also suggested testing with @($null,123)
- extremely close, but still a bit of way away...
After watching numerous brute-force attempts with random scalar values (and 2 people who DM'd me a correct solution, although they were unsure how to explain why it worked), I decided to drop a hint - well, a spoiler actually - into the public channel.
A working solution...
Here's an expression that'll result in the kind of argument that'll solve the challenge:
Needless to say, it caused even more confusion to most, so let's try and step through how this is evaluated. At first, we can discard the enclosing scriptblock, as it's being immediately executed thanks to the &
call operator.
Now, to figure out how the remaining part (,$null*2 + 1
) is evaluated, we'll need to know in which order the different operators will apply - we see a ,
, a *
and a +
- so let's check out the about_Operator_Precedence
help file to see how they rank in order of precedence.
On inspection, we'll find that the comma operator ,
ranks higher than any arithmetic operators, so that one will apply first - after which we'll just need to remember some 4th grade math - namely that multiplication takes precedence over addition. I decided to expand the expression with qualifying sub expressions using parentheses below:
,$null*2 + 1
(,$null)*2 + 1 # , takes precedence
((,$null)*2) + 1 # * goes next
(@($null)*2) + 1 # first expression expands to a one-item array
(@($null,$null)) + 1 # repeated twice gives us a two-item array
@($null,$null,1) # and the addition of 1 leaves us with a three-item array
(Am I the only one who thinks it looks kinda like a pirate ship sailing along?)
... Going back to the original puzzle, this ends up working:
PS C:\> $Value = $null,$null,1
PS C:\> Get-True $Value
True
In fact, any array containing at least one non-$null
value, and at least 2 $null
values will make it work - but why?!
Tricksy little operatesors
In order to understand why @($null,$null,1)
makes it give the correct answer, we'll need to inspect the individual components of Get-True
, so let's break it down, using our newfound insights about operator precedence in PowerShell:
# This is the full boolean expression we're evaluating
$args[0] -eq $null -and $args[0] -ne $null
($args[0] -eq $null) -and ($args[0] -ne $null)
While we're at it, let's rename $args[0]
for the purposes of this explanation:
$something = $args[0]
$something -eq $null
$something -ne $null
Do you see it yet? What if I substitute a known correct solution?
$something = $null,$null,1
$something -eq $null
$something -ne $null
Now we're getting to the crux of the challenge: how do PowerShell comparison operators even work?
Well, it turns out that PowerShell comparison operators are magical contextually dependent - they have two modes of operation, depending on whether the left-hand side expression is a scalar (a single thing) or a list (an array, a list, a datatable, any sort of collection really).
Comparisons in scalar mode
In scalar mode, the comparisons work just the way you expect - they compare the left-hand expression to the (potentially type-coerced) right-hand side operand, and return a boolean result of the comparison - it's either $true
or $false
:
$thing = Get-Random -Min 0 -Max 10
$aValue = 5
$result = $thing -eq $aValue # $result now has a value of either $true or $false
Comparisons in list mode
In list mode (or collection mode), the comparison operators becomes something else completely - they start acting as sieves or filters (which is why you may have heard me or others refer to this as filter mode). So what does that mean? Let's look at an example:
$list = 1,2,3,4,5,1,2,3,4,5
$result = $list -eq 2 # $result now has a value of @(2,2) - wat?
That's right, in list mode, applying a comparison operator is almost like piping to Where-Object
or calling the .Where({})
extension method:
$list = 1,2,3,4,5,1,2,3,4,5
# These are functionally equivalent!
$result = $list -eq 2
$result = $list.Where({$_ -eq 2})
$result = $list |Where {$_ -eq 2}
What is $true
?
Now that we know about list mode comparisons, we can go back to our broken down version of the puzzle:
$something = $null,$null,1
# The result of filtering with `-eq $null`
$something -eq $null
$null,$null
# The result of filtering with `-ne $null`
$something -ne $null
1
So now the topmost expression in our Get-True
function - the -and
operation ends up looking like this:
($null,$null) -and (1)
The -and
operation returns $true
if, and only if both operands evaluate to $true
. This also implies that it attempts to coerce the value of each operand to [bool]
.
If we start from the right, the first one is easy - [bool](1)
is $true
- the general rule of thumb to apply here being that any numeric type (an integer, a float, a byte, doesn't matter) with a non-zero value - such as 1
- will be treated as $true
, any value zero will be treated as $false
.
For the second one, it becomes a bit more tricky - for collections, the rule of thumb is that non-empty collections evaluate to $true
, empty collections evaluate to $false
- but there is an exception. The exception is single-item arrays - PowerShell will squash the array and coerce the single element inside, and so @($null)
- even thought it's non-empty it will still evaluate to $false
when cast to [bool]
, because $null
is $false
when cast to [bool]
- hence the failure of @($null,123)
to solve the puzzle:
[bool]@() # $false
[bool]@($null) # $false
[bool]@($null,$null) # $true
Conclusion
I hope you found my puzzle fun, or at the very least gained some insights into how PowerShell operators work!
In closing, I've tried to expand the entire expression step-by-step below for your viewing pleasure:
$args[0] -eq $null -and $args[0] -ne null
($args[0] -eq $null) -and ($args[0] -ne null)
(@($null,$null,1) -eq $null) -and (@($null,$null,1) -ne $null)
@($null,$null) -and @(1)
$true -and $true
$true
PS - PSScriptAnalyzer can help
If you want to avoid having to hunt down and troubleshoot behavior stemming from this (and other quirky behaviors related to how $null
is treated in different contexts in PowerShell), consider running PSScriptAnalyzer
against your codebase and correct instances flagged by the rule PossibleIncorrectComparisonWithNull
- it might save you a future headache!
If you want to know more about $null
and its behavior, check out Kevin Marquette's excellent article on the subject here