有多种原因导致将整个文件读入模式空间会出错。围绕最后一行的问题中的逻辑问题是一个常见问题。它与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
时,大多数退出版本都不会打印任何内容N
。sed
除非-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
因此定义了行为:
关于这一点,问题中还展示了其他一些GNU主义-尤其是:
label,b
ranch和{
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
}
这同样适用于真正的r
,w
,t
,a
,i
,和c
(可能还有几个是我此刻遗忘)。在几乎每种情况下,它们都可能被编写为:
sed -e :arbitrary_label_name -e b\ to_arbitary_label_name -e \
"//{ do arbitrary list of commands" -e \}
...新的-e
xecution语句代表\n
ewline分隔符的位置。因此,在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
...因为t
est命令-像大多数sed
命令一样-取决于行周期来刷新其返回寄存器,并且在这里允许行周期完成大部分工作。当您处理文件时,这是您要做出的另一个权衡-行周期不会再刷新,因此许多测试将表现异常。
上面的命令不会冒输入过多的风险,因为它只是做一些简单的测试来验证它在读取时所读取的内容。对于H
旧的行,所有行都将追加到保留空间,但是如果一行匹配,/foo/
它将覆盖h
旧的空间。接下来x
更改缓冲区,s///
如果缓冲区的内容与所//
寻址的最后一个模式匹配,则尝试条件缓冲。换句话说//s/\n/&/3p
尝试更换与自己保持空间的第三个新行并打印结果,如果保持目前的空间相匹配/foo/
。如果这t
成功了,脚本将分支到n
ot d
elete标签-该标签将执行l
ook并包装脚本。
如果/foo/
在保持空间中两个和第三个换行符都不能同时匹配,那么//!g
如果/foo/
不匹配,则将覆盖缓冲区;如果匹配,则如果\n
ewline不匹配,则将覆盖缓冲区(从而替换/foo/
为本身)。这个小巧的测试可以防止缓冲区长时间不必要地充满,/foo/
并确保过程保持快速,因为输入不会堆积。在出现“否” /foo/
或“ //s/\n/&/3p
失败”情况下,将再次交换缓冲区,并删除除最后一行以外的所有行。
最后(最后一行$!d
)简单演示了如何sed
制作自上而下的脚本以轻松处理多种情况。当您的常规方法是从最普通的情况开始修剪有害的情况并针对最具体的情况进行处理时,则可以更轻松地处理边缘情况,因为只允许它们与其他所需数据一起进入脚本的末尾,以及何时这一切只剩下您想要的数据。但是,必须从一个封闭的循环中取出这种边缘情况要困难得多。
因此,这是我要说的最后一件事:如果您必须真正提取整个文件,则可以依靠行周期为您完成一些工作。通常,您将使用N
ext和n
ext进行超前 -因为它们在行周期之前提前。与其在一个循环中冗余地实现一个闭环,不如说,因为sed
循环周期只是一个简单的读取循环,如果您的目的只是不加选择地收集输入,那么这样做可能会更容易:
sed 'H;1h;$!d;x;...'
...这将收集整个文件或进行尝试。
关于N
最后一行行为的注释...
尽管我没有可供测试的工具,但请注意,如果所编辑的文件是下一次通读的脚本文件,则N
读取和就地编辑的行为会有所不同。