Essential PowerShell: Understanding foreach (Addendum)

I need to add an important addendum to the Essential PowerShell: Understanding foreach article I posted (if you haven’t read it already, you might find it worthwhile to start with that article first before reading this article).

As Dmitry Sotnikov discovered first-hand and blogged about earlier today, there is another important difference between what might appear as two similar constructs in PowerShell: the foreach statement and the foreach alias (which is an alias to the ForEach-Object cmdlet).  The foreach statement is a loop construct.  The foreach alias (and therefore the ForEach-Object cmdlet) is not a loop construct.  It’s a cmdlet.  Why is this important?  Because you can control the logic within a loop construct using the break and continue statements, but not within a cmdlet.

Let’s examine this in more detail using a new example.  This time I’ll write a PowerShell script that will take in a string and output each character of that string that is not a vowel.  Note that this is just an example and it is not necessarily the best way to do this sort of thing.  I’m just using it to get the point across.

Here is the foreach statement example:

foreach ($character in [char[]]”Poshoholic”) { if (@(‘a’,’e’,’i’,’o’,’u’) -contains $character ) { continue } $character }

And now what appears to be the equivalent foreach alias example:

[char[]]”Poshoholic” | foreach { if (@(‘a’,’e’,’i’,’o’,’u’) -contains $_ ) { continue } $_ }

If you run each of these two examples, you’ll quickly see that they don’t function the same way at all.

In the foreach statement example script you get what you would expect: each consonant in the string “Poshoholic” is output to the host, so you’ll see P, s, h, h, l and c, each on a separate line.  In the foreach alias example script, though, only the consonant characters that were before the first vowel in the string are output to the host.  In this case, all that is output is P.  Why?  Because the continue statement (and the break statement) only affect the logic of the program loop in which they are contained.  Since the second example doesn’t actually contain a program loop (remember that the foreach alias and ForEach-Object cmdlet are not a loop construct), the continue statement will apply to the entire script in which it is contained.

You could correct this by removing the continue statement and changing the script as follows:

[char[]]”Poshoholic” | foreach { if ((@(‘a’,’e’,’i’,’o’,’u’) -contains $_) -eq $false ) {$_} }

This can be confusing, so if you have any questions feel free to post them in the comments and I’ll respond as quickly as I can.  And with a little luck, Microsoft will use this information to clarify their documentation in the about_foreach help topic in a future release of PowerShell.

Kirk out.

Technorati Tags: , ,

Advertisements

6 thoughts on “Essential PowerShell: Understanding foreach (Addendum)

  1. FYI, your last example is essentially a filter so I think this would be a bit more straight forward:

    [char[]]”Poshoholic” | where { @(’a’,’e’,’i’,’o’,’u’) -notcontains $_}

    Nice post!

    Like

  2. I agree completely that it would be more straightforward to use Where-Object. I just wanted to come up with a simple example comparing foreach with ForEach-Object and wasn’t that concerned that the example was not the best approach to whatever the script was designed to do (although I usually make the effort, coming up with the perfect example doesn’t always work), The examples in entries I write like this are usually written solely with the purpose of illustrating a point and not necessarily the recommended way to perform those operations.

    Thanks Keith!

    Like

  3. You can use the return statement for the ForEach-Object:
    [char[]]”Poshoholic” | foreach { if (@(‘a’,’e’,’i’,’o’,’u’) -contains $_ ) { return } $_ }

    Like

    1. That’s a good point, return works in place of continue inside of ForEach-Object by returning to the calling scope (the pipeline in which the ForEach-Object script block is contained). But return does not work in place of continue in the foreach statement, because in that scenario the calling scope is the scope above the script block in which the foreach statement is contained. For example, this does not work like you might expect it to after looking at your example:

      foreach ($char in [char[]]”Poshoholic”) {if (@(‘a’,’e’,’i’,’o’,’u’) -contains $char ) { return } $char }

      Another discrepancy to be aware of between the two commands that seem so similar at face value.

      Thanks for participating in the discussion! 🙂

      Kirk out.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s