How to check if a collection has a null reference - powershell

I just discovered that when you apply bool operators on a collection, it acts as a filter on that collection.
So the following will return all elements that are not null:
$objectArray = #('a','b','c','d')
$objectArray -ne $null
But how can I check if the collection's reference is null?

Trevor Sullivan's if () test forces the $objectArray to cast to a boolean.
[bool]$null #is $false
[bool]#(1,2,3) # is $true , so it looks good.
But empty arrays mislead it:
[bool]#() # is $false , so it's not an accurate test.
I suggest $null -eq $objectArray:
NB. It really opens the question of why you want to know if it's $null, specifically. Trevor's answer is typical and good enough for any common use.
NB. My answer includes an uncommon, but useful suggestion - when you have a literal value for one side of a comparison, put it on the left if you can.
0 -lt $counter
$null -eq $thing
"text" -eq $variable
4 -in $collection
It's less common, so looks less familiar, but it's more resilient against PowerShell implicit casting doing something you don't expect.

All you have to do is test the variable for $true or $false. If it's $false, then it's a null reference, otherwise the opposite is true.
if (!$objectArray) {
}

The following tells you if the reference is null:
[Object]::ReferenceEquals($objectArray, $null)
Testing if the variable is $true or $false does not always work because an empty collection will cast to false:
$objectArray = #()
if (!$objectArray) {
'The array is not actually null'
}

Related

float variable that should be empty or null is being set to 0

I'm working on a function that accepts [Single]$DurationMS as an optional parameter. This is supposed to be float value. So in my function I have the following code to check if its been provided to the function. if it is provided I want to add the value to an object nested in another object.
if ($DurationMS -ne $null) {
$MyObject.attributes | Add-Member -MemberType NoteProperty -Name 'duration.ms' -Value $DurationMS
}
All looks fine except when I test it I get 0 in the duration and I can't figure out why.
duration.ms
-----------
0
so my condition is evaluating to true but I don't understand why.
[single] is a .NET value type, and instances of such types can never be $null.
[single].IsValueType returning $true tells you that it is a value type.
$null only applies to .NET reference types and tells you that is a reference to no object.
It is therefore pointless to test your [single]-typed $DurationMS parameter variable for being $null:
A [single] instance's default value is 0, so that your $DurationMS -ne $null conditional is effectively 0 -ne $null by default, which is $true.
The robust way to check if an argument was passed to a given (non-mandatory) parameter in a given invocation is to consult the automatic $PSBoundParameters variable, as Santiago Squarzon suggests.
This variable contains a dictionary that has entries for all explicitly passed arguments, keyed by their parameter names (sans prefix -); e.g., if your function is invoked with -DurationMS 1.2, $PSBoundParameters['DurationMS'] returns 1.2, and $PSBoundParameters.ContainsKey('DurationMS') indicates $true
Therefore:
# Was an argument passed to -DurationMS?
if ($PSBoundParameters.ContainsKey('DurationMS')) {
$MyObject.attributes |
Add-Member -MemberType NoteProperty -Name 'duration.ms' -Value $DurationMS
}
The following aspects are incidental:
if ($DurationMs) would only work if you also wanted to consider an explicit argument of 0 to signal "no value was provided", because with a [single]-typed $DurationMs, if ($DurationMs) is the same as if ($DurationMs -ne 0)
PowerShell allows you to use an expression of any type in a Boolean context; with numeric types, 0 maps to $false, and any nonzero value to $true.
While this implicit to-Boolean conversion behavior is generally convenient, it has its pitfalls - see the bottom section of this answer for a summary of the rules.
Given that many PowerShell operators can implicitly operate on arrays (collections) as the LHS - in which case they act as filters, returning the subarray of matching items - it is generally better to place a scalar comparison operand on the LHS (in the case at we know that the non-literal operand is by definition also a scalar - a [single] instance - so that doesn't matter).
Placing the scalar on the LHS avoids false positives / negatives, such as in the following example:
$arr = 0, $null
# !! -> 'null', because (0, $null) -ne $null filters the
# !! array to #(0), and [bool] #() - perhaps surprisingly - is $false
if ($arr -ne $null) { 'not null' } else { 'null' }
# OK, with $null on the LHS
# -> 'not null'
if ($null -ne $arr) { 'not null' } else { 'null' }
However, even on the LHS $null can exhibit unexpected behavior, namely with the -lt, -le, -gt, and -ge operators, as discussed in this answer; e.g.:
$null -lt 0 # !! -> $true - even though [int] $null yields 0
If PowerShell offered a dedicated test for $null, these pitfalls could be avoided; implementing such a test - in the form $var -is $null or $var -isnull - was the subject of GitHub PR #10704; unfortunately, that PR was abandoned by its creator, and no one has picked up the work since, which is why no such test exists as of PowerShell 7.2.2.
As Lee Dailey points out, a property name such as duration.ms can be problematic, given that it contains ., which normally suggests a nested property access, given that an (unquoted) . serves as the member-access operator.

Searching through large Powershell arrays

Warning: Powershell novice, terms/code may be incorrect
Say I have a Powershell array with data from Invoke-Sqlcmd, say 10,000 "rows" with a half dozen (Col1-Col6) columns each. Say I want to find every row with an empty Col5. Right now I'm doing
foreach ($row in $sqlimport.col1)
{ $col5data = $sqlimport.Where({$_.col1 -eq $row}) | Select-Object -ExpandProperty Col5
if ( '' -eq $col5data ) {$col5data+1} else {} $col5data
Returns 1, which it should in my test. Code seems wrong and slow. It takes several minutes to run through. When something like
$sqlimport.Where({$_.col5 -eq 'somedatathatisthere'})
takes milliseconds
However,
$sqlimport.Where({$_.col5 -eq ''})
Returns blank
($sqlimport.Where({$_.col5 -eq ''})).Count
Returns 0
Right now, you're asking PowerShell to create an array consisting of all the values in column col1, and then for each iteration you search the entire array again to find the corresponding col5 value. That's entirely unnecessary.
Simply loop over the array itself:
foreach($row in $sqlimport){
if($row.Col5 -like ''){
Write-Host "Col5 is empty in row with id $($row.Col1)"
}
}
This only iterates over the entire array once.
Mathias' answer explains the problem with the inefficiency of your first attempt well and offers a solution that ultimately performs best, due to use of a (single) foreach statement (as opposed to the much slower ForEach-Object cmdlet).
However, a solution using a (single) .Where() array method call (or the related .ForEach() method):
is only slightly slower[1]
while being more concise and arguably conceptually more elegant.
It is what you tried with $sqlimport.Where({ $_.col5 -eq '' }), which, based on your feedback, requires only one small tweak to make it work:
Instead of -eq '' use -like '', which is a somewhat obscure[2] shortcut to testing for an empty string ('') and also for a $null value (as also used in Mathias' answer) - it is, in essence, the equivalent of .NET's [string]::IsNullOrEmpty() method.
# -like matches both '' and $null
$sqlimport.Where({ $_.col5 -like '' })
Note:
If you wanted to test for $null values only, use the following; note how $null is placed on the LHS, which is generally advisable for robust comparisons[3]:
$sqlimport.Where({ $null -eq $_.col5 })
As an aside: GitHub issue #10656 proposes introducing a simplified test for $null with something like -is $null; unfortunately, the associated PR has been abandonded.
As an aside: [string]-type-constrained PowerShell variables never store $null values: [string] $str = $null causes $str to contain '', not $null. However, [string]-typed property values not populated in PowerShell code can contain $null values, as in your case.
For passing a true $null value to a string-typed .NET API in PowerShell code, a special singleton must be used (passing $null directly would again result in ''): [NullString]::Value`
See this answer for background information.
[1] For a performance comparison of PowerShell's various enumeration (iteration) features, see the bottom section of this answer.
[2] The real purpose of the -like operator is to perform wildcard-expression matching. The shortcut relies on -like - which operates on (non-null) strings only - auto-converting non-string operands to strings, which in the case of $null causes conversion to '' (the empty string).
[3] To reliably test for $null, place it on the LHS of an -eq / -ne operation; e.g., $null -eq $var. If you place $null on the RHS - $var -eq $null - and $var happens to be a collection (such as an array), the return value is the array of matching elements, i.e. the array of those elements in $var whose value is $null, which is a different operation - see about_Comparison_Operators.

What operator should be used to detect an empty psobject?

Using the following example:
$test = '{ }' | ConvertFrom-Json
How can I detect that $test is empty?
Does not work:
$test -eq $null
-not $test
This does work, but does not feel right:
$test.ToString() -eq ''
This is a simplified example, but my use-case is the response I get from a REST api using invoke-restmethod, certain properties come back as empty psobjects.
It is the simplest solution to test for an empty (property-less) custom object ([pscustomobject]) via its string representation, but you need to use an expandable string (string interpolation, "...") rather than .ToString() to obtain it:
# Returns $True, if custom object $test is empty, i.e. has no properties
-not "$test"
Note: -not $test.ToString() should be equivalent, but currently (as of PowerShell Core 6.1) isn't, due to a bug. With the bug present, any [pscustomobject] instance returns the empty string from.ToString().
Another workaround is to use .psobject.ToString().
Only an empty (property-less) custom object stringifies to the empty string inside an expandable string, and coercing an empty string to a Boolean in PowerShell yields $False, whereas any nonempty string yields $True.
The alternative is to compare against an empty string as the LHS, which implicitly forces the [pscustomobject] on the RHS to be stringified:
# NOTE: Works ONLY with '' on the LHS.
'' -eq $test
A conceptually clearer approach, though it relies on the hidden .psobject property PowerShell adds to all objects, containing reflection information:
0 -eq #($test.psobject.Properties).Count
Note the need to use #(...) to force enumeration of the properties so that they can be counted - see next section.
The above methods are convenient, but if $test is a large object with many properties, it can be expensive - though in absolute terms that will propbably rarely matter in practice.
A less expensive, but more obscure solution is to access the .psobject.Properties collection without enumerating all its members:
# Returns $true, if $test has no properties
-not $test.psobject.Properties.GetEnumerator().MoveNext()
The .psobject.Properties collection is apparently lazily enumerated and therefore doesn't have a .Count property; using .GetEnumerator().MoveNext() is therefore a way to limit enumeration to the first property, if any.
As for what you tried:
$test -eq $null
$test is still an object, even if it happens to have no properties, and an object is by definition never $null.
-not $test
PowerShell's implicit to-Boolean conversion treats any [pscustomobject] instance as $True, whether or not it happens to have properties; e.g., [bool] ([pscustomobject] #{}) yields $True.
To see how other data types are coerced to Booleans, see this answer.
Probably more expensive, but less obscure; is using the the native Get-Member cmdlet:
[Bool]($Test | Get-Member -MemberType NoteProperty)
Note that $Test should not be $Null (rather than an empty object) otherwise it will produce an error (as with using methods on $Null). To avoid this you might also consider using:
$Test -and ($Test | Get-Member -MemberType NoteProperty)
use string tests & test with the $Var on the right side of the comparison so that it is coerced to the type on the left. you can also test with the [string] methods below ... [grin]
$Test = '{ }' | ConvertFrom-Json
$Test -eq $Null
$Null -eq $Test
$Test -eq ''
''
'' -eq $Test
[string]::IsNullOrEmpty($Test)
[string]::IsNullOrWhiteSpace($Test)
output ...
False
False
False
True
True
True
This works for me...
if ((ConvertTo-Json -Depth 1 $testObj) -eq (ConvertTo-Json #{})) {
echo 'empty object'
}

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.

powershell logical operators and $Error variable

Recently I had to check whether some errors occurred when my script is executed. First I've tried to check whether $Error is $null. The strange thing for me was that I haven't got any result from (neither True, nor False). Then i've wrote:
if (($error -eq $null) -or ($error -ne $null)) {Write-Host "NULL"}
And nothing was in the output. This made me very confused. I've found that such thing happens for all variables which are of System.Collections.ArrayList type.
Maybe someone knows the explanation why this happens, because for me this looks like a bug?
Version of Powershell, on which I found this, is 3.0.
#mjolinor's answer tries to explain it, but is incomplete.
When you do (1,2,3) -eq 1, you get back 1. In this case what -eq does with an array is to return the element that is equal to the RHS, and nothing if no match occurs.
On the other hand, if you do 1 -eq (1,2,3), you get False, because the above occurs only when the array is the LHS. So it is not true that the -eq operator always does behaves like the above case when it comes to arrays.
Now, coming on to the -ne usage. When you do (1,2,3) -ne 1, you get the array 2,3. That is, it returns the elements that are not equal to the RHS. And similar to -eq, 1 -ne (1,2,3), will return True
Coming to your condition - ($error -eq $null) -or ($error -ne $null)
When $error is empty, $error -eq $null will return nothing ( and is hence False in a bool statement). This is of course because there is no element matching $null in $error. Also, $error -ne $null will also return nothing ( and hence is False in a bool statement) because $error is empty and there is no element in it that is not $null.
Hence, when $error is empty, your statement will be false and the block inside if will not be executed.
If $error were not empty, either of the condition would have been true, and you would have seen the write-hostexecuted.
So how do you really solve this problem?
The straightforward way is to check the length of the $error array:
if($error.length -gt 0){
write-host "error occured"
}
Also, read this article that talks about various error handling strategies - http://blogs.technet.com/b/heyscriptingguy/archive/2011/05/12/powershell-error-handling-and-why-you-should-care.aspx
When the -eq operator is used against an array (or arraylist), it returns all the members of the array that satisfiy the condition.
($error -eq $null) says "I want all the members of the $error arraylist that are nulls." It can't return anything but $null.
When you use it in an IF, the result is going to be cast as [bool]. $Null evaluates to $false when cast as [bool].
($error -eq $null) can never be True.
$x = new-object collections.arraylist
[void]$x.Add('a')
[void]$x.add('b')
($x -eq $null).count
[bool]($x -eq $null)
[void]$x.Add($Null)
($x -eq $null).count
[bool]($x -eq $null)
0
False
1
False