93

I have the following code:

$project.PropertyGroup | Foreach-Object {
    if($_.GetAttribute('Condition').Trim() -eq $propertyGroupConditionName.Trim()) {
        $a = $project.RemoveChild($_);
        Write-Host $_.GetAttribute('Condition')"has been removed.";
    }
};

Question #1: How do I exit from ForEach-Object? I tried using "break" and "continue", but it doesn't work.

Question #2: I found that I can alter the list within a foreach loop... We can't do it like that in C#... Why does PowerShell allow us to do that?

Eddie Kumar
  • 1,216
  • 18
  • 20
Michael Sync
  • 4,834
  • 10
  • 40
  • 58

11 Answers11

146

First of all, Foreach-Object is not an actual loop and calling break in it will cancel the whole script rather than skipping to the statement after it.

Conversely, break and continue will work as you expect in an actual foreach loop.

Item #1. Putting a break within the foreach loop does exit the loop, but it does not stop the pipeline. It sounds like you want something like this:

$todo=$project.PropertyGroup 
foreach ($thing in $todo){
    if ($thing -eq 'some_condition'){
        break
    }
}

Item #2. PowerShell lets you modify an array within a foreach loop over that array, but those changes do not take effect until you exit the loop. Try running the code below for an example.

$a=1,2,3
foreach ($value in $a){
  Write-Host $value
}
Write-Host $a

I can't comment on why the authors of PowerShell allowed this, but most other scripting languages (Perl, Python and shell) allow similar constructs.

Agostino
  • 2,723
  • 9
  • 48
  • 65
smeltplate
  • 1,729
  • 1
  • 11
  • 7
  • I think you meant to add a line like, `$a = 1..23;` in your loop to show that changes aren't reflected mid-loop execution. – ruffin Jun 09 '15 at 21:44
  • 2
    Item #2 is incorrect (at least with v5 with a fixed sized array). You can modify the array and have it visible within the `ForEach` construct. If you were to add `$a[1] = 9` inside of the `ForEach` construct, it will show 9 as the 2nd element, but you cannot add/remove elements from the array, and anything you add using the `+=` operator will not be shown until the end. If not a fixed sized array (i.e. `$a=[System.Collections.ArrayList]@(1,2,3)`) then any attempt to modify the contents will cause `Collection was modified; enumeration operation may not execute.` error, terminating the loop. – Adrian Jan 26 '16 at 20:19
  • 8
    This is an entirely incorrect answer. The question is in regards to `Foreach-Object`. In powershell (majorly annoying gotcha btw) `foreach` is an "operator" and an "alias." `foreach` provided in this answer by smeltplate is the "operator" and not the "alias". See [here.](https://blogs.technet.microsoft.com/heyscriptingguy/2014/07/08/getting-to-know-foreach-and-foreach-object/) Note: this answer is an entire refactoring of code and may not work for many situations. – Jamie Marshall Jul 16 '17 at 00:40
  • 5
    @JamieMarshall Yes, you are absolutely correct. break in a ForEach-Object loop ends the whole script in my test cases – Tom Jan 11 '18 at 14:21
  • This doesn't work for me - I get a Break Exception on `Get-ADGroupMember` – PeterX Feb 02 '18 at 03:47
41

There are differences between foreach and foreach-object.

A very good description you can find here: MS-ScriptingGuy

For testing in PS, here you have scripts to show the difference.

ForEach-Object:

# Omit 5.
1..10 | ForEach-Object {
if ($_ -eq 5) {return}
# if ($_ -ge 5) {return} # Omit from 5.
Write-Host $_
}
write-host "after1"
# Cancels whole script at 15, "after2" not printed.
11..20 | ForEach-Object {
if ($_ -eq 15) {continue}
Write-Host $_
}
write-host "after2"
# Cancels whole script at 25, "after3" not printed.
21..30 | ForEach-Object {
if ($_ -eq 25) {break}
Write-Host $_
}
write-host "after3"

foreach

# Ends foreach at 5.
foreach ($number1 in (1..10)) {
if ($number1 -eq 5) {break}
Write-Host "$number1"
}
write-host "after1"
# Omit 15. 
foreach ($number2 in (11..20)) {
if ($number2 -eq 15) {continue}
Write-Host "$number2"
}
write-host "after2"
# Cancels whole script at 25, "after3" not printed.
foreach ($number3 in (21..30)) {
if ($number3 -eq 25) {return}
Write-Host "$number3"
}
write-host "after3"
Stoffi
  • 595
  • 6
  • 9
  • 3
    This is a proper answer. It explains commandlet ForEach-Object‚ as in question. Example uses its alias foreach, but still it is commandlet. Accepted answer works, but it explains keyword foreach. I know PowerShell is confusing in this matter, but still these are two different things. – Igor Nov 09 '20 at 23:04
  • I spent so many hours debugging code, wondering why `break` was breaking out of my whole script, instead of just the `ForEach-Object` loop, until I finally found this answer. Thank you! – martorad Oct 01 '21 at 07:13
11

To stop the pipeline of which ForEach-Object is part just use the statement continue inside the script block under ForEach-Object. continue behaves differently when you use it in foreach(...) {...} and in ForEach-Object {...} and this is why it's possible. If you want to carry on producing objects in the pipeline discarding some of the original objects, then the best way to do it is to filter out using Where-Object.

Makyen
  • 31,849
  • 12
  • 86
  • 121
darlove
  • 317
  • 2
  • 10
  • 1
    +1. Interesting. I never knew this. It seems to work the same whether you use `break` or `continue`. I wonder why this didn't work for the author of the question since they said they tried both. – Lance U. Matthews Aug 22 '17 at 22:35
  • 1
    Any idea where this is documented? [`about_break`](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_break) and [`about_continue`](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_continue) only talk about `for`, `foreach`, `while`, and `switch` statements. [`ForEach-Object`](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/foreach-object) makes no mention of `break` or `continue`. – Lance U. Matthews Aug 22 '17 at 22:35
  • This doesn't work for me - I get a Continue Exception on `Get-ADGroupMember` – PeterX Feb 02 '18 at 03:48
  • Continue can break the execution of a parent loop in this case. See explainations here : https://powershell.org/forums/topic/best-way-to-break-out-of-foreach-loop/#post-19909 – Alsatian Sep 14 '18 at 07:23
  • The link is now https://forums.powershell.org/t/best-way-to-break-out-of-foreach-loop/3289/3 – Vopel Feb 08 '22 at 02:18
8

Since ForEach-Object is a cmdlet, break and continue will behave differently here than with the foreach keyword. Both will stop the loop but will also terminate the entire script:

break:

0..3 | foreach {
    if ($_ -eq 2) { break }
    $_
}
echo "Never printed"

# OUTPUT:
# 0
# 1

continue:

0..3 | foreach {
    if ($_ -eq 2) { continue }
    $_
}
echo "Never printed"

# OUTPUT:
# 0
# 1

So far, I have not found a "good" way to break a foreach script block without breaking the script, except "abusing" exceptions, although powershell core uses this approach:

throw:

class CustomStopUpstreamException : Exception {}
try {
    0..3 | foreach {
        if ($_ -eq 2) { throw [CustomStopUpstreamException]::new() }
        $_
    }
} catch [CustomStopUpstreamException] { }
echo "End"

# OUTPUT:
# 0
# 1
# End

The alternative (which is not always possible) would be to use the foreach keyword:

foreach:

foreach ($_ in (0..3)) {
    if ($_ -eq 2) { break }
    $_
}
echo "End"

# OUTPUT:
# 0
# 1
# End
Joel Pearson
  • 1,622
  • 1
  • 16
  • 28
marsze
  • 15,079
  • 5
  • 45
  • 61
  • 2
    It turns out that your "throw" example is how [powershell core](https://github.com/PowerShell/PowerShell/blob/883ca98dd74ea13b3d8c0dd62d301963a40483d6/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Select-Object.cs#L725) does it. So I think it's the only solution really. I tried to figure out how to use the `System.Management.Automation.StopUpstreamCommandsException`, but couldn't quite figure out how. But I guess throwing a specific exception and only catching that exception could make it so that you don't ignore other exceptions – Joel Pearson Aug 09 '21 at 05:01
  • It will probably confuse readers that you used `foreach` alias for `ForEach-Object` since your answer covers differences between the two – Jason S Jun 14 '23 at 01:58
  • @JasonS That is really the point of the answer, to show they look alike but behave differently, and I hope I sufficiently described the differences. It can only be beneficial to train your eye to spot which one it is, since real-world scripts won't go easy on you either. – marsze Jun 14 '23 at 07:00
  • @marsze oh, I see your point now in intentionally making it real world where readability often falls by the wayside. I guess I just thought some readers, here for help, still won't realise one of those `foreach` is an unfortunate alias to `ForEach-Object` – Jason S Jun 14 '23 at 23:22
  • @JoelPearson you can certainly do it if you write a binary cmdlet https://gist.github.com/santisq/85e322d0e88545b60bb7dcd70af26c4b#file-testanycommand-cs-L34-L39 – Santiago Squarzon Jul 17 '23 at 00:44
7

There is a way to break from ForEach-Object without throwing an exception. It employs a lesser-known feature of Select-Object, using the -First parameter, which actually breaks the pipeline when the specified number of pipeline items have been processed.

Simplified example:

$null = 1..5 | ForEach-Object {

    # Do something...
    Write-Host $_
 
    # Evaluate "break" condition -> output $true
    if( $_ -eq 2 ) { $true }  
 
} | Select-Object -First 1     # Actually breaks the pipeline

Output:

1
2

Note that the assignment to $null is there to hide the output of $true, which is produced by the break condition. The value $true could be replaced by 42, "skip", "foobar", you name it. We just need to pipe something to Select-Object so it breaks the pipeline.

zett42
  • 25,437
  • 3
  • 35
  • 72
5

If you insist on using ForEach-Object, then I would suggest adding a "break condition" like this:

$Break = $False;

1,2,3,4 | Where-Object { $Break -Eq $False } | ForEach-Object {

    $Break = $_ -Eq 3;

    Write-Host "Current number is $_";
}

The above code must output 1,2,3 and then skip (break before) 4. Expected output:

Current number is 1
Current number is 2
Current number is 3
Rikki
  • 3,338
  • 1
  • 22
  • 34
4

Below is a suggested approach to Question #1 which I use if I wish to use the ForEach-Object cmdlet. It does not directly answer the question because it does not EXIT the pipeline. However, it may achieve the desired effect in Q#1. The only drawback an amateur like myself can see is when processing large pipeline iterations.

    $zStop = $false
    (97..122) | Where-Object {$zStop -eq $false} | ForEach-Object {
    $zNumeric = $_
    $zAlpha = [char]$zNumeric
    Write-Host -ForegroundColor Yellow ("{0,4} = {1}" -f ($zNumeric, $zAlpha))
    if ($zAlpha -eq "m") {$zStop = $true}
    }
    Write-Host -ForegroundColor Green "My PSVersion = 5.1.18362.145"

I hope this is of use. Happy New Year to all.

ThePennyDrops
  • 151
  • 1
  • 10
3

I found this question while looking for a way to have fine grained flow control to break from a specific block of code. The solution I settled on wasn't mentioned...

Using labels with the break keyword

From: about_break

A Break statement can include a label that lets you exit embedded loops. A label can specify any loop keyword, such as Foreach, For, or While, in a script.

Here's a simple example

:myLabel for($i = 1; $i -le 2; $i++) {
        Write-Host "Iteration: $i"
        break myLabel
}

Write-Host "After for loop"

# Results:
# Iteration: 1
# After for loop

And then a more complicated example that shows the results with nested labels and breaking each one.

:outerLabel for($outer = 1; $outer -le 2; $outer++) {

    :innerLabel for($inner = 1; $inner -le 2; $inner++) {
        Write-Host "Outer: $outer / Inner: $inner"
        #break innerLabel
        #break outerLabel
    }

    Write-Host "After Inner Loop"
}

Write-Host "After Outer Loop"

# Both breaks commented out
# Outer: 1 / Inner: 1
# Outer: 1 / Inner: 2
# After Inner Loop
# Outer: 2 / Inner: 1
# Outer: 2 / Inner: 2
# After Inner Loop
# After Outer Loop

# break innerLabel Results
# Outer: 1 / Inner: 1
# After Inner Loop
# Outer: 2 / Inner: 1
# After Inner Loop
# After Outer Loop

# break outerLabel Results
# Outer: 1 / Inner: 1
# After Outer Loop

You can also adapt it to work in other situations by wrapping blocks of code in loops that will only execute once.

:myLabel do {
    1..2 | % {

        Write-Host "Iteration: $_"
        break myLabel

    }
} while ($false)

Write-Host "After do while loop"

# Results:
# Iteration: 1
# After do while loop
Alex Hague
  • 1,756
  • 1
  • 13
  • 20
0

You have two options to abruptly exit out of ForEach-Object pipeline in PowerShell:

  1. Apply exit logic in Where-Object first, then pass objects to Foreach-Object, or
  2. (where possible) convert Foreach-Object into a standard Foreach looping construct.

Let's see examples: Following scripts exit out of Foreach-Object loop after 2nd iteration (i.e. pipeline iterates only 2 times)":

Solution-1: use Where-Object filter BEFORE Foreach-Object:

[boolean]$exit = $false;
1..10 | Where-Object {$exit -eq $false} | Foreach-Object {
     if($_ -eq 2) {$exit = $true}    #OR $exit = ($_ -eq 2);
     $_;
}

OR

1..10 | Where-Object {$_ -le 2} | Foreach-Object {
     $_;
}

Solution-2: Converted Foreach-Object into standard Foreach looping construct:

Foreach ($i in 1..10) { 
     if ($i -eq 3) {break;}
     $i;
}

PowerShell should really provide a bit more straightforward way to exit or break out from within the body of a Foreach-Object pipeline. Note: return doesn't exit, it only skips specific iteration (similar to continue in most programming languages), here is an example of return:

Write-Host "Following will only skip one iteration (actually iterates all 10 times)";
1..10 | Foreach-Object {
     if ($_ -eq 3) {return;}  #skips only 3rd iteration.
     $_;
}

HTH

Eddie Kumar
  • 1,216
  • 18
  • 20
0

While this is a bit hacky, it works:

$array = 'a'..'z'
:pipe do{
    $array | ForEach-Object {
        $_
        if ($_ -eq 'c') {
            break :pipe
        }
    }
} until ($true)

Output:
a
b
c

The code can also be condensed easily, like so:

$array = 'a'..'z'
:_ do{ $array | ForEach-Object {
    $_
    if ($_ -eq 'c') {
        break :_
    }
}}until(1)

# OR:
#}}until({})
# not sure why {} evaluates to True, but whatever
# anything that evaluates to True will work, e.g.: !''
Vopel
  • 662
  • 6
  • 11
-1

Answer for Question #1 - You could simply have your if statement stop being TRUE

$project.PropertyGroup | Foreach {
    if(($_.GetAttribute('Condition').Trim() -eq $propertyGroupConditionName.Trim()) -and !$FinishLoop) {
        $a = $project.RemoveChild($_);
        Write-Host $_.GetAttribute('Condition')"has been removed.";
        $FinishLoop = $true
    }
};
  • 2
    The problem is that this doesn't stop the loop; it will just evaluate to `$false` over and over again (this will be pretty slow for very large and/or expensive collections). – Bill_Stewart Nov 06 '19 at 23:06
  • Yeah, the way you do it, is not what the author asked for. Your pipeline is run from the beginning to the very end for each and every object fed into the pipeline. It does not stop it midway but just stops creating output for some of the objects, still consuming them which can be very costly if there are many of them. That's definitely not what the author asked for. – darlove Feb 09 '22 at 10:40