【问题标题】:PowerShell equivalent of LINQ Any()?PowerShell 等效于 LINQ Any()?
【发布时间】:2022-03-11 07:48:52
【问题描述】:

我想从存储在 subversion 中的脚本位置查找顶层的所有目录。

在 C# 中会是这样的

Directory.GetDirectories(".")
  .Where(d=>Directories.GetDirectories(d)
     .Any(x => x == "_svn" || ".svn"));

我很难在 PowerShell 中找到“Any()”的等价物,而且我不想经历调用扩展方法的尴尬。

到目前为止,我得到了这个:

 Get-ChildItem | ? {$_.PsIsContainer} | Get-ChildItem -force | ? {$_.PsIsContainer -and $_.Name -eq "_svn" -or $_.Name -eq ".svn"

这会为我找到svn 目录本身,而不是它们的父目录——这正是我想要的。如果你能告诉我为什么要加分,加分

 | Select-Object {$_.Directory}

该命令列表的末尾仅显示一系列空白行。

【问题讨论】:

    标签: linq powershell


    【解决方案1】:

    使用 PowerShell v3+ 解决方案回答直接问题

    (Get-ChildItem -Force -Directory -Recurse -Depth 2 -Include '_svn', '.svn').Parent.FullName
    

    -Directory 将匹配限制为目录,-Recurse -Depth 2 递归最多三个级别(子、孙和曾孙),Include 允许指定多个(文件名-组件)过滤器,.Parent.FullName 返回parent 目录的完整路径。匹配的目录,使用member-access enumeration(隐式访问集合的elements'属性)。

    至于红利问题:select-object {$_.Directory}不行, 因为Get-ChildItem返回的\[System.IO.DirectoryInfo\]实例没有.Directory属性,只有.Parent属性; Select-Object -ExpandProperty Parent 应该已经被使用了。

    除了只返回感兴趣的属性value-ExpandProperty 还强制属性的存在。相比之下,Select-Object {$_.Directory} 返回一个自定义对象,其属性字面名称为$_.Directory,其值为$null,假设输入对象没有.Directory 属性;这些$null 值在控制台中打印为空行。


    至于关于 PowerShell 等效于 LINQ's .Any() 方法的更多一般问题,它表明 [with a Boolean result] 是否给定的可枚举(集合)有任何元素/任何满足给定条件的元素:

    PowerShell 本身没有提供这样的等价物,但行为可以模拟


    使用 PowerShell v4+ .Where() 收集方法

    警告:这需要首先将整个输入集合收集到内存中,这对于大型集合和/或长时间运行的输入命令可能会出现问题。

    (...).Where({ $_ ... }, 'First').Count -gt 0
    

    ...表示感兴趣的命令,$_ ...表示感兴趣的条件,应用于每个输入对象,其中PowerShell的自动$_变量指的是手头的输入对象;参数'First' 确保一旦找到第一个匹配项,该方法就会返回。

    例如:

    # See if there's at least one value > 1
    PS> (1, 2, 3).Where({ $_ -gt 1 }, 'First').Count -gt 0
    True
    

    使用管道:测试一个命令是否产生了至少一个输出对象[匹配一个条件]:

    基于管道的解决方案的优点在于它可以在命令的输出在生成时逐个处理,而无需首先将整个输出收集到内存中

    • 如果您不介意枚举所有对象 - 即使您只关心至少有一个 - 请使用@ 987654324@ 至JaredPar's helpful answer。 这种方法的缺点是您总是必须等待(可能长时间运行的)命令完成生成 all 输出对象,即使 - 从逻辑上讲 - 确定是否存在 只要收到第一个对象,就可以生成任何输出对象。

    • 如果您想在遇到一个 [匹配] 对象时退出管道,您有两种选择:

      • [Ad-hoc:易于理解,但实施起来很麻烦] 将管道封闭在一个虚拟循环中并使用break跳出管道和那个循环(...代表要测试其输出的命令,@987654364 @匹配条件):

             # Exit on first input object.
             [bool] $haveAny = do { ... | % { $true; break } } while ($false)
        
             # Exit on first input object that matches a condition.
             [bool] $haveAny = do { ... | % { if ($_ ...) { $true ; break } } } while ($false)
        
      • [使用 PowerShell v3+ 独立的实用程序函数,实现起来并不简单] 请参阅下面的函数Test-Any的实现。 它可以添加到脚本中,或者在交互式会话中使用,添加到您的 $PROFILE 文件中。


    PowerShell v3+:优化实用功能Test-Any

    该功能非常重要,因为从 Windows PowerShell v5.1、PowerShell Core v6 开始,没有直接方法可以提前退出管道,因此基于解决方法.NET 反射和私有类型目前是必要的。

    如果您同意应该有这样的功能,请参与对话on GitHub

    #requires -version 3
    Function Test-Any {
    
        [CmdletBinding()]
        param(
            [ScriptBlock] $Filter,
            [Parameter(ValueFromPipeline = $true)] $InputObject
        )
    
        process {
          if (-not $Filter -or (Foreach-Object $Filter -InputObject $InputObject)) {
              $true # Signal that at least 1 [matching] object was found
              # Now that we have our result, stop the upstream commands in the
              # pipeline so that they don't create more, no-longer-needed input.
              (Add-Type -Passthru -TypeDefinition '
                using System.Management.Automation;
                namespace net.same2u.PowerShell {
                  public static class CustomPipelineStopper {
                    public static void Stop(Cmdlet cmdlet) {
                      throw (System.Exception) System.Activator.CreateInstance(typeof(Cmdlet).Assembly.GetType("System.Management.Automation.StopUpstreamCommandsException"), cmdlet);
                    }
                  }
                }')::Stop($PSCmdlet)
          }
        }
        end { $false }
    }
    
    • 如果未指定$Filterif (-not $Filter -or (Foreach-Object $Filter -InputObject $InputObject)) 默认为 true,否则将使用手头的对象评估过滤器(脚本块)。

      • 使用ForEach-Object 评估过滤器脚本块可确保$_所有 场景中绑定到当前管道对象,如PetSerAl 的有用答案here 中所示.
    • (Add-Type ... 语句使用使用 C# 代码创建的即席类型,该类型使用反射引发 Select-Object -First (PowerShell v3+) 在内部用于停止管道的相同异常,即 [System.Management.Automation.StopUpstreamCommandsException],从 PowerShell 开始v5 仍然是 private 类型。 背景在这里: http://powershell.com/cs/blogs/tobias/archive/2010/01/01/cancelling-a-pipeline.aspx 非常感谢 PetSerAl 在 cmets 中贡献此代码。

    示例:

    • PS> @() | Test-Any false

    • PS> Get-EventLog Application | Test-Any # should return *right away* true

    • PS> 1, 2, 3 | Test-Any { $_ -gt 1 } # see if any object is > 1 true


    背景资料

    JaredPar's helpful answerPaolo Tedesco's helpful extension 在一个方面存在不足:一旦找到匹配项,它们就不会退出管道,这可能是一项重要的优化。

    遗憾的是,即使从 PowerShell v5 开始,也没有直接的方法可以提前退出管道。 如果您同意应该有这样的功能,请参与对话on GitHub

    JaredPar's answernaïve 优化实际上缩短了代码:

    # IMPORTANT: ONLY EVER USE THIS INSIDE A PURPOSE-BUILT DUMMY LOOP (see below)
    function Test-Any() { process { $true; break } end { $false } }
    
    • process 块只有在管道中至少有一个元素时才会进入。

      • 小警告:按照设计,如果根本没有管道,process 块仍会进入,$_ 设置为 $null,因此调用 Test-Any 在管道之外 无济于事地返回$true。要区分$null | Test-AnyTest-Any,请检查$MyInvocation.ExpectingInput,这是$true 仅在管道中:谢谢,PetSerAlfunction Test-Any() { process { $MyInvocation.ExpectingInput; break } end { $false } }
    • $true,写入输出流,表示至少找到一个对象。

    • break 然后终止管道,从而防止对其他对象进行多余的处理。 但是,它也会退出任何封闭循环 - break 并非旨在退出管道谢谢,PetSerAl .

      • 如果有 退出管道的命令,这就是它应该去的地方。
      • 请注意,return 只会移动到 next 输入对象。
    • 由于process 块无条件执行break,因此只有在从未进入process 块的情况下才会到达end 块,这意味着一个空管道,因此$false 被写入输出流以发出信号。

    【讨论】:

      【解决方案2】:

      不幸的是,PowerShell 中没有等效项。我写了一篇关于此的博客文章,并提出了通用 Test-Any 函数/过滤器的建议。

      function Test-Any() {
          begin {
              $any = $false
          }
          process {
              $any = $true
          }
          end {
              $any
          }
      }
      

      博文:Is there anything in that pipeline?

      【讨论】:

      • 这是一个相当简单但有效的答案。
      • 这个答案比公认的更正确。虽然被接受的可能更有效,但甚至很难在所有细节和警告中找到有效的答案。这至少具有足够简单的优点,可以看出它在所有情况下都是正确的,即使它不是最有效的。
      【解决方案3】:

      @JaredPar 答案的变体,将测试合并到 Test-Any 过滤器中:

      function Test-Any {
          [CmdletBinding()]
          param($EvaluateCondition,
              [Parameter(ValueFromPipeline = $true)] $ObjectToTest)
          begin {
              $any = $false
          }
          process {
              if (-not $any -and (& $EvaluateCondition $ObjectToTest)) {
                  $any = $true
              }
          }
          end {
              $any
          }
      }
      

      现在我可以编写“任何”测试,例如

      > 1..4 | Test-Any { $_ -gt 3 }
      True
      
      > 1..4 | Test-Any { $_ -gt 5 }
      False
      

      【讨论】:

      • 我对你的回答投了赞成票,认为这是一个不错的方法;但是,我要指出 if 语句只需要说 if (& $EvaluateCondition $ObjectToTest) { $any = $true }
      • @msorens:如果 $any 已经为真,我想避免评估条件。
      • & $EvaluateCondition $ObjectToTest 没有将对象绑定到$_。您的示例之所以有效,是因为您从父作用域引用 $_,如果函数在同一模块或全局状态中定义和执行,则该作用域将是 Test-Any 作用域。但是,如果函数在一个模块中定义并从全局状态的其他模块中使用,那么$_ 中的值将是错误的。你应该使用这样的东西:ForEach-Object $EvaluateCondition -InputObject $ObjectToTest.
      • @mklement0 查看我的answer 链接问题。 Test-Any 的范围不必要将是 $EvaluateCondition 脚本块的父级。
      • @PetSerAl:你说的完全正确;感谢您将我指向您的链接答案,该答案清楚地表明了问题。我已经删除了我原来的评论。
      【解决方案4】:

      你可以使用原来的LINQAny

      [Linq.Enumerable]::Any($list)
      

      【讨论】:

      • 虽然这可行,但它要求 $list 已经完全在内存中,这违背了使用流式 PowerShell 管道的目的:PowerShell cmdlet 不会发出惰性 IEnumerables,它们会发出对象一一到管道。如果将 cmdlet 调用的输出传递给 .NET 方法,则该调用必须首先运行到完成,这意味着您可能会等待很长时间并浪费大量内存才能获得结果;例如,考虑[Linq.Enumerable]::Any((Get-ChildItem / -Recurse))(在下一条评论中继续)。
      • (续上一条评论)如果您愿意愿意支付先运行命令完成的代价,您可以简单地使用(...).Count -gt 0 - 否需要 LINQ;后者仅在您碰巧调用返回惰性枚举的 .NET 方法时才有帮助。
      【解决方案5】:

      我现在的做法是:

      gci -r -force `
          | ? { $_.PSIsContainer -and $_.Name -match "^[._]svn$" } `
          | select Parent -Unique
      

      原因

      select-object {$_.Directory}
      

      没有返回任何有用的东西是DirectoryInfo 对象上没有这样的属性。至少不在我的 PowerShell 中。


      详细说明您自己的答案:PowerShell可以将大多数非空集合视为$true,因此您可以简单地这样做:

      $svnDirs = gci `
          | ? {$_.PsIsContainer} `
          | ? {
              gci $_.Name -Force `
                  | ? {$_.PSIsContainer -and ($_.Name -eq "_svn" -or $_.Name -eq ".svn") }
              }
      

      【讨论】:

      • Get-ChildItem 同时返回 FileInfoDirectoryInfo 对象 :-)
      • IMO,这是比@JaredPar 更好的答案,因为您可以按照此处所述测试管道是否为空。我认为可以通过移动 | 来改进答案。到前一行,并删除`.
      • 为了便于阅读,我倾向于像这样分解我的 piepelines。可能只是个人口味问题。但恕我直言,它绝对比 200 个字符的行更好读。
      • Jay 的意思是一个 |行尾的字符会自动将代码继续到下一行(就像没有右括号的左括号一样),因此您不需要转义返回字符。
      • 是的,但是我需要查看 previous 行以了解 为什么 出现了换行符(管道、块启动等.) 而不是在我看到行首时立即知道管道是如何流动的。
      【解决方案6】:

      其实很简单 - 只需选择第一个 $true(为清晰起见格式化):

      [bool] ($source `
              | foreach { [bool] (<predicate>) } `
              | where { $_ } `
              | select -first 1)
      

      另一种方式:

      ($source `
              | where { <predicate> } `
              | foreach { $true } `
              | select -first 1)
      

      【讨论】:

      • 我认为您不需要在顶部添加[bool],因为您已经选择了bools 枚举中的第一个
      • 不,这是必需的,因为您可能无法选择任何元素,结果将是空(null) - [bool] 确保您始终返回 true 或 false。
      • 如果你只在末尾有foreach { $true }(在select 之后)并且像平常一样在where 中有谓词,而不是将两者结合起来,这会更简单。
      • 不知道为什么这会更简单。行数不是一样吗?
      • 变成($source | where { &lt;predicate&gt; } | foreach { $true } | select -first 1)。这消除了强制转换,使您想要返回的内容更加明确,并将谓词放在管道中更预期的部分。
      【解决方案7】:

      我最终做了一个计数:

      $directoryContainsSvn = {
          (Get-ChildItem $_.Name -force | ? {$_.PsIsContainer -and $_.Name -eq "_svn" -or $_.Name -eq ".svn"} | Measure-Object).Count -eq 1
      }
      $svnDirs = Get-ChildItem | ? {$_.PsIsContainer} | ? $directoryContainsSvn
      

      【讨论】:

      • 请注意,无论如何,您都可以将完整的管道用作布尔测试,因为任何非空集合的计算结果都是 $true(除了包含 $false 作为唯一元素的集合 - 但这是这里不用担心)。此外,您的测试也搞砸了,因为-and-or 的优先级意味着您将捕获恰好名为".svn"文件,因为PSIsContainer 测试仅适用于名称匹配"_svn"
      【解决方案8】:

      你可以稍微收紧一点:

      gci -fo | ?{$_.PSIsContainer -and `
                  (gci $_ -r -fo | ?{$_.PSIsContainer -and $_ -match '[_.]svn$'})}
      

      注意 - 将 $__.Name 传递给嵌套的 gci 是不必要的。传递它 $_ 就足够了。

      【讨论】:

        【解决方案9】:

        我推荐以下解决方案:

        <#
        .SYNOPSIS 
           Tests if any object in an array matches the expression
        
        .EXAMPLE
            @( "red", "blue" ) | Where-Any { $_ -eq "blue" } | Write-Host
        #>
        function Where-Any 
        {
            [CmdletBinding()]
            param(
                [Parameter(Mandatory = $True)]
                $Condition,
        
                [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
                $Item
            )
        
            begin {
                [bool]$isMatch = $False
            }
        
            process {
              if (& $Condition $Item) {
                  [bool]$isMatch = $true
              }
            }
        
            end {
                Write-Output $isMatch
            }
        }
        
        # optional alias
        New-Alias any Where-Any
        

        【讨论】:

          【解决方案10】:

          这是迄今为止我发现的最好的方法(如果已经找到一个 true,则不会遍历所有元素,并且不会破坏管道):

          来自LINQ Any() equivalent in PowerShell

          可以使用内置的 $input 变量,该变量包含函数范围内的整个管道。

          因此,所需的代码可能如下所示:

          function Test-Any([scriptBlock] $scriptBlock = {$true}, [scriptBlock] $debugOut = $null)
          {
              if ($debugOut)
              {
                  Write-Host(“{0} | % {{{1}}}” -f $input, $scriptBlock)
              }
          
              $_ret = $false;
              $_input = ($input -as [Collections.IEnumerator])
          
              if ($_input)
              {
                  while ($_input.MoveNext())
                  {
                      $_ = $_input.Current;
          
                      Write-Host $_
          
                      if ($debugOut)
                      {
                          Write-Host(“Tested: [{0}]” -f (&$debugOut))
                      }
          
                      if (&$scriptBlock)
                      {
                          if ($debugOut)
                          {
                              Write-Host(“Matched: [{0}]” -f (&$debugOut))
                          }
          
                          $_ret = $true
                          break
                      }
                  }
              }
          
              $_ret
          }
          

          【讨论】:

            【解决方案11】:

            我认为这里最好的答案是@JaredPar 提出的函数,但如果你像我一样喜欢单线,我想建议以下Any one-liner

            # Any item is greater than 5
            $result = $arr | %{ $match = $false }{ $match = $match -or $_ -gt 5 }{ $match }
            

            %{ $match = $false }{ $match = $match -or YOUR_CONDITION }{ $match } 检查至少一项符合条件。

            注意 - 通常 Any 操作会评估数组,直到找到与条件匹配的第一个项目。但是这段代码会评估所有项目

            顺便提一下,您可以轻松将其调整为All one-liner

            # All items are greater than zero
            $result = $arr | %{ $match = $false }{ $match = $match -and $_ -gt 0 }{ $match }
            

            %{ $match = $false }{ $match = $match -and YOUR_CONDITION }{ $match } 检查所有项目是否符合条件。

            注意,检查任何你需要的-or 并检查你需要的所有-and

            【讨论】:

              【解决方案12】:

              我采用了更 linq 风格的方法。

              我知道这个问题可能已经很老了。我用它来满足我的需求:

              PS> $searchData = "unn"
              PS> $StringData = @("unn", "dew", "tri", "peswar", "pymp")
              PS> $delegate =  [Func[string,bool]]{ param($d); return $d -eq $searchData }
              PS> [Linq.Enumerable]::Any([string[]]$StringData, $delegate)
              

              取自这里:

              https://www.red-gate.com/simple-talk/dotnet/net-framework/high-performance-powershell-linq/#post-71022-_Toc482783751

              【讨论】:

                猜你喜欢
                • 2011-08-22
                • 2018-08-05
                • 2015-04-13
                • 1970-01-01
                • 1970-01-01
                • 2018-01-02
                • 1970-01-01
                • 2013-02-18
                • 1970-01-01
                相关资源
                最近更新 更多