powershellnull

Why is piping null to set-variable causing different outcomes?


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?


Solution

  • Let me add some background information to Santiago Squarzon's helpful answer:

    Indeed, PowerShell has two types of null values:

    The 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:


    Given the fundamental behavioral differences, it is important:

    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.