【问题标题】:Matching strings in PowerShellPowerShell 中的匹配字符串
【发布时间】:2018-12-26 13:44:11
【问题描述】:

这个问题的要点是:

所以if ($C -match $b.Name) 似乎认为字符串的部分 匹配是匹配的?有没有更好的方法来强制 complete [match] 字符串?

我有一个目录,其中包含大量 .7z 文件。我需要经常清理这个目录。还有另一个脚本,早于我在这里工作,目前正在运行,但它由 3000 行组成,并且不断生成不正确的匹配项,并且不记录移动的内容或删除的内容。它如此庞大的部分原因是它有大量的路径,这些文件需要在其中进行硬编码。有时,这些路径会发生变化,更新起来很麻烦。

所以我开始制作一个小得多的脚本,其中包含在 CSV 文件中引用的所有这些路径。除了这些路径之外,CSV 文件中还记录了已知的文件名。

我正在尝试将文件名与我的 CSV 文件中记录的名称进行匹配。它通常有效,但有时我会得到不正确的匹配项。

假设我有两个以相似开头的文件,Apple 和 Apple_Pie。 Apple 将匹配 Apple 并移动到正确的目录,但 Apple_Pie 将首先匹配 Apple 并移动到错误的目录。在 $C 变量被清除之前,它会将 Apple_Pie 匹配到正确的目录,但此时 Apple_Pie 不再存在于需要从中移动的原始目录中。

所以if ($C -match $b.Name) 似乎认为字符串的部分匹配是匹配的?有没有更好的方法来强制一个完整的字符串?

我认为我对 -match 应该如何工作的期望有点偏离。

我在这里使用的正则表达式是删除由另一个自动化过程添加到文件名的日期时间的每个文件名。我用它来隔离我想要匹配的文件名。

$Wild = "C:\Some\Folder\With\Files\"

$CSV = "C:\Another\Folder\Paths.csv"

$Content = gci $wild

$Reg1 = [regex] '_[0-9]{4}-[0-9]{2}-[0-9]{2}[A-Z]{1}[0-9]{2}_[0-9]{2}_[0-9]{2}'

$Reg2 = [regex] '[0-9]{4}-[0-9]{2}-[0-9]{2}[A-Z]{1}[0-9]{2}_[0-9]{2}_[0-9]{2}'

$Paths = import-csv -path $CSV -header Name, Path

foreach ($a in $content) {
    $c = $a.BaseName

    if ($c -match $reg1) {
        $c = $c -replace $regyear
    }
    elseif ($c -match $reg2) {
        $c = $c -replace $reg2
    }

    foreach ($b in $Paths) {

        if ($c -match $b.Name) {
            Do something
        }
    }
}

【问题讨论】:

  • 我将首先指出,如果可以在字符串中的任何位置找到模式,则默认情况下正则表达式将为真。如果要在字符串中的某些位置查找字符串,请使用^ 指示字符串的开头,使用$ 指示字符串的结尾。要匹配整个字符串,请同时使用。

标签: regex powershell string-matching


【解决方案1】:

tl;dr

  • -match 确实使 RHS(右侧)上的正则表达式默认匹配 substrings
    • 'foo' -match 'o' # true
  • 但是,您可以将正则表达式锚定^ 匹配输入字符串的开始,和/或$ 匹配结束
    • 'foo' -match '^foo$' # true - full match
    • 'foot' -match '^foo$' # false

继续阅读以了解有关其他字符串匹配运算符的详细信息。


前言:

  • PowerShell string-comparison operators 默认情况下不区分大小写 并且使用invariant culture 而不是比现在的文化。

    • 您可以使用前缀c选择区分大小写
      例如,-cmatch 而不是 -match
  • 所有比较运算符都可以否定,前缀为not;例如,-notmatch 否定 -match

  • 单个字符串作为LHS,比较运算符返回$True$False,但使用字符串数组充当过滤器;也就是说,它们返回比较为真的元素的子数组


EBGreen's对该问题的评论提供了最好的解释(稍加编辑并添加了重点):

[...] 默认情况下,如果可以在字符串中任何地方找到 [RHS] 模式(正则表达式),-match 将返回 $True如果要在字符串中的某些位置查找字符串,请使用^ 表示字符串的开头,使用$ 表示字符串的结尾。要匹配整个字符串,请同时使用。

应用于你的部分代码:

$Reg2 = '^[0-9]{4}-[0-9]{2}-[0-9]{2}[A-Z]{1}[0-9]{2}_[0-9]{2}_[0-9]{2}$'

# ...

$c -match $Reg2

注意开头的 ^ 和结尾的 $,以确保 整个 输入字符串必须匹配。

还请注意,我省略了 [regex] 演员表,因为这不是必需的,因为 -match 可以直接接受 字符串

在相关说明中,您可以使用断言 \b 来修改子字符串匹配,以便匹配仅在 单词边界(其中单词被定义为任何非空字母、数字和下划线);例如'a10' -match 'a1' 是真的,但'a10' -match 'a1\b' 不是,因为输入字符串中的1 不在单词的末尾。

请注意,使用带有单个字符串的-match 作为 LHS(而不是数组)会在自动 $Matches 变量中记录最近匹配的详细信息,这是一个哈希表,其0 条目包含整个匹配项(匹配的输入字符串部分);如果在正则表达式中使用了捕获组((...) 中包含的子表达式) - 条目 1 包含第一个捕获组捕获的内容,2 包含第二个捕获的内容,依此类推; 命名捕获组(例如,
(?<foo>...))按名称(例如,foo)获取条目。

此外,您可以将 switch statement-regex 选项一起使用,而不是冗长的 if / elseif 构造来按顺序匹配多个正则表达式:

代替:

if ($c -match $reg1) {
  $c = $c -replace $regyear 
}
elseif ($c -match $reg2) {
  $c = $c -replace $reg2 
}

你可以写得更干净:

switch -regex ($c) {
  $reg1 { $c = $c -replace $regyear; break }
  $reg2 { $c = $c -replace $reg2;    break }
  default { <# handles the case where nothing above matched #> }
}

break 确保不再进行匹配。

  • switch 的默认匹配(或使用选项-exact)与-eq 运算符类似(见下文)。

  • 您还可以使用
    -wildcard 选项使其执行通配符表达式匹配 - 如 -like 运算符(见下文)。

  • -casesensitive 选项使任何匹配模式的匹配都区分大小写。

  • 如果输入是数组,则对每个元素进行匹配;请注意,break 然后停止处理更多元素,而continue 立即继续处理下一个元素。


PowerShell中字符串匹配的其他方法

-like 允许您根据wildcard expressions 匹配字符串。

简单地说,* 匹配任何一系列字符,包括 none? 完全匹配 1 个字符,[...] 匹配一个字符中的任何一个字符指定 set范围 个字符。

-match 不同,-like 始终匹配整个字符串,但请注意通配符表达式的语法与正则表达式有根本的不同,而且功能远没有那么强大 - -like-match 不能互换使用。

因此,要获得 substring 匹配,请在表达式的两端放置一个 * o;例如:

'ingot' -like '*go*'  # true

-eq 比较整个字符串,字面意思(大小写变化除外)。

请注意,PowerShell 没有文字子字符串匹配运算符,但您可以(有点笨拙地)用-match[regex]::Escape() 模拟一个:

 'Cost: 7$.' -match [regex]::Escape('7$') # true

[regex]::Escape() 对其参数进行转义,以便其内容在解释为 regex-match 的 RHS 始终如此)时被字面意思处理。

这有点低效,因为没有充分的理由一开始就使用正则表达式。

直接使用.NET [string] type's .IndexOf() 方法一个选项,但也很重要;以下与上一条命令等效:

 'Cost: 7$.'.IndexOf('7$', [StringComparison]::InvariantCultureIgnoreCase) -ne -1 # true

注意需要使用InvariantCultureIgnoreCase 来匹配PowerShell 的默认行为,并且需要与-1 进行比较,因为返回了子字符串开始位置的字符索引

另一方面,此方法可让您通过[System.StringComparison] 枚举的其他成员更好地控制匹配的执行方式。
如果您正在寻找基于 current 文化的 case-sensitive 子字符串匹配,那么您可以简单地依赖 .IndexOf() 的默认行为;例如,
'I am here.'.IndexOf('am') -ne -1 # true vs.
'I am here.'.IndexOf('AM') -ne -1 # false, because matching is case-sensitive


最后,请注意 Select-String cmdlet管道 中执行字符串匹配,它同时支持正则表达式(默认情况下)和文字子字符串匹配(使用 @987654407 @) 开关。

与比较运算符不同,Select-Object 为每个匹配的输入行输出一个 [Microsoft.PowerShell.Commands.MatchInfo] 类型的匹配信息对象,其中包含原始行和有关匹配的元数据。

【讨论】:

    【解决方案2】:

    我认为您的主要问题是您使用“匹配”。

    它检查右边的字符串是否是......左边的一部分,而不是它是否是你所期望的实际匹配。

    $a = "Test"
    $b = "Test_me"
    
    $a -match $b
    False
    
    $b -match $a
    True
    

    我会将-match 替换为-like

    【讨论】:

    • 如果您要推荐使用 -like 而不是 -match,请明确说明 (a) 您将无法使用您的 regex -like 和 (b) -like(专门)支持的通配符表达式远不如正则表达式强大。另外,我会澄清-match 不会将 RHS 视为 string,而是将其视为 regex - 只是为了明确-match 不会执行 literal 子串匹配。
    猜你喜欢
    • 2018-04-25
    • 1970-01-01
    • 2016-11-11
    • 2020-12-04
    • 2011-10-31
    • 1970-01-01
    • 2019-04-26
    • 1970-01-01
    • 2018-06-13
    相关资源
    最近更新 更多