我正在使用 netcat 并将输出通过管道传输到 gawk。以下是 gawk 将接收的示例字节序列:

=AAAA=AAAA;AAAA;AAAA=

数据可以包含任意字符,唯一的规定是=;是分隔符,并且任一分隔符都可以随时出现。但是,在写入任意字符块时,每个块始终会以其中一个分隔符作为前缀,并且始终会以其中一个分隔符作为后缀 – 它永远不会在不写入合适的前缀和后缀的情况下写入块。

由于这是一个网络流,因此在读取此序列后,stdin 保持打开状态,等待未来的数据。我希望 gawk 读取直到找到分隔符,然后使用找到的任何数据执行我的 gawk 脚本的主体,同时确保它正确处理连续的 stdin 流。我将在下面更详细地解释这部分。请注意,我确实需要区分使用了哪个分隔符,因为我在代码的其他地方使用了该信息。

这是我到目前为止尝试过的方法(zsh 脚本,使用 gawk,在 macOS 上)。对于这个问题,我简化了主体以仅打印数据 – 我的完整 gawk 脚本的主体要复杂得多。我还将 netcat 流简化为cat文件以及 stdin,以模仿流行为。

cat -u example.txt - | gawk '
BEGIN {
    RS = "=|;";
}
{
    if ($0 != "") {
        print $0;
        fflush();
    }
}
'

example.txt

=AAAA=AAAA;AAAA;AAAA=

我的尝试成功处理了大部分数据……直到最新的记录。它挂起等待来自 stdin 的更多数据,无法执行最新记录的脚本主体,尽管 stdin 中显然有适当的分隔符。

当前输出:(仅处理3条记录)

AAAA
AAAA
AAAA
[hang here, waiting for future data]

期望输出:(所有 4 条记录都已处理)

AAAA
AAAA
AAAA
AAAA
[hang here, waiting for future data]

经过一些调试,我确定如果 stdin 关闭并且RS 使用正则表达式,则不会出现问题。相反,如果 stdin 保持打开状态并且RS 是纯文本字符串,也不会出现问题。只有当 stdin 保持打开状态并且RS 是正则表达式时才会出现问题。据推测,它挂起是因为它正在等待更多数据以评估正则表达式……但 stdin 上显然有匹配的分隔符,所以我希望 gawk 立即处理它。这似乎有点极端情况。

我该如何实现这一点?非常感谢大家的帮助!

5

  • 如果您的输入流中没有换行符,您可以尝试gawk -v FPAT='[^;=]+' '{ for(i = 1; i <= NF; i++) { print $i; fflush() }}'


    – 


  • @RenaudPacalet 不幸的是,输入流中有换行符,而且数量很多。输入流可以是任意字符,唯一的规定是=;是分隔符。


    – 

  • 1
    然后,您可能应该编辑您的问题并提供更具代表性的输入/输出示例。


    – 

  • 如果对每条记录使用不同的字符串,则更容易理解您的示例,例如=AAAA=BBBB;CCCC;DDDD=,输出向我们显示正在打印哪些输入记录,而不仅仅是打印了多少条输入记录。


    – 

  • 输入是否包含空字节?


    – 


5 个回答
5

awk 正在等待记录被分隔。当发生以下两件事时,记录将被分隔:正则表达式匹配RS,或者输入结束。

您也没有给出它,因为您使用了cat <file> -,这意味着在用尽cat之后, 的输出流将继续使用标准输入(您的 TTY) 。<file>

您必须Ctrl-D在空行上使用来生成 Gawk 正在寻找的必要 EOF 条件。

编辑:

问题是,为什么最后一条记录没有出现,即使它被尾随的 分隔开来=

此行为在我用 Lisp 语言编写的 Awk 实现中准确重现,与 GNU Awk 并列。

$ (echo -n 'AAAA=AAAA;AAAA;AAAA='; cat) | gawk 'BEGIN { RS = "=|;"; } { print $0; fflush(); }'
AAAA
AAAA
AAAA
# hangs here until Ctrl-D, then:
AAAA

完全一样的事情:

$ (echo -n 'AAAA=AAAA;AAAA;AAAA='; cat) | txr -e '(awk (:set rs #/=|;/) (t))'
AAAA
AAAA
AAAA
# hangs here until Ctrl-D, then:
AAAA

对于第二个 Awk 实现,由于我从头开始编写了所有内容,包括正则表达式引擎,因此我可以解释其行为,从而形成关于为什么 Gawk 相同的假设。

正则表达式分隔读取基于用 C 编写的函数,该函数read_until_match是名为 的辅助程序的包装器。此函数的工作原理是将字符从流中逐个输入正则表达式状态机,检查状态。

事情是这样的。当正则表达式状态机说“我们有一个匹配!”时,我们不能就此止步。原因是我们需要找到最长的匹配。

该函数不知道正则表达式是一个简单的单字符正则表达式,对于该正则表达式来说,第一个匹配已经是最长的匹配。因此,它需要再输入一个字符。此时,正则表达式状态机说“失败!”。然后,该函数知道之前有一个成功的匹配。它回溯到该点,将额外的字符推回到流中。

因此,如果流中没有下一个可用的字符,我们就会得到 I/O 阻塞挂起。

之所以必须这样工作,是因为有些正则表达式能够成功匹配最长匹配的前缀。一个简单的例子是:假设我们有#+一个分隔符。当#看到一个时,那就是匹配!但是当#看到另一个时,那也是匹配!我们必须看到所有字符#才能获得完全匹配,这意味着我们必须看到后面第一个不匹配的字符。

GNU Awk 无法轻易避免做非常类似的事情;理论要求这样做。

解决这个问题的方法是使用一个函数maxmatchlen(R),它为正则表达式R报告正则表达式匹配的最大长度(可能是无限的)。maxmatchlen(/.*/)Inf,但是matchmatchlen(/abc/)是 3。你明白了。有了这个函数,我们就知道,如果我们刚刚输入了正则表达式matchmatchlen字符,并且正则表达式状态机报告了匹配状态,我们就完成了;我们不必提前查看流。

9

  • 1
    谢谢指点!不幸的是,发送 EOF 不是一个选项,因为我希望网络流无限期地继续下去。您提到,当 RS 正则表达式匹配时,gawk 会进行分隔。您知道为什么我的正则表达式与标准输入数据不匹配吗?我正在使用RS = "=|;";。我猜想这会在第一个 =或时匹配;,而我的标准输入中最新的字符确实是=。如果我错了,请纠正我,但我想这会导致匹配,即使标准输入仍处于打开状态?谢谢!


    – 


  • 我明白了。我们有一个尾随=记录分隔符,那么为什么它会挂起而不处理最后一个明确分隔的记录?我怀疑正则表达式引擎在 Gawk 中使用了前瞻字符,即使该特定正则表达式不需要它。


    – 


  • 我不太清楚它为什么会挂起,但经过更多调试后,我对两件事产生了怀疑:1)就像你说的,正则表达式引擎正在尝试读取更多数据以完成正则表达式,这会阻塞,尽管我的正则表达式不需要这种读取。2)某种缓冲问题。我注意到,如果我的输入是=AAAA=AAAA;AAAA;AAAA=AA(23 个字符),它会挂起……但如果我再添加一个A(使其成为 24 个字符,并跨越 4 字节边界)……那么一切都会正常工作并产生我想要的输出。很奇怪。我在使用 macOS。有什么建议吗?非常感谢!


    – 


  • 1
    是的,正则表达式不需要读取,但正则表达式通常需要扫描更多字符,尽管其状态机中已达到匹配状态。这似乎值得在我的实现中修复。我们可以在maxmatchlen编译正则表达式时计算属性,使其在执行时以低成本获得。


    – 

  • 1
    除此之外,我还观察到……如果我的正则表达式长度为 5 个字符……那么我的输入数据在最近的分隔符后必须至少有 5 个字符才能输出正确的结果。如果我的输入后面的字符少于 5 个,那么它将执行此截断行为。同样,如果我调整正则表达式字符串的长度,也会调整在最近的分隔符后需要多少个字符才能调用此行为。鉴于此,这似乎表明它是正则表达式引擎的产物。


    – 


一个解决方法是将 shell 读取循环插入到管道中,将原始 awk 输入(OP 的实际netcat输出)分割成单个字符,然后一次一个地将它们提供给 awk:

cat example.txt - |
while IFS= read -r -d '' -N1 char; do printf '%s\0' "$char"; done |
awk -v RS='\0' '
    /[;=]/ { if (rec != "") { print rec; fflush() }; rec=""; next }
    { rec=rec $0 }
'
AAAA
AAAA
AAAA
AAAA

这需要 GNU awk 或其他可以处理NUL字符的程序,因为RS这是非 POSIX 行为。它确实假设您的输入不能包含 NUL 字节,即它是一个有效的 POSIX 文本“文件”。

如果感兴趣的话请继续阅读以了解我们是如何到达那里的…

我认为这里至少有 1 个错误,因为我发现了多个奇怪之处(见下文),所以我在打开了一个 gawk 错误报告,但根据 gawk 提供商 Arnold 的说法,这种情况下的行为差异只是必须提前读取以确保正则表达式匹配正确的字符串的实现细节。

这里似乎有 3 个问题,例如在 cygwin 上使用 GNU awk 5.3.0:

  1. 不同的所谓等效正则表达式会产生不同的行为:
$ printf 'A;B;C;\n' > file
$ cat file - | awk -v RS='(;|=)' '{print NR, $0}'
1 A
$ cat file - | awk -v RS=';|=' '{print NR, $0}'
1 A
2 B
$ cat file - | awk -v RS='[;=]' '{print NR, $0}'
1 A
2 B
3 C

(;|=);|=并且[;=]应该是等价的,但在这种情况下显然它们不是等价的。

好消息是,您显然可以使用括号表达式(如上面的第 3 种情况)而不是“或”来解决该问题。

  1. 当记录分隔符是输入中的最后一个时,输出记录落后于输入记录,例如,最后一个之后没有换行符;
$ printf 'A;B;C;' > file
$ cat file - | awk -v RS='(;|=)' '{print $0; fflush()}'
$ cat file - | awk -v RS=';|=' '{print $0; fflush()}'
A
$ cat file - | awk -v RS='[;=]' '{print $0; fflush()}'
A
B

坏消息是,这会影响 OP 的示例:

$ printf ';AAAA;BBBB;CCCC;DDDD;' > file

使用文字字符 RS:

$ cat file - | awk -v RS=';' '{print $0; fflush()}'

AAAA
BBBB
CCCC
DDDD

使用正则表达式 RS 也应该使该字符成为文字:

$ cat file - | awk -v RS='[;]' '{print $0; fflush()}'

AAAA
BBBB
CCCC
$ printf ';AAAA;BBBB;CCCC;DDDD;x' > file
$ cat file - | awk -v RS='[;]' '{print $0; fflush()}'

AAAA
BBBB
CCCC
DDDD
  1. 在 RS 括号表达式中添加不同的字符会产生不一致的行为(我偶然发现了这一点):
$ printf 'A;B;C;\n' > file
$ cat file - | awk -v RS='[;|=]' '{print $0; fflush()}'
A
$ cat file - | awk -v RS='[;a=]' '{print $0; fflush()}'
A
B
C

FWIW我尝试设置超时:

$ cat file - | awk -v RS='[;]' 'BEGIN{PROCINFO["-", "READ_TIMEOUT"]=100} {print $0; fflush()}'
A
B
awk: cmd. line:1: (FILENAME=- FNR=3) fatal: error reading input file `-': Connection timed out
$ cat file - | awk -v RS='[;]' -v GAWK_READ_TIMEOUT=1 '{print $0; fflush()}'
A
B

并使用 stdbuf 禁用缓冲:

$ cat file - | stdbuf -i0 -o0 -e0 awk -v RS='[;]' '{print $0; fflush()}'
A
B

并匹配每个字符(我想我可以用它来RT ~ /[=;]/查找分隔符):

$ cat file - | awk -v RS='(.)' '{print RT; fflush()}'
A
;
B
;
C

但是它们都不让我读取最后一个记录分隔符,所以此时我不知道 OP 可以做什么才能使用正则表达式成功读取连续输入的最后一条记录,除了这样的方法:

$ printf 'A;B;C;' > file
$ cat file - |
    while IFS= read -r -d '' -N1 char; do printf '%s\0' "$char"; done |
    awk -v RS='\0' '/[;=]/ { print rec; fflush(); rec=""; next } { rec=rec $0 }'
A
B
C

并使用 OP 示例输入但每个记录使用不同的文本以使输入到输出记录的映射更清晰:

$ printf '=AAAA=BBBB;CCCC;DDDD=' > example.txt
$ cat example.txt - |
    while IFS= read -r -d '' -N1 char; do printf '%s\0' "$char"; done |
    awk -v RS='\0' '/[;=]/ { print rec; fflush(); rec=""; next } { rec=rec $0 }'

AAAA
BBBB
CCCC
DDDD

我们使用 NUL 字符作为分隔符,并使用上面的各种选项,使 shell 读取循环足够强大,能够处理输入中的空行和其他空白,有关这些问题的详细信息,请参阅。我们还为 awk RS 使用 NUL 字符,以便它可以区分来自原始输入的换行符和由 shell 添加的作为终止字符的换行符printf,否则recawk 脚本中永远不会包含换行符,因为它们会全部被默认 RS 匹配所消耗。

我们正在使用一个到/来自 while-read 循环的管道而不是进程替换,这只是为了便于理解,因为 OP 已经在使用管道。

6

  • \n在文件中放入是作弊行为。;-)


    – 


  • @Armali 不管有没有 ,这都不会对 3 个原本等价的正则表达式产生不同结果的问题产生影响\n。我把它放在那里是为了排除没有它(因此输入不是有效的 POSIX 文本文件)导致问题的可能性。


    – 


  • 1
    \n如果没有(GNU Awk 4.1.4),即使是第三种情况也不起作用。


    – 

  • @Armali 获取较新版本的 gawk,该版本已过时 8 年,我们现在使用的是 gawk 5.3.0,其间已经修复了几个错误并进行了增强。


    – 

  • 1
    @WalterA 虽然这解决了一个问题,但它并不能解决 OP 的整个问题,因为我的列表中的项目“2”仍然存在,所以他们仍然看不到输入的最后一条记录,除非他们按照我在答案顶部所显示的方式去做。


    – 


RS == any single character

记录由该字符的每次出现分隔。多个连续出现的字符会界定空记录。(…)

RS == regexp

记录由与正则表达式匹配的字符分隔。正则表达式的前导匹配和尾随匹配分隔空记录。(…)

请注意,只有后者才提到了前导和尾随,因此我怀疑问题的根源可能是它在 GNU 中的实现方式AWK

如果你不需要辨别=;我建议遵循以下解决方法

cat -u example.txt - | sed -u 'y/;/=/' | gawk '
BEGIN {
    RS = "=";
}
{
    if ($0 != "") {
        print $0;
        fflush();
    }
}
'

对于example.txt内容而言

=AAAA=AAAA;AAAA;AAAA=

给出输出

AAAA
AAAA
AAAA
AAAA

并挂起。解释:我添加了 GNUsed在非缓冲模式下运行 ( ),其中只有-u一个

将模式空间中与源字符匹配的任意字符与目标字符中的对应字符进行音译。

在此替换;使用=。然后将命令更改RSgawk单字符串=

(在 GNU sed 4.8 和 GNU Awk 5.1.0 中测试)

1

  • 1
    谢谢你的信息!不幸的是,我需要辨别这两个分隔符,因为我的复杂 gawk 主体脚本需要处理这部分。考虑到这个限制,有什么建议吗?谢谢!


    – 

如果输入不包含任何NUL-byte 那么可能的解决方法是将其附加到每个;=

cat -u example.txt - |

LC_ALL=C perl -npe 'BEGIN{$/ = \1; $| = 1} $_ .= "\0" if /[;=]/' |

gawk -v RS='\0' '
{
    sz = length($0);
    RT = substr($0, sz);
    $0 = substr($0, 1, sz-1);

    # now you can use your original code:

    if ($0 != "") {
        print $0;
        fflush();
    }
}
'
AAAA
AAAA
AAAA
AAAA

如果输入确实包含NUL-bytes 那么您可能必须使用zshperl来实现您的逻辑:

#!/bin/zsh

cat -u example.txt - | {

while read -r -u0 -k1 char
do
    case $char in
        [=\;])
            [[ ${str:+1} ]] && printf '%s\n' "$str"
            str=
        ;;
        *) str+=$char
        ;;
    esac
done
[[ ${str:+1} ]] && printf '%s\n' "$str"

}
AAAA
AAAA
AAAA
AAAA

6

  • 1
    如果 OP 的实际代码依赖于知道当前分隔符是还是,那么替换=;不会有帮助。相反,如果您在每个和之后添加一个 NUL,那么他们仍然会拥有该信息,只需手动从每个记录中删除最后一个字符即可。=;=;


    – 

  • @EdMorton OP 是否需要知道边界?这会改变问题。


    – 

  • 他们没有明确说明,但他们说“对于这个问题,我简化了正文,只打印数据 – 我的完整 gawk 脚本的正文要复杂得多”,我预计输入中分隔符的变化背后一定有某种含义(可能标识不同类型的数据或子字段),所以。就我个人而言。我认为他们确实需要知道分隔符(或者至少不假设他们不需要知道它)。


    – 


  • 啊,我看到他们实际上在评论中提到了这一点——


    – 


  • 1
    RT嗯,如果在示例代码中添加


    – 

@daweo 和 @EdMorton 的解决方案的组合:

OP 希望有基于辨别两个分隔符的逻辑,并且可能希望使用 RT。


首先使用 Ed 的解决方法一次读取一个字符的输入。当
找到


a 时,添加 a
作为分隔符。
在 中,当
是行的一部分时修复 RT 。
=;
awk=

打印完之后我会把RT打印出来$0

cat example.txt - | 
while IFS= read -r -d '' -N1 char; do
  if [[ "$char" == '=' ]]; then
    printf "=;"
  else
    printf '%s' "$char"
  fi
done  | awk '
  BEGIN {
    RS = ";"
  }
  /=/ {
        RT="=";
        sub(/=/,"", $0) 
  }
  {
    if ($0 != "") {
        print $0 "(RT=" RT ")";
        fflush();
    }
  }
'

结果:

AAAA(RT==)
AAAA(RT=;)
AAAA(RT=;)
AAAA(RT==)