AWK:如何从行模式中访问捕获组

300

如果我有一个awk命令

pattern { ... }

如果模式使用了一个捕获组,我该如何在代码块中访问被捕获的字符串?


3
https://dev59.com/f3I_5IYBdhLWcg3wBub4 - lt1776
1
有时(在简单情况下),可以调整字段分隔符(FS)并选择要与$ field匹配的内容。 预先格式化输入也可能有所帮助。 - Krzysztof Jabłoński
1
在重复的问题上有一个更好的答案:https://dev59.com/sGLVa4cB1Zd3GeqP1PKX#10254791。 - Samuel Edwin Ward
4
Samuel Edwin Ward说:“这也是一个不错的答案!但它需要使用gawk(因为它使用了gensub)。 - rampion
不用说,如果你正在进行简单的转换,sed可以很自然地处理捕获组。 - Rob
7个回答

426

使用gawk,你可以使用match函数来捕获带括号的组。

gawk 'match($0, pattern, ary) {print ary[1]}' 
例子:
echo "abcdef" | gawk 'match($0, /b(.*)e/, a) {print a[1]}' 

输出cd

请注意使用的gawk实现了相关功能。

如果需要可移植的替代方案,您可以使用match()substr来实现类似的结果。

示例:

echo "abcdef" | awk 'match($0, /b[^e]*/) {print substr($0, RSTART+1, RLENGTH-1)}'

输出 cd 命令。


4
是的,gxxx变体具有许多额外的GNU特性和功能。 - Peter Tillemans
1
在 BusyBox awk 中也适用。 - MrMas

225

这是一次追溯回忆的经历...

我很久以前就用 Perl 替代了 awk。

显然 AWK 正则表达式引擎无法捕获其组。

你可以考虑使用类似于:

perl -n -e'/test(\d+)/ && print $1'

-n标志使得Perl像awk一样循环遍历每一行。


4
显然有人持不同意见。这个网页是2005年的:http://www.tek-tips.com/faqs.cfm?fid=5674它证实了在awk中无法重复使用匹配组。 - Peter Tillemans
5
在几乎所有情况下,我更喜欢使用“perl -n -p -e…”而不是awk,因为它更加灵活、强大,并且在我的看法中语法更加合理。 - Peter Tillemans
22
"gawk" 不等同于 "awk"。它们是不同的工具,并且在大多数地方默认情况下无法使用 "gawk"。 - Oli
12
楼主明确要求使用awk解决方案,因此我认为这不是一个答案。 - Joppe
15
如果没有解决方案,您无法提供awk解决方案。在第3行中,我解释了AWK不支持捕获组,并且给出了一种替代方法,显然OP很欣赏这个答案,因此该答案被接受。我如何更好地回答这个问题? - Peter Tillemans
显示剩余7条评论

36

我经常需要这个东西,所以我创建了一个基于 glenn jackman 答案的 bash 函数。

定义

将此添加到您的 .bash_profile 等文件中。

function regex { gawk 'match($0,/'$1'/, ary) {print ary['${2:-'0'}']}'; }

使用方法

在文件中为每行捕获正则表达式

$ cat filename | regex '.*'

捕获文件中每一行的第一个正则表达式捕获组。

$ cat filename | regex '(.*)' 1

2
与使用 grep -o 有何不同? - bfontaine
1
@bfontaine 能否让 grep -o 输出捕获组? - Olle Härstedt
2
@OlleHärstedt 不行。它只适用于您没有捕获组的用例。在这种情况下,使用链接的 grep -o 会变得很丑陋。 - bfontaine
这需要支持多个捕获。 - SgtPooki

20

你可以使用GNU awk:

$ cat hta
RewriteCond %{HTTP_HOST} !^www\.mysite\.net$
RewriteRule (.*) http://www.mysite.net/$1 [R=301,L]

$ gawk 'match($0, /.*(http.*?)\$/, m) { print m[1]; }' < hta
http://www.mysite.net/

5
基本上,这就是 Glenn Jackman 的回答所说的。 - rampion
1
Ed Morton:我认为这值得一个高级别的回答。编辑:嗯...对我来说,它打印了RewriteRule (.*) http://www.mysite.net/$,这比子组更多。 - rampion
3
看起来RSTARTRLENGTH指的是模式匹配到的子字符串。 - rampion
@EdMorton - 不,那会选择包含 http... 模式的整行。 - KFL
@KFL 你说得对,但实际上还有一个更糟糕的问题,即发布的答案(以及我建议使其不依赖于gawk)都包含 .*?,这是PCRE的一种语法,而在ERE中是未定义行为。我会删除我的评论。 - Ed Morton

7

注意:使用gensub不符合POSIX标准。

在没有扩展的情况下,您也可以在vanilla awk中模拟捕获。虽然这不是很直观:

步骤1. 使用gensub将匹配项用某个在字符串中不存在的字符括起来。 步骤2. 对该字符使用split。 步骤3. 分割后数组中每隔一个元素就是一个捕获组。

$ echo 'ab cb ad' | awk '{ split(gensub(/a./,SUBSEP"&"SUBSEP,"g",$0),cap,SUBSEP); print cap[2]"|" cap[4] ; }'
ab|ad

5
我几乎可以确定gensubgawk特有的函数。如果您在awk中输入awk --version,那么您会得到什么呢?祝大家好运。 - shellter
8
我很确定 gensub 是 gawk 的语法,尽管 BusyBox awk 也有它。不过,这个答案也可以用 gsub 实现: echo 'ab cb ad' | awk '{gsub(/a./,SUBSEP"&"SUBSEP);split($0,cap,SUBSEP);print cap[2]"|"cap[4]}' - dubiousjim
4
gensub() 是 gawk 的扩展功能,gawk 手册明确说明了这一点。其他的 awk 变种可能也会实现它,但它仍然不符合 POSIX 标准。尝试运行 gawk --posix '{gsub(...)}' 命令,它会报错。 - MestreLion
2
@MestreLion,你的意思是说它会抱怨 gawk --posix '{gensub(...)}' 吗? - dubiousjim
2
尽管你关于POSIX awk拥有gensub函数的说法是错误的,但你的例子只适用于非常有限的情况:整个模式被分组,无法匹配像所有key=(value)这样的内容,当我只想提取value部分时。 - Meow
显示剩余2条评论

2

我为了封装Peter Tillemans的答案而努力了一些时间,但是这是我想出来的:

function regex { perl -n -e "/$1/ && printf \"%s\n\", "'$1' }

我发现,与opsb基于awk的bash函数相比,这个针对以下正则表达式参数的函数工作得更好,因为我不想打印出“ms”。

'([0-9]*)ms$'

我更喜欢这个解决方案,因为你可以看到限定捕获的组成部分,同时也可以省略它们。但是,有人能解释一下这是如何工作的吗?我无法在BASH中正确地使用这个perl语法,因为我不太理解它 - 特别是围绕$1的双引号/单引号标记。 - Demis
这不是我以前或现在做过的事情,但回想起来它所做的是将两个字符串连接起来,第一个字符串在双引号中(该字符串包含用反斜杠转义的嵌入式双引号),而第二个字符串则在单引号中。然后将该连接的结果作为参数提供给perl -e。此外,您需要知道第一个$1(在双引号内)将被替换为函数的第一个参数,而第二个$1(在单引号内)将保持不变。请参见[此示例](https://i.imgur.com/Bfp2TmA.png)。 - wytten
我明白了,现在有点更清楚了。那么在perl命令中,正则表达式匹配/组捕获定义在哪里呢?我看到你写了'([0-9]*)ms$' - 这是作为参数提供的吗(而字符串是另一个参数)?然后,perl -e的输出被插入到bash的printf命令中,以替换%s,是这样吗?谢谢,我希望能够使用它。 - Demis
1
你可以将一个用单引号括起来的正则表达式作为唯一参数传递给regex bash函数。示例 - wytten
我给你的回答点了踩,因为问题是关于 awk 的,所以这个回答与问题无关。 - bfontaine

0

我认为gawk的match()-to-array仅适用于捕获组的第一个实例。

如果有多个要捕获并执行任何复杂操作的内容,也许可以考虑使用其他方法。

gawk 'BEGIN { S = SUBSEP 
          } { 
              nx=split(gensub(/(..(..)..(..))/, 
                              "\\1"(S)"\\2"(S)"\\3", "g", str), 
                       arr, S)
              for(x in nx) { perform-ops-over arr[x] } }'

这样做可以避免受到 gensub() 的限制,该函数会限制您的修改复杂度,或者受到 match() 的限制。

通过纯试错,我注意到 gawk 在 Unicode 模式下有一个警告:对于一个有效的 Unicode 字符串뀇꿬,其包含以下 6 个八进制代码:

情况 1:匹配单个字节没有问题,但它也会报告 RSTART 的多字节值为 1,而不是字节级别的答案 2。它也无法提供关于\207 是第一个连续字节还是第二个连续字节的信息,因为此处 RLENGTH 始终为 1。

$ gawk 'BEGIN{ print match("\353\200\207\352\277\254", "\207") }' 
$ 1 

方案2: Match 功能也可以针对 Unicode 无效模式进行匹配,例如:
$ gawk 'BEGIN{ match("\353\200\207\352\277\254", "\207\352"); 
$                print RSTART, RLENGTH }' 
$ 1 2

场景3:您可以针对一个Unicode非法字符串(\300 \xC0 对于所有可能的字节配对都是UTF8无效的)检查模式的存在。
$ gawk 'BEGIN{ print ("\300\353\200\207\352\277\254" ~ /\200/) }' 
$ 1

场景4/5/6:无论是(a)使用Unicode无效字符串的match(),还是使用Unicode无效/不完整参数的index(),都会显示错误消息。
$ gawk 'BEGIN{ match("\300\353\200\207\352\277\254", "\207\352"); print RSTART, RLENGTH }' gawk: cmd. line:1: warning: Invalid multibyte data detected. There may be a mismatch between your data and your locale. 2 2

$ gawk 'BEGIN{ print index("\353\200\207\352\277\254", "\352") }' gawk: cmd. line:1: warning: Invalid multibyte data detected. There may be a mismatch between your data and your locale. 0

$ gawk 'BEGIN{ print index("\353\200\207\352\277\254", "\200") }' gawk: cmd. line:1: warning: Invalid multibyte data detected. There may be a mismatch between your data and your locale. 0

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