【问题标题】:How to exit from ForEach-Object in PowerShell and continue remaining script如何在 PowerShell 中退出 ForEach-Object 并继续剩余脚本
【发布时间】:2021-10-19 20:19:54
【问题描述】:

这是How to exit from ForEach-Object in PowerShell 的副本 - 我不知道如何标记它,但我再次问这个问题,因为该问题的答案中有大量有用的信息,包括处理它的方法。然而,从 2012 年开始接受的答案似乎并不正确,智慧的金块被埋葬了,不应该被埋葬。

所以我的脚本循环遍历目录中的所有 CSV 文件,然后对包含列数错误的记录的那些文件执行操作。当我第一次运行它时,它花了很长时间,我意识到一些 CSV 实际上对所有行都有正确的列数,我不需要对它们采取行动,所以我决定尝试实现一些东西来检查至少一行列数不正确,然后对文件采取某种措施。为此,我需要在找到符合条件的行后退出 ForEach-Object 循环。

这是原始脚本:

$ParentPath = "C:\Users\<username>\Documents\Temporary\*"
$Files = Get-ChildItem -Path $ParentPath -Include *.csv
foreach ($File in $Files) {
    $OldPath = $File | % { $_.FullName }
    $OldContent = Get-Content $OldPath
    $NewContent = $OldContent |
    Where-Object {$_.split(",").Count -eq 11}
    Set-Content -Path "$(Split-Path $OldPath)\$($File.BaseName).txt" -Value $NewContent
    }

对文件实施某种类型的“预检查”证明很困难,即使我能够使用an answer from SO 正确写出“坏行”——我无法退出 ForEach-Object循环而不处理所有行(从而破坏了预先检查文件是否存在违规行的整个目的)。

下面的代码非常适合识别第一个坏行,如果您删除 ;break,那么它会写出每个违规行:

Get-Content $OldPath | ?{$_} | %{if(!($_.split(",").Count -eq 11)){"Process stopped at line number $($_.ReadCount), incorrect column count of: $($_.split(",").Count).";break}}

那么我如何结合这两个脚本来预先检查文件中是否存在违规行,然后对需要它的文件采取措施?请参阅下面的建议答案!

【问题讨论】:

    标签: powershell


    【解决方案1】:

    这是您的解决方案 (PSv4+) 的 PowerShell 惯用重新表述,它应该会执行得更好。

    但请注意,它假定每个 .csv 文件作为一个整体适合内存:

    Get-ChildItem -LiteralPath $HOME\Documents\Temporary -Filter *.csv | ForEach-Object {
      $okRows, $brokenRows = (Get-Content -ReadCount 0 -LiteralPath $_.FullName).
                               Where({ $_.split(",").Count -eq 11 }, 'Split')
      if ($brokenRows) {
        Set-Content -LiteralPath "$($_.DirectoryName)\$($_.BaseName).txt" -Value $okRows
      } 
    }
    

    解决问题的标题所暗示的问题:

    • 不幸的是,从 PowerShell Core 7.2 开始,仍然没有直接的方法可以按需提前停止管道

      • Select-Object-First能够做到这一点,但它使用非公共异常类型,因此行为仅限于在前 N 个输入对象之后退出管道。

      • GitHub issue #3821 是一个长期存在的功能请求,以使此功能普遍可用

    • 虽然 break statement 直接适合按需退出管道 - 它会寻找一个封闭循环 statement 来突破的,在整个调用堆栈上,如果没有找到,则终止执行总体 - 你可以使用一个虚拟循环 em>

      • 请参阅this answer 了解更多信息。

      • 快速示例:

        # Use of the dummy `do { ... } while ($false)` loop enables `break`
        # to exit the pipeline.
        do { 1..10 | ForEach-Object { $i=0 } { $_; if (++$i -eq 2) { break } } } while ($false)
        
    • Select-Object -Firstbreak + 虚拟循环的 警告它们跳过了 end 块(对于用 PowerShell 编写的 cmdlet)/EndProcessing() 方法(对于所有上游 cmdlet 的二进制 cmdlet)可能会导致问题。

    【讨论】:

    • 通常不喜欢当答案具有 Powershell-idomatic 做某事的方式时,但在这种情况下 - 感谢并谢谢你!它确实更快,并且实际上是开箱即用的,但答案并不总是如此。我需要一点时间来解开它并弄清楚为什么它更快,但是 -Filter 与 -Include 绝对是一个优势。如果我将其加载到内存中,您是否知道是否有更快的选择是用 Foreach 循环替换 ForEach-Object?借鉴我读过的东西here
    • 重新性能:-Filter 确实加快了速度(它将过滤委托给文件系统),尽管使用单个目录并不重要。是的,使用foreach 语句比ForEach-Object 更快,尽管使用 cmdlet 调用(它必须流式传输其结果,引擎必须在开始枚举之前收集这些结果),加速并不显着。有关成员枚举、foreach 语句、ForEach-Object cmdlet 和 PSv4+ .ForEach() 数组方法的性能比较,请参见 this answer 的底部部分。
    • RE 惯用语:有些人会发布一个答案或评论,只是将ForEach-Object{} 等作品替换为%{},然后说这是一个真正的惯用版本。在这种情况下,几乎没有增值。至于 -Filter/-Include,差别只是毫秒,但即使您只是使用 Get-ChildItem 删除所有 .txt 文件并使用一组新文件重新启动,也能获得 50% 的收益
    • 明白,@immobile2。至于-Include:请注意,使用单个通配符模式Get-ChildItem -LiteralPath $HOME\Documents\Temporary\*.csv 也可以工作(尽管-Filter 仍然是最快的)。一个潜在的问题-Filter:它使用平台 API 的通配符语言,而不是 PowerShell 的,这意味着不支持字符集和范围 ([...]);此外,在 Windows 上,-Filter 有许多遗留问题。
    • 你能帮忙解释一些我以前没见过或没用过的东西吗?我已经弄清楚了 $okRows 和 $brokenRows,但是$brokenRows = 之后的代码有点不同。 Get-Content前面需要括号吗? ).Where( 的功能是否与Where-Object/?{} 相同?如果是这样,为什么用点而不是管道来调用它?
    【解决方案2】:

    Tl;dr - 这是我更新的脚本:

    $ParentPath = "C:\Users\<username>\Documents\Temporary\*"
    $Files = Get-ChildItem -Path $ParentPath -Include *.csv
    foreach ($File in $Files){
        $OldPath = $File | %{$_.FullName}
        Get-Content $OldPath | ?{!($_.split(",").Count -eq 11)} | Select -First 1 | %{[bool]$NeedsFix = 1}
        If($NeedsFix){Get-Content $OldPath | ?{($_.split(",").Count -eq 11)} | Set-Content -Path "$(Split-Path $OldPath)\$($File.BaseName).txt"}
        $NeedsFix = 0
    }
    

    为了到达这里,让我们总结一下相关问题的一些答案:

    • Stoffi 出色地概述了在 ForEach-Object 循环和 ForEach 方法中使用 breakcontinuereturn 所期望的不同输出。另见MS-ScriptingGuy - 但这并不能解决我的难题
    • first viable answer 以抛出和捕获异常的形式出现。这些术语目前对我来说是陌生的,但你可能会喜欢它!
    • 另一个可行的选择是使用 Where-Object 限制循环,非常巧妙! (请参阅ThePennyDropsRikkiEddi Kumar 的答案,如果这会让你的船浮起来的话)
    • Alex Hague 的解决方案我没有测试过,但是使用带有 break 关键字的标签似乎对 ForEach-Object 循环和 ForEach 循环都有效
    • 解决问题如果您只是告诉您的管道在 ForEach-Object 循环选择了您要查找的第一个实例后停止它会怎样?听起来太简单了? 也许...但它似乎工作!
      • 这里归功于@zett42 的incredibly simple solution,它的赞成票太少,太容易在管道中实施,并且有可能比许多其他答案/解决方案更灵活。
        • 在退出循环之前需要检查 6 次出现的问题行吗?使用-First 6 怎么样?肯定比创建计数器变量要好

    【讨论】:

      猜你喜欢
      • 2012-05-03
      • 2018-02-12
      • 2016-08-19
      • 2013-07-06
      • 1970-01-01
      • 2016-01-13
      相关资源
      最近更新 更多