How do I assign a null value to a variable in PowerShell? - powershell

I want to assign a null value to a variable called $dec, but it gives me errors. Here is my code:
import-module activedirectory
$domain = "domain.example.com"
$dec = null
Get-ADComputer -Filter {Description -eq $dec}

These are automatic variables, like $null, $true, $false etc.
about_Automatic_Variables, see https://technet.microsoft.com/en-us/library/hh847768.aspx?f=255&MSPPError=-2147217396
$NULL
$null is an automatic variable that contains a NULL or empty
value. You can use this variable to represent an absent or undefined
value in commands and scripts.
Windows PowerShell treats $null as an object with a value, that is, as
an explicit placeholder, so you can use $null to represent an empty
value in a series of values.
For example, when $null is included in a collection, it is counted as
one of the objects.
C:\PS> $a = ".dir", $null, ".pdf"
C:\PS> $a.count
3
If you pipe the $null variable to the ForEach-Object cmdlet, it
generates a value for $null, just as it does for the other objects.
PS C:\ps-test> ".dir", $null, ".pdf" | Foreach {"Hello"}
Hello
Hello
Hello
As a result, you cannot use $null to mean "no parameter value." A
parameter value of $null overrides the default parameter value.
However, because Windows PowerShell treats the $null variable as a
placeholder, you can use it scripts like the following one, which
would not work if $null were ignored.
$calendar = #($null, $null, “Meeting”, $null, $null, “Team Lunch”, $null)
$days = Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"
$currentDay = 0
foreach($day in $calendar)
{
if($day –ne $null)
{
"Appointment on $($days[$currentDay]): $day"
}
$currentDay++
}
output:
Appointment on Tuesday: Meeting
Appointment on Friday: Team lunch

Use $dec = $null
From the documentation:
$null is an automatic variable that contains a NULL or empty value. You can use this variable to represent an absent or undefined value in commands and scripts.
PowerShell treats $null as an object with a value, that is, as an explicit placeholder, so you can use $null to represent an empty value in a series of values.

If the goal simply is to list all computer objects with an empty description attribute try this
import-module activedirectory
$domain = "domain.example.com"
Get-ADComputer -Filter '*' -Properties Description | where { $_.Description -eq $null }

As others have said, use $null.
However, the handling of $null is not so simple.
In lists (or, more precisely, System.Array objects) $null is treated as a placeholding object when indexing the list, so ($null, $null).count outputs 2.
But otherwise $null is treated as a flag signifying that there is no content (no object; or, more precisely, a "null-valued expression", as reported by .GetType()), so ($null).count outputs 0.
Thus
$null.count; # Output = 0
($null).count; # Output = 0
(, $null).count; # Output = 1
($null, $null).count; # Output = 2
($null, $null, $null).count; # Output = 3
Note: the same output is returned from .count and .length in the above context.
Similarly if explicitly assigning any of the above to a variable, as in
$aaa = $null; $aaa.count
$bbb = ($null, $null); $bbb.count
which output, respectively, 0 and 2.
Similarly if looping with ForEach, as in
$aaa = $null; ForEach ($a in $aaa) {write-host "Foo" -NoNewLine}
$bbb = ($null, $null); ForEach ($b in $bbb) {write-host "Bar" -NoNewLine}
which output, respectively, nothing and BarBar.
However, note well that when operating on an individual item that has been returned from a list $null is again treated as a "null-valued expression", as can be confirmed by running
$xxx = ($null, "foo", $null); ForEach ($x in $xxx) {write-host "C=" $x.count "| " -NoNewLine}
which outputs C= 0 | C= 1 | C= 0 | .

Related

Comparing Variables within Powershell

Ok. So I thought this would have been easy, but I am hitting a snag.
$var = (Get-ItemProperty "HKCU:\SOFTWARE\SAP\General" -Name "BrowserControl")."BrowserControl"
$var2 = "HKCU:\SOFTWARE\SAP\General"
$var3 = #('1','0')
#if (($var -eq ($var3 -join'')))
#if (Compare-Object -IncludeEqual $var $var3 -SyncWindow 0)
if ($var -eq $var3)
{
Write-Output "Registry hive exists"
exit 1
}
else
{
Write-Output "Registry hive doesn't exists"
#New-ItemProperty -Path $var2 -name "BrowserControl" -Value "1" -PropertyType "DWORD" -Force | Out-Null
}
If 1 or 0 is returned from BrowserControl, I want it to be a match. If anything else is returned, no match.
If BrowserControl is set to 1, it works. If it is set to 0 or any number other than 1 it doesn't match.
I know I can use else-if and add a couple more lines of code, but I was really wanting to get this to work.
As you can see, I have tried different comparison methods. I also tried (0,1), ('0','1'), 0,1 for var3. None of those worked either.
So... what am I missing?
You cannot meaningfully use an array as the RHS (right-hand side) of the -eq operator.[1]
However, PowerShell has dedicated operators for testing whether a given single value is contained in a collection (more accurately: equal to one of the elements of a collection), namely -in and its operands-reversed counterpart, -contains.
In this case, -in makes for more readable code:
if ($var -in $var3) # ...
[1] PowerShell quietly accepts an array (collection) as the RHS, but - uselessly - stringifies it, by concatenating the elements with a single space by default. E.g., '1 2' -eq 1, 2 yields $true.
By contrast, using an array as the LHS of -eq is meaningfully supported: the RHS scalar then acts as a filter, returning the sub-array of equal LHS elements; e.g. 1, 2, 3, 2 -eq 2 returns 2, 2

Unexpected Behaviour with Where-Object

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
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).
This null is contained in the automatic $null variable.
.NET methods may return it. (While PowerShell code may output it too, doing so is best avoided).
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.
This value is technically output by the pipeline when PowerShell commands (both binary cmdlets and PowerShell scripts/functions) produce no output.
The simplest way to get this value is with & {} , 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:
E.g., $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.
See GitHub issue #3866.
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.
This is discussed in GitHub issue #10905, which proposes introducing an -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:
E.g., (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.
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

Why do PowerShell comparison operators not enumerate collections of size 1?

When checking variables and collections of variables for nullity, comparison operators seem to enumerate collections of size 2 or more:
> if ( #( $null, $null ) -eq $null ) { $True } else { $False }
True
But they do not for collections of size 1:
> if ( #( $null ) -eq $null ) { $True } else { $False }
False
I'm aware that it's best practice to null-compare using the left-hand side ($null -eq #( $null )), but can someone explain what's happening here? I suspect there's something more subtle happening that impacts other code that I write.
Why are these two results different?
tl;dr
In PowerShell conditionals / implicit Boolean contexts:
Single-element arrays are treated like scalars: that is, their one and only element itself is interpreted as a Boolean.[1]
2+-element arrays are always $true, irrespective of their content.
With an array as the LHS, array-aware operators such as -eq invariably also output an array.
Since your array elements are all $null and you compare to $null, your comparison is an effective no-op - e.g., #( $null ) -eq $null results in #( $null ) - and your conditionals are equivalent to:
[bool] #( $null, $null ) # -> $true - array with 2+ elements is always $True
[bool] #( $null ) # -> $false(!) - treated like: [bool] $null
Perhaps surprisingly, the implicit Boolean logic applies pipeline logic to an array:
That is, a single-element array is (conceptually) unwrapped and its element is interpreted as a Boolean.
Therefore, [bool] #( $null ) is treated the same as [bool] $null, which is $false.
Generally, #( <one-and-only-element> ) (or , <one-and-only-element>) is treated the same as <one-and-only-element> in a Boolean context.
By contrast, if an array has 2 or more elements, it is always $true in a Boolean context, even if all its elements would individually be considered $false.
Workaround for testing whether an arbitrary array is empty:
Base your conditional on the .Count property:
if ( (<array>).Count ) { $true } else { $false }
You could append -gt 0, but that's not strictly necessary, because any nonzero value is implicitly $true.
Applied to your example:
PS> if ( ( #($null) -eq $null ).Count ) { $true } else { $false }
True
Testing an arbitrary value for being a (scalar) $null:
if ($null -eq <value>) { $true } else { $false }
Note how $null must be used as the LHS in order to prevent the array-filtering logic from taking effect, should <value> be an array.
That's also the reason why Visual Studio Code with the PowerShell extension advises "$null should be on the left side of comparisons" if you write something like $var -eq $null.
[1] To-Boolean conversion summary:
Among scalars:
The following are implicitly $false:
''/"" (empty string)
0 (of any numeric type).
$null
Pitfall: Comparing $null to a Boolean explicitly with -eq is always $false, even with $null as the RHS (despite the RHS normally getting coerced to the type of the LHS):
$false -eq $null # !! $false - unlike `$false -eq [bool] $null`
Pitfall: Any non-empty string evaluates to $true
e.g., [bool] 'False' is $true
Note that this differs from explicit string parsing: [bool]::Parse('false') does return$false (and $true for 'true', but recognizes nothing else).
Instances of any other (non-collection) type are implicitly $true, including of type [pscustomobject] and [hashtable] (which PowerShell treats as a single object, not as a collection of entries).
Unfortunately, this includes types that define explicit [bool] .NET conversion operators, meaning that these operators are - mostly - not honored; see this answer.
Among collections such as arrays (more accurately, collection-like types that implement the IList interface - see the source code):
Empty collections are always $false, as is the special "null collection" value indicating the absence of output from a command, [System.Management.Automation.Internal.AutomationNull]::Value.
Pitfall: Single-element collections evaluate to:
If the one and only element is a scalar: its Boolean value
If that element is itself a collection: $true if it has at least 1 element (irrespective of what that element is).
2+-element collections are always $true.
The following items evaluate to $false:
#()
0
$null
$false
''
In your first example:
#($null, $null) -eq $null
This evaluates to $null, $null which is a non-zero collection, so it is $true. You can observe this with the following:
[bool]($null, $null)
In your second example, what you're observing is filtering of an array like the first case, but returning a scalar (instead of an array) since only one item of the array matched the filter:
#($null) -eq $null
This evaluates to #($null) but powershell is evaluating it as a scalar in a boolean context, so it returns $false, observed by:
[bool]#($null)
Footnote: in powershell v2, there was a bug with $null filtering which spawned the left-hand $null comparison. This bug caused if/else blocks to be skipped entirely.

Printing useful variable values in powershell (in particular, involving $null/empty string)

If I have:
$a=$null
$b=''
$c=#($null,$null)
$d='foo'
write-host $a
write-host $b
write-host $c
write-host $d
the output is
foo
I'd really like to be able to easily get output that shows the variable values, e.g.,
$Null
''
#($Null,$Null)
'foo'
I can write a function to do this, but I'm guessing/hoping there's something built-in that I'm missing. Is there, or does everyone just roll their own function for something like this?
At the moment the quickest thing I've come up with is running a value through ConvertTo-Json before printing it. It doesn't handle a plain $null, but it shows me the other values nicely.
What you're looking for is similar to Ruby's .inspect method. It's something I always loved in Ruby and do miss in PowerShell/.Net.
Unfortunately there is no such thing to my knowledge, so you will somewhat have to roll your own.
The closest you get in .Net is the .ToString() method that, at a minimum, just displays the object type (it's inherited from [System.Object]).
So you're going to have to do some checking on your own. Let's talk about the edge case checks.
Arrays
You should check if you're dealing with an array first, because PowerShell often unrolls arrays and coalesces objects for you so if you start doing other checks you may not handle them correctly.
To check that you have an array:
$obj -is [array]
1 -is [array] # false
1,2,3 -is [array] # true
,1 -is [array] #true
In the case of an array, you'll have to iterate it if you want to properly serialize its elements as well. This is basically the part where your function will end up being recursive.
function Format-MyObject {
param(
$obj
)
if ($obj -is [array]) {
# initial array display, like "#(" or "["
foreach ($o in $obj) {
Format-MyObject $obj
}
# closing array display, like ")" or "]"
}
}
Nulls
Simply check if it's equal to $null:
$obj -eq $null
Strings
You can first test that you're dealing with a string by using -is [string].
For empty, you can compare the string to an empty string, or better, to [string]::Empty. You can also use the .IsNullOrEmpty() method, but only if you've already ruled out a null value (or checked that it is indeed a string):
if ($obj -is [string) {
# pick one
if ([string]::IsNullOrEmpty($obj)) {
# display empty string
}
if ($obj -eq [string]::Empty) {
# display empty string
}
if ($obj -eq "") { # this has no advantage over the previous test
# display empty string
}
}
Alternative
You could use the built-in XML serialization, then parse the XML to get the values out of it.
It's work (enough that I'm not going to do it in an SO answer), but it removes a lot of potential human error, and sort of future-proofs the approach.
The basic idea:
$serialized = [System.Management.Automation.PSSerializer]::Serialize($obj) -as [xml]
Now, use the built in XML methods to parse it and pull out what you need. You still need to convert some stuff to other stuff to display the way you want (like interpreting <nil /> and the list of types to properly display arrays and such), but I like leaving the actual serialization to an official component.
Quick example:
[System.Management.Automation.PSSerializer]::Serialize(#(
$null,
1,
'string',
#(
'start of nested array',
$null,
'2 empty strings next',
'',
([string]::Empty)
)
)
)
And the output:
<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
<Obj RefId="0">
<TN RefId="0">
<T>System.Object[]</T>
<T>System.Array</T>
<T>System.Object</T>
</TN>
<LST>
<Nil />
<I32>1</I32>
<S>string</S>
<Obj RefId="1">
<TNRef RefId="0" />
<LST>
<S>start of nested array</S>
<Nil />
<S>2 empty strings next</S>
<S></S>
<S></S>
</LST>
</Obj>
</LST>
</Obj>
</Objs>
I shared two functions that reveal PowerShell values (including the empty $Null's, empty arrays etc.) further than the usually do:
One that the serializes the PowerShell objects to a PowerShell
Object Notation (PSON)
which ultimate goal is to be able to reverse everything with the
standard command Invoke-Expression and parse it back to a
PowerShell object.
The other is the ConvertTo-Text (alias CText) function that I used in
my Log-Entry
framework. note the
specific line: Log "Several examples that usually aren't displayed
by Write-Host:" $NotSet #() #(#()) #(#(), #()) #($Null) that I wrote
in the example.
Function Global:ConvertTo-Text1([Alias("Value")]$O, [Int]$Depth = 9, [Switch]$Type, [Switch]$Expand, [Int]$Strip = -1, [String]$Prefix, [Int]$i) {
Function Iterate($Value, [String]$Prefix, [Int]$i = $i + 1) {ConvertTo-Text $Value -Depth:$Depth -Strip:$Strip -Type:$Type -Expand:$Expand -Prefix:$Prefix -i:$i}
$NewLine, $Space = If ($Expand) {"`r`n", ("`t" * $i)} Else{"", ""}
If ($O -eq $Null) {$V = '$Null'} Else {
$V = If ($O -is "Boolean") {"`$$O"}
ElseIf ($O -is "String") {If ($Strip -ge 0) {'"' + (($O -Replace "[\s]+", " ") -Replace "(?<=[\s\S]{$Strip})[\s\S]+", "...") + '"'} Else {"""$O"""}}
ElseIf ($O -is "DateTime") {$O.ToString("yyyy-MM-dd HH:mm:ss")}
ElseIf ($O -is "ValueType" -or ($O.Value.GetTypeCode -and $O.ToString.OverloadDefinitions)) {$O.ToString()}
ElseIf ($O -is "Xml") {(#(Select-XML -XML $O *) -Join "$NewLine$Space") + $NewLine}
ElseIf ($i -gt $Depth) {$Type = $True; "..."}
ElseIf ($O -is "Array") {"#(", #(&{For ($_ = 0; $_ -lt $O.Count; $_++) {Iterate $O[$_]}}), ")"}
ElseIf ($O.GetEnumerator.OverloadDefinitions) {"#{", (#(ForEach($_ in $O.Keys) {Iterate $O.$_ "$_ = "}) -Join "; "), "}"}
ElseIf ($O.PSObject.Properties -and !$O.value.GetTypeCode) {"{", (#(ForEach($_ in $O.PSObject.Properties | Select -Exp Name) {Iterate $O.$_ "$_`: "}) -Join "; "), "}"}
Else {$Type = $True; "?"}}
If ($Type) {$Prefix += "[" + $(Try {$O.GetType()} Catch {$Error.Remove($Error[0]); "$Var.PSTypeNames[0]"}).ToString().Split(".")[-1] + "]"}
"$Space$Prefix" + $(If ($V -is "Array") {$V[0] + $(If ($V[1]) {$NewLine + ($V[1] -Join ", $NewLine") + "$NewLine$Space"} Else {""}) + $V[2]} Else {$V})
}; Set-Alias CText ConvertTo-Text -Scope:Global -Description "Convert value to readable text"
ConvertTo-Text
The ConvertTo-Text function (Alias CText) recursively converts PowerShell object to readable text this includes hash tables, custom objects and revealing type details (like $Null vs an empty string).
Syntax
ConvertTo-Text [<Object>] [[-Depth] <int>] [[-Strip] <int>] <string>] [-Expand] [-Type]
Parameters
<Object>
The object (position 0) that should be converted a readable value.
-⁠Depth <int>
The maximal number of recursive iterations to reveal embedded objects.
The default depth for ConvertTo-Text is 9.
-Strip <int>
Truncates strings at the given length and removes redundant white space characters if the value supplied is equal or larger than 0. Set -Strip -1 prevents truncating and the removal of with space characters.
The default value for ConvertTo-Text is -1.
-Expand
Expands embedded objects over multiple lines for better readability.
-Type
Explicitly reveals the type of the object by adding [<Type>] in front of the objects.
Note: the parameter $Prefix is for internal use.
Examples
The following command returns a string that describes the object contained by the $var variable:
ConvertTo-Text $Var
The following command returns a string containing the hash table as shown in the example (rather then System.Collections.DictionaryEntry...):
ConvertTo-Text #{one = 1; two = 2; three = 3}
The following command reveals values (as e.g. $Null) that are usually not displayed by PowerShell:
ConvertTo-Text #{Null = $Null; EmptyString = ""; EmptyArray = #(); ArrayWithNull = #($Null); DoubleEmptyArray = #(#(), #())} -Expand
The following command returns a string revealing the WinNT User object up to a level of 5 deep and expands the embedded object over multiple lines:
ConvertTo-Text ([ADSI]"WinNT://./$Env:Username") -Depth 5 -Expand
A quick self-rolled option good for some datatypes.
function Format-MyObject {
param(
$obj
)
#equality comparison order is important due to array -eq overloading
if ($null -eq $obj)
{
return 'null'
}
#Specify depth because the default is 2, because powershell
return ConvertTo-Json -Depth 100 $obj
}

Why is an empty PowerShell pipeline not the same as null?

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?
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
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.