如何在 PowerShell 中使用 FINDSTR 查找搜索字符串中所有单词以任意顺序匹配的行

How to use FINDSTR in PowerShell to find lines where all words in the search string match in any order

下面的 findstr.exe 命令 几乎 做了我想要的,但不完全是:

findstr /s /i /c:"word1 word2 word3" *.abc

我用过:

以上将 word1 word2 word3 查找为 文字 ,因此只能找到 确切顺序 中的单词。

相比之下,我希望所有个单词以任意顺序单独匹配(AND逻辑,合取).

如果我从上面的命令中删除 /c:,则返回匹配 any 的行(OR 逻辑,析取),这不是我想要的.

这可以在 PowerShell 中完成吗?

您可以使用 Select-String 在多个文件中进行基于正则表达式的搜索。

要使用正则表达式匹配单个字符串中的所有多个搜索词,您必须使用 a lookaround assertion:

Get-ChildItem -Filter *.abc -Recurse |Select-String -Pattern '^(?=.*\bword1\b)(?=.*\bword2\b)(?=.*\bword3\b).*$'

在上面的示例中,这是第一个命令发生的情况:

Get-ChildItem -Filter *.abc -Recurse

Get-ChildItem searches for files in the current directory
-Filter *.abc shows us only files ending in *.abc
-Recurse searches all subfolders

然后我们将生成的 FileInfo 对象通过管道传输到 Select-String 并使用以下正则表达式模式:

^(?=.*\bword1\b)(?=.*\bword2\b)(?=.*\bword3\b).*$
^              # start of string  
 (?=           # open positive lookahead assertion containing
    .*         # any number of any characters (like * in wildcard matching)
      \b       # word boundary
        word1  # the literal string "word1"
      \b       # word boundary
 )             # close positive lookahead assertion
 ...           # repeat for remaining words
 .*            # any number of any characters
$              # end of string

由于每个先行组只是为了正确性而断言并且字符串中的搜索位置永远不会改变,因此顺序无关紧要。


如果你想让它匹配包含任何个单词的字符串,你可以使用一个简单的非捕获组:

Get-ChildItem -Filter *.abc -Recurse |Select-String -Pattern '\b(?:word1|word2|word3)\b'
\b(?:word1|word2|word3)\b
\b          # start of string  
  (?:       # open non-capturing group
     word1  # the literal string "word1"
     |      # or
     word2  # the literal string "word2"
     |      # or
     word3  # the literal string "word3"
  )         # close positive lookahead assertion
\b          # end of string

这些当然可以在中抽象掉。

我生成了 param 块和下面 Select-Match 函数定义的大部分主体:

$slsmeta = [System.Management.Automation.CommandMetadata]::new((Get-Command Select-String))
[System.Management.Automation.ProxyCommand]::Create($slsmeta)

然后删除不必要的参数(包括-AllMatches-Pattern),然后添加模式生成器(见内联注释):

function Select-Match
{
    [CmdletBinding(DefaultParameterSetName='Any', HelpUri='http://go.microsoft.com/fwlink/?LinkID=113388')]
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [string[]]
        ${Substring},

        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [Alias('PSPath')]
        [string[]]
        ${LiteralPath},

        [Parameter(ParameterSetName='Any')]
        [switch]
        ${Any},

        [Parameter(ParameterSetName='Any')]
        [switch]
        ${All},

        [switch]
        ${CaseSensitive},

        [switch]
        ${NotMatch},

        [ValidateNotNullOrEmpty()]
        [ValidateSet('unicode','utf7','utf8','utf32','ascii','bigendianunicode','default','oem')]
        [string]
        ${Encoding},

        [ValidateNotNullOrEmpty()]
        [ValidateCount(1, 2)]
        [ValidateRange(0, 2147483647)]
        [int[]]
        ${Context}
    )

    begin
    {
        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
            {
                $PSBoundParameters['OutBuffer'] = 1
            }

            # Escape literal input strings
            $EscapedStrings = foreach($term in $PSBoundParameters['Substring']){
                [regex]::Escape($term)
            }

            # Construct pattern based on whether -Any or -All was specified 
            if($PSCmdlet.ParameterSetName -eq 'Any'){
                $Pattern = '\b(?:{0})\b' -f ($EscapedStrings -join '|')
            } else {
                $Clauses = foreach($EscapedString in $EscapedStrings){
                    '(?=.*\b{0}\b)' -f $_
                }
                $Pattern = '^{0}.*$' -f ($Clauses -join '')
            }

            # Remove the Substring parameter argument from PSBoundParameters
            $PSBoundParameters.Remove('Substring') |Out-Null

            # Add the Pattern parameter argument
            $PSBoundParameters['Pattern'] = $Pattern

            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Select-String', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = {& $wrappedCmd @PSBoundParameters }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        } catch {
            throw
        }
    }

    process
    {
        try {
            $steppablePipeline.Process($_)
        } catch {
            throw
        }
    }

    end
    {
        try {
            $steppablePipeline.End()
        } catch {
            throw
        }
    }
    <#

    .ForwardHelpTargetName Microsoft.PowerShell.Utility\Select-String
    .ForwardHelpCategory Cmdlet

    #>

}

现在你可以像这样使用它,它的行为几乎像 Select-String:

Get-ChildItem -Filter *.abc -Recurse |Select-Match word1,word2,word3 -All

注:

  • 此答案的第一部分 解决 OP 的问题 - 有关解决方案,请参阅 and ;或者,请参阅此答案的底部,它提供了一个根据 Mathias 代码改编的通用解决方案。

    • (由于一开始看错了题),这部分答案使用了disjunctive逻辑-至少有 一个 匹配搜索词的匹配行 - 这是 findstr.exe 和 PowerShell 的 Select-String(直接)支持的唯一逻辑.

    • 相比之下,OP 要求 conjunctive 逻辑,这需要额外的工作。

  • 关于使用 Select-String.

    findstr.exe 命令转换为 PowerShell,这部分答案可能仍然很有趣

PowerShell 等效于问题中的 findstr 命令但没有 /c: -
FINDSTR /s /i "word1 word2 word3" *.abc

  • 是:

    (Get-ChildItem -File -Filter *.abc -Recurse | Select-String -SimpleMatch -Pattern 'word1', 'word2', 'word3').Count

  • /s -> Get-ChildItem -File -Filter *.abc -Recurse输出当前目录子树匹配*.abc

    的所有文件
    • 注意wile Select-String 能够接受文件名模式(通配符表达式)如*.abc,它不支持递归,因此需要单独的Get-ChildItem调用,其输出通过管道传输到Select-String.
  • findstr -> Select-String,PowerShell 更灵活的对应物:

    • -SimpleMatch 指定 -Pattern 参数被解释为 文字 而不是正则表达式正则表达式)。请注意它们的默认值有何不同:

      • findstr 默认需要 文字 (您可以使用 /R 切换到正则表达式)。
      • Select-String 默认需要 regexes(您可以使用 -SimpleMatch 切换到文字)。
    • -i ->(默认行为);与大多数 PowerShell 一样,case-in 灵敏度是 Select-String 的默认 行为 - 添加 -CaseSensitive 以更改它。

    • "word1 word2 word3" -> -Pattern 'word1', 'word2', 'word3'; 指定一个数组 模式在每行 上查找至少一个 模式的匹配项( 析取 逻辑)。

      • 也就是说,以下所有行都会匹配:... word1 ...... word2 ...... word2 word1 ...... word3 word1 word2 ...
  • /c -> (...).CountSelect-String 输出一个 对象集合 表示匹配行,这表达很重要。 对象输出是[Microsoft.PowerShell.Commands.MatchInfo]实例,它不仅包含匹配行,而且关于输入和匹配的细节.


基于的解决方案:

Select-StringAll 是仅结合 wrapper 函数,围绕仅分离 Select-String cmdlet 使用与后者完全相同的语法,除了不支持 -AllMatches 开关。

也就是说,Select-StringAll 要求传递给它的 所有 模式——无论它们是正则表达式(默认情况下)还是文字(使用 -SimpleMatch) - 匹配一行。

应用于OP的问题,我们得到:

(Get-ChildItem -File -Filter *.abc -Recurse |
  Select-StringAll -SimpleMatch word1, word2, word3).Count

注意与顶部命令相比的变化:

  • -Pattern 参数按参数位置隐式绑定。
  • 为方便起见,模式被指定为裸词(未加引号),尽管通常引用更安全,因为不容易记住需要引用的内容。

另一种(诚然不太复杂)方法是简单的菊花链过滤器,因为单词的顺序无关紧要。首先过滤文件中的一个单词,然后过滤包含第二个单词的行的输出,然后过滤 that 还包含第三个单词的行的输出。

findstr /s /i "word1" *.abc | findstr /i "word2" | findstr /i "word3"

使用 PowerShell cmdlet 上面的内容如下所示:

Get-ChildItem -Filter '*.abc' -Recurse | Get-Content | Where-Object {
  $_ -like '*word1*' -and
  $_ -like '*word2*' -and
  $_ -like '*word3*'
}

或(使用别名):

ls '*.abc' -r | cat | ? {
  $_ -like '*word1*' -and
  $_ -like '*word2*' -and
  $_ -like '*word3*'
}

请注意,别名只是为了节省在命令行上键入的时间,所以我不建议在脚本中使用它们。

如果您没有在同一行中重复任何单词,则以下内容将起作用: word1 你好 word1 再见 word1

findstr /i /r /c:"word[1-3].*word[1-3].*word[1-3]" *.abc

如果重复 word1/word2/word3 不存在,或者您确实希望在结果中出现这些情况,则可以使用它。