When using set-variable I am seeing different behaviors when piping a null value to it.
In the first setup, we create a variable, and overwriting it with null, so far so good.
# Setup 1: Passing a null value
$test1 = "test1"
$null | set-variable test1
Write-Host "It is: $test1" # "It is: "
# $null.gettype()
# You cannot call a method on a null-valued expression.
However in the second setup, to my believe, we are also overwriting it with null, however the variable is not changed.
# Setup 2: Passing a null value by deducting a list
$testList = New-Object -TypeName 'System.Collections.ArrayList'
$testList.Add("test")
$test2 = "test2"
$testList | Select-Object -SkipLast 1 | set-variable test2
Write-Host "It is: $test2" # "It is: test2"
# $($testList | Select-Object -SkipLast 1).gettype()
# You cannot call a method on a null-valued expression.
Why is the first setup overwriting the variable and the second setup is not?
Let me add some background information to Santiago Squarzon's helpful answer:
Indeed, PowerShell has two types of null values:
The scalar null, so to speak, analogous to null
in C# and other languages, for instance, which is the value of the automatic $null
variable.
$null
is sent through the pipeline as-is.$null
as the input to a foreach
statement: foreach ($val in $null) { 'here!' }
produces no output, implying that the loop is never entered.$null
is what trying to access nonexistent variables evaluates to, unfortunately,[1] but its explicit use - other than in tests such as $null -eq $value
- is rare in PowerShell.The enumerable null, which is a PowerShell-specific concept:
You can think of it as an enumerable that enumerates nothing.
The term enumerable null for this special value is now used in the deep dive entitled Everything you wanted to know about $null, but historically it has also been called "Automation null" and sometimes "empty null".
Also, there is no automatic variable for it, i.e. there is no analog to the automatic $null
variable for the scalar null.
Given that the enumerable null is technically the "return value" of commands that produce no output, the simplest way to obtain it is to execute $nullEnum = & {}
, i.e. to execute an empty script block. Specifically, the enumerable null is the [System.Management.Automation.Internal.AutomationNull]::Value
singleton (the documentation link provides no meaningful information).
In a pipeline (e.g., $value | Write-Output
)...
... the enumerable null behaves like a collection with no elements, which means that, given that collections are enumerated in the pipeline (have their elements sent one by one), no data is sent through the pipeline, which is (typically) a no-op: the commands in subsequent pipeline segments receive no input to operate on.
Note that the automatic enumeration logic also applies to the switch
statement and to the LHS of comparison operators (which act as filters with enumerable LHS values); providing the enumerable null as input to switch
effectively skips the statement (e.g. switch (& {}) { default { 'never get here' } }
); whether the enumerable null is treated as an enumerable as the LHS of a comparison operation depends on the specific operator; -match
treats it as an enumerable ((& {}) -match ''
-> empty array), -eq
does not ((& {}) -eq ''
-> $false
)
In an expression (e.g, $null -eq $value
)...
$null
$null
invariably happens - see GitHub issue #9150The above explains the difference between $null | Set-Variable test1
(variable test
is set to the $null
value received via the pipeline) and & {} | Set-Variable test2
(variable test2
is never created or updated, because Set-Variable
receives no input; your Select-Object -SkipLast 1
call on the 1-element input collection produced no output, and therefore emitted the enumerable null).
See also:
This answer also touches on historic aspects of $null
vs. [System.Management.Automation.Null]
handling, covering the behavioral changes that happened in the transition from v2 to v3+.
This comment on GitHub issue #9150 summarizes how the null dichotomy could be handled in a consistent manner, if backward compatibility weren't a concern.
Given the fundamental behavioral differences, it is important:
to properly document these two null types, as well as give the enumerable null an official name
to make it easy to programmatically distinguish the two types.
As of this writing (PowerShell 7.5.0), the latter requirement isn't yet met.
Detecting the enumerable null is currently cumbersome and obscure:
$value = & {} # Obtain the enumerable null.
# Without the `-and $value -is [psobject]` part, you couldn't distinguish
# $null from the enumerable null.
$isEnumerableNull =
$null -eq $value -and $value -is [psobject]
While $null -eq $value
returns $true
for both a true $null
and the enumerable null, $value -is [psobject]
is only $true
for the enumerable null. The reason is that, unlike $null
, the enumerable null is technically an object of type [psobject]
(System.Management.Automation.PSObject
).
As a result of the discussion in GitHub issue #13465, the following improvement has been green-lit, but is yet to be implemented:
# NOT YET IMPLEMENTED as of PowerShell 7.5.0
$isNullEnumerable =
$value -is [System.Management.Automation.Null]
That is, you'll be able to use -is
, the type(-inheritance) / interface test operator with the yet-to-be-introduced [System.Management.Automation.Null]
type, which will supersede the "pubternal" [System.Management.Automation.Internal.AutomationNull]
type.[2]
Unfortunately, also introducing a type accelerator - which would simplify the test to $value -is [AutomationNull]
- was decided against.
[1] This default value is unfortunate, because it means that $noSuchVariable | ...
sends $null
through the pipeline. If the default were the "enumerable null" ("Automation null", [System.Management.Automation.Null]::Value
) instead, no data would be sent, the way the foreach
statement already - but surprisingly - handles $null
(too). With the enumerable null as the default, there would be no need for the asymmetry between pipeline and foreach
behavior, and $null
could consistently be preserved as such.
[2] This change involves more than just a new type name: the new type's singleton ([System.Management.Automation.Null]::Value
), i.e. the actual enumerable null, will then be of that same type, whereas the current singleton ([System.Management.Automation.Internal.AutomationNull]::Value
) is of a different type, namely just [psobject]
- see this GitHub comment for details.