![是否可以使用“sed”或其他方式将正则表达式转换应用于可能不包含换行符的输入流?](https://linux22.com/image/69807/%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8%E2%80%9Csed%E2%80%9D%E6%88%96%E5%85%B6%E4%BB%96%E6%96%B9%E5%BC%8F%E5%B0%86%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E8%BD%AC%E6%8D%A2%E5%BA%94%E7%94%A8%E4%BA%8E%E5%8F%AF%E8%83%BD%E4%B8%8D%E5%8C%85%E5%90%AB%E6%8D%A2%E8%A1%8C%E7%AC%A6%E7%9A%84%E8%BE%93%E5%85%A5%E6%B5%81%EF%BC%9F.png)
我有一个包含 sed 表达式的 shell 管道:
source | sed -e 's:xxxxx:yyyyy:g' | sink
它有效,但它有一个潜在的缺陷,因为sed
它适用于整行。这意味着在发送换行符sink
之前将看不到任何内容。source
如果源从不发送换行符,就会出现问题。
外壳是bash
,但我希望这不相关。字符串xxxxx
和yyyyy
可以是任何正则表达式,并且可以使用捕获组将一些内容复制x
到y
.
是否可以使用sed
或以其他方式将正则表达式转换应用于可能不包含换行符的输入流?
我已经通过用 Ruby 编写一个过滤器解决了这个问题,但我想知道是否可以使用现有的工具来代替编写代码。
答案1
其实想一想,首先可以影响全部可能:
source |
tr '\n<' '<\n' |
paste -sd\\n - -|
sed -e'/^[0-9]\{1,\}>/!{$!H;1h;$!d'\
-e\} -e'x;y/\n</<\n/;s//<&/' \
-ew\ /dev/fd/1 |
filter ... | sink
这将首先中断您的插播内容联合国有条件地交换所有<
for\n
和 之后有条件的适当地交换回来。这是必要的,因为您提到的分隔符是不是单个字符(作为换行符)因此,简单的翻译不足以保证您的编辑上下文,除非您第一的界定您的流。换句话说,您提到的编辑可能是必需的 - 例如捕获组和其他上下文相关的匹配,这些匹配被理解为适用于记录- 在您验证终点之前无法可靠地完成。
无缓冲的
sed
仅缓冲正则表达式匹配输入中的第一次出现<[0-9]+>
首先翻译所有<
到\n
ewlines ,反之亦然,然后在sed
的H
旧空间中按行堆叠输入,直到^[0-9]\{1,\}>
已匹配。
但是在写入管道时会执行块缓冲区输出 - 4kb 块或更多tr
。paste
还有两个版本可以处理这个问题:
sol1(){
{ cat; printf '\n\4\n'; } |
{ dd obs=1 cbs=512 conv=sync,unblock \
<"$(pts;stty eol \";cat <&3 >&4&)"
} 3<&0 <&- <>/dev/ptmx 2>/dev/null 4>&0 |
sed -ne'/<[0-9]\{1,\}>/!{H;$!d' -e\} \
-e'x;s/\n//g;w /dev/fd/1'
}
这会将所有输入推送到 pty 并设置dd
从中读取。它使用您其他问题中的那个小 C 程序pts
- 来解锁和分配dd
fd。在上面的情况下,由内核进行定界。 pty 线路规则配置"
为stty
eol
char - 不会从输出中删除eof
char 确实如此,但确实将 pty 缓冲区推送到dd
每次出现并满足其read()
。对于每一个dd
的read()
s它首先用空格将其输出缓冲区的尾部填充到 512 个字符,然后将任何/所有尾随空格压缩为单个换行符。
这是相同的修改版本,可以解决最后一行被阻止的问题:
sol1_5(){
{ cat; printf '\n\4\n'; } |
{ dd ibs=16k obs=2 cbs=4k conv=sync,unblock <"$(pts
stty raw isig quit \^- susp \^- min 1 time 2
cat <&3 >&4&)"
} 3<&0 <&- <>/dev/ptmx 2>/dev/null 4>&0 |
sed -ne's/<[0-9]\{1,\}>/\n&/g;/./w /dev/fd/1'
}
这是另一个完全不同的版本,它取消缓冲tr
和paste
:
sol2(){
stdbuf -o0 tr '\n<' '<\n' |
stdbuf -o0 paste -sd\\n - -|
sed -ue'/^[0-9]\{1,\}>/!{$!H;1h;$!d'\
-e\} -e'x;y/\n</<\n/;s//<&/'
}
我用您的示例数据测试了这两种方法:
for sol in 1 2
do printf '<37> Jul 28 10:40:47 127.0.0.1 time="2015-07-28 10:40:47" msg="LOGOUT User admin logged out on TELNET (10.0.200.1)"<37> Jul 28 10:45:58 127.0.0.1 time="2015-07-28 10:45:58" msg="LOGIN User admin logged in on TELNET (10.0.200.1)"<37> Jul 28 10:40:47 127.0.0.1 time="2015-07-28 10:40:47" msg="LOGOUT User admin logged out on TELNET (10.0.200.1)"<37> Jul 28 10:45:58 127.0.0.1 time="2015-07-28 10:45:58" msg="LOGIN User admin logged in on TELNET (10.0.200.1)"' |
cat - /dev/tty | "sol$sol" | cat
在这两种情况下,前三行都会立即打印,但第四行会保留在缓冲区中 - 因为sed
在找到下一行的开头之前不会打印缓冲区,因此它会在输入后面保留一行,直到 EOF。紧迫CTRL+D
打印出来了。
<37> Jul 28 10:40:47 127.0.0.1 time="2015-07-28 10:40:47" msg="LOGOUT User admin logged out on TELNET (10.0.200.1)"
<37> Jul 28 10:45:58 127.0.0.1 time="2015-07-28 10:45:58" msg="LOGIN User admin logged in on TELNET (10.0.200.1)"
<37> Jul 28 10:40:47 127.0.0.1 time="2015-07-28 10:40:47" msg="LOGOUT User admin logged out on TELNET (10.0.200.1)"
<37> Jul 28 10:45:58 127.0.0.1 time="2015-07-28 10:45:58" msg="LOGIN User admin logged in on TELNET (10.0.200.1)"
但sol1_5
完全使用另一种方法 - 它不依赖于字符上下文来分隔输入,而是相信每个write()
4k 或更少的字节必须代表至少 1 个完整上下文,因此它会在每个字节中添加它认为合适的换行符并立即刷新输出。
它的工作原理是设置stty
min
和time
dd
的 pty的值。如果你设置min > 0
和 time > 0
在非规范终端设备上,该终端将阻止读取,直到它至少接收到min
字符,然后继续阻塞直到time
十分之一秒过去了。这样的话,如果你可以依赖每一个write()
终端有这么多字节并需要这么多时间完成 - 我个人认为 4k 和 0.2 秒对于日志写入来说是相当公平的假设 - 然后你可以读取输入并刷新输出同步地。
所以sol1_5
立即打印了所有 4 行。
sed 脚本
它实际上是一种非常简单的方法,并且可以相当普遍地进行调整,以处理多个字符分隔符sed
(默认情况下仅在单个字符上分隔记录)换行符。
将分隔符模式中出现的所有第一个字符转换为换行符,并将任何出现的换行符转换为该字符。
下面提到的复杂性的一部分:确保您的流末尾有换行符。
tr '\n<' '<\n' | paste -sd\\n - -
扫描新的换行符分隔输入以查找分隔符模式的其余部分 - 但仅限于出现在行首的情况。
除了简单之外,这也非常高效。您只需检查任何输入行的前几个字符。
sed
几乎不需要工作。/^[0-9]\{1,\}>/
H
将任何不匹配的行的副本附加到旧空间!
并d
删除它,但对于那些不匹配的行,请x
更改编辑和保留缓冲区,以便您当前的模式空间是最后一个完全分隔的记录的所有内容,而保留空间仅包含第一个下一个分隔序列的一部分。最复杂的一点是您需要注意第一条和最后一条输入线。这里的复杂性源于
sed
的基本效率 - 您可以真正为每个缓冲区处理一条记录。您不想
\n
在第一行无缘无故地插入额外的行,因此在这种情况下您应该覆盖h
旧空间而不是附加到旧空间。H
你们都应该
!
不是H
old 或d
删除$
最后一行,因为它是空的,但您的保留缓冲区不是。没有更多的输入需要扫描,但您仍然需要处理上次保存的记录。/.../!{$!H;1h;$!d;};x
您可以使用自己的音译函数来更有效地一次性将所有保存的插入ewline 字符替换为分隔符的第一个字符,而不是应用昂贵的
s///
ubstitution regexp 来恢复现在完全分隔的上下文。sed
y///
\n
y/\n</<\n/
<
最后,您只需要在模式空间的头部插入一个新的- 因为\n
您需要插入的 ewline 已经在打印时在最后一个缓冲周期的末尾添加了。最有效的方法就是重复使用
//
您一直在测试输入行的相同正则表达式。这样sed
就可以摆脱只需要regcomp()
编译单个正则表达式并重复regexec()
重复执行相同的自动机以可靠地界定整个流内。s//<&/
您现在应该能够将该输出流作为常规的\n
行分隔文本文件进行处理。
测试
printf '%s\n' the string \
"<9>more $(printf %050s|tr ' ' '<') string" \
and \<9\> "<more<string and <9> more<string" |
tr '<\n' '\n<' |
paste -sd\\n - - |
sed -e'/^[0-9]\{1,\}>/!{$!H;1h;$!d' \
-e\} -e'x;y/\n</<\n/;s//<&/'
the
string
<9>more <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< string
and
<9>
<more<string and
<9> more<string
现在,如果您的目标是将编辑应用到可能描述如下的字符串^<[0-9]+>(^(<[0-9]+>))*
那么此时您可能甚至不需要第二个过滤器 - 因为这正是sed
's 模式空间 在小脚本末尾打印出来之前所代表的内容 - \n
ewlines 等等。
再次使用我之前示例的修改版本......
字符串>数据
printf '%s\n' the string \
"<1>more $(printf %050s|tr ' ' '<') string" \
and \<2\> "<more<string and <3> more<string" |
tr '<\n' '\n<' |
paste -sd\\n - - |
sed -e'/^[0-9]\{1,\}>/!{$!H;1h;$!d' \
-e\} -e'x;y/\n</<\n/;s//<&/' \
-e'/^<[2-9][0-9]*>/s/string/data/g'
the
string
<1>more <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< string
and
<2>
<more<data and
<3> more<data
答案2
当程序写入终端时,缓冲区会在每个换行符上刷新,但您可以使用该程序解缓冲 (注意在某些发行版中该命令是 stdbuf )
尝试这样的事情
unbuffer source | sed -e 's:xxxxx:yyyyy:g' | sink
答案3
至少 GNU sed 可以处理末尾没有换行符的输入(如果最后一个不完整的行被传递,它会生成没有最终换行符的输出)。 Unix 下的文本文件根据定义必须以换行符结尾(如果它不为空),而 sed 是一个文本实用程序,因此不能保证对非文本输入的这种宽大处理。
由于 sed 旨在转换行,因此我希望大多数实现在应用任何转换之前将整个输入行读入内存,特别是在生成与该输入行相对应的任何输出之前。
如果您想使用 sed 方便地处理该输入,请选择一个与模式不匹配xxxxx
或由yyyyy
替换文本生成的字符,但在输入中经常出现。在调用 sed 之后将其转换为换行符,反之亦然。
source | tr ':\n' '\n:' | sed -e 's:foo:bar:g' | tr ':\n' '\n:' | sink
如果没有好的字符可供选择,sed 可能不会为您提供帮助,而 ruby 是一个合理的选择。
答案4
我用 Ruby 实现了一个小脚本来解决这个问题。它的使用方式如下:
source | myscript.rb | sink
这是来源
$stdout.sync # no outbound buffering by Ruby
buf=''
$stdin.each_char do |c|
if buf.length>0 || c=='<' # buffering starts when '<' received
buf << c # and continues until flushed
buf.gsub!(/(<\d+>)/,"\n\\1") if (c == '>') # regex transform matching buffer
unless (buf =~ /<\d*$/) # flush buffer when regex fails
STDOUT << buf
buf.replace '' # empty buffer stops buffering
end
else
STDOUT << c; # unbuffered output
end
$stdout.flush # no buffering, please!
end
红宝石专家可能会对此进行改进,但这是一个快速的“肮脏的黑客”,为我解决了问题。
基本上,它一次读取 stdin 一个字符,并检查它是否有第一个匹配字符,即<
.如果没有找到,它会立即将其写入标准输出。如果匹配,它会写入缓冲区,然后继续执行此操作,除非缓冲区内容无法通过正则表达式匹配有效分隔符(<
后跟零个或多个数字),在这种情况下,它会刷新缓冲区并停止缓冲。如果在缓冲时它得到 a,>
则它通过正则表达式执行转换。
更新
上面的脚本可以工作,但下游进程如果等待换行符,可能会缓冲其输入。这意味着最后一行输入可能会滞留在下游管道中。下面的版本使用非阻塞读取,并在输入阻塞时插入换行符:
STDOUT.sync # no outbound buffering by Ruby
buf=''
def read_from_stdin()
last=''
while true
begin
c = STDIN.read_nonblock(1) # read 1 character; don't block
rescue Errno::EWOULDBLOCK # exception if nothing to read
yield "\n" unless last=="\n" # send a newline if prior character wasn't
IO.select([STDIN]) # block (wait for input)
retry # go back to 'begin' again
end
yield last=c # remember and send read character
end
end
read_from_stdin do |c|
if buf.length>0 || c=='<' # buffering starts when '<' received
buf << c # and continues until flushed
buf.gsub!(/(<\d+>)/,"\n\\1") if (c == '>') # regex transform matching buffer
unless (buf =~ /<\d*$/) # flush buffer when regex fails
STDOUT << buf
buf.replace '' # empty buffer stops buffering
end
else
STDOUT << c; # unbuffered output
end
STDOUT.flush # no buffering, please!
end