Reputation: 842
I just came across an unexpected behaviour of Where-Object
which I couldn't find any explanation for:
$foo = $null | Where-Object {$false}
$foo -eq $null
> True
($null, 1 | Measure-Object).Count
> 1
($foo, 1 | Measure-Object).Count
> 1
($null, $null, 1 | Measure-Object).Count
> 1
($foo, $foo, 1 | Measure-Object).Count
> 0
If the condition of Where-Object
is false, $foo
should be $null
(which appears to be correct).
However, piping $foo
at least twice before any value into the pipeline seems to break it.
What is causing this?
Other inconsistencies:
($foo, $null, 1 | Measure-Object).Count
> 1
($foo, $null, $foo, 1 | Measure-Object).Count
> 0
($null, $foo, $null, 1 | Measure-Object).Count
> 1
($foo, 1, $foo, $foo | Measure-Object).Count
> 1
($null, $foo, $null, $foo, 1 | Measure-Object).Count
> 0
Upvotes: 4
Views: 907
Reputation: 437197
tl;dr:
Not all apparent $null
values are the same, as Jeroen Mostert's comments indicate: PowerShell has two types of null that situationally behave differently - see the next section.
Additionally, you're seeing perhaps surprising Measure-Object
behavior and a pipeline bug - see the bottom section.
It's best to eliminate Measure-Object
from your test commands and simply invoke .Count
directly on your arrays; e.g. (the simplest way to create the type of null as in your question is: $foo = & {}
):
($foo, $null, 1).Count
yields 3
($null, $foo, $null, $foo, 1).Count
yields 5
As you can see, both types of null (discussed below) properly become elements of an array.
There are two distinct kinds of null values in PowerShell:
There's bona fide scalar null (corresponding to null
in C#, for instance).
$null
variable.There's also the enumerable "collection null" (also called "AutomationNull", based on its class name), which is technically the System.Management.Automation.Internal.AutomationNull.Value
singleton, which is itself a [psobject]
instance.
& {}
, i.e. by executing an empty script block; of course, you can also use [System.Management.Automation.Internal.AutomationNull]::Value
explicitly).Unfortunately, the collection null value is nontrivial to distinguish from the scalar null, as of PowerShell 7.2:
GitHub issue #13465 proposes allowing detection of collection null via $var -is [AutomationNull]
in a future PowerShell version.
For now, there are several workarounds for testing whether a given value $var
contains collection null; perhaps the simplest (but non-obvious) is:
$null -eq $var -and $var -is [psobject]
is $true
only if $var
contains the collection null value, because only collection null is technically an object.Behavioral differences:
In expression contexts and in parameter binding, there is no difference in that collection null is implicitly converted to $null
.
Note that this means that you cannot pass collection null as an argument - see the discussion in GitHub issue #9150.
The exception in the context of expressions is the LHS of operators that support collections as their LHS: they treat collection null as an empty collection and therefore evaluate to an empty array (@()
) rather than $null
:
$var -replace 'foo' | ForEach-Object { 'hi' }
prints 'hi'
only if $var
is scalar $null
, not with with collection null, because the -replace
operation then outputs an empty array, which sends nothing through the pipeline.In the pipeline:
Scalar $null
is sent through the pipeline - it behaves like a single object: $null | ForEach-Object { '$_ is $null? ' + ($null -eq $_) }
prints '$_ is $null? True'
;
Collection null is not sent through the pipeline - it behaves like a collection without elements; that is, just like @() | ForEach-Object { 'hi' }
(sending an empty array), & {} | ForEach-Object { 'hi' }
sends nothing through the pipeline, because there is nothing to enumerate, and therefore never outputs 'hi'
.
Curiously, by contrast, in a foreach
loop statement (as opposed to the ForEach-Object
cmdlet) scalar $null
too is not enumerated and the loop body is never entered in the following (ditto for collection null):
foreach ($i in $null) { 'hi' }
Measure-Object
and pipeline problems:
Measure-Object
generally ignores $null
values, presumably by design.
-IncludeNull
switch to support considering $null
values on an opt-in basis. (The default behavior will not change so as not to break backward compatibility.)However, you've discovered an outright bug in PowerShell's pipeline with respect to multi-object input involving collection nulls (as of PowerShell 7.1.2) , which Measure-Object
only surfaces, as you've noted yourself:
On encountering a second collection null in multi-object input, sending objects through the pipeline unexpectedly stops:
(1, (& {}), 2, (& {}), 3, 4, 5 | Measure-Object).Count
yields just 2
: only 1
and 2
are counted (the collection nulls themselves are not sent through the pipeline), because the second collection null unexpectedly stops enumeration, so that the remaining objects - 3
, 4
, and 5
- aren't even sent to Measure-Object
.See GitHub issue #14920.
Upvotes: 6
Reputation: 842
To add to mklement0's very detailed and much appreciated answer, I want to share the workaround I used:
$numbers = 3, 42, 7, 69, 13
$no1 = $numbers | Where-Object {$_ -eq 1}
$no2 = $numbers | Where-Object {$_ -eq 2}
$no3 = $numbers | Where-Object {$_ -eq 3}
Instead of piping the variables directly to ForEach-Object
, which produces no output ... :
$no1, $no2, $no3 | ForEach-Object {$_}
>
... pipe the variable names to ForEach-Object
and make use of Get-Variable
to get the desired result:
'no1', 'no2', 'no3' | ForEach-Object {(Get-Variable $_).Value}
> 3
Upvotes: 0