Powershell:使用变量替换正则表达式命名组

7
假设我拥有以下正则表达式,但是我将其加载到变量$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中的$regex命名组的值替换为$version1、$version2、$version3、$version4的值,如果我不知道它们在$regex中出现的顺序(我只知道$regex包括这些命名组)?

我找不到任何描述使用组名称作为匹配索引来用变量的值替换命名组语法的参考资料 - 这个功能是否受支持?

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

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

Version "1.2.3.4"

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

用户可以指定一个文件集和一个正则表达式来匹配包含字段的行,最初的想法是通过命名组捕获各个字段。工具具有应替换文件中的各个版本字段值,但必须保留将包含替换的原始行的原始格式,并仅替换请求的字段。
编辑-2: 我认为我可以通过基于每个匹配项的位置和范围的子字符串计算获得所需的结果,但希望Powershell的替换操作能为我节省一些工作。
编辑-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
2个回答

7

简单的解决方案

在你想要替换掉$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.  '

You'd get:

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).

注意:上面的内容可以写成一行,但我将其分解以使其更易于阅读。
这利用了正则表达式的前后查找功能;一种在捕获字符串之前和之后检查内容的方法,而不包括它们在匹配中。也就是说,当我们选择要替换的内容时,我们可以说“匹配单词版本后出现的数字”,而不是说“替换版本单词”。
有关更多信息,请参见此处:http://www.regular-expressions.info/lookaround.html 你的示例:
将上述内容调整为适用于你的示例(即版本可能由逗号或点分隔,并且除了四组数字外没有统一的格式):
$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 }

4

正则表达式不是这样工作的,所以你不能直接这么做。除非使用更合适的正则表达式来分组保留你想要的部分。但你可以通过两个步骤来实现:首先提取版本字符串,然后在第二步中用新的版本字符串替换该子字符串:

$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

同意这个想法很好,但这是一个实用程序,用户需要指定一个正则表达式和一个文件集。我不知道正则表达式,也不知道文件内容的样子,所以我不能使用你回答中的第一行,否则就必须重新格式化原始文件内容,这是不可取的。我必须让文件内容保持原样,只替换匹配行上的子字符串为各自的版本字段。 - Hoobajoob
也许您可以用实际的旧/新数字替换正则表达式中的命名组,然后进行字符串替换。但是,如果正则表达式包含除命名组之外的表达式,则无法正确工作。 - Ansgar Wiechers
这个几乎可以工作,但我事先不知道正则表达式中的命名组实际上是如何定义的(例如,它们可能正在寻找\d、\d{2}、\d+、一个字面量等)。我可以对命名组定义引入一些约束,并更改上面的for循环中使用的正则表达式,以允许一个或多个字符来自正则表达式语法以及字母数字(例如,在for循环中的正则表达式中用"[a-zA-Z0-9\+.*?^${}|[]]+"替换"\d")。无论如何,这种方法比子字符串操作更可取。 - Hoobajoob
另一个问题是,如果要匹配的字符串包含一个或多个正则表达式字符,这些字符位于组定义之外,但需要匹配该字符串。例如:Version\0,0,0,0 - 此字符串的正则表达式应为“Version\\ (?<version1> \ d),(?<version2> \ d),0,0”,但使用上述算法,最终替换的字符串将为“Version\ 1,2,0,0”而不是“Version\ 1,2,0,0”。 - Hoobajoob
你觉得为什么我事先告诉你,如果正则表达式中包含其他表达式,它就无法工作?处理用户可能提出的每个可能的正则表达式是不可行的(如果不是完全不可能的)。 - Ansgar Wiechers
抱歉!看到你上面的评论时漏掉了那个。 - Hoobajoob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接