【问题标题】:How to exit from ForEach-Object in PowerShell如何在 PowerShell 中退出 ForEach-Object
【发布时间】:2012-05-03 21:56:17
【问题描述】:

我有以下代码:

$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 允许我们这样做?

【问题讨论】:

    标签: powershell foreach


    【解决方案1】:

    首先,Foreach-Object 不是真正的循环,在其中调用 break 将取消整个脚本,而不是跳到后面的语句。

    相反,breakcontinue 将在实际的 foreach 循环中正常工作。

    项目#1。将break 放入foreach 循环中会退出循环,但不会停止管道。听起来你想要这样的东西:

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

    项目 #2。 PowerShell 允许您在 foreach 循环中修改该数组上的数组,但这些更改在您退出循环之前不会生效。尝试运行下面的代码作为示例。

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

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

    【讨论】:

    • 我认为您的意思是在循环中添加一行 $a = 1..23; 以表明更改不会反映在循环执行中。
    • 第 2 项不正确(至少对于具有固定大小数组的 v5)。您可以修改数组并使其在 ForEach 构造中可见。如果您要在 ForEach 构造中添加 $a[1] = 9,它将显示 9 作为第二个元素,但您不能从数组中添加/删除元素,并且您使用 += 运算符添加的任何内容都不会显示直到最后。如果不是固定大小的数组(即$a=[System.Collections.ArrayList]@(1,2,3)),那么任何修改内容的尝试都会导致Collection was modified; enumeration operation may not execute. 错误,终止循环。
    • 这是一个完全错误的答案。问题是关于Foreach-Object。在 powershell 中(顺便说一句,这很烦人)foreach 是一个“操作员”和一个“别名”。 smeltplate 在此答案中提供的foreach 是“操作员”而不是“别名”。请参阅here. 注意:此答案是对代码的完整重构,可能不适用于许多情况。
    • @JamieMarshall 是的,你是绝对正确的。中断 ForEach-Object 循环会在我的测试用例中结束整个脚本
    • 这对我不起作用 - 我在 Get-ADGroupMember 上遇到了中断异常
    【解决方案2】:

    foreachforeach-object 之间存在差异。

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

    为了在 PS 中进行测试,这里有显示差异的脚本。

    ForEach-对象:

    # 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"
    

    【讨论】:

    • 这是一个正确的答案。它解释了 commandlet ForEach-Object,就像有问题的一样。示例使用它的别名 foreach,但它仍然是命令行开关。接受的答案有效,但它解释了关键字 foreach。我知道 PowerShell 在这件事上令人困惑,但这仍然是两件不同的事情。
    • 我花了这么多时间调试代码,想知道为什么break 打破了我的整个脚本,而不仅仅是ForEach-Object 循环,直到我终于找到了这个答案。谢谢!
    【解决方案3】:

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

    【讨论】:

    • +1。有趣的。我从来不知道这一点。无论您使用break 还是continue,它似乎都一样。我想知道为什么这对问题的作者不起作用,因为他们说他们都试过了。
    • 知道记录在哪里吗? about_breakabout_continue 只讨论 forforeachwhileswitch 语句。 ForEach-Object 没有提到 breakcontinue
    • 这对我不起作用 - 我在 Get-ADGroupMember 上得到一个 Continue Exception
    • Continue 在这种情况下会中断父循环的执行。在此处查看说明:powershell.org/forums/topic/…
    • 感谢您的回答!
    【解决方案4】:

    由于ForEach-Objectcmdletbreakcontinue 在此处的行为与foreach keyword 不同。两者都会停止循环,但也会终止整个脚本:

    休息:

    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 core uses this approach

    投掷:

    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
    

    【讨论】:

    • 事实证明,您的“投掷”示例就是powershell core 的做法。所以我认为这是唯一的解决方案。我试图弄清楚如何使用System.Management.Automation.StopUpstreamCommandsException,但不太清楚如何使用。但我想抛出一个特定的异常并且只捕获该异常可以使它不会忽略其他异常
    【解决方案5】:

    如果我希望使用 ForEach-Object cmdlet,以下是问题 #1 的建议方法。 它没有直接回答这个问题,因为它没有退出管道。 但是,它可能会在 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"
    

    我希望这是有用的。 祝大家新年快乐。

    【讨论】:

    • 哇。摆脱不可阻挡的管道真的很有帮助。谢谢
    • 感谢“Where-Object”正是我所需要的。 X
    【解决方案6】:

    如果您坚持使用 ForEach-Object,那么我建议您添加这样的“中断条件”:

    $Break = $False;
    
    1,2,3,4 | Where-Object { $Break -Eq $False } | ForEach-Object {
    
        $Break = $_ -Eq 3;
    
        Write-Host "Current number is $_";
    }
    

    上面的代码必须输出1,2,3然后跳过(break before) 4.预期输出:

    Current number is 1
    Current number is 2
    Current number is 3
    

    【讨论】:

      【解决方案7】:

      我在寻找一种方法来进行细粒度流控制以打破特定代码块时发现了这个问题。没有提到我确定的解决方案...

      使用带有 break 关键字的标签

      发件人:about_break

      Break 语句可以包含允许您退出嵌入的标签 循环。标签可以指定任何循环关键字,例如 Foreach、For 或 同时,在脚本中。

      这是一个简单的例子

      :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
      

      【讨论】:

        【解决方案8】:

        有一种方法可以在不引发异常的情况下从ForEach-Object 中断。它采用了一个鲜为人知的特性Select-Object,使用-First parameter,当指定数量的管道项目被处理时,它实际上会中断管道。

        简化示例:

        $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 的赋值是为了隐藏由中断条件产生的$true 的输出。值$true 可以替换为42"skip""foobar",随你的便。我们只需要将一些东西通过管道传递给Select-Object,这样它就会中断管道。

        【讨论】:

          【解决方案9】:

          问题 #1 的答案 - 你可以简单地让你的 if 语句停止为真

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

          【讨论】:

          • 问题是这并没有停止循环;它只会一遍又一遍地评估为$false(这对于非常大和/或昂贵的集合来说会很慢)。
          【解决方案10】:

          您有两个选项可以在 PowerShell 中突然退出 ForEach-Object 管道:

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

          让我们看示例:以下脚本在第二次迭代后退出 Foreach-Object 循环(即管道仅迭代 2 次)“:

          Solution-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 {
               $_;
          }
          

          Solution-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

          【讨论】:

            猜你喜欢
            • 2021-10-19
            • 2018-02-12
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多