1

Foreach-Object is treating $null value differently in different cases. What is the ideal behavior?

## Case 1: 
 $someitem = $null 
 if($null -EQ $someitem) { write-host "true" } # prints true
 $someitem | ForEach-Object { write-host "hello" } # prints hello
## Case 2 :
 $items = Get-ChildItem -Path "someNotExistingPath" # throws error
 if($null -EQ $items) { write-host "true"} # prints true
 $items | ForEach-Object { write-host "hello" } # prints nothing

The expected behavior would have been that both foreach-object print "hello", but in the case when we have tried to assign some value to $items, it behaves unually inside foreach loop

  • `$someitem` is `$null` in the singular form (the same as an array of 1), `$items` is an empty array and so `$items | ForEach-Object` has 0 elements to iterate over – dunck May 17 '19 at 09:30

1 Answers1

4

I traced the differences between $items | forEach-Object { Write-host "hello"} and $null | ForEach-Object { Write-Host "hello"} via Trace-Command.

   PS C:>Trace-Command -Name parameterbinding -Expression {  $items | ForEach-Object { write-host "hello" } } -PSHost
   DEBUG: ParameterBinding Information: 0 : BIND NAMED cmd line args [Out-Null]
   DEBUG: ParameterBinding Information: 0 : BIND POSITIONAL cmd line args [Out-Null]
   DEBUG: ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Out-Null]
   ....


   PS C:> Trace-Command -Name parameterbinding -Expression {  $null | ForEach-Object { write-host "hello" } } -PSHost
   DEBUG: ParameterBinding Information: 0 : BIND NAMED cmd line args [ForEach-Object]
   DEBUG: ParameterBinding Information: 0 : BIND POSITIONAL cmd line args [ForEach-Object]
   DEBUG: ParameterBinding Information: 0 :     BIND arg [ write-host "hello" ] to parameter [Process]
   DEBUG: ParameterBinding Information: 0 :         Binding collection parameter Process: argument type [ScriptBlock], parameter type [System.Management.Automation.ScriptBlock[]], collection type Array, element type [System.Management.Automation.ScriptBlock], no

It seems that $items points to the Out-Null cmdlet, which is shown via:

DEBUG: ParameterBinding Information: 0 : BIND NAMED cmd line args [Out-Null]

So it seems that Get-ChildItem returns a reference to Out-Null in case of an error. If you compare this to $null | ForEach-Object ... you'll that the ForEach-Object will be invoked directly:

DEBUG: ParameterBinding Information: 0 : BIND NAMED cmd line args [ForEach-Object]

What's also interesting, if you use ForEach-Object with the -InputObject parameter the code works as requested:

 PS C:> Trace-Command -Name parameterbinding -Expression {  ForEach-Object -InputObject $items -Process { write-host "hello" } } -PSHost
 DEBUG: ParameterBinding Information: 0 : BIND NAMED cmd line args [ForEach-Object]
 DEBUG: ParameterBinding Information: 0 :     BIND arg [] to parameter [InputObject]

So my "guess" is the following. In case of an error (of Get-ChildItem) you won't that below code prints an output:

PS C:\> Get-ChildItem -Path "notExisting" | ForEach-Object { Write-Host "found" }

That makes perfectly sense, Get-ChildItem "calls" Out-Null which will clear the pipeline, which will break the pipeline chain (= if nothing is found, nothing shall be printed).

Base on that, this call statement $items = Get-ChildItem -Path "someNotExistingPath" is invoked, but Get-ChildItem returns a null type that is not equal to $null. When performing this code if($null -EQ $items) PowerShell will more or less perform a implicit cast of the Get-ChildItem-null-type to $null. When it comes to this call $items | ForEach-Object, nothing else should be send to the pipeline since $items contains the reulst of Out-Null.

UPDATE:

In the meanwhile @iRon also added a duplicate link, which explains the details. I'll keep the answer since this link doesn't show the usage of Trace-Command. Hope thats ok for the community.

Hope that helps.

Moerwald
  • 10,448
  • 9
  • 43
  • 83