如何在PowerShell中退出ForEach-Object

97

我有以下代码:

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

问题#1:如何从ForEach-Object中退出?我尝试使用“break”和“continue”,但它不起作用。

问题#2:我发现我可以在foreach循环中更改列表... 我们不能像C#那样做... 为什么PowerShell允许我们这样做呢?

11个回答

148

首先,Foreach-Object 不是一个真正的循环语句。如果在其中调用 break 将会取消整个脚本而不是跳过该语句继续执行。

相反,在实际的 foreach 循环中,breakcontinue 会按照预期工作。

第一点,将 break 放置在 foreach 循环内可以退出循环,但它并不会停止管道的流转。听起来你想要像这样的效果:

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

第二项。PowerShell允许您在对数组进行foreach循环时修改该数组,但这些更改直到退出循环后才生效。请尝试运行下面的代码以获得示例。

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

我无法评论PowerShell的作者为什么允许这种操作,但大多数其他脚本语言(如PerlPython和shell)都允许类似的结构。


1
我认为你的意思是在循环中添加一行代码,例如 $a = 1..23;,以显示在执行循环期间不会反映出更改。 - ruffin
2
第二项是不正确的(至少在使用固定大小数组的v5版本中)。您可以修改数组并在ForEach结构内可见。如果您在ForEach结构内添加$a[1] = 9,它将显示为第二个元素为9,但您无法添加/删除数组元素,并且使用+=运算符添加的任何内容直到结束时才会显示。如果不是固定大小的数组(即$a=[System.Collections.ArrayList]@(1,2,3)),则任何尝试修改其内容的操作都会导致“集合已修改;枚举操作可能无法执行。”错误,终止循环。 - Adrian
9
这是一个完全不正确的答案。问题涉及到 Foreach-Object。在PowerShell中(非常恼人的陷阱),foreach是一个"运算符"和"别名"。此答案中提供的foreach是"运算符",而不是"别名"。请参见这里。注意:此答案是代码的完全重构,可能不适用于许多情况。 - Jamie Marshall
5
@JamieMarshall 没错,你是完全正确的。在我的测试案例中,ForEach-Object循环中的break语句会结束整个脚本。 - Tom
这对我不起作用 - 我在“Get-ADGroupMember”上收到了一个Break Exception。 - PeterX

44

foreachforeach-object之间存在差异。

你可以在这里找到一个非常好的描述:MS-ScriptingGuy

为了在PS中进行测试,这里有一些脚本来展示差异。

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"

3
这是一个恰当的答案。它解释了问题中的命令行Let ForEach-Object。示例使用了它的别名foreach,但它仍然是一个命令行Let。接受的答案是正确的,但它解释的是关键字foreach。我知道PowerShell在这方面很令人困惑,但它们仍然是两个不同的东西。 - Igor
我花了很多时间调试代码,想知道为什么“break”会跳出整个脚本,而不仅仅是“ForEach-Object”循环,直到我最终找到这个答案。谢谢! - martorad

11

要停止其中 ForEach-Object 所在的管道,只需在 ForEach-Object 下的脚本块中使用语句 continue。当您在 foreach(...) {...}ForEach-Object {...} 中使用时,continue 的行为不同,这就是为什么它是可能的。如果您想继续在管道中生成对象并且丢弃一些原始对象,那么最好的方法是使用 Where-Object 进行过滤。


1
+1。有趣。我从来不知道这一点。无论你使用break还是continue,它似乎都能起作用。我想知道为什么这对问题的作者没有起作用,因为他们说他们尝试了两者。 - Lance U. Matthews
1
有没有想过这个在哪里有记录?about_breakabout_continue 只讲述了 forforeachwhileswitch 语句。ForEach-Object 没有提到 breakcontinue - Lance U. Matthews
这对我不起作用 - 我在“Get-ADGroupMember”上得到了一个继续异常。 - PeterX
Continue 可以在这种情况下跳出父循环的执行。请参阅此处的解释:https://powershell.org/forums/topic/best-way-to-break-out-of-foreach-loop/#post-19909 - Alsatian
链接现在是 https://forums.powershell.org/t/best-way-to-break-out-of-foreach-loop/3289/3。 - Vopel

9
有一种方法可以从ForEach-Object中跳出,而不会抛出异常。它使用了Select-Object的一个较少为人知的功能,使用-First 参数,当处理指定数量的管道项时,它实际上会打破管道。
简化示例:
$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

输出:

1
2

请注意,将$null赋值的目的是隐藏由break条件产生的$true的输出。可以用42"skip""foobar"等值替换$true。我们只需要向Select-Object管道输送一些东西,以便打破管道。

这是最好的答案。 - dns

8
由于ForEach-Object是一个命令,因此breakcontinue在这里的行为将与foreach循环关键字不同。两者都会停止循环但也会终止整个脚本: break:
0..3 | foreach {
    if ($_ -eq 2) { break }
    $_
}
echo "Never printed"

# OUTPUT:
# 0
# 1

继续:

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

# OUTPUT:
# 0
# 1

到目前为止,我还没有找到一种“好”的方法来中断foreach脚本块而不中断脚本,除了“滥用”异常。尽管PowerShell核心采用了这种方法(参见此处): 抛出异常:
class CustomStopUpstreamException : Exception {}
try {
    0..3 | foreach {
        if ($_ -eq 2) { throw [CustomStopUpstreamException]::new() }
        $_
    }
} catch [CustomStopUpstreamException] { }
echo "End"

# OUTPUT:
# 0
# 1
# End

另一种(并非总是可行的)方法是使用 foreach 关键字:

foreach:

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

# OUTPUT:
# 0
# 1
# End

2
原来你的“throw”示例就是powershell core的实现方式。所以我认为这确实是唯一的解决方案。我试图弄清楚如何使用System.Management.Automation.StopUpstreamCommandsException,但是无法完全理解。但我想抛出特定的异常并仅捕获该异常可能会使您不忽略其他异常。 - Joel Pearson
你使用foreach作为ForEach-Object的别名可能会让读者感到困惑,因为你的回答涵盖了两者之间的区别。 - Jason S
@JasonS 这确实是答案的要点,展示它们看起来相似但行为不同,我希望我已经足够描述了这些区别。训练自己的眼力去辨别它们是非常有益的,因为现实世界中的脚本也不会对你手下留情。 - marsze
@JasonS 这确实是回答的重点,展示它们看起来相似但行为不同,我希望我已经足够描述了它们的区别。训练自己的眼力来辨别它们是非常有益的,因为现实世界的脚本也不会对你手下留情。 - undefined
@marsze哦,我现在明白你的观点了,有意地让它更贴近现实世界,在这种情况下可读性常常被忽视。我猜想一些寻求帮助的读者可能还没有意识到其中一个foreach是对ForEach-Object的不幸别名。 - Jason S
@JoelPearson 如果你编写一个二进制的 cmdlet,你肯定可以做到这一点。https://gist.github.com/santisq/85e322d0e88545b60bb7dcd70af26c4b#file-testanycommand-cs-L34-L39 - Santiago Squarzon

6
如果您坚持使用ForEach-Object,那么我建议像这样添加“中断条件”:(break condition)
$Break = $False;

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

    $Break = $_ -Eq 3;

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

上述代码必须输出1、2、3,然后跳过(在输出4之前中断)。预期输出:
Current number is 1
Current number is 2
Current number is 3

1
这是最好的答案。我发现如果你有一些外部循环,比如一个while循环,并且一个cmdlet除非你提前打破或限制它,否则它将永远不会停止生成对象,那么你不能简单地停止管道而不逃离while循环...你必须使用管道来正确实现。这提供了一种在不逃离外部范围的情况下停止管道的方法。很好。通过break逃离外部范围可能是一个错误。 - undefined
谢谢 @Chris - 是的。这是一个处理打破循环需求的巧妙方式,同时也能从否则在管道外无法获得的功能中获益。 - undefined

4
以下是我使用ForEach-Object命令时,对第一道问题的建议方法。 它并没有直接回答这个问题,因为它没有退出管道。 然而,在Q#1中可能可以实现所需的效果。 唯一的缺点是,当处理大型管道迭代时,像我这样的业余人士可能会看到。
    $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"

我希望这对你有用。 祝大家新年快乐。


哇,真的很有帮助,可以摆脱一个无法停止的管道。谢谢。 - Garric
谢谢你的 "Where-Object",正是我所需要的。X - TinyRacoon

3
我在寻找一种可以精细控制流程以从特定代码块中跳出的方法时发现了这个问题。我采用的解决方案没有被提到...

使用带有 break 关键字的标签

来自:about_break

Break 语句可以包括一个标签,让您退出嵌套循环。标签可以指定脚本中任何循环关键字(例如 Foreach、For 或 While)。

以下是一个简单的示例:

: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

然后是一个更复杂的例子,展示了嵌套标签并分别打断每个标签的结果。

: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

您可以通过将代码块包装在仅执行一次的循环中来适应其他情况。
:myLabel do {
    1..2 | % {

        Write-Host "Iteration: $_"
        break myLabel

    }
} while ($false)

Write-Host "After do while loop"

# Results:
# Iteration: 1
# After do while loop

0

你有两种选项来突然退出 PowerShell 中 ForEach-Object 管道:

  1. 先在 Where-Object 中应用退出逻辑,然后将对象传递给 Foreach-Object,或者
  2. (如果可能的话)将 Foreach-Object 转换为标准的 Foreach 循环结构。

让我们看一些例子: 以下脚本在第二次迭代后退出 Foreach-Object 循环 (即,管道只迭代2次):

解决方法1: 在 Foreach-Object 之前使用 Where-Object 过滤器:

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

或者

1..10 | Where-Object {$_ -le 2} | Foreach-Object {
     $_;
}
解决方案-2:将Foreach-Object转换为标准的Foreach循环结构:
Foreach ($i in 1..10) { 
     if ($i -eq 3) {break;}
     $i;
}

PowerShell 应该提供一种更加直观的方法来退出或从 Foreach-Object 管道体内部 中断。请注意:return 并不能退出,它只会跳过特定的迭代(类似于大多数编程语言中的 continue),这里是 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


0
虽然这有点取巧,但它确实有效:
$array = 'a'..'z'
:pipe do{
    $array | ForEach-Object {
        $_
        if ($_ -eq 'c') {
            break :pipe
        }
    }
} until ($true)

输出: a b c
代码也可以很容易地被压缩,像这样:
$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.: !''

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接