3

In PowerShell (5.1.14393.1944), when I have a varibale with a value of $null, a ForEach-Object loop attempts to iterate over the object, causing undesired results.

In this examaple, I am attempting to retrun the highest maximum result from a set of data. However, if there is no data, -1 should be returned.

function Get-MaximumResult
{
    param(
        [parameter(Mandatory,Position=0)]
        [AllowNull()]
        [object]$MetricsData
    )
    if (-not($MetricsData)) { Write-Host "Debug: Variable is null" }
    [int]$maximum = -1
    $MetricsData | ForEach-Object {
       if ([int]$_.maximum -gt $maximum) { $maximum = [int]$_.maximum }
       Write-Host "Debug: Maximum: $maximum"
    }
    return $maximum
}
Get-MaximumResult -MetricsData $null

The output generated is as follows -

Debug: Variable is null
Debug: Maximum: 0
0

What I would expect to see is this -

Debug: Variable is null
-1

I have found a couple of similar questions from a few years ago, however the answers indicate that this bug was fixed in version 3. Has anyone else experienced this bug?

David Gard
  • 11,225
  • 36
  • 115
  • 227
  • it is the foreach-loop that doesn't iterate over `$null` since version 3. The Foreach-Object cmdlet, however, doesn't protect you from iterating over `$null`. You can simply fix this by using a foreach-loop instead of the Foreach-Object cmdlet. – J. Bergmann Feb 21 '18 at 12:01
  • Many thanks, that does indeed do the trick. If only there were any sort of consistancy in PowerShell... – David Gard Feb 21 '18 at 12:07

2 Answers2

5

Per the comments, the issue occurs because you're using the ForEach-Object cmdlet, which processes items via the pipeline.

If you use the foreach statement instead then it will not perform recursion for a $null valued collection (apparently as of PS version 3). You can see that behaviour with this example:

$null | ForEach-Object { write-host 'In feo' }

foreach ($thing in $null) {
    write-host 'in fe'
}

I assume this occurs because the foreach statement does some upfront evaluation of what its working with before it performs any operations and if it finds that collection to be empty it doesn't perform its operation.

ForEach-Object on the other hand is just taking input from the pipeline, processing it one at a time and sending any output down the pipe, regardless of whether that input has value or not. I can see a scenario where this might make sense, as you might not necessarily want the pipeline to stop as a result of a $null value.

Mark Wragg
  • 22,105
  • 7
  • 39
  • 68
2

I found this answer on another question very helpful.

In a nutshell, there are two different nulls in PowerShell: regular null, and AutomationNull. The purpose of AutomationNull is, basically, to stop pipelines when there is no output produced. AutomationNull is transparently converted to regular null in various situations, so it is hard to see with normal introspection functions.

Here's a simple example.

$null | ForEach-Object { 'Hello world!' } # --> output: 'Hello world!'

$null | Where-Object { $_ } | ForEach-Object { 'Hello world!' } # --> no output

In the second line, Where-Object produces an AutomationNull which causes ForEach-Object to stop iterating before it has even started.

The reason for this mess is that $null might be a valid output of a previous command, and we might want ForEach-Object to process it and keep going.

Toy example:

@(1, 2, $null, 4, 5, 6, $null, 8) | ForEach-Object {
    if ($null -eq $_) {
        Write-Host "Got null"
    } else {
        Write-Host "Got something"
    }
}

We don't want the ForEach loop to stop when it hits the first null.

PowerShell is trying to be both a powerful OO scripting language and a convenient CLI, and this feels like one of those times where it's forced into an annoying compromise.

John Fouhy
  • 41,203
  • 19
  • 62
  • 77