How to exit from ForEach-Object in PowerShell
PowershellForeachPowershell Problem Overview
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?
Powershell Solutions
Solution 1 - Powershell
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.
Solution 2 - Powershell
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"
Solution 3 - Powershell
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
.
Solution 4 - Powershell
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
Solution 5 - Powershell
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.
Solution 6 - Powershell
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
Solution 7 - Powershell
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
Solution 8 - Powershell
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.
Solution 9 - Powershell
You have two options to abruptly exit out of ForEach-Object
pipeline in PowerShell:
- Apply exit logic in
Where-Object
first, then pass objects toForeach-Object
, or - (where possible) convert
Foreach-Object
into a standardForeach
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
Solution 10 - Powershell
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
}
};