Powershell

Break the pipeline

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


Chop your own wood, and it will warm you twice – Henry Ford

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.