gawk FS将记录拆分为单个字符

7
如果字段分隔符为空字符串,则每个字符都成为一个单独的字段。
$ echo hello | awk -F '' -v OFS=, '{$1 = NF OFS $1} 1'
5,h,e,l,l,o

然而,如果FS是一个可能匹配零次的正则表达式,则不会发生相同的行为:
$ echo hello | awk -F ' *' -v OFS=, '{$1 = NF OFS $1} 1'
1,hello

有人知道为什么吗?我在gawk手册中找不到任何内容。这是FS=""的特例吗?
我最感兴趣的是为什么第二种情况没有将记录分成更多字段。就好像awk将FS=" *"视为FS="+"一样。

值得一提的是,在Mac OSX上,您的第一个示例会打印出1,hello,并显示警告awk:字段分隔符FS为空。正如其他答案所提到的,这是未定义的行为。还要注意,*不是正则表达式——它只是字符*。要使用正则表达式,您需要像.*这样的东西——您将获得“所有内容”。 - Floris
2
" <space><star> " 是一个有效的正则表达式,可以匹配零个或多个空格。 - glenn jackman
@glennjackman 我认为目前的帖子没有回答你的问题。我用awk的split()match()函数进行了一些测试,结果相同。所以我猜我们必须阅读awk的正则表达式匹配代码,才能理解如果匹配start=0,length=0,gawk如何处理结果。很可能(我还没有阅读代码)awk认为它不匹配,因此整个字符串/行将作为字段。<space>*是正则表达式,在你的问题中,实际上与:echo hello|awk -F 'm*' ...相同。无论如何,这是一个有趣的问题。 - Kent
@glennjackman 对不起,我没有注意到空格。结论似乎是`awk regex无法匹配零个字符" ... 但我没有好的来源(除了像您一样的“观察”)。 - Floris
如果你在comp.lang.awk新闻组中提出这个问题,Arnold Robbins几乎肯定会在那里回答你,并引用他编写的最有用的awk书籍("Effective Awk Programming")中的章节和节。只是说一下... - Ed Morton
5个回答

4

有趣的问题!

我刚刚获取了gnu-awk 4.1.0的代码,我认为我们可以在文件field.c中找到答案。

line 371:
 * re_parse_field --- parse fields using a regexp.
 *
 * This is called both from get_field() and from do_split()
 * via (*parse_field)().  This variation is for when FS is a regular
 * expression -- either user-defined or because RS=="" and FS==" "
 */
static long
re_parse_field(lo...

此行也是:(第425行):
if (REEND(rp, scan) == RESTART(rp, scan)) {   /* null match */

这里是与您问题中的<space>*匹配相关的案例。实现没有增加nf,也就是说,它认为整行都是一个单独的字段。请注意,此函数也用于do_split()函数中。
首先,如果FS为空字符串,则gawk将每个字符分隔为自己的一个字段。gawk的文档已经清楚地写明了这一点,在代码中,我们也可以看到:
line 613:
 * null_parse_field --- each character is a separate field
 *
 * This is called both from get_field() and from do_split()
 * via (*parse_field)().  This variation is for when FS is the null string.
 */
static long
null_parse_field(long up_to,

如果 FS 只有一个字符,awk 不会将其视为正则表达式。文档中也提到了这一点。在代码中也是如此:
#line 667
 * sc_parse_field --- single character field separator
 *
 * This is called both from get_field() and from do_split()
 * via (*parse_field)().  This variation is for when FS is a single character
 * other than space.
 */
static long
sc_parse_field(l

如果我们阅读这个函数,就会发现没有进行正则表达式匹配处理。
在函数re_parse_field()和sc_parse_field()的注释中,我们看到do_split也调用了它们。这解释了为什么以下命令中有1而不是3:
kent$  echo "foo"|awk '{split($0,a,/ */);print length(a)}'
1

注意,为了避免帖子过长,我没有在此处粘贴完整的代码,我们可以在这里找到代码:

http://git.savannah.gnu.org/cgit/gawk.git/


感谢您挖掘出来。我看到RESTART将等于REEND,因为该正则表达式在字符串开头匹配空字符串。因此,问题的答案是“gawk之所以这样工作是因为它是这样实现的”。 - glenn jackman
@glennjackman 我认为这是合理的。如果我们写一个正则表达式FS,但一行不匹配,我们期望什么?我认为在大多数情况下,我们期望NF==1而不是NF=300。唯一需要注意的是当我们使用split()时。在那里,我们有与字段处理相同的规则。如果我们真的想要用0长度匹配来分割字符串,那么我们必须使用"" - Kent
如果不匹配,research() 函数将返回 -1。问题在于当它匹配时该怎么办,但匹配的文本长度为零。gawk 开发人员选择不增加 NF,perl 开发人员则做出了相反的决定。 - glenn jackman
@glennjackman 是的,你说得对。那就是我想表达的意思。我的“不匹配”实际上指的是零长度匹配。 x*x*x* 的情况,而不是 ^foo$。我没有清楚地描述它。 - Kent

2

正如提到的那样,空字段分隔符会导致未定义的行为。在不同的平台/ awk 版本上,相同的代码将产生不同的结果。例如(所有 Mac OSX 10.8.5):

> echo hello | awk -F '' -v OFS=, '{$1 = NF OFS $1} 1'
awk: field separator FS is empty

1,hello

所以,awk 抱怨了,但是继续执行。

让我们看一些其他的例子:

> echo hello | awk -F '.' -v OFS=, '{$1 = NF OFS $1} 1'
1,hello

单独的 . 并不被视为正则表达式

> echo hello | awk -F '[.]' -v OFS=, '{$1 = NF OFS $1} 1'
1,hello

仍然没有任何内容

> echo hello | awk -F '.?' -v OFS=, '{$1 = NF OFS $1} 1'
6,,,,,,

现在我们有类似于正则表达式的东西:.?代表“零或一个字符”。它扩展为一个字符(被消耗),所以输出结果为“一大堆无用的东西”。
> echo hello | awk -F '*' -v OFS=, '{$1 = NF OFS $1} 1'
1,hello

不是一个正则表达式

> echo hello | awk -F '.*' -v OFS=, '{$1 = NF OFS $1} 1'
2,,

消耗整个字符串的正则表达式
> echo hello | awk -F 'l' -v OFS=, '{$1 = NF OFS $1} 1'
3,he,,o

匹配字母l两次 - 两个空字符串

> echo hello | awk -F 'ell' -v OFS=, '{$1 = NF OFS $1} 1'
2,h,o

一次匹配所有的ell

> echo hello | awk -F '.?|' -v OFS=, '{$1 = NF OFS $1} 1'
awk: illegal primary in regular expression .?| at 
 input record number 1, file 
 source line number 1

尝试聪明一点:有时在一个|的一边使用空字符串会匹配“任何东西”,但是awk的正则表达式引擎不喜欢它。

结论 - 正则表达式无法匹配“空”,而匹配的任何内容都将被消耗。 尝试使用(?:.)甚至(?=.)会生成错误。


1

看起来这是gawk中的特殊情况

传统上,FS等于""的行为未定义。在这种情况下,大多数版本的Unix awk只将整个记录视为仅具有一个字段。(d.c.) 在兼容模式下(参见选项),如果FS是空字符串,则gawk也会以此方式处理。


1

以下是 POSIX关于此事的说明:

如果FS是一个空字符串,则行为未指定。

因此,gawk的行为是与实现相关的,并且可以解释为什么您的两个示例没有产生相同的输出。


0
另一个数据点:gawk和perl在如何处理此问题上存在分歧:
$ perl -E '$,=","; $s="hello"; $r=qr( *); @s=split($r,$s); say scalar(@s), @s'
5,h,e,l,l,o

$ gawk 'BEGIN {s="hello";r=" *";n=split(s,a,r); print n,a[n]; if (s~r) print "match"}'
1 hello
match
$ gawk 'BEGIN {s="hello";r="";  n=split(s,a,r); print n,a[n]; if (s~r) print "match"}'
5 o
match

1
我刚刚发布了一个答案,我认为它解释了awk的行为方式。顺便说一下,这不是你问题的答案。 :) - Kent

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