sed:将整个文件读入模式空间,而不会在单行输入时失败

sed:将整个文件读入模式空间,而不会在单行输入时失败

将整个文件读入模式空间对于替换换行符等很有用。并且有很多实例建议以下内容:

sed ':a;N;$!ba; [commands...]'

但是,如果输入仅包含一行,则会失败。

例如,对于两行输入,每一行都受到替换命令的影响:

$ echo $'abc\ncat' | sed ':a;N;$!ba; s/a/xxx/g'
xxxbc
cxxxt

但是,使用单行输入时,执行替换:

$ echo 'abc' | sed ':a;N;$!ba; s/a/xxx/g'
abc

如何编写一个sed命令来一次读取所有输入而不出现此问题?

答案1

将整个文件读入模式空间可能会出错的原因有很多。围绕最后一行的问题中的逻辑问题是常见的。它与 的行周期有关sed- 当没有更多行并且sed遇到 EOF 时,它就结束了 - 它退出处理。因此,如果您在最后一行并指示sed获取另一行,它就会停在那里,不再执行任何操作。

也就是说,如果您确实需要将整个文件读入模式空间,那么无论如何可能值得考虑另一个工具。事实是,sed同名的是溪流编辑器 - 它被设计为一次工作一行或一个逻辑数据块。

有许多类似的工具可以更好地处理完整的文件块。例如,edand可以用类似的语法做 and 能做的大部分事情 - 除此之外还有很多其他事情 - 但它们不是只对输入流进行操作,同时将其转换为输出,它们还在文件系统中维护临时备份文件。他们的工作根据需要缓冲到磁盘,并且不会在文件末尾突然退出exsedsed(并且在缓冲压力下内爆的频率要低得多)。此外,它们还提供了许多有用的函数,sed这些函数在流上下文中根本没有意义,例如行标记、撤消、命名缓冲区、连接等。

sed的主要优势在于它能够在读取数据后立即处理数据 - 快速、高效、连续。当你吞食一个文件时,你就会把它扔掉您往往会遇到边缘情况困难,例如您提到的最后一行问题,以及缓冲区溢出和糟糕的性能 - 随着它解析的数据长度的增长,正则表达式引擎在枚举匹配时的处理时间会增加指数地

关于最后一点,顺便说一句:虽然我知道示例案例s/a/A/g很可能只是一个幼稚的示例,并且可能不是您想要在输入中收集的实际脚本,但您可能会发现值得花时间熟悉一下y///。如果您经常发现自己g在全局上用一个角色替换另一个角色,那么这y对您来说可能非常有用。它是一种转换,而不是替换,并且速度要快得多,因为它不意味着正则表达式。后一点在尝试保留和重复空地址时也很有用,//因为它不会影响它们,但可能会受到它们的影响。无论如何,y/a/A/这是完成相同任务的更简单的方法 - 并且交换也是可能的,例如:y/aA/Aa/它将交换一行上的所有大写/小写字母。

您还应该注意,您所描述的行为无论如何都不是应该发生的。

info sed来自GNU的常见的错误报告部分:

  • N命令在最后一行

    • 当在文件的最后一行发出命令时,大多数版本的sed退出都不打印任何内容。 NGNUsed在退出之前打印模式空间,当然除非-n已指定命令开关。这种选择是设计使然。

    • 例如, 的行为sed N foo bar取决于 foo 的行数是偶数还是奇数。或者,当编写脚本来读取模式匹配后的接下来几行时, 的传统实现sed将迫使您编写类似的内容,/foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N }而不仅仅是/foo/{ N;N;N;N;N;N;N;N;N; }.

    • 无论如何,最简单的解决方法是$d;N在依赖传统行为的脚本中使用,或者将POSIXLY_CORRECT变量设置为非空值。

提到环境POSIXLY_CORRECT变量是因为 POSIX 指定如果sed在尝试时遇到 EOF 则N应该退出而不输出,但 GNU 版本在这种情况下故意违反标准。另请注意,即使上述行为是合理的,假设错误情况是流编辑之一 - 不会将整个文件放入内存中。

标准定义N的行为:

  • N

    • 将下一行输入(减去其终止\newline)附加到模式空间,使用嵌入的\newline 将附加的材料与原始材料分开。请注意,当前行号发生变化。

    • 如果没有下一行输入可用,则N命令动词应分支到脚本末尾并退出,而不开始新的循环或将模式空间复制到标准输出。

:在这一点上,问题中还展示了一些其他 GNU 主义 - 特别是标签、b牧场和{函数上下文括号的使用}。根据经验,任何sed接受任意参数的命令都被理解为\n在脚本中的下一行处进行分隔。所以命令...

:arbitrary_label_name; ...
b to_arbitrary_label_name; ...
//{ do arbitrary list of commands } ...

...都很有可能执行不稳定,具体取决于sed读取它们的实现。可移植的是,它们应该写成:

...;:arbitrary_label_name
...;b to_arbitrary_label_name
//{ do arbitrary list of commands
}

这同样适用于rwtai、 和c (可能还有一些我现在忘记的)。几乎在所有情况下,它们也可以写成:

sed -e :arbitrary_label_name -e b\ to_arbitary_label_name -e \
    "//{ do arbitrary list of commands" -e \}

...其中新的-execution 语句代表\newline 分隔符。因此,GNUinfo文本建议传统的sed实施会迫使你做:

/foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N }

……其实应该是……

/foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N
}

……当然,这也不是真的。以这种方式编写脚本有点愚蠢。还有更简单的方法可以实现相同的目的,例如:

printf %s\\n foo . . . . . . |
sed -ne 'H;/foo/h;x;//s/\n/&/3p;tnd
         //!g;x;$!d;:nd' -e 'l;$a\' \
     -e 'this is the last line' 

...打印:

foo
.
.
.
foo\n.\n.\n.$
.$
this is the last line

...因为test 命令 - 与大多数sed命令一样 - 取决于行周期来刷新其返回寄存器,并且这里允许行周期完成大部分工作。这是您在读取文件时所做的另一个权衡 - 行周期不会再次刷新,因此许多测试将表现异常。

上面的命令不会有过度输入的风险,因为它只是做了一些简单的测试来验证它在读取时读取的内容。对于Hold,所有行都会附加到保留空间,但如果一行匹配,/foo/它将覆盖h旧空间。接下来更改缓冲区,如果缓冲区的内容与最后寻址的模式匹配,x则尝试条件替换。换句话说,尝试将保留空间中的第三个换行符替换为自身并打印结果s///////s/\n/&/3p如果保留空间当前匹配/foo/。如果t成功,脚本将分支到not delete 标签 - 该标签执行look 操作并结束脚本。

但是,如果两个/foo/换行符和第三个换行符在保留空间中无法匹配在一起,则//!g如果不匹配,则将覆盖缓冲区/foo/,或者,如果匹配,则如果换行符\n不匹配,则将覆盖缓冲区(从而替换/foo/为自身)。这个微妙的小测试可以防止缓冲区在长时间的“否”中不必要地被填满,/foo/并确保过程保持快速,因为输入不会堆积。在没有/foo/或失败的情况下,//s/\n/&/3p缓冲区再次交换,并且除最后一行之外的每一行都被删除。

最后 - 最后一行- 简单演示了如何制作$!d自上而下的脚本来轻松处理多种情况。sed当您的一般方法是从最一般的情况开始并朝着最具体的方向修剪不需要的情况时,则可以更轻松地处理边缘情况,因为它们可以简单地与您需要的其他数据一起落到脚本的末尾,并且当这一切都将您包裹起来,只剩下您想要的数据。然而,必须从闭环中获取此类边缘情况可能要困难得多。

因此,这是我要说的最后一件事:如果您确实必须提取整个文件,那么您可以通过依靠行周期来为您完成这项工作,从而减少一些工作。通常您会使用Next 和next展望- 因为他们前进的行周期。而不是在循环中冗余地实现闭环 - 因为sed行循环只是一个简单的读取循环 - 如果您的目的只是不加区别地收集输入,那么它可能更容易做到:

sed 'H;1h;$!d;x;...'

...这将收集整个文件或尝试失败。


N关于最后一行行为的旁注......

虽然我没有可供测试的工具,但N在阅读和阅读时请考虑到位如果编辑的文件是下次通读的脚本文件,则编辑行为会有所不同。

答案2

它会失败,因为该N命令出现在模式匹配之前$!(不是最后一行)并且 sed 在执行任何工作之前退出:

将换行符添加到模式空间,然后将下一行输入附加到模式空间。如果没有更多输入,则 sed 退出而不处理任何更多命令

N通过简单地将和b命令分组在模式后面,可以轻松修复此问题以处理单行输入(并且实际上在任何情况下都更加清晰) :

sed ':a;$!{N;ba}; [commands...]'

其工作原理如下:

  1. :a创建一个名为“a”的标签
  2. $!如果不是最后一行,那么
  3. N将下一行追加到模式空间(如果没有下一行则退出)并ba分支(转到)标签“a”

不幸的是,它不可移植(因为它依赖于 GNU 扩展),但以下替代方案(由 @mikeserv 建议)是可移植的:

sed 'H;1h;$!d;x; [commands...]'

答案3

正如@mikeserv 详尽解释的那样,N 不适合这个。

该片段将累积整个文件,您可以将其用作脚本其余部分的前缀:

H;$!d;x;s/^\n//

它使用 H 累积文件直到读取最后一行

使用 *tested with GNU sed 的示例),注意缺少尾随 \n:

$ printf 'a\nb\nc' | sed -e 'H;$!d;x;s/^\n//' -e 's/^/[/;s/$/]/'
[a
b
c]$ echo $?
0

相关内容