【问题标题】:Powershell: Replacing regex named groups with variablesPowershell:用变量替换正则表达式命名组
【发布时间】:2012-08-26 20:47:04
【问题描述】:

假设我有一个如下所示的正则表达式,但我将它从文件加载到变量 $regex 中,因此在设计时不知道它的内容是什么,但在运行时我可以发现它包含“ version1”、“version2”、“version3”和“version4”命名组:

"Version (?<version1>\d),(?<version2>\d),(?<version3>\d),(?<version4>\d)"

...我有这些变量:

$version1 = "3"
$version2 = "2"
$version3 = "1"
$version4 = "0"

...我在文件中遇到以下字符串:

Version 7,7,0,0

...存储在变量 $input 中,因此 ($input -match $regex) 的计算结果为 $true。

如果我不知道它们在 $regex 中出现的顺序,如何将字符串 $input 中的 $regex 中的命名组替换为 $version1、$version2、$version3、$version4 的值(我只知道 $regex 包含这些命名组吗?

我找不到任何描述通过使用组名作为匹配索引将命名组替换为变量值的语法的参考资料 - 这甚至受支持吗?

编辑: 澄清一下 - 目标是替换任何类型的文本文件中的模板化版本字符串,其中给定文件中的版本字符串需要替换可变数量的版本字段(可以是 2、3 或所有 4 个字段)。例如,文件中的文本可能看起来像以下任何一种(但不限于这些):

#define SOME_MACRO(4, 1, 0, 0)

Version "1.2.3.4"

SomeStruct vs = { 99,99,99,99 }

用户可以指定一个文件集和一个正则表达式来匹配包含字段的行,最初的想法是各个字段将被命名组捕获。该实用程序具有应在文件中替换的各个版本字段值,但必须保留将包含替换的行的原始格式,并且仅替换请求的字段。

EDIT-2: 我想我可以根据每个匹配项的位置和范围通过子字符串计算得到我需要的结果,但希望 Powershell 的替换操作能够为我节省一些工作。

EDIT-3: 因此,正如 Ansgar 在下面正确而简洁地描述的那样,没有办法(仅使用原始输入字符串、您只知道命名组的正则表达式以及结果匹配项)使用“-replace”操作(或其他正则表达式操作)来执行命名组的捕获替换,同时保持原始字符串的其余部分不变。对于这个问题,如果有人好奇,我最终使用了下面的解决方案。 YMMV,其他可能的解决方案。非常感谢 Ansgar 提供的反馈和选项。

在以下代码块中:

  • $input 是要执行替换的一行文本
  • $regex 是从已验证包含至少一个受支持的命名组的文件中读取的正则表达式([string] 类型)
  • $regexToGroupName 是一个哈希表,它将一个正则表达式字符串映射到一个组名数组,该数组按照 [regex]::GetGroupNames() 返回的数组的顺序排列,匹配从左到右的顺序它们出现在表达式中
  • $groupNameToVersionNumber 是一个将组名映射到版本号的哈希表。

$regex 中命名组的约束只是(我认为)命名组中的表达式不能嵌套,并且应该在输入字符串中最多匹配一次。

# This will give us the index and extent of each substring
# that we will be replacing (the parts that we will not keep)
$matchResults = ([regex]$regex).match($input)

# This will hold substrings from $input that were not captured
# by any of the supported named groups, as well as the replacement
# version strings, properly ordered, but will omit substrings captured
# by the named groups
$lineParts = @()
$startingIndex = 0
foreach ($groupName in $regexToGroupName.$regex)
{
    # Excise the substring leading up to the match for this group...
    $lineParts = $lineParts + $input.Substring($startingIndex, $matchResults.groups[$groupName].Index - $startingIndex)

    # Instead of the matched substring, we'll use the substitution
    $lineParts = $lineParts + $groupNameToVersionNumber.$groupName

    # Set the starting index of the next substring that we will keep...
    $startingIndex = $matchResults.groups[$groupName].Index + $matchResults.groups[$groupName].Length
}

# Keep the end of the original string (if there's anything left)
$lineParts = $lineParts + $input.Substring($startingIndex, $input.Length - $startingIndex)

$newLine = ""
foreach ($part in $lineParts)
{
   $newLine = $newLine + $part
}
$input= $newLine

【问题讨论】:

    标签: regex powershell regex-group named


    【解决方案1】:

    简单的解决方案

    如果您只想替换在 $input 文本中某处找到的版本号,您可以简单地这样做:

    $input -replace '(Version\s+)\d+,\d+,\d+,\d+',"`$1$Version1,$Version2,$Version3,$Version4"
    

    在 PowerShell 中使用命名捕获

    关于您关于命名捕获的问题,可以通过使用大括号来完成。即

    'dogcatcher' -replace '(?<pet>dog|cat)','I have a pet ${pet}.  '
    

    给予:

    I have a pet dog.  I have a pet cat.  cher
    

    多次捕获问题和解决方案

    您不能在同一个替换语句中替换多个值,因为替换字符串用于所有内容。即如果你这样做:

     'dogcatcher' -replace '(?<pet>dog|cat)|(?<singer>cher)','I have a pet ${pet}.  I like ${singer}''s songs.  '
    

    你会得到:

    I have a pet dog.  I like 's songs.  I have a pet cat.  I like 's songs.  I have a pet .  I like cher's songs.  
    

    ...这可能不是您所希望的。

    相反,您必须对每个项目进行匹配:

    'dogcatcher' -replace '(?<pet>dog|cat)','I have a pet ${pet}.  ' -replace '(?<singer>cher)', 'I like ${singer}''s songs.  ' 
    

    ...得到:

    I have a pet dog.  I have a pet cat.  I like cher's songs.  
    

    更复杂的解决方案

    回到您的场景中,您实际上并没有使用捕获的值;相反,您希望用新值替换它们所在的空间。为此,您只需要这样:

    $input = 'I''m running Programmer''s Notepad version 2.4.2.1440, and am a big fan.  I also have Chrome v    56.0.2924.87 (64-bit).' 
    
    $version1 = 1
    $version2 = 3
    $version3 = 5
    $version4 = 7
    
    $v1Pattern = '(?<=\bv(?:ersion)?\s+)\d+(?=\.\d+\.\d+\.\d+)'
    $v2Pattern = '(?<=\bv(?:ersion)?\s+\d+\.)\d+(?=\.\d+\.\d+)'
    $v3Pattern = '(?<=\bv(?:ersion)?\s+\d+\.\d+\.)\d+(?=\.\d+)'
    $v4Pattern = '(?<=\bv(?:ersion)?\s+\d+\.\d+\.\d+\.)\d+'
    
    $input -replace $v1Pattern, $version1 -replace $v2Pattern, $version2 -replace $v3Pattern,$version3 -replace $v4Pattern,$version4
    

    这会给:

    I'm running Programmer's Notepad version 1.3.5.7, and am a big fan.  I also have Chrome v    1.3.5.7 (64-bit).
    

    注意:以上内容可以写成 1 行,但我已将其分解以使其更易于阅读。

    这利用了正则表达式的外观;一种在您捕获的字符串之前和之后检查内容的方法,而不包括匹配中的内容。也就是说,当我们选择要替换的内容时,我们可以说“匹配单词版本之后出现的数字”而不说“替换单词版本”。

    更多信息请点击此处:http://www.regular-expressions.info/lookaround.html

    你的例子

    调整上述内容以适用于您的示例(即版本可能用逗号或点分隔,并且除了 4 组数字之外,它们的格式没有一致性:

    $input = @'
    #define SOME_MACRO(4, 1, 0, 0)
    
    Version "1.2.3.4"
    
    SomeStruct vs = { 99,99,99,99 }
    '@
    
    $version1 = 1
    $version2 = 3
    $version3 = 5
    $version4 = 7
    
    $v1Pattern = '(?<=\b)\d+(?=\s*[\.,]\s*\d+\s*[\.,]\s*\d+\s*[\.,]\s*\d+\b)'
    $v2Pattern = '(?<=\b\d+\s*[\.,]\s*)\d+(?=\s*[\.,]\s*\d+\s*[\.,]\s*\d+\b)'
    $v3Pattern = '(?<=\b\d+\s*[\.,]\s*\d+\s*[\.,]\s*)\d+(?=\s*[\.,]\s*\d+\b)'
    $v4Pattern = '(?<=\b\d+\s*[\.,]\s*\d+\s*[\.,]\s*\d+\s*[\.,]\s*)\d+\b'
    
    $input -replace $v1Pattern, $version1 -replace $v2Pattern, $version2 -replace $v3Pattern,$version3 -replace $v4Pattern,$version4
    

    给予:

    #define SOME_MACRO(1, 3, 5, 7)
    
    Version "1.3.5.7"
    
    SomeStruct vs = { 1,3,5,7 }
    

    【讨论】:

      【解决方案2】:

      正则表达式不能那样工作,所以你不能。不是直接的,就是这样。您可以做的(不使用更合适的正则表达式来分组您想要保留的部分)是提取版本字符串,然后在第二步中用新版本字符串替换该子字符串:

      $oldver = $input -replace $regexp, '$1,$2,$3,$4'
      $newver = $input -replace $oldver, "$Version1,$Version2,$Version3,$Version4"
      

      编辑:

      如果你甚至不知道结构,你也必须从正则表达式中提取它。

      $version = @($version1, $version2, $version3, $version4)
      $input -match $regexp
      $oldver = $regexp
      $newver = $regexp
      for ($i = 1; $i -le 4; $i++) {
        $oldver = $oldver -replace "\(\?<version$i>\\d\)", $matches["version$i"]
        $newver = $newver -replace "\(\?<version$i>\\d\)", $version[$i-1]
      }
      $input -replace $oldver, $newver
      

      【讨论】:

      • 同意这会很好,但这是用于用户指定正则表达式和文件集的实用程序。我不知道正则表达式,也不知道文件内容是什么样的,所以如果不重新格式化原始文件内容,我就无法使用答案中的第一行,这是不可取的。之后我必须让文件内容看起来相同,只用各个版本字段替换匹配行上的子字符串。
      • 也许您可以用实际的旧/新数字替换正则表达式中的命名组,然后进行字符串替换。但是,如果正则表达式包含命名组以外的表达式,那将无法正常工作。
      • 这几乎可以工作,虽然我事先不知道正则表达式中的命名组是如何实际定义的(例如,他们可能正在寻找 \d、\d{2}、\d+ ,文字等)。我可以对命名组定义引入一些约束,并更改上面 for 循环中使用的正则表达式,以允许来自正则表达式语法和字母数字的一个或多个字符(例如,替换正则表达式中的“\\d”带有“[a-zA-Z0-9\\+\.*\?\^\$\{\}\|[]]+”的for循环。无论如何,这种方法比子字符串操作更可取。
      • 另一个问题是,如果要匹配的字符串包含一个或多个组定义之外的正则表达式字符,但它们是匹配字符串所必需的。例如: Version\0,0,0,0 - 正则表达式将是“Version\\(?\d),(?\d),0,0”,但使用上面的算法最终替换的字符串将是 "Version\\1,2,0,0" 而不是 "Version\1,2,0,0"。
      • 为什么你认为我提前告诉你,如果正则表达式也包含其他表达式,它会不起作用?处理用户可能想出的每一个可能的正则表达式是不可行的(如果不是完全不可能的话)。
      猜你喜欢
      • 2012-08-01
      • 2016-02-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-20
      • 2016-02-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多