Write-Host strips newline characters where Write-Output does not - powershell

The command $output = Invoke-Expression "cfn-lint *.yaml" gives me an array for $output:
$output.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Write-Output $output gives me this nice output:
Write-Output $output
W2506 Parameter AMIID should be of type [AWS::EC2::Image::Id,
AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>]
trace3-ec2-virtual-machine-creation.yaml:27:3
W2001 Parameter VpcId not used.
trace3-ec2-virtual-machine-creation.yaml:32:3
But Write-Host $output gives me a version with all the newlines stripped out:
Write-Host $output
W2506 Parameter AMIID should be of type [AWS::EC2::Image::Id, AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>] trace3-ec2-virtual-machine-creation.yaml:27:3 W2001 Parameter VpcId not used. trace3-ec2-virtual-machine-creation.yaml:32:3
Why is it doing this?
I know that I can get around this with the call Write-Host ($output | Out-String), but I don't understand why that works.

Write-Output takes [-InputObject] <PSObject[]> as argument while Write-Host takes [[-Object] <Object>] as argument.
Write-Output returns the objects that are submitted as input.
Write-Host sends the objects to the host. It does not return any objects. However, the host displays the objects that Write-Host sends to it.
As you can see, Write-Ouput takes an array of PSObject as Input parameter and returns the same array as Output.
Out-String works fine with Write-Host because it is converting your array to a multiline string hence Write-Host will return it as literal to the Information Stream while without the Out-String it is taking all lines of your array and joining them into a single line string.
PS /> $test = #(
'this is a'
'test'
)
$writeHost = Write-Host -Object $test 6>&1
$writeOutput = Write-Output -Object $test
$writeHost.GetType()
$writeOutput.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False InformationRecord System.Object
True True Object[] System.Array
Edit
Following your comment, I don't know for sure why, I couldn't find any relevant information on MS Docs for the cmdlet but we can assume it is doing some sort of manipulation on the input objects. We do know that Write-Host is a wrapper for Write-Information.
Take this function as an example, it receives a [object] in this case it receives an array with 2 items as input and writes to the information stream a string representation of it's input:
$test = #(
'this is a'
'test'
)
function TestString ([object]$String) {
Write-Information ([string]$String) -InformationAction Continue
}
PS /> TestString $test
this is a test

Related

PowerShell wait for output from function or invoke-command

I have a script block/function that returns PSCustomObject followed by Write-Host.
I want to get the output first then print the write-host but I can't seem to figure it out.
function ReturnArrayList {
param (
[int] $number
)
[System.Collections.ArrayList]$folderList = #()
$folderObject = [PSCustomObject]#{
Name = 'John'
number = $number
}
#Add the object to the array
$folderList.Add($folderObject) | Out-Null
return $folderList
}
$sb = {
param (
[int] $number
)
[System.Collections.ArrayList]$folderList = #()
$folderObject = [PSCustomObject]#{
Name = 'John'
number = $number
}
#Add the object to the array
$folderList.Add($folderObject) | Out-Null
return $folderList
}
ReturnArrayList -number 5
#Invoke-Command -ScriptBlock $sb -ArgumentList 5
Write-Host "This write host should come later"
Result:
This write host should come after
Name number
---- ------
John 5
Desired result:
Name number
---- ------
John 5
This write host should come after
How can I get the return result first and print the write-host message?
Thank you for your help in advance!
You can force PowerShell to write the output from ReturnArrayList to the screen before reaching Write-Host by piping it to either one of the Format-* cmdlets or Out-Default:
$object = ReturnArrayList -number 5
$object |Out-Default
Write-Host "This write host should come later"
Result:
Name number
---- ------
John 5
This write host should come later
Beware that your ReturnArrayList function does not actually return an ArrayList - PowerShell will automatically enumerate the item(s) in $folderlist, and since it only contains one item, the result is just a single PSCustomObject, "unwrapped" from the ArrayList so to speak:
PS ~> $object = ReturnArrayList -number 5
PS ~> $object.GetType().Name
PSCustomObject
To preserve enumerable types as output from functions, you'll need to either use Write-Output -NoEnumerate, or wrap the it in an array using the , operator:
function ReturnArrayList {
param (
[int] $number
)
[System.Collections.ArrayList]$folderList = #()
$folderObject = [PSCustomObject]#{
Name = 'John'
number = $number
}
#Add the object to the array
$folderList.Add($folderObject) | Out-Null
return ,$folderList
# or
Write-Output $folderList -NoEnumerate
}
Data is usually output to the pipeline, while Write-Host bypasses the pipeline and writes to the console directly.
Using Write-Output instead of Write-Host will fix this issue. You can easily find more in-depth information on this topic, and when not to Write-Host.

Force Imported Value as Integer

I'm trying to save values between instances of the script running.
I'm doing that by reading a text file at the beginning of the script, and then overwriting that file with the new values at the end of the script.
tracker.txt:
x=1
y=4
z=3
I'm reading the script using:
Get-Content "$Root\tracker.txt" | Foreach-Object{
$Position = $_.Split("=")
New-Variable -Name $Position[0] -Value $Position[1]
}
Unfortunately my $x $y and $z variables are being interpreted as a string instead of an integer.
Looking up New-Variable parameters, it doesn't seem like I can specify the value type.
Also I tried:
New-Variable -Name $Position[0] -Value [int]$Position[1]
And:
New-Variable -Name $Position[0] -Value ($Position[1] + 0)
But both did not work as expected.
How can I set these variables as an integer? I'm trying to use them later in a loop and that keeps failing because the variable can't be a string.
In order to save values, consider using Powershell's own object serialization. That is, Export-Clixml and Import-Clixml cmdlets. When an object is serialized, its contents are written on a file. In addition to values, data types are there too.
Handling multiple variables is easier, if those are stored in a collection such as a hash table. Like so,
# Save some values in a hash table
$myKeys =#{ "a" = 1; "b" = 2 }
$myKeys["a"]
1
# Check variable a's type. Int32 is as expected
$myKeys["a"].gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
# Serialize the hash table
Export-Clixml -Path keys.xml -InputObject $myKeys
# Create a new hash table by deserializing
$newKeys = Import-Clixml .\keys.xml
# Check contents
$newkeys["a"]
1
# Is the new a also an int32? Yes, it is
$newkeys["a"].gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
The keys.xml is, as expected an XML file. Check out its contents with, say, Notepad to see how the objects are stored. When working with complex objects, -Depth switch needs to be used. Otherwise, serialization saves only two levels of nesting, and that'll break complex objects.
I will use this:
(Get-Content tracker.txt) | foreach {
invoke-expression "[int]`$$($_.split("=")[0])=$($_.split("=")[1])"
}
It will create three variables with type int32.
To answer your specific question, you can force it like this.
#'
var1=1
var2=2
'# | out-file "$Root\tracker.txt"
Get-Content "$Root\tracker.txt" | Foreach-Object{
$Position = $_.Split("=")
New-Variable -Name $Position[0] -Value ([int]$Position[1])
}
$var1.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
However I really like this approach.
Create a template
$template = #'
{name*:var1}={[int]value:1}
{name*:var2}={[int]value:2}
'#
Now use ConvertFrom-String and assign to $data
#'
test1=1
test2=2
test3=3
test4=4
test5=5
'# | ConvertFrom-String -TemplateContent $template -OutVariable data
Let's create our variables now. Very easy to read.
$data | foreach {New-Variable -Name $_.name -Value $_.value}
Get-Variable test*
Name Value
---- -----
test1 1
test2 2
test3 3
test4 4
test5 5
And most importantly, they are int.
$test1.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
Putting it all together and pulling from a file
$template = #'
{name*:var1}={[int]value:1}
{name*:var2}={[int]value:2}
'#
Get-Content "$Root\tracker.txt" -raw |
ConvertFrom-String -TemplateContent $template |
foreach {New-Variable -Name $_.name -Value $_.value}

Why and how are these two $null values different?

Apparently, in PowerShell (ver. 3) not all $null's are the same:
>function emptyArray() { #() }
>$l_t = #() ; $l_t.Count
0
>$l_t1 = #(); $l_t1 -eq $null; $l_t1.count; $l_t1.gettype()
0
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
>$l_t += $l_t1; $l_t.Count
0
>$l_t += emptyArray; $l_t.Count
0
>$l_t2 = emptyArray; $l_t2 -eq $null; $l_t2.Count; $l_t2.gettype()
True
0
You cannot call a method on a null-valued expression.
At line:1 char:38
+ $l_t2 = emptyArray; $l_t2 -eq $null; $l_t2.Count; $l_t2.gettype()
+ ~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
>$l_t += $l_t2; $l_t.Count
0
>$l_t3 = $null; $l_t3 -eq $null;$l_t3.gettype()
True
You cannot call a method on a null-valued expression.
At line:1 char:32
+ $l_t3 = $null; $l_t3 -eq $null;$l_t3.gettype()
+ ~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
>$l_t += $l_t3; $l_t.count
1
>function addToArray($l_a, $l_b) { $l_a += $l_b; $l_a.count }
>$l_t = #(); $l_t.Count
0
>addToArray $l_t $l_t1
0
>addToArray $l_t $l_t2
1
So how and why is $l_t2 different from $l_t3? In particular, is $l_t2 really $null or not? Note that $l_t2 is NOT an empty array ($l_t1 is, and $l_t1 -eq $null returns nothing, as expected), but neither is it truly $null, like $l_t3. In particular, $l_t2.count returns 0 rather than an error, and furthermore, adding $l_t2 to $l_t behaves like adding an empty array, not like adding $null. And why does $l_t2 suddenly seem to become "more $null" when it gets passed in the the function addToArray as a parameter???????
Can anyone explain this behaviour, or point me to documentation that would explain it?
Edit:
The answer by PetSerAl below is correct. I have also found this stackOverflow post on the same issue.
Powershell version info:
>$PSVersionTable
Name Value
---- -----
WSManStackVersion 3.0
PSCompatibleVersions {1.0, 2.0, 3.0}
SerializationVersion 1.1.0.1
BuildVersion 6.2.9200.16481
PSVersion 3.0
CLRVersion 4.0.30319.1026
PSRemotingProtocolVersion 2.2
In particular, is $l_t2 really $null or not?
$l_t2 is not $null, but a [System.Management.Automation.Internal.AutomationNull]::Value. It is a special instance of PSObject. It is returned when a pipeline returns zero objects. That is how you can check it:
$a=&{} #shortest, I know, pipeline, that returns zero objects
$b=[System.Management.Automation.Internal.AutomationNull]::Value
$ReferenceEquals=[Object].GetMethod('ReferenceEquals')
$ReferenceEquals.Invoke($null,($a,$null)) #returns False
$ReferenceEquals.Invoke($null,($a,$b)) #returns True
I call ReferenceEquals thru Reflection to prevent conversion from AutomationNull to $null by PowerShell.
$l_t1 -eq $null returns nothing
For me it returns an empty array, as I expect from it.
$l_t2.count returns 0
It is a new feature of PowerShell v3:
You can now use Count or Length on any object, even if it didn’t have the property. If the object didn’t have a Count or Length property, it will will return 1 (or 0 for $null). Objects that have Count or Length properties will continue to work as they always have.
PS> $a = 42
PS> $a.Count
1
 
And why does $l_t2 suddenly seem to become "more $null" when it gets passed in the the function addToArray as a parameter???????
It seems that PowerShell converts AutomationNull to $null in some cases, like calling .NET methods. In PowerShell v2, even when saving AutomationNull to a variable it gets converted to $null.
To complement PetSerAl's great answer with a pragmatic summary:
Commands that happen to produce no output do not return $null, but the [System.Management.Automation.Internal.AutomationNull]::Value singleton,
which can be thought of as an "array-valued $null" or, to coin a term, null enumeration. It is sometimes also called "AutomationNull", for its type name.
Note that, due to PowerShell's automatic enumeration of collections, even a command that explicitly outputs an empty collection object such as #() has no output (unless enumeration is explicitly prevented, such as with Write-Output -NoEnumerate).
In short, this special value behaves like $null in scalar contexts, and like an empty array in enumeration contexts, notably in the pipeline, as the examples below demonstrate.
Given that $null and the null enumeration situationally behave differently, distinguishing between the two via reflection may be necessary, which is currently far from trivial; GitHub issue #13465 proposes implementing a test that would allow you to use $someValue -is [AutomationNull].
As of PowerShell 7.3.0, the following, obscure test is required:
$null -eq $someValue -and $someValue -is [psobject]
Caveats:
Passing [System.Management.Automation.Internal.AutomationNull]::Value as a cmdlet / function parameter value invariably converts it to $null.
See GitHub issue #9150.
In PSv3+, even an actual (scalar) $null is not enumerated in a foreach loop; it is enumerated in a pipeline, however - see bottom.
In PSv2-, saving a null enumeration in a variable quietly converted it to $null and $null was enumerated in a foreach loop as well (not just in a pipeline) - see bottom.
# A true $null value:
$trueNull = $null
# An operation with no output returns
# the [System.Management.Automation.Internal.AutomationNull]::Value singleton,
# which is treated like $null in a scalar expression context,
# but behaves like an empty array in a pipeline or array expression context.
$automationNull = & {} # calling (&) an empty script block ({}) produces no output
# In a *scalar expression*, [System.Management.Automation.Internal.AutomationNull]::Value
# is implicitly converted to $null, which is why all of the following commands
# return $true.
$null -eq $automationNull
$trueNull -eq $automationNull
$null -eq [System.Management.Automation.Internal.AutomationNull]::Value
& { param($param) $null -eq $param } $automationNull
# By contrast, in a *pipeline*, $null and
# [System.Management.Automation.Internal.AutomationNull]::Value
# are NOT the same:
# Actual $null *is* sent as data through the pipeline:
# The (implied) -Process block executes once.
$trueNull | % { 'input received' } # -> 'input received'
# [System.Management.Automation.Internal.AutomationNull]::Value is *not* sent
# as data through the pipeline, it behaves like an empty array:
# The (implied) -Process block does *not* execute (but -Begin and -End blocks would).
$automationNull | % { 'input received' } # -> NO output; effectively like: #() | % { 'input received' }
# Similarly, in an *array expression* context
# [System.Management.Automation.Internal.AutomationNull]::Value also behaves
# like an empty array:
(#() + $automationNull).Count # -> 0 - contrast with (#() + $trueNull).Count, which returns 1.
# CAVEAT: Passing [System.Management.Automation.Internal.AutomationNull]::Value to
# *any parameter* converts it to actual $null, whether that parameter is an
# array parameter or not.
# Passing [System.Management.Automation.Internal.AutomationNull]::Value is equivalent
# to passing true $null or omitting the parameter (by contrast,
# passing #() would result in an actual, empty array instance).
& { param([object[]] $param)
[Object].GetMethod('ReferenceEquals').Invoke($null, #($null, $param))
} $automationNull # -> $true; would be the same with $trueNull or no argument at all.
The [System.Management.Automation.Internal.AutomationNull]::Value documentation states:
Any operation that returns no actual value should return AutomationNull.Value.
Any component that evaluates a Windows PowerShell expression should be prepared to deal with receiving and discarding this result. When received in an evaluation where a value is required, it should be replaced with null.
PSv2 vs. PSv3+, and general inconsistencies:
PSv2 offered no distinction between [System.Management.Automation.Internal.AutomationNull]::Value and $null for values stored in variables:
Using a no-output command directly in a foreach statement / pipeline did work as expected - nothing was sent through the pipeline / the foreach loop wasn't entered:
Get-ChildItem nosuchfiles* | ForEach-Object { 'hi' }
foreach ($f in (Get-ChildItem nosuchfiles*)) { 'hi' }
By contrast, if a no-output commands was saved in a variable or an explicit $null was used, the behavior was different:
# Store the output from a no-output command in a variable.
$result = Get-ChildItem nosuchfiles* # PSv2-: quiet conversion to $null happens here
# Enumerate the variable.
$result | ForEach-Object { 'hi1' }
foreach ($f in $result) { 'hi2' }
# Enumerate a $null literal.
$null | ForEach-Object { 'hi3' }
foreach ($f in $null) { 'hi4' }
PSv2: all of the above commands output a string starting with hi, because $null is sent through the pipeline / being enumerated by foreach:
Unlike in PSv3+, [System.Management.Automation.Internal.AutomationNull]::Value is converted to $null on assigning to a variable, and $null is always enumerated in PSv2.
PSv3+: The behavior changed in PSv3, both for better and worse:
Better: Nothing is sent through the pipeline for the commands that enumerate $result: The foreach loop is not entered, because the [System.Management.Automation.Internal.AutomationNull]::Value is preserved when assigning to a variable, unlike in PSv2.
Possibly Worse: foreach no longer enumerates $null (whether specified as a literal or stored in a variable), so that foreach ($f in $null) { 'hi4' } perhaps surprisingly produces no output.
On the plus side, the new behavior no longer enumerates uninitialized variables, which evaluate to $null (unless prevented altogether with Set-StrictMode).
Generally, however, not enumerating $null would have been more justified in PSv2, given its inability to store the null-collection value in a variable.
In summary, the PSv3+ behavior:
takes away the ability to distinguish between $null and [System.Management.Automation.Internal.AutomationNull]::Value in the context of a foreach statement
thereby introduces an inconsistency with pipeline behavior, where this distinction is respected.
For the sake of backward compatibility, the current behavior cannot be changed. This comment on GitHub proposes a way to resolve these inconsistencies for a (hypothetical) potential future PowerShell version that needn't be backward-compatible.
When you return a collection from a PowerShell function, by default PowerShell determines the data type of the return value as follows:
If the collection has more than one element, the return result is an array. Note that the data type of the return result is System.Array even if the object being returned is a collection of a different type.
If the collection has a single element, the return result is the value of that element, rather than a collection of one element, and the data type of the return result is the data type of that element.
If the collection is empty, the return result is $null
$l_t = #() assigns an empty array to $l_t.
$l_t2 = emptyArray assigns $null to $l_t2, because the function emptyArray returns an empty collection, and therefore the return result is $null.
$l_t2 and $l_t3 are both null, and they behave the same way. Since you've pre-declared $l_t as an empty array, when you add either $l_t2 or $l_t3 to it, either with the += operator or the addToArray function, an element whose value is **$null* is added to the array.
If you want to force the function to preserve the data type of the collection object you're returning, use the comma operator:
PS> function emptyArray {,#()}
PS> $l_t2 = emptyArray
PS> $l_t2.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
PS> $l_t2.Count
0
Note: The empty parentheses after emtpyArray in the function declaration is superfluous. You only need parentheses after the function name if you're using them to declare parameters.
An interesting point to be aware of is that the comma operator doesn't necessarily make the return value an array.
Recall that as I mentioned in the first bullet point, by default the data type of the return result of a collection with more than one element is System.Array regardless of the actual data type of the collection. For example:
PS> $list = New-Object -TypeName System.Collections.Generic.List[int]
PS> $list.Add(1)
PS> $list.Add(2)
PS> $list.Count
2
PS> $list.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
Note that the data type of this collection is List`1, not System.Array.
However, if you return it from a function, within the function the data type of $list is List`1, but it's returned as a System.Array containing the same elements.
PS> function Get-List {$list = New-Object -TypeName System.Collections.Generic.List[int]; $list.Add(1); $list.Add(2); return $list}
PS> $l = Get-List
PS> $l.Count
2
PS> $l.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
If you want the return result to be a collection of the same data type as the one within the function that you're returning, the comma operator will accomplish that:
PS> function Get-List {$list = New-Object -TypeName System.Collections.Generic.List[int]; $list.Add(1); $list.Add(2); return ,$list}
PS> $l = Get-List
PS> $l.Count
2
PS> $l.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
This isn't limited to array-like collection objects. As far as I've seen, any time PowerShell changes the data type of the object you're returning, and you want the return value to preserve the object's original data type, you can do that by preceding the object being returned with a comma. I first encountered this issue when writing a function that queried a database and returned a DataTable object. The return result was an array of hashtables instead of a DataTable. Changing return $my_datatable_object to return ,$my_datatable_object made the function return an actual DataTable object.

Confused about Nulls in Powershell

I'm new to Powershell and I'm confused about something, more specifically null and $null.
For example. let's say you have a function:
function myFunction([ref]$foo){
if($foo -ne $null){
...do stuff
}
}
And when you call this function, you do so like:
[ref]$foo = $null
myFunction $foo
If you execute the code above, the condition if($foo -ne $null) will return true.
However, if you call the function like:
$foo = $null
myFunction $foo
the condition if($foo -ne $null) will return false.
Why is this? Why is it that when you assign a ref variable $null, it isn't considered null when it is checked?
PowerShell seems to cast $null to some non-null value when you attempt to store it in a strongly typed variable. I believe this is the cause. Notice the following:
PS C:\> [ref]$foo = $null
PS C:\> $foo
Value
-----
PS C:\> $foo.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSReference System.Object
It appears that PowerShell is casting $null to a PSReference. I've seen similar behavior with strings:
PS C:\> [String]$str = $null
PS C:\> $str -eq $null
False
PS C:\> $str -eq [String]::Empty
True
It confused the heck out of me when I had a [String] parameter on a function that I was defaulting to $null and my null check was never true.
Why would they do this? I wonder the same thing.
For strings, I ended up using [String]::IsNullOrEmpty (although there may be a more PowerShell-ish way). For PSReference, Value might suit your needs for the null check:
PS C:\> [ref]$foo = $null
PS C:\> $foo.Value -eq $null
True
Note that we have just proven you won't run into a null reference error by accessing Value.
Because $null is a special variable which is actually null, where as [ref]$foo is a strongly typed variable containing a reference to a location, which just so happens to be null.
So of course $ref -ne $null. $null is the null, $ref is a reference which could be null later.

How to test for $null array in PowerShell

I'm using an array variable in PowerShell 2.0. If it does not have a value, it will be $null, which I can test for successfully:
PS C:\> [array]$foo = $null
PS C:\> $foo -eq $null
True
But when I give it a value, the test for $null does not return anything:
PS C:\> [array]$foo = #("bar")
PS C:\> $foo -eq $null
PS C:\>
How can "-eq $null" give no results? It's either $null or it's not.
What is the correct way to determine if an array is populated vs. $null?
It's an array, so you're looking for Count to test for contents.
I'd recommend
$foo.count -gt 0
The "why" of this is related to how PSH handles comparison of collection objects
You can reorder the operands:
$null -eq $foo
Note that -eq in PowerShell is not an equivalence relation.
if($foo -eq $null) { "yes" } else { "no" }
help about_comparison_operators
displays help and includes this text:
All comparison operators except the
containment operators (-contains,
-notcontains) and type operators (-is, -isnot) return a Boolean value when the input to the operator (the value
on the left side of the operator) is a
single value (a scalar). When the
input is a collection of values, the
containment operators and the type
operators return any matching values.
If there are no matches in a
collection, these operators do not
return anything. The containment
operators and type operators always
return a Boolean value.
If your solution requires returning 0 instead of true/false, I've found this to be useful:
PS C:\> [array]$foo = $null
PS C:\> ($foo | Measure-Object).Count
0
This operation is different from the count property of the array, because Measure-Object is counting objects. Since there are none, it will return 0.
The other answers address the main thrust of the question, but just to comment on this part...
PS C:\> [array]$foo = #("bar")
PS C:\> $foo -eq $null
PS C:\>
How can "-eq $null" give no results? It's either $null or it's not.
It's confusing at first, but that is giving you the result of $foo -eq $null, it's just that the result has no displayable representation.
Since $foo holds an array, $foo -eq $null means "return an array containing the elements of $foo that are equal to $null". Are there any elements of $foo that are equal to $null? No, so $foo -eq $null should return an empty array. That's exactly what it does, the problem is that when an empty array is displayed at the console you see...nothing...
PS> #()
PS>
The array is still there, even if you can't see its elements...
PS> #().GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
PS> #().Length
0
We can use similar commands to confirm that $foo -eq $null is returning an array that we're not able to "see"...
PS> $foo -eq $null
PS> ($foo -eq $null).GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
PS> ($foo -eq $null).Length
0
PS> ($foo -eq $null).GetValue(0)
Exception calling "GetValue" with "1" argument(s): "Index was outside the bounds of the array."
At line:1 char:1
+ ($foo -eq $null).GetValue(0)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : IndexOutOfRangeException
Note that I am calling the Array.GetValue method instead of using the indexer (i.e. ($foo -eq $null)[0]) because the latter returns $null for invalid indices and there's no way to distinguish them from a valid index that happens to contain $null.
We see similar behavior if we test for $null in/against an array that contains $null elements...
PS> $bar = #($null)
PS> $bar -eq $null
PS> ($bar -eq $null).GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
PS> ($bar -eq $null).Length
1
PS> ($bar -eq $null).GetValue(0)
PS> $null -eq ($bar -eq $null).GetValue(0)
True
PS> ($bar -eq $null).GetValue(0) -eq $null
True
PS> ($bar -eq $null).GetValue(1)
Exception calling "GetValue" with "1" argument(s): "Index was outside the bounds of the array."
At line:1 char:1
+ ($bar -eq $null).GetValue(1)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : IndexOutOfRangeException
In this case, $bar -eq $null returns an array containing one element, $null, which has no visual representation at the console...
PS> #($null)
PS> #($null).GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
PS> #($null).Length
1
How do you want things to behave?
If you want arrays with no elements to be treated the same as unassigned arrays, use:
[array]$foo = #() #example where we'd want TRUE to be returned
#($foo).Count -eq 0
If you want a blank array to be seen as having a value (albeit an empty one), use:
[array]$foo = #() #example where we'd want FALSE to be returned
$foo.PSObject -eq $null
If you want an array which is populated with only null values to be treated as null:
[array]$foo = $null,$null
#($foo | ?{$_.PSObject}).Count -eq 0
NB: In the above I use $_.PSObject over $_ to avoid [bool]$false, [int]0, [string]'', etc from being filtered out; since here we're focussed solely on nulls.
Watch out for switch. It will never run with a null array, for example as the output of an empty directory.
switch ( $null ) { default { 'yes' } }
yes
switch ( #() ) { default { 'yes' } } # no output
mkdir foo
switch ( dir foo ) { default { 'yes' } } # no output
Not all of these answers work for me. I ended up doing this, treating it like a boolean.
$foo = #()
if (! $foo) { 'empty array' }
empty array
Actually, I came across an arraylist inside an object.
[pscustomobject]#{config = [Collections.ArrayList]#()} | ? { ! $_.config }
config
------
{}