TheGeekery

The Usual Tech Ramblings

PowerShell: Loops, ForEach, ForEach-Object, and control functions

In my previous post about PowerShell and BITS, I stumbled on a weird quirk in the ForEach-Object function, which had me scratching my head for a bit.

In most programming languages, where you have a for, loop, while, foreach, or other such loop, there is often a set of control functions that go with it. Continue and Break. These functions alter the way a loop is behaving at that time. A call to Continue will stop the current loop iteration at that point in the code, and jump onto the next. A simple example would look like this:

for( $i = 0; $i -lt 5; $i++ ) {
  if ($i -eq 2) {
    continue;
  }
  $i
}

This code simply takes the variable $i, sets it to 0, and whilst $i is less than 5, it loops through the code, each iteration increasing the value of $i by 1. The if statement executes special code when $i gets to the number 2. Now if we run this code, we get the following:

0
1
3
4

What happened was the counter hit 2, and the code said that it was done with this iteration, and continue processing the outer loop. A Break on the other hand just tells the loop it’s done processing, and to stop looping, and get out. Using the same example as above, but changing Continue to Break we get the output that looks like this:

0
1

This is because PowerShell has been told to stop processing any further.

Now for the quirk. I discovered that ForEach-Object isn’t actually a programmatic keyword, it’s a cmdlet. How does this affect the usage of the control functions? Lets look at my code from my BITS article, and compare it to what I had.

    $images = $_.images

    $job = $null
    $img_split = $images.Split('|')
    for($i = 0; $i -lt $img_split.Count; $i++) {
        if ($img_split[$i].Length -eq 0) {
            continue
        }

As you can see, I’m using the continue function inside the array of strings for the images. If the string value is empty, I skip onto the next one. This is what the original code looked like:

    $images = $_.images
    if ($images.Length -eq 0) {
        continue
    }
    $job = $null

Whilst the code seems pretty similar, there is one distinct difference. The first block of code is inside a loop, the second set is not. Even though you think that ForEach-Object seems like a loop, it’s a cmdlet that does not behave the same way that a loop does. How does this affect the behavior of the code? Badly! Because there is no loop, the control function looks for the next operation it can operate on. It turns out that the next function it can operate on is the entire script. It essentially kills the script right where the line is. This means no processing of other objects in the ‘loop’, no continuing with the code further in the script, no nice handling of anything else in the script. It’s done, finished, over. My hint something wasn’t behaving right was the fact I should have had several thousand images, and yet I only had about 150. It took 4 or 5 attempts, as well as various debug statements throughout the code to figure out why this was happening. After I realized where it was going wrong, a Google search dropped me over to James Manning’s post PowerShell gotcha - foreach keyword vs. foreach-object cmdlet.

So if you ever find yourself in need of an object loop, remember that ForEach-Object isn’t really a keyword, it’s a cmdlet, and does not behave the same.

See any mistakes? Want to add your feedback? Leave me a note in the comments, I love to hear from you.

Comments