8

I am trying to understand the behavior of the @() array constructor, and I came across this very strange test.

It seems that the value of an empty pipeline is "not quite" the same as $null, even though it is -eq $null

The output of each statement is shown after the ###

$y = 1,2,3,4 | ? { $_ -ge 5 }
$z = $null

if ($y -eq $null) {'y is null'} else {'y NOT null'}  ### y is null
if ($z -eq $null) {'z is null'} else {'z NOT null'}  ### z is null

$ay = @($y)
$az = @($z)

"ay.length = " + $ay.length  ### ay.length = 0
"az.length = " + $az.length  ### az.length = 1

$az[0].GetType()  ### throws exception because $az[0] is null

So the $az array has length one, and $az[0] is $null.

But the real question is: how is it possible that both $y and $z are both -eq $null, and yet when I construct arrays with @(...) then one array is empty, and the other contains a single $null element?

John Rees
  • 1,553
  • 17
  • 24

2 Answers2

11

Expanding on Frode F.'s answer, "nothing" is a mostly magical value in PowerShell - it's called [System.Management.Automation.Internal.AutomationNull]::Value. The following will work similarly:

$y = 1,2,3,4 | ? { $_ -ge 5 }
$y = [System.Management.Automation.Internal.AutomationNull]::Value

PowerShell treats the value AutomationNull.Value like $null in most places, but not everywhere. One notable example is in a pipeline:

$null | % { 'saw $null' }
[System.Management.Automation.Internal.AutomationNull]::Value | % { 'saw AutomationNull.Value' }

This will only print:

saw $null

Note that expressions are themselves pipelines even if you don't have a pipeline character, so the following are roughly equivalent:

@($y)
@($y | Write-Output)

Understanding this, it should be clear that if $y holds the value AutomationNull.Value, nothing is written to the pipeline, and hence the array is empty.

One might ask why $null is written to the pipeline. It's a reasonable question. There are some situations where scripts/cmdlets need to indicate "failed" without using exceptions - so "no result" must be different, $null is the obvious value to use for such situations.

I've never run across a scenario where one needs to know if you have "no value" or $null, but if you did, you could use something like this:

function Test-IsAutomationNull
{
    param(
        [Parameter(ValueFromPipeline)]
        $InputObject)

    begin
    {
        if ($PSBoundParameters.ContainsKey('InputObject'))
        {
            throw "Test-IsAutomationNull only works with piped input"
        }
        $isAutomationNull = $true
    }
    process
    {
        $isAutomationNull = $false
    }
    end
    {
        return $isAutomationNull
    }
}

dir nosuchfile* | Test-IsAutomationNull
$null | Test-IsAutomationNull
Jason Shirk
  • 7,734
  • 2
  • 24
  • 29
  • 1
    Excellent description. I'm stunned that there is more than one null. And after googling further I find a third: System.Management.Automation.Language.NullString. Since all these nulls are -eq $null, how can I actually test which null is in a variable? (Not that it's a common requirement!) – John Rees Mar 13 '14 at 19:12
  • 2
    Special nulls are nothing new, e.g. there is System.DBNull.Value. PowerShell doesn't treat these other special null values like null though, e.g. [NullString]::Value -ne $null. – Jason Shirk Mar 13 '14 at 19:34
  • Thanks for the update with the Test-IsAutomationNull. I find it almost comical that you need to run a pipeline, detect that it doesn't process anything, and conclude that it must have received a AutomationNull. I guess that's what you get when a language starts using magic values. Cheers. – John Rees Mar 13 '14 at 19:38
  • I find the DBNull approach sometimes frustrating (in that the code ends up being more verbose to test for DBNull), but at least it is less magical than AutomationNull. – John Rees Mar 13 '14 at 19:41
  • 1
    Now I understand why "Test-IsAutomationNull only works with piped input". When you pass AutomationNull as a function parameter, it seems to be automatically converted to a normal $null. Right? – John Rees Mar 13 '14 at 19:47
  • 2
    You can test for AutomationNull without using a pipeline. Just wrap your "null" value in array enclosures. If the count of that array is 0, you have an AutomationNull. If the count of that array is 1, you have $null. – Kirk Munro May 30 '17 at 20:11
4

The reason you're experiencing this behaviour is becuase $null is a value. It's a "nothing value", but it's still a value.

PS P:\> $y = 1,2,3,4 | ? { $_ -ge 5 }

PS P:\> Get-Variable y | fl *

#No value survived the where-test, so y was never saved as a variable, just as a "reference"

Name        : y
Description : 
Value       : 
Visibility  : Public
Module      : 
ModuleName  : 
Options     : None
Attributes  : {}


PS P:\> $z = $null


PS P:\> Get-Variable z | fl *

#Our $null variable is saved as a variable, with a $null value.

PSPath        : Microsoft.PowerShell.Core\Variable::z
PSDrive       : Variable
PSProvider    : Microsoft.PowerShell.Core\Variable
PSIsContainer : False
Name          : z
Description   : 
Value         : 
Visibility    : Public
Module        : 
ModuleName    : 
Options       : None
Attributes    : {}

The way @() works, is that it guarantees that the result is delievered inside a wrapper(an array). This means that as long as you have one or more objects, it will wrap it inside an array(if it's not already in an array like multiple objects would be).

$y is nothing, it's a reference, but no variable data was stored. So there is nothing to create an array with. $z however, IS a stored variable, with nothing(null-object) as the value. Since this object exists, the array constructor can create an array with that one item.

Frode F.
  • 52,376
  • 9
  • 98
  • 114
  • Thanks for the insight and the clarification about @($null), but Jason explains the difference between the two nulls a bit better. Cheers. – John Rees Mar 13 '14 at 19:06