是否可以使用“sed”或其他方式将正则表达式转换应用于可能不包含换行符的输入流?

是否可以使用“sed”或其他方式将正则表达式转换应用于可能不包含换行符的输入流?

我有一个包含 sed 表达式的 shell 管道:

source | sed -e 's:xxxxx:yyyyy:g' | sink

它有效,但它有一个潜在的缺陷,因为sed它适用于整行。这意味着在发送换行符sink之前将看不到任何内容。source如果源从不发送换行符,就会出现问题。

外壳是bash,但我希望这不相关。字符串xxxxxyyyyy可以是任何正则表达式,并且可以使用捕获组将一些内容复制xy.

是否可以使用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]+>首先翻译所有<\newlines ,反之亦然,然后在sedH旧空间中按行堆叠输入,直到^[0-9]\{1,\}>已匹配。

但是在写入管道时会执行块缓冲区输出 - 4kb 块或更多trpaste

还有两个版本可以处理这个问题:

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- 来解锁和分配ddfd。在上面的情况下,由内核进行定界。 pty 线路规则配置"stty eolchar - 不会从输出中删除eofchar 确实如此,但确实将 pty 缓冲区推送到dd每次出现并满足其read()。对于每一个ddread()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'
}

这是另一个完全不同的版本,它取消缓冲trpaste

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 mintimedd的 pty的值。如果你设置min > 0 time > 0在非规范终端设备上,该终端将阻止读取,直到它至少接收到min字符,然后继续阻塞直到time十分之一秒过去了。这样的话,如果你可以依赖每一个write()终端有这么多字节并需要这么多时间完成 - 我个人认为 4k 和 0.2 秒对于日志写入来说是相当公平的假设 - 然后你可以读取输入并刷新输出同步地

所以sol1_5立即打印了所有 4 行。


sed 脚本


它实际上是一种非常简单的方法,并且可以相当普遍地进行调整,以处理多个字符分隔符sed(默认情况下仅在单个字符上分隔记录)换行符。

  1. 将分隔符模式中出现的所有第一个字符转换为换行符,并将任何出现的换行符转换为该字符。

    • 下面提到的复杂性的一部分:确保您的流末尾有换行符。

    • tr '\n<' '<\n' | paste -sd\\n - -

  2. 扫描新的换行符分隔输入以查找分隔符模式的其余部分 - 但仅限于出现在行首的情况。

    • 除了简单之外,这也非常高效。您只需检查任何输入行的前几个字符。sed几乎不需要工作。

    • /^[0-9]\{1,\}>/

  3. H将任何不匹配的行的副本附加到旧空间!d删除它,但对于那些不匹配的行,请x更改编辑和保留缓冲区,以便您当前的模式空间是最后一个完全分隔的记录的所有内容,而保留空间仅包含第一个下一个分隔序列的一部分。

    • 最复杂的一点是您需要注意第一条和最后一条输入线。这里的复杂性源于sed的基本效率 - 您可以真正为每个缓冲区处理一条记录。

    • 您不想\n在第一行无缘无故地插入额外的行,因此在这种情况下您应该覆盖h旧空间而不是附加到旧空间。H

    • 你们都应该! 不是 Hold 或d删除$最后一行,因为它是空的,但您的保留缓冲区不是。没有更多的输入需要扫描,但您仍然需要处理上次保存的记录。

    • /.../!{$!H;1h;$!d;};x

  4. 您可以使用自己的音译函数来更有效地一次性将所有保存的插入ewline 字符替换为分隔符的第一个字符,而不是应用昂贵的s///ubstitution regexp 来恢复现在完全分隔的上下文。sedy///\n

    • y/\n</<\n/
  5. <最后,您只需要在模式空间的头部插入一个新的- 因为\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 模式空间 在小脚本末尾打印出来之前所代表的内容 - \newlines 等等。

再次使用我之前示例的修改版本......

字符串>数据

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

相关内容