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

2

3

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?

Keos answered 6/7, 2023 at 16:35 Comment(0)
P
2

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.

    • You can think of it as placeholder for a missing object.
    • $null is sent through the pipeline as-is.
    • Curiously, the same is not true for using $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, so to speak, which is a PowerShell-specific concept:

    • You can think of it as an enumerable that enumerates nothing.

    • Sadly, this special value has no official name as of this writing, thought it is often 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)...

      • ... the enumerable null behaves like $null
      • Additionally, when passing the enumerable null as an argument (parameter value) to a command, conversion to $null invariably happens - see GitHub issue #9150

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:

  • 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.3.6), neither requirement is met.

Detecting the enumerable null is currently cumbersome and obscure:

$value = & {} # Obtain the enumerable null.

# Without the `-and $value.psobject` part, you couldn't distinguish 
# $null from the enumerable null.
$isNullEnumerable = 
  $null -eq $value -and $value.psobject

While $null -eq $value returns $true for both a true $null and the enumerable null, $value.psobject only returns a value for the enumerable null (which, when coerced to a Boolean, evaluates to $true). The reason is that, unlike $null, the enumerable null is technically an object and therefore returns a value for the intrinsic psobject property.

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.3.6
$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.

Pylorectomy answered 6/7, 2023 at 19:26 Comment(0)
S
3

This is pretty much the difference between AutomationNull and $null. See Everything you wanted to know about $null, great in depth article.

$test2 = 'test2'
& { } | Set-Variable test2 # AutomationNull, Process block does not run!
$test2 # Outputs: test2
$null | Set-Variable test2 # $null is not AutomationNull, it does get piped
$test2 # Outputs: $null

You can use this example to get a better understanding of what is going on. As you may see, the process block of our function is never invoked when AutomationNull is piped through.

function Set-Var {
    param(
        [Parameter(ValueFromPipeline)]
        [object] $Value,

        [Parameter(Mandatory, Position = 0)]
        [string] $Name
    )

    process {
        $PSCmdlet.WriteVerbose("Setting value [$Value] to PSVariable [$Name]")
        $psvar = $PSCmdlet.SessionState.PSVariable.Get($Name)
        $psvar.Value = $Value
        $PSCmdlet.SessionState.PSVariable.Set($psvar)
    }
}

$test = 'hello'
$null | Set-Var test -Verbose
# VERBOSE: Setting value [] to PSVariable [test]
$test # Outputs: $null
'foo' | Set-Var test -Verbose
# VERBOSE: Setting value [foo] to PSVariable [test]
$test # Outputs: foo
& { } | Set-Var test -Verbose
# No verbose output, meaning the Process Block was never invoked!
$test # Outputs: foo
Sofiasofie answered 6/7, 2023 at 17:17 Comment(0)
P
2

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.

    • You can think of it as placeholder for a missing object.
    • $null is sent through the pipeline as-is.
    • Curiously, the same is not true for using $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, so to speak, which is a PowerShell-specific concept:

    • You can think of it as an enumerable that enumerates nothing.

    • Sadly, this special value has no official name as of this writing, thought it is often 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)...

      • ... the enumerable null behaves like $null
      • Additionally, when passing the enumerable null as an argument (parameter value) to a command, conversion to $null invariably happens - see GitHub issue #9150

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:

  • 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.3.6), neither requirement is met.

Detecting the enumerable null is currently cumbersome and obscure:

$value = & {} # Obtain the enumerable null.

# Without the `-and $value.psobject` part, you couldn't distinguish 
# $null from the enumerable null.
$isNullEnumerable = 
  $null -eq $value -and $value.psobject

While $null -eq $value returns $true for both a true $null and the enumerable null, $value.psobject only returns a value for the enumerable null (which, when coerced to a Boolean, evaluates to $true). The reason is that, unlike $null, the enumerable null is technically an object and therefore returns a value for the intrinsic psobject property.

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.3.6
$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.

Pylorectomy answered 6/7, 2023 at 19:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.