我正在使用 netcat 传输数据并将输出传输到 gawk。以下是 gawk 将接收的示例字节序列:
=AAAA;=BBBB;;CCCC==DDDD;
数据几乎包含任意字符,但绝不会包含 NULL 字符,其中=
和;
保留为分隔符。在写入任意字符块时,每个块将始终以其中一个分隔符作为前缀,并始终以其中一个分隔符作为后缀,但可以随时使用任一分隔符:=
并不总是前缀,也不;
总是后缀。它永远不会在没有写入适当的前缀和后缀的情况下写入块。在解析数据时,我需要区分使用了哪个分隔符,以便我的下游代码可以正确解释该信息。
由于这是一个网络流,因此在读取此序列后,stdin 保持打开状态,因为它在等待未来的数据。我希望 gawk 读取直到遇到分隔符,然后使用找到的任何数据执行我的 gawk 脚本的主体,同时确保它正确处理连续的 stdin 流。我将在下面更详细地解释这一点。
迄今
这是我迄今为止尝试过的方法(zsh 脚本,使用 gawk,在 macOS 上)。对于这篇文章,我将主体简化为仅打印数据 - 我的完整 gawk 脚本的主体要复杂得多。我还将 netcat 流简化为cat
一个文件(以及cat
'ing stdin 以模仿流行为)。
cat example.txt - | gawk '
BEGIN {
RS = "=|;";
}
{
if ($0 != "") {
print $0;
fflush();
}
}
'
example.txt
=AAAA;=BBBB;=CCCC;=DDDD;
我的尝试成功处理了大部分数据......直到最新的记录。它挂起等待来自 stdin 的更多数据,并且无法执行最新记录的脚本主体,尽管 stdin 中显然有适当的分隔符。
当前输出:(无法处理最新的记录DDDD
)
AAAA
BBBB
CCCC
[hang here, waiting for future data]
期望输出:(成功处理所有记录,包括最近的记录)
AAAA
BBBB
CCCC
DDDD
[hang here, waiting for future data]
这个问题究竟可能是什么原因造成的?我该如何解决?我意识到这似乎是一种极端情况。非常感谢大家的帮助!
编辑:评论整合、其他澄清以及各种观察/认识
以下是我在调试过程中发现的一些杂项观察,包括我最初发布此帖子之前和之后。这些编辑还澄清了评论中出现的一些问题,并将分散在各个评论中的信息整合到一个地方。还包括我根据评论中极具洞察力的信息对 gawk 内部工作原理的一些认识。此编辑中的信息取代了评论中可能讨论的任何潜在冲突信息。
我简单调查了一下这是否是操作系统强加的管道缓冲问题。在使用该
stdbuf
工具禁用所有管道缓冲后,似乎缓冲根本不是问题,至少不是传统意义上的问题(参见第 3 条)。我注意到,如果 stdin 已关闭并且RS 使用正则表达式,则不会出现问题。相反,如果 stdin 保持打开状态并且RS 不是正则表达式(即纯文本字符串),也不会出现问题。只有当 stdin 保持打开状态并且RS 是正则表达式时才会出现问题。因此,我们可以合理地假设它与正则表达式如何处理连续的 stdin 流有关。
我注意到,如果我的带有正则表达式的 RS (
RS = "=|;";
) 有 3 个字符长...并且 stdin 保持打开状态...它会在 stdin 中出现 3 个额外字符后停止挂起。如果我将正则表达式的长度调整为 5 个字符(RS = "(=|;)"
),则从挂起返回所需的额外字符数量也会相应调整。结合与 Kaz 的极具洞察力的讨论,这确定挂起是正则表达式引擎本身的产物。就像 Kaz 所说的那样,当正则表达式引擎解析时RS = "=|;";
,它最终会尝试从 stdin 读取其他字符以确保正则表达式匹配,尽管这种额外的读取对于所讨论的正则表达式而言并非绝对必要,这显然会导致在等待 stdin 时挂起。我还尝试向正则表达式添加惰性量词,这在理论上意味着正则表达式引擎可以立即返回,但遗憾的是它没有,因为这是正则表达式引擎的实现细节。此处和此处的gawk文档指出,当 RS 是单个字符时,它将被视为纯文本字符串,并导致 RS 匹配而不调用正则表达式引擎。相反,如果 RS 有 2 个或更多字符,它将被视为正则表达式,并将调用正则表达式引擎(随后引发第 3 项中讨论的问题)。但是,这似乎有点误导,这是 gawk 的实现细节。我尝试过
RS = "xy";
(并相应地调整了我的数据),并重新测试了第 3 项中的实验。没有发生挂起,并且打印了正确的输出,这一定意味着尽管 RS 是 2 个字符,但它仍然被视为纯文本字符串 - 正则表达式引擎从未被调用,并且挂起问题从未发生。因此,似乎对 RS 是被视为纯文本还是正则表达式进行了进一步的过滤。那么……既然我们已经找出了问题的根本原因……我们该怎么做呢?一个显而易见的想法是避免使用正则表达式引擎……但我的脚本仍然需要使用某种 OR 子句来匹配分隔符……因此,这似乎需要编写一个自定义数据解析器作为 C 程序或其他程序。虽然我可以这样做,而且它肯定会解决问题,但考虑到手头的任务,我宁愿不走这条杂草丛生的道路。
这让我们想到了 Ed Morton 的解决方法,这很可能是最好的方法,或者可能是其一些衍生方法。下面总结了他的方法:
基本上,在将数据提供给主要 gawk 调用之前,使用其他 CLI 工具或 shell 读取循环,甚至多次调用 gawk,提前进行转换。就像 Ed 说的那样,将每个分隔符替换为全部以 NULL 字符为后缀。由于这是在 gawk 看到任何数据之前完成的,因此可以将 gawk 设置为使用 NULL 字符作为 RS,这将被视为纯文本字符串而不是正则表达式,这意味着正则表达式挂起问题永远不会发生。从那里,真正的分隔符和数据块可以以您想要的任何方式进行解码和处理。
gawk 本身甚至能够进行提前转换……只要可以使用纯文本 RS 而不是正则表达式 RS 找到每个相关分隔符。请小心包含正则表达式专用字符的分隔符,因为这可能会导致 gawk 在您不期望的情况下将其视为正则表达式。
虽然我现在已经将 Ed 的答案标记为解决方案,但我认为我最终的解决方案将是 Ed 的方法、Kaz 的洞察力以及我后来对他们的一些认识的结合。希望我能将两个答案标记为解决方案!谢谢大家的帮助,特别是 Ed Morton 和 Kaz!
awk 正在等待记录被分隔。当发生以下两件事时,记录将被分隔:正则表达式匹配
RS
,或者输入结束。您也没有给出它,因为您使用了
cat <file> -
,这意味着在用尽cat
之后, 的输出流将继续使用标准输入(您的 TTY) 。<file>
您必须Ctrl-D在空行上使用来生成 Gawk 正在寻找的必要 EOF 条件。
编辑:
问题是,为什么最后一条记录没有出现,即使它被尾随的 分隔开来
=
?此行为在我用 Lisp 语言编写的 Awk 实现中准确重现,与 GNU Awk 并列。
完全一样的事情:
对于第二个 Awk 实现,由于我从头开始编写了所有内容,包括正则表达式引擎,因此我可以解释其行为,从而形成关于为什么 Gawk 相同的假设。
正则表达式分隔读取基于用 C 编写的函数,该函数
read_until_match
是名为 的辅助程序的包装器scan_until_common
。此函数的工作原理是将字符从流中逐个输入正则表达式状态机,检查状态。事情是这样的。当正则表达式状态机说“我们有一个匹配!”时,我们不能就此止步。原因是我们需要找到最长的匹配。
该函数不知道正则表达式是一个简单的单字符正则表达式,对于该正则表达式来说,第一个匹配已经是最长的匹配。因此,它需要再输入一个字符。此时,正则表达式状态机说“失败!”。然后,该函数知道之前有一个成功的匹配。它回溯到该点,将额外的字符推回到流中。
因此,如果流中没有下一个可用的字符,我们就会得到 I/O 阻塞挂起。
之所以必须这样工作,是因为有些正则表达式能够成功匹配最长匹配的前缀。一个简单的例子是:假设我们有
#+
一个分隔符。当#
看到一个时,那就是匹配!但是当#
看到另一个时,那也是匹配!我们必须看到所有字符#
才能获得完全匹配,这意味着我们必须看到后面第一个不匹配的字符。GNU Awk 无法轻易避免做非常类似的事情;理论要求这样做。
解决这个问题的方法是使用一个函数
maxmatchlen(R)
,它为正则表达式R
报告正则表达式匹配的最大长度(可能是无限的)。maxmatchlen(/.*/)
是Inf
,但是matchmatchlen(/abc/)
是 3。你明白了。有了这个函数,我们就知道,如果我们刚刚输入了正则表达式matchmatchlen
字符,并且正则表达式状态机报告了匹配状态,我们就完成了;我们不必提前查看流。一个解决方法是将 shell 读取循环插入到管道中,将原始 awk 输入(OP 的实际
netcat
输出)分割成单个字符,然后一次一个地将它们提供给 awk:这需要 GNU awk 或其他可以处理
NUL
字符的程序,因为RS
这是非 POSIX 行为。它确实假设您的输入不能包含 NUL 字节,即它是一个有效的 POSIX 文本“文件”。如果感兴趣的话请继续阅读以了解我们是如何到达那里的...
我认为这里至少有 1 个错误,因为我发现了多个奇怪之处(见下文),所以我在https://lists.gnu.org/archive/html/bug-gawk/2024-07/msg00006.html打开了一个 gawk 错误报告,但根据 gawk 提供商 Arnold 的说法,这种情况下的行为差异只是必须提前阅读以确保正则表达式匹配正确的字符串的实现细节。
这里似乎有 3 个问题,例如在 cygwin 上使用 GNU awk 5.3.0:
(;|=)
,;|=
并且[;=]
应该是等价的,但在这种情况下显然它们不是等价的。好消息是,您显然可以使用括号表达式(如上面的第 3 种情况)而不是“或”来解决该问题。
;
:坏消息是,这会影响 OP 的示例:
使用文字字符 RS:
使用正则表达式 RS 也应该使该字符成为文字:
FWIW我尝试设置超时:
并使用 stdbuf 禁用缓冲:
并匹配每个字符(我想我可以用它来
RT ~ /[=;]/
查找分隔符):但是它们都不让我读取最后一个记录分隔符,所以此时我不知道 OP 可以做什么才能使用正则表达式成功读取连续输入的最后一条记录,除了这样的方法:
并使用 OP 示例输入但每个记录使用不同的文本以使输入到输出记录的映射更清晰:
We're using NUL chars as the delimiters and various options above to make the shell read loop robust enough to handle blank lines and other white space in the input, see https://unix.stackexchange.com/a/49585/133219 and https://unix.stackexchange.com/a/169765/133219 for details on those issues. We're additionally using a NUL char for the awk RS so it can distinguish between newlines coming from the original input vs a newline as a terminating character being added by the shell
printf
, otherwiserec
in the awk script could never contain a newline as they'd ALL be consumed by matching the default RS.We're using a pipe to/from the while-read loop instead of process substitution just to ease clarity since the OP is already using pipes.
多行(GNU Awk 用户指南)说
请注意,只有后者才提到了前导和尾随,因此我怀疑问题的根源可能是它在 GNU 中的实现方式
AWK
。如果你不需要辨别
=
,;
我建议遵循以下解决方法对于
example.txt
内容而言给出输出
并挂起。解释:我添加了 GNU
sed
在非缓冲模式下运行 ( ),其中只有-u
一个y
命令在此替换
;
使用=
。然后将命令更改RS
为gawk
单字符串=
。(在 GNU sed 4.8 和 GNU Awk 5.1.0 中测试)
A combination of the solutions of @daweo and @EdMorton:
OP wants to have logic based on discern the two delimiters, and might want to use RT for it.
First use Ed's work-around for reading the input one character a time.
When a
=
is found, add a;
as a delimiter.In
awk
, fix the RT when the=
is part of the line.I will print the RT after printing
$0
.Result: