foreach
can be confusing if you don’t pay attention to the context. Googling (or Binging, is that even a word? 🤔) around you can easily find articles and blog posts about foreach
, how it is both a keyword and an alias (shortcut) for Foreach-Object
and how foreach
as keyword is different from Foreach-Object
. Here are a couple of examples:
foreach
as keyword is a pattern common across many programming and scripting languages used to loop through a list:
PS > $files = Get-ChildItem foreach ($file in $files) { $file.FullName } /hooks /info /objects /refs /src /config /description /foreachtest.ps1 /HEAD /LICENSE /post.txt /README.md
foreach
as alias for Foreach-Object
can be used to get a similar output:
PS > Get-ChildItem | ForEach-Object { $_.FullName } /hooks /info /objects /refs /src /config /description /foreachtest.ps1 /HEAD /LICENSE /post.txt /README.md
In a more complex, real life scenario I may need to exit the loop without going through the whole list, that’s easily done using the break
keyword:
PS > Get-ChildItem ./src/ Directory: /src Mode LastWriteTime Length Name ---- ------------- ------ ---- ----- 9/6/2019 10:31 PM 372 array.ps1 ----- 9/5/2019 9:55 PM 685 foreachparallel.ps1 ----- 9/2/2019 9:20 PM 1261 getchilditem.ps1 ----- 9/2/2019 9:20 PM 417 jobs.ps1 ----- 9/1/2019 8:57 PM 52 nopipeline.ps1 ----- 9/1/2019 8:33 PM 303 object.ps1 ----- 9/1/2019 8:58 PM 99 pipeline.ps1 ----- 9/2/2019 9:20 PM 123 pipelinearray.ps1 ----- 9/5/2019 10:28 PM 680 runspace.ps1 ----- 9/1/2019 8:23 PM 248 string.ps1 PS > $files = dir ./src/ foreach ($f in $files) { $f.name if ($f.name -eq 'jobs.ps1') { break } } Write-Host 'after foreach' array.ps1 foreachparallel.ps1 getchilditem.ps1 jobs.ps1 after foreach
Ok, this is not a real, real world scenario but you get the idea: I’m looping through the list of files till a reach one named jobs.ps1 and skip the rest of the iteration, and as expected, the sample still prints the after loop message. Let’s try with Foreach-Object
:
PS > Get-ChildItem ./src | ForEach-Object { $_.Name if ($_.Name -eq 'jobs.ps1') { break } } Write-Host 'after foreach-object' array.ps1 foreachparallel.ps1 getchilditem.ps1 jobs.ps1
Uhm… The loop was interrupted but at the wrong moment (jobs.ps1 should have not been printed to the console), moreover the output from Write-Host
is missing, why? 🤔. After all, foreach
as keyword and Foreach-Object
as cmdlet both allow to loop through a list of objects, so why this difference?
Well, (part) of the answer is in my previous sentence: foreach
is a keyword and Foreach-Object
is a cmdlet… 💡
This is an important difference especially if we consider the role of the other keyword in these statements: break
. foreach
statements are executed in a new scope (for example this behavior is very evident working with variables), therefore break
acts on this nested scope and allows to break free and interrupts the loop.
Foreach-Object
on the other hand does not create its own nested scope: when execution enters this type of loop, the current scope is still script (the default execution context), which means that the break
keyword does not have any other scopes to exit from than script
which in turn terminates the script execution. That’s why my Write-Host
statement is not executed.
So, how do we exit a Foreach-Object
loop but continue with the script execution? While technically there is a solution it is not very elegant and the proper answer as of this writing is that, simply put, it is not possible to cleanly exit from a Foreach-Object
loop and continue the script execution. The not elegant solution consist in wrapping Foreach-Object
into a for
loop and use either continue
or break
:
PS > for ($i = 0; $i -lt 1; $i++) { Get-ChildItem ./src | ForEach-Object { if ($_.Name -ne 'jobs.ps1') { $_.Name } else { continue } } } Write-Host 'after foreach-object' array.ps1 foreachparallel.ps1 getchilditem.ps1 after foreach-object
Here I am using a for
loop with just one iteration and the continue
keyword when I want to interrupt the Foreach-Object
loop: continue
cannot properly interrupt Foreach-Object
so it will walk up the call stack looking for a proper loop or switch
statement to act upon. Since Foreach-Object
is directly wrapped into the for
loop, this is where continue
will act and execution will continue with the rest of the script. The more elegant approach is to use a proper foreach
loop as shown in the example above.
Finally, here are a couple of interesting discussions on Github
- Foreach loop using | % {} syntax doesn’t support continue
- Allow user code to stop a pipeline on demand / to terminate upstream cmdlets