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


9

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

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命令一次读取所有输入而又没有此问题的命令?


我编辑了您的问题,使其包含一个实际的问题。如果愿意,您可以等待其他答案,但最终还是将最佳答案标记为已接受(请参见答案左侧,上下箭头按钮下方的管道按钮)。
John1024

@ John1024谢谢,很好的例子。找到这种事情往往会提醒我“一切都错了”,但我很高兴我们当中有些人不放弃。:}
dicktyr

2
还有第三种选择!使用GNU的sed -z选项。如果您的文件没有null,它将一直读取到文件结尾!从这里找到:stackoverflow.com/a/30049447/582917
CMCDragonkai '16

Answers:


13

有多种原因导致将整个文件读入模式空间会出错。围绕最后一行的问题中的逻辑问题是一个常见问题。它与sed的行周期有关-当没有更多行并且sed遇到EOF时-它退出处理。因此,如果您在最后一行,并且指示sed要再买另一条,它将就此停下来,并且不再执行其他操作。

就是说,如果您确实需要将整个文件读入模式空间,那么无论如何都值得考虑使用另一种工具。事实是,sed同义地,编辑器-它被设计为一次可以处理一行-或逻辑数据块。

有许多类似的工具可以更好地处理完整文件块。ed并且ex,例如,它们可以执行许多sed操作,并且具有相似的语法(此外还有许多其他功能),但是它们不仅在将输入流转换为输出时仅对输入流进行sed操作,还可以在文件系统中维护临时备份文件。他们的工作根据需要缓冲到磁盘,并且它们不会在文件末尾突然退出(并且在缓冲区紧张的情况下,发生内爆的可能性要小得多)。而且,它们提供了许多有用的功能,这些功能sed在流上下文中根本就没有意义,例如行标记,撤消,命名缓冲区,联接等。

sed的主要优势在于,它能够在读取数据后立即快速,有效地处理数据流,并且能够对其进行处理。当您对文件进行处理时您会丢掉它,并且往往会遇到诸如上面提到的最后一行问题之类的极端案例难题,以及缓冲区超限和糟糕的性能-随着它解析的数据长度的增加,枚举匹配时正则表达式引擎的处理时间增加呈指数

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

您还应该注意,您描述的行为实际上并不是应该发生的事情。

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

  • N 最后一行的命令

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

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

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

POSIXLY_CORRECT环境变量被提及,因为POSIX指定如果sed遇到当试图EOF的N应该无输出退出,但GNU版本故意在这种情况下,标准的断裂。还要注意,即使行为在上述假设之上是合理的,错误情况也是流编辑之一,而不是将整个文件都提取到内存中。

标准N因此定义了行为:

  • N

    • \n使用嵌入的\newline将附加的材料与原始材料分开,将输入的下一行(减去其终止ewline)附加到图案空间。请注意,当前行号会更改。

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

关于这一点,问题中还展示了其他一些GNU主义-尤其是:label,branch和{function-context括号的使用}。根据经验,任何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分隔符的位置。因此,在GNU info文本建议采用传统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命令一样-取决于行周期来刷新其返回寄存器,并且在这里允许行周期完成大部分工作。当您处理文件时,这是您要做出的另一个权衡-行周期不会再刷新,因此许多测试将表现异常。

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

如果/foo/在保持空间中两个和第三个换行符都不能同时匹配,那么//!g如果/foo/不匹配,则将覆盖缓冲区;如果匹配,则如果\newline不匹配,则将覆盖缓冲区(从而替换/foo/为本身)。这个小巧的测试可以防止缓冲区长时间不必要地充满,/foo/并确保过程保持快速,因为输入不会堆积。在出现“否” /foo/或“ //s/\n/&/3p失败”情况下,将再次交换缓冲区,并删除除最后一行以外的所有行。

最后(最后一行$!d)简单演示了如何sed制作自上而下的脚本以轻松处理多种情况。当您的常规方法是从最普通的情况开始修剪有害的情况并针对最具体的情况进行处理时,则可以更轻松地处理边缘情况,因为只允许它们与其他所需数据一起进入脚本的末尾,以及何时这一切只剩下您想要的数据。但是,必须从一个封闭的循环中取出这种边缘情况要困难得多。

因此,这是我要说的最后一件事:如果您必须真正提取整个文件,则可以依靠行周期为您完成一些工作。通常,您将使用Next和next进行超前 -因为它们在行周期之前提前。与其在一个循环中冗余地实现一个闭环,不如说,因为sed循环周期只是一个简单的读取循环,如果您的目的只是不加选择地收集输入,那么这样做可能会更容易:

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

...这将收集整个文件或进行尝试。


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

尽管我没有可供测试的工具,但请注意,如果所编辑的文件是下一次通读的脚本文件,则N读取和就地编辑的行为会有所不同。


1
把无条件H放在首位是很可爱的。
jthill 2015年

@mikeserv感谢您的输入。我可以看到保持生产线周期的潜在好处,但是如何减少工作量呢?
dicktyr

@dicktyr很好,语法采用了:a;$!{N;ba}我上面提到的一些捷径-从长远来看,当您尝试在不熟悉的系统上运行正则表达式时,使用标准格式会更容易。但这并不是我真正的意思:实现一个闭环-您无法尽可能轻松地进入闭环之中,而是分支出去-修剪不需要的数据-让循环发生。这就像自上而下的事情-所做的一切sed都是其刚完成的工作的直接结果。也许您对它的看法有所不同-但是,如果尝试使用它,可能会发现脚本更容易。
mikeserv

11

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

ñ

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

只需将模式后面的Nand b命令分组,就可以很容易地将其固定为与单行输入一起使用(并且在任何情况下都更加清晰):

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

其工作方式如下:

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

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

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

我在这里发布这个信息是因为我在其他地方找不到信息,我想公开提供这些信息,这样其他人可以避免广泛传播的麻烦:a;N;$!ba;
dicktyr

感谢您的发布!请记住,接受您自己的答案也是可以的。您只需要稍等片刻,系统便会执行此操作。
terdon
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.