编译自: https://linuxhandbook.com/sed-reference-guide/
作者: Sylvain Leroux
译者: qhwdw

在前面的文章中,我展示了 Sed 命令的基本用法 , Sed 是一个实用的流编辑器。本日,我们准备去理解关于 Sed 更多的知识,深入理解 Sed 的运行模式。这将是你全面理解 Sed 命令的一个机会,深入挖掘它的运行细节和精妙之处。因此,如果你已经做好了准备,那就打开终端吧, 下载测试文件 然后坐在电脑前:开始我们的探索之旅吧!
要准确理解 Sed 命令,你必须先理解工具的运行模式。
当处理数据时,Sed 从输入源一次读入一行,并将它保存到所谓的 模式空间(pattern space)中。所有 Sed 的变换都发生在模式空间。变换都是由命令行上或外部 Sed 脚本文件供应的单字母命令来描述的。大多数 Sed 命令都可以由一个地址或一个地址范围作为前导来限定它们的浸染范围。
默认情形下,Sed 在结束每个处理循环后输出模式空间中的内容,也便是说,输出发生在输入的下一个行覆盖模式空间之前。我们可以将这种运行模式总结如下:
考试测验将下一个行读入到模式空间中如果读取成功:按脚本中的顺序将所有命令运用到与那个地址匹配确当前输入行上如果 sed 没有以静默模式(-n)运行,那么将输出模式空间中的所有内容(可能会是修正过的)。重新回到 1。因此,在每个行被处理完毕之后,模式空间中的内容将被丢弃,它并不适宜永劫光保存内容。基于这种目的,Sed 有第二个缓冲区: 保持空间(hold space)。除非你显式地哀求它将数据置入到保持空间、或从保持空间中取得数据,否则 Sed 从不用除保持空间的内容。在我们后面学习到 exchange、get、hold 命令时将深入研究它。
Sed 的抽象机制你将在许多的 Sed 教程中都会看到上面阐明的模式。的确,这是充分精确理解大多数基本 Sed 程序所必需的。但是当你深入研究更多的高等命令时,你将会创造,仅这些知识还是不足的。因此,我们现在考试测验去理解更深入的一些知识。
的确,Sed 可以被视为是 抽象机制 的实现,它的 状态 由三个 缓冲区 、两个 寄存器 和两个 标志 来定义的:
三个缓冲区用于去保存任意长度的文本。是的,是三个!在前面的基本运行模式中我们谈到了两个:模式空间和保持空间,但是 Sed 还有第三个缓冲区: 追加行列步队(append queue)。从 Sed 脚本的角度来看,它是一个只写缓冲区,Sed 将在它运行时的预定义阶段来自动刷新它(一样平常是在从输入源读入一个新行之前,或仅在它退出运行之前)。Sed 也掩护两个寄存器: 行计数器(line counter)(LC)用于保存从输入源读取的行数,而 程序计数器(program counter)(PC)总是用来保存下一个将要运行的命令的索引(便是脚本中的位置),Sed 将它作为它的主循环的一部分来自动增加 PC。但在利用特定的命令时,脚本也可以直接修正 PC 去跳过或重复实行程序的一部分。这就像利用 Sed 实现的一个循环或条件语句。更多内容将不才面的专用分支一节中描述。末了,两个标志可以修正某些 Sed 命令的行为: 自动输出(auto-print)(AP)标志和<ruby更换 substitution(SF)标志。当自动输出标志 AP 被设置时,Sed 将在模式空间的内容被覆盖前自动输出(尤其是,包括但不限于,在从输入源读入一个新行之前)。当自动输出标志被打消时(即:没有设置),Sed 在脚本中没有显式命令的情形下,将不会输出模式空间中的内容。你可以通过在“静默模式”(利用命令行选项 -n 或者在第一行或脚本中利用分外注释 #n)运行 Sed 命令来打消自动输出标志。当它的地址和查找模式与模式空间中的内容都匹配时,更换标志 SF 将被更换命令(s 命令)设置。更换标志在每个新的循环开始时、或当从输入源读入一个新行时、或得到条件分支之后将被打消。我们将在分支一节中详细研究这一话题。
其余,Sed 掩护一个进入到它的地址范围(关于地址范围的更多知识将在地址范围一节详细描述)的命令列表,以及用于读取和写入数据的两个文件句柄(你将在读取和写入命令的描述中得到更多有关文件句柄的内容)。
一个更精确的 Sed 运行模式一图胜千言,以是我画了一个流程图去描述 Sed 的运行模式。我将两个东西放在了阁下,像处理多个输入文件或缺点处理,但是我认为这足够你去理解任何 Sed 程序的行为了,并且可以避免你在编写你自己的 Sed 脚本时摧残浪费蹂躏在摸索上的韶光。
The Sed execution model
你可能已经把稳到,在上面的流程图上我并没有描述特定的命令动作。对付命令,我们将逐个详细讲解。因此,不用焦急,我们立时开始!
打印命令(p)是用于输出在它运行那一刻模式空间中的内容。它并不会以任何办法改变 Sed 抽象机制中的状态。
The Sed `print` command
示例:
sed -e 'p' inputfile
上面的命令将输出输入文件中每一行的内容……两次,由于你一旦显式地哀求利用 p 命令时,将会在每个处理循环结束时再隐式地输出一次(由于在这里我们不是在“静默模式”中运行 Sed)。
如果我们不想每个行看到两次,我们可以用两种办法去办理它:
sed -n -e 'p' inputfile # 在静默模式中显式输出
sed -e '' inputfile # 空的“什么都不做的”程序,隐式输出
把稳:-e 选项是引入一个 Sed 命令。它被用于区分命令和文件名。由于一个 Sed 表达式必须包含至少一个命令,以是对付第一个命令,-e 标志不是必需的。但是,由于我个人利用习气问题,为了与在这里的大多数的一个命令行上给出多个 Sed 表达式的更繁芜的案例保持同等性,我添加了它。你自己去判断这是一个好习气还是坏习气,并且在本文的后面部分还将延用这一习气。
地址显而易见,print 命令本身并没有太多的用途。但是,如果你在它之前添加一个地址,这样它就只输出输入文件的一些行,这样它就溘然变得能够从一个输入文件中过滤一些不肯望的行。那么 Sed 的地址又是什么呢?它是如何来辨别输入文件的“行”呢?
行号Sed 的地址既可以是一个行号($ 表示“末了一行”)也可以是一个正则表达式。在利用行号时,你须要记住 Sed 中的行数是从 1 开始的 —— 并且须要把稳的是,它不是从 0 行开始的。
sed -n -e '1p' inputfile # 仅输出文件的第一行
sed -n -e '5p' inputfile # 仅输出第 5 行
sed -n -e '$p' inputfile # 输出文件的末了一行
sed -n -e '0p' inputfile # 结果将是报错,由于 0 不是有效的行号
根据 POSIX 规范 ,如果你指定了几个输出文件,那么它的行号是累加的。换句话说,当 Sed 打开一个新输入文件时,它的行计数器是不会被重置的。因此,以下的两个命令所做的事情是一样的。仅输出一行文本:
sed -n -e '1p' inputfile1 inputfile2 inputfile3
cat inputfile1 inputfile2 inputfile3 | sed -n -e '1p'
实际上,确实在 POSIX 中规定了多个文件是如何处理的:
如果指定了多个文件,将按指定的文件命名顺序进行读取并被串联编辑。
但是,一些 Sed 的实现供应了命令行选项去改变这种行为,比如, GNU Sed 的 -s 标志(在利用 GNU Sed -i 标志时,它也被隐式地运用):
sed -sn -e '1p' inputfile1 inputfile2 inputfile3
如果你的 Sed 实现支持这种非标准选项,那么关于它的详细细节请查看 man 手书页。
正则表达式我前面说过,Sed 地址既可以是行号也可以是正则表达式。那么正则表达式是什么呢?
正如它的名字,一个 正则表达式 是描述一个字符串凑集的方法。如果一个指定的字符串符合一个正则表达式所描述的凑集,那么我们就认为这个字符串与正则表达式匹配。
正则表达式可以包含必须完备匹配的文本字符。例如,所有的字母和数字,以及大部分可以打印的字符。但是,一些符号有特定意义:
它们相称于锚,像 ^ 和 $ 它们分别表示一个行的开始和结束;能够做为全体字符集的占位符的其它符号(比如圆点 . 可以匹配任意单个字符,或者方括号 [] 用于定义一个自定义的字符集);其余的是表示重复涌现的数量(像 克莱尼星号() 表示前面的模式涌现 0、1 或多次);这篇文章的目的不是给大家讲正则表达式。因此,我只粘几个示例。但是,你可以在网络上随便找到很多关于正则表达式的教程,正则表达式的功能非常强大,它可用于许多标准的 Unix 命令和编程措辞中,并且是每个 Unix 用户该当节制的技能。
下面是利用 Sed 地址的几个示例:
sed -n -e '/systemd/p' inputfile # 仅输出包含字符串“systemd”的行
sed -n -e '/nologin$/p' inputfile # 仅输出以“nologin”结尾的行
sed -n -e '/^bin/p' inputfile # 仅输出以“bin”开头的行
sed -n -e '/^$/p' inputfile # 仅输出空行(即:开始和结束之间什么都没有的行)
sed -n -e '/./p' inputfile # 仅输出包含字符的行(即:非空行)
sed -n -e '/^.$/p' inputfile # 仅输出只包含一个字符的行
sed -n -e '/admin.false/p' inputfile # 仅输出包含字符串“admin”后面有字符串“false”的行(在它们之间有任意数量的任意字符)
sed -n -e '/1[0,3]/p' inputfile # 仅输出包含一个“1”并且后面是一个“0”或“3”的行
sed -n -e '/1[0-2]/p' inputfile # 仅输出包含一个“1”并且后面是一个“0”、“1”、“2”或“3”的行
sed -n -e '/1.2/p' inputfile # 仅输出包含字符“1”后面是一个“2”(在它们之间有任意数量的字符)的行
sed -n -e '/1[0-9]2/p' inputfile # 仅输出包含字符“1”后面随着“0”、“1”、或更多数字,末了面是一个“2”的行
如果你想在正则表达式(包括正则表达式分隔符)中去除字符的分外意义,你可以在它前面利用一个反斜杠:
# 输出所有包含字符串“/usr/sbin/nologin”的行
sed -ne '/\/usr\/sbin\/nologin/p' inputfile
并不限定你只能利用斜杠作为地址中正则表达式的分隔符。你可以通过在第一个分隔符前面加上反斜杠(\)的办法,来利用任何你认为适宜你须要和偏好的其它字符作为正则表达式的分隔符。当你用地址与带文件路径的字符一起来匹配的时,是非常有用的:
# 以下两个命令是完备相同的
sed -ne '/\/usr\/sbin\/nologin/p' inputfile
sed -ne '\=/usr/sbin/nologin=p' inputfile
扩展的正则表达式默认情形下,Sed 的正则表达式引擎仅理解 POSIX 基本正则表达式 的语法。如果你须要用到 扩展正则表达式 ,你必须在 Sed 命令上添加 -E 标志。扩展正则表达式在基本正则表达式根本上增加了一组额外的特性,并且很多都是很主要的,它们所哀求的反斜杠要少很多。我们来比较一下:
sed -n -e '/\(www\)\|\(mail\)/p' inputfile
sed -En -e '/(www)|(mail)/p' inputfile
花括号量词正则表达式之以是强大的一个缘故原由是 范围量词 {,}。事实上,当你写一个不太精确匹配的正则表达式时,量词 便是一个非常完美的符号。但是,(用花括号量词)你可以显式在它边上添加一个下限和上限,这样就有了很好的灵巧性。当量词范围的下限省略时,下限被假定为 0。当上限被省略时,上限被假定为无限大:
括号速记词阐明{,}前面的规则涌现 0、1、或许多遍{,1}?前面的规则涌现 0 或 1 遍{1,}+前面的规则涌现 1 或许多遍{n,n}{n}前面的规则精确地涌现 n 遍
花括号在基本正则表达式中也是可以利用的,但是它哀求利用反斜杠。根据 POSIX 规范,在基本正则表达式中可以利用的量词仅有星号()和花括号(利用反斜杠,如 \{m,n\})。许多正则表达式引擎都扩展支持 \? 和 \+。但是,为什么妖怪如此有诱惑力呢?由于,如果你须要这些量词,利用扩展正则表达式将不但易于写而且可移植性更好。
为什么我要花点韶光去谈论关于正则表达式的花括号量词,这是由于在 Sed 脚本中常常用这个特性去计数字符。
sed -En -e '/^.{35}$/p' inputfile # 输出精确包含 35 个字符的行
sed -En -e '/^.{0,35}$/p' inputfile # 输出包含 35 个字符或更少字符的行
sed -En -e '/^.{,35}$/p' inputfile # 输出包含 35 个字符或更少字符的行
sed -En -e '/^.{35,}$/p' inputfile # 输出包含 35 个字符或更多字符的行
sed -En -e '/.{35}/p' inputfile # 你自己指出它的输出内容(这是留给你的测试题)
地址范围到目前为止,我们利用的所有地址都是唯一地址。在我们利用一个唯一地址时,命令是运用在与那个地址匹配的行上。但是,Sed 也支持地址范围。Sed 命令可以运用到那个地址范围中从开始到结束的所有地址中的所有行上:
sed -n -e '1,5p' inputfile # 仅输出 1 到 5 行
sed -n -e '5,$p' inputfile # 从第 5 行输出到文件结尾
sed -n -e '/www/,/systemd/p' inputfile # 输出与正则表达式 /www/ 匹配的第一行到与接下来匹配正则表达式 /systemd/ 的行为止
(LCTT 译注:下面用的一个天生的列表例子,如下供参考:)
printf \"大众%s\n\"大众 {a,b,c}{d,e,f} | cat -n
1 ad
2 ae
3 af
4 bd
5 be
6 bf
7 cd
8 ce
9 cf
如果在开始和结束地址上利用了同一个行号,那么范围就缩小为那个行。事实上,如果第二个地址的数字小于或即是地址范围中选定的第一个行的数字,那么仅有一个行当选定:
printf \"大众%s\n\"大众 {a,b,c}{d,e,f} | cat -n | sed -ne '4,4p'
4 bd
printf \公众%s\n\公众 {a,b,c}{d,e,f} | cat -n | sed -ne '4,3p'
4 bd
下面有点难了,但是在前面的段落中给出的规则也适用于起始地址是正则表达式的情形。在那种情形下,Sed 将对正则表达式匹配的第一个行的行号和给定的作为结束地址的显式的行号进行比较。再强调一次,如果结束行号小于或即是起始行号,那么这个范围将缩小为一行:
(LCTT 译注:此处作者陈述有误,Sed 会在处理以正则表达式表示的开始行时,并不会同时测试结束表达式:从匹配开始行的正则表达式开始,直到不匹配时,才会测试结束行的表达式——无论是否是正则表达式——并在结束的表达式测试不通过期停滞,并循环此测试。)
# 这个 /b/,4 地址将匹配三个单行
# 由于每个匹配的行有一个行号 >= 4
#(LCTT 译注:结果精确,但是解释禁绝确。4、5、6 行都会由于匹配开始正则表达式而通过,第 7 行由于不匹配开始正则表达式,以是开始比较行数: 7 > 4,遂停滞。)
printf \"大众%s\n\公众 {a,b,c}{d,e,f} | cat -n | sed -ne '/b/,4p'
4 bd
5 be
6 bf
# 你自己指出匹配的范围是多少
# 第二个例子:
printf \公众%s\n\"大众 {a,b,c}{d,e,f} | cat -n | sed -ne '/d/,4p'
1 ad
2 ae
3 af
4 bd
7 cd
但是,当结束地址是一个正则表达式时,Sed 的行为将不一样。在那种情形下,地址范围的第一行将不会与结束地址进行检讨,因此地址范围将至少包含两行(当然,如果输入数据不敷的情形除外):
(LCTT 译注:如上译注,当知足开始的正则表达式时,并不会测试结束的表达式;仅当不知足开始的表达式时,才会测试结束表达式。)
printf \"大众%s\n\"大众 {a,b,c}{d,e,f} | cat -n | sed -ne '/b/,/d/p'
4 bd
5 be
6 bf
7 cd
printf \公众%s\n\"大众 {a,b,c}{d,e,f} | cat -n | sed -ne '4,/d/p'
4 bd
5 be
6 bf
7 cd
(LCTT 译注:对地址范围的总结,当知足开始的条件时,从该行开始,并不测试该行是否知足结束的条件;从下一行开始测试结束条件,并在结束条件不知足时结束;然后对剩余的行,再从开始条件开始匹配,以此循环——也便是说,匹配结果可以是非连续的单/多行。大家可以调度上述命令行的条件以理解。)
补集在一个地址选择行后面添加一个感叹号(!)表示不匹配那个地址。例如:
sed -n -e '5!p' inputfile # 输出除了第 5 行外的所有行
sed -n -e '5,10!p' inputfile # 输出除了第 5 到 10 之间的所有行
sed -n -e '/sys/!p' inputfile # 输出除了包含字符串“sys”的所有行
交集(LCTT 译注:原文标题为“合集”,应为“交集”)
Sed 许可在一个块中利用花括号 {…} 组合命令。你可以利用这个特性去组合几个地址的交集。例如,我们来比较下面两个命令的输出:
sed -n -e '/usb/{
/daemon/p
}' inputfile
sed -n -e '/usb.daemon/p' inputfile
通过在一个块中嵌套命令,我们将在任意顺序中选择包含字符串 “usb” 和 “daemon” 的行。而正则表达式 “usb.daemon” 将仅匹配在字符串 “daemon” 前面包含 “usb” 字符串的行。
离题太永劫光后,我们现在重新回去学习各种 Sed 命令。
退出命令退出命令(q)是指在当前的迭代循环处理结束之后停滞 Sed。
The Sed quit command
q 命令是在到达输入文件的尾部之前停滞处理输入的方法。为什么会有人想去那样做呢?
很好的问题,如果你还记得,我们可以利用下面的命令来输出文件中第 1 到第 5 的行:
sed -n -e '1,5p' inputfile
对付大多数 Sed 的实现办法,工具将循环读取输入文件的所有行,那怕是你只处理结果中的前 5 行。如果你的输入文件包含了几百万行(或者更糟糕的情形是,你从一个无限的数据流,比如像 /dev/urandom 中读取)将有重大影响。
利用退出命令,相同的程序可以被修正的更高效:
sed -e '5q' inputfile
由于我在这里并不该用 -n 选项,Sed 将在每个循环结束后隐式输出模式空间的内容。但是在你处理完第 5 行后,它将退出,并且因此不会去读取更多的数据。
我们能够利用一个类似的技巧只输出文件中一个特定的行。这也是从命令行中供应多个 Sed 表达式的几种方法。下面的三个变体都可以从 Sed 中接管几个命令,要么是不同的 -e 选项,要么是在相同的表达式中新起一行,或用分号(;)隔开:
sed -n -e '5p' -e '5q' inputfile
sed -n -e '
5p
5q
' inputfile
sed -n -e '5p;5q' inputfile
如果你还记得,我们在前面看到过能够利用花括号将命令组合起来,在这里我们利用它来防止相同的地址重复两次:
# 组合命令
sed -e '5{
p
q
}' inputfile
# 可以简写为:
sed '5{p;q;}' inputfile
# 作为 POSIX 扩展,有些实现办法可以省略闭花括号之前的分号:
sed '5{p;q}' inputfile
更换命令你可以将更换命令(s)想像为 Sed 的“查找更换”功能,这个功能在大多数的“所见即所得”的编辑器上都能找到。Sed 的更换命令与之类似,但比它们更强大。更换命令是 Sed 中最著名的命令之一,在网上有大量的关于这个命令的文档。
The Sed `substitution` command
在前一篇文章 中我们已经讲过它了,因此,在这里就不再重复了。但是,如果你对它的利用不是很熟习,那么你须要记住下面的这些关键点:
更换命令有两个参数:查找模式和更换字符串:sed s/:/-----/ inputfiles 命令和它的参数是用任意一个字符来分隔的。这紧张看你的习气,在 99% 的韶光中我都利用斜杠,但也会用其它的字符:sed s%:%-----% inputfile、sed sX:X-----X inputfile 或者乃至是 sed 's : ----- ' inputfile默认情形下,更换命令仅被运用到模式空间中匹配到的第一个字符串上。你可以通过在命令之后指定一个匹配指数作为标志来改变这种情形:sed 's/:/-----/1' inputfile、sed 's/:/-----/2' inputfile、sed 's/:/-----/3' inputfile、…如果你想实行一个全局更换(即:在模式空间上的每个非重叠匹配上进行),你须要增加 g 标志:sed 's/:/-----/g' inputfile在字符串更换中,涌现的任何一个 & 符号都将被与查找模式匹配的子字符串更换:sed 's/:/-&&&-/g' inputfile、sed 's/.../& /g' inputfile圆括号(在扩展的正则表达式中的 (...) ,或者基本的正则表达式中的 \(...\))被当做 捕获组(capturing group)。那是匹配字符串的一部分,可以在更换字符串中被引用。\1 是第一个捕获组的内容,\2 是第二个捕获组的内容,依次类推:sed -E 's/(.)(.)/\2\1/g' inputfile、sed -E 's/(.):x:(.):(.)/\1:\3/' inputfile(后者之所能正常事情是由于 正则表达式中的量词星号表示尽可能多的匹配,直到不匹配为止 ,并且它可以匹配许多个字符)在查找模式或更换字符串时,你可以通过利用一个反斜杠来去除任何字符的分外意义:sed 's/:/--\&--/g' inputfile,sed 's/\//\\/g' inputfile所有的这些看起来有点抽象,下面是一些示例。首先,我想去显示我的测试输入文件的第一个字段并给它在右侧附加 20 个空格字符,我可以这样写:
sed < inputfile -E -e '
s/:/ / # 用 20 个空格更换第一个字段的分隔符
s/(.{20})./\1/ # 只保留一行的前 20 个字符
s/./| & |/ # 为了输出好看添加竖条
'
第二个示例是,如果我想将用户 sonia 的 UID/GID 修正为 1100,我可以这样写:
sed -En -e '
/sonia/{
s/[0-9]+/1100/g
p
}' inputfile
把稳在更换命令结束部分的 g 选项。这个选项改变了它的行为,因此它将查找全部的模式空间并更换,如果没有那个选项,它只更换查找到的第一个。
顺便说一下,这也是利用前面讲过的输出(p)命令的好机会,可以在命令运行时输出修正前后的模式空间的内容。因此,为了得到更换前后的内容,我可以这样写:
sed -En -e '
/sonia/{
p
s/[0-9]+/1100/g
p
}' inputfile
事实上,更换后输出一个行是很常见的用法,因此,更换命令也接管 p 选项:
sed -En -e '/sonia/s/[0-9]+/1100/gp' inputfile
末了,我就不详细讲更换命令的 w 选项了,我们将在稍后的学习中详细先容。
删除命令删除命令(d)用于打消模式空间的内容,然后立即开始下一个处理循环。这样它将会跳过隐式输出模式空间内容的行为,即便是你设置了自动输出标志(AP)也不会输出。
The Sed `delete` command
只输出一个文件前五行的一个很低效率的方法将是:
sed -e '6,$d' inputfile
你猜猜看,我为什么说它很低效率?如果你猜不到,建议你再次去阅读前面的关于退出命令的章节,答案就在那里!
当你组合利用正则表达式和地址,从输出中删除匹配的行时,删除命令将非常有用:
sed -e '/systemd/d' inputfile
次行命令如果 Sed 命令没有运行在静默模式中,这个命令(n)将输出当前模式空间的内容,然后,在任何情形下它将读取下一个输入行到模式空间中,并利用新的模式空间中的内容来运行当前循环中剩余的命令。
The Sed next command
用次行命令去跳过行的一个常赐教例:
cat -n inputfile | sed -n -e 'n;n;p'
在上面的例子中,Sed 将隐式地读取输入文件的第一行。但是次行命令将丢弃对模式空间中的内容的输出(不输出是由于利用了 -n 选项),并从输入文件中读取下一行来更换模式空间中的内容。而第二个次行命令做的事情和前一个是千篇一律的,这就实现了跳过输入文件 2 行的目的。末了,这个脚本显式地输出包含在模式空间中的输入文件的第三行的内容。然后,Sed 将启动一个新的循环,由于次行命令,它会隐式地读取第 4 行的内容,然后跳过它,同样地也跳过第 5 行,并输出第 6 行。如此循环,直到文件结束。总体来看,这个脚本便是读取输入文件然后每三行输出一行。
利用次行命令,我们也可以找到一些显示输入文件的前五行的几种方法:
cat -n inputfile | sed -n -e '1{p;n;p;n;p;n;p;n;p}'
cat -n inputfile | sed -n -e 'p;n;p;n;p;n;p;n;p;q'
cat -n inputfile | sed -e 'n;n;n;n;q'
更有趣的是,如果你须要根据一些地址来处理行时,次行命令也非常有用:
cat -n inputfile | sed -n '/pulse/p' # 输出包含 “pulse” 的行
cat -n inputfile | sed -n '/pulse/{n;p}' # 输出包含 “pulse” 之后的行
cat -n inputfile | sed -n '/pulse/{n;n;p}' # 输出包含 “pulse” 的行的下一行的下一行
利用保持空间到目前为止,我们所看到的命令都是仅利用了模式空间。但是,我们在文章的开始部分已经提到过,还有第二个缓冲区:保持空间,它完备由用户管理。它便是我们在第二节中描述的目标。
交流命令正如它的名字所表示的,交流命令(x)将交流保持空间和模式空间的内容。记住,你只要没有把任何东西放入到保持空间中,那么保持空间便是空的。
The Sed exchange command
作为第一个示例,我们可利用交流命令去反序输出一个输入文件的前两行:
cat -n inputfile | sed -n -e 'x;n;p;x;p;q'
当然,在你设置保持空间之后你并没有立即利用它的内容,由于只要你没有显式地去修正它,保持空间中的内容就保持不变。不才面的例子中,我在输入一个文件的前五行后,利用它去删除第一行:
cat -n inputfile | sed -n -e '
1{x;n} # 交流保持和模式空间
# 保存第 1 行到保持空间中
# 然后读取第 2 行
5{
p # 输出第 5 行
x # 交流保持和模式空间
# 去取得第 1 行的内容放回到模式空间
}
1,5p # 输出第 2 到第 5 行
# (并没有输错!
考试测验找出这个规则
# 没有在第 1 行上运行的缘故原由 ;)
'
保持命令保持命令(h)是用于将模式空间中的内容保存到保持空间中。但是,与交流命令不同的是,模式空间中的内容不会被改变。保持命令有两种用法:
h 将复制模式空间中的内容到保持空间中,覆盖保持空间中任何已经存在的内容。H 将模式空间中的内容追加到保持空间中,利用一个新行作为分隔符。The Sed hold command
上面利用交流命令的例子可以利用保持命令重写如下:
cat -n inputfile | sed -n -e '
1{h;n} # 保存第 1 行的内容到保持缓冲区并连续
5{ # 在第 5 行
x # 交流模式和保持空间
# (现在模式空间包含了第 1 行)
H # 在保持空间的第 5 行后追加第 1 行
x # 再次交流第 5 行和第 1 行,第 5 行回到模式空间
}
1,5p # 输出第 2 行到第 5 行
# (没有输错!
考试测验去找到为什么这个规则
# 不在第 1 行上运行 ;)
'
获取命令获取命令(g)与保持命令恰好相反:它从保持空间中取得内容并将它置入到模式空间中。同样它也有两种办法:
g 将复制保持空间中的内容并将其放入到模式空间,覆盖模式空间中已存在的任何内容G 将保持空间中的内容追加到模式空间中,并利用一个新行作为分隔符The Sed get command
将保持命令和获取命令一起利用,可以许可你去存储并调回数据。作为一个小寻衅,我让你重写前一节中的示例,将输入文件的第 1 行放置在第 5 行之后,但是这次必须利用获取和保持命令(利用大写或小写命令的版本)而不能利用交流命令。带点小运气,可以更大略!
同时,我可以给你展示另一个示例,它能给你一些灵感。目标是将拥有登录 shell 权限的用户与其它用户分开:
cat -n inputfile | sed -En -e '
\=(/usr/sbin/nologin|/bin/false)$= { H;d; }
# 追回匹配的行到保持空间
# 然后连续下一个循环
p # 输出其它行
$ { g;p } # 在末了一行上
# 获取并打印保持空间中的内容
'
复习打印、删除和次行命令现在你已经更熟习利用保持空间了,我们回到打印、删除和次行命令。我们已经谈论了小写的 p、d 和 n 命令了。而它们也有大写的版本。由于每个命令都有大小写版本,彷佛是 Sed 的习气,这些命令的大写版本将与多行缓冲区有关:
P 将模式空间中第一个新行之前的内容输出D 删除模式空间中第一个新行之前的内容(包含新行),然后不读取任何新的输入而是利用剩余的文本去重启一个循环N 读取输入并追加一个新行到模式空间,用一个新行作为新旧数据的分隔符。连续运行当前的循环。The Sed uppercase `Delete` command
The Sed uppercase `Next` command
这些命令的利用场景紧张用于实现行列步队( FIFO 列表 )。从一个输入文件中删除末了 5 行便是一个很威信的例子:
cat -n inputfile | sed -En -e '
1 { N;N;N;N } # 确保模式空间中包含 5 行
N # 追加第 6 行到行列步队中
P # 输出行列步队的第 1 行
D # 删除行列步队的第 1 行
'
作为第二个示例,我们可以在两个列上显示输入数据:
# 输出两列
sed < inputfile -En -e '
$!N # 追加一个新行到模式空间
# 除了输入文件的末了一行
# 当在输入文件的末了一行利用 N 命令时
# GNU Sed 和 POSIX Sed 的行为是有差异的
# 须要利用一个技巧去处理这种情形
# https://www.gnu.org/software/sed/manual/sed.html#N_005fcommand_005flast_005fline
# 用空间添补第 1 行的第 1 个字段
# 并丢弃别的行
s/:.\n/ \n/
s/:.// # 除了第 2 行上的第 1 个字段外,丢弃别的的行
s/(.{20}).\n/\1/ # 修剪并连接行
p # 输出结果
'
分支我们刚才已经看到,Sed 由于有保持空间以是有了缓存的功能。实在它还有测试和分支的指令。由于有这些特性使得 Sed 是一个 图灵完备 的措辞。虽然它可能看起来很傻,但意味着你可以利用 Sed 写任何程序。你可以实现任何你的目的,但并不虞味其实现起来会很随意马虎,而且结果也不一定会很高效。
不过不用担心。在本文中,我们将利用能够展示测试和分支功能的最大略的例子。虽然这些功能乍一看彷佛很有限,但请记住,有些人用 Sed 写了 http://www.catonmat.net/ftp/sed/dc.sed [打算器]、 http://www.catonmat.net/ftp/sed/sedtris.sed [俄罗斯方块] 或许多其它类型的运用程序!
从某些方面,你可以将 Sed 看到是一个功能有限的汇编措辞。因此,你不会找到在高等措辞中常见的 “for” 或 “while” 循环,或者 “if … else” 语句,但是你可以利用分支来实现同样的功能。
The Sed branch command
如果你在本文开始部分看到了用流程图描述的 Sed 运行模型,那么你该当知道 Sed 会自动增加程序计数器(PC)的值,命令是按程序的指令顺序来运行的。但是,利用分支(b)指令,你可以通过选择实行程序中的任意命令来改变顺序运行的程序。跳转目的地是利用一个标签(:)来显式定义的。
The Sed label command
这是一个这样的示例:
echo hello | sed -ne '
:start # 在程序的该行上放置一个 “start” 标签
p # 输出模式空间内容
b start # 连续在 :start 标签上运行
' | less
那个 Sed 程序的行为非常类似于 yes 命令:它获取一个字符串并产生一个包含那个字符串的无限流。
切换到一个标签就像我们绕开了 Sed 的自动化特性一样:它既不读取任何输入,也不输出任何内容,更不更新任何缓冲区。它只是跳转到源程序指令顺序中下一条的其余一个指令。
值得一提的是,如果在分支命令(b)上没有指定一个标签作为它的参数,那么分支将直接切换到程序结束的地方。因此,Sed 将启动一个新的循环。这个特性可以用于去跳过一些指令并且因此可以用于作为“块”的替代者:
cat -n inputfile | sed -ne '
/usb/!b
/daemon/!b
p
'
条件分支到目前为止,我们已经看到了无条件分支,这个术语可能有点误导嫌疑,由于 Sed 命令总是基于它们的可选地址来作为条件的。
但是,在传统意义上,一个无条件分支也是一个分支,当它运行时,将跳转到特定的目的地,而条件分支既有可能也或许不可能跳转到特定的指令,这取决于系统确当前状态。
Sed 只有一个条件指令,便是测试(t)命令。只有在当前循环的开始或由于前一个条件分支运行了更换,它才跳转到不同的指令。更多的情形是,只有更换标志被设置时,测试命令才会切换分支。
The Sed `test` command
利用测试指令,你可以在一个 Sed 程序中很轻松地实行一个循环。作为一个特定的示例,你可以用它将一个行添补到某个长度(这是利用正则表达式无法实现的):
# 居中文本
cut -d: -f1 inputfile | sed -Ee '
:start
s/^(.{,19})$/ \1 / # 用一个空格添补少于 20 个字符的行的开始处
# 并在结束处添加另一个空格
t start # 如果我们已经添加了一个空格,则返回到 :start 标签
s/(.{20})./| \1 |/ # 只保留一个行的前 20 个字符
# 以修复由于奇数行引起的差一缺点
'
如果你仔细读前面的示例,你可能把稳到,在将要把数据“喂”给 Sed 之前,我通过 cut 命令做了一点小改动去预处理数据。
不过,我们也可以只利用 Sed 对程序做一些小修正来实行相同的任务:
cat inputfile | sed -Ee '
s/:.// # 除第 1 个字段外删除剩余字段
t start
:start
s/^(.{,19})$/ \1 / # 用一个空格添补少于 20 个字符的行的开始处
# 并在结束处添加另一个空格
t start # 如果我们已经添加了一个空格,则返回到 :start 标签
s/(.{20})./| \1 |/ # 仅保留一个行的前 20 个字符
# 以修复由于奇数行引起的差一缺点
'
在上面的示例中,你或许对下列的构造感到惊奇:
t start
:start
乍一看,在这里的分支并没有用,由于它只是跳转到将要运行的指令处。但是,如果你仔细阅读了测试命令的定义,你将会看到,如果在当前循环的开始或者前一个测试命令运行后发生了一个更换,分支才会起浸染。换句话说便是,测试指令有打消更换标志的副浸染。这也正是上面的代码片段的真实目的。这是一个在包含条件分支的 Sed 程序中常常看到的技巧,用于在利用多个更换命令时避免涌现 误报(false positive)的情形。
通过它并不能绝对逼迫地打消更换标志,我赞许这一说法。由于在将字符串添补到精确的长度时我利用的特定的更换命令是 幂等(idempotent)的。因此,一个多余的迭代并不会改变结果。不过,我们可以现在再次看一下第二个示例:
# 基于它们的登录程序来分类用户帐户
cat inputfile | sed -Ene '
s/^/login=/
/nologin/s/^/type=SERV /
/false/s/^/type=SERV /
t print
s/^/type=USER /
s/:.//p
'
我希望在这里根据用户默认配置的登录程序,为用户帐户打上 “SERV” 或 “USER” 的标签。如果你运行它,估量你将看到 “SERV” 标签。然而,并没有在输出中跟踪到 “USER” 标签。为什么呢?由于 t print 指令不论行的内容是什么,它总是切换,更换标志总是由程序的第一个更换命令来设置。一旦更换标志设置完成后,不才一个行被读取或直到下一个测试命令之前,这个标志将保持不变。下面我们给出修复这个程序的办理方案:
# 基于用户登录程序来分类用户帐户
cat inputfile | sed -Ene '
s/^/login=/
t classify # clear the \"大众substitution flag\"大众
:classify
/nologin/s/^/type=SERV /
/false/s/^/type=SERV /
t print
s/^/type=USER /
s/:.//p
'
精确地处理文本Sed 是一个非交互式文本编辑器。虽然是非交互式的,但仍旧是文本编辑器。而如果没有在输出中插入一些东西的功能,那它就不算一个完全的文本编辑器。我不是很喜好它的文本编辑的特性,由于我创造它的语法太难用了(即便是以 Sed 的标准而言),但有时你难免会用到它。
采取严格的 POSIX 语法的只有三个命令:改变(c)、插入(i)或追加(a)一些笔墨文本到输出,都遵照相同的特定语法:命令字母后面随着一个反斜杠,并且文本从脚本的下一行上开始插入:
head -5 inputfile | sed '
1i\
# List of user accounts
$a\
# end
'
插入多行文本,你必须每一行结束的位置利用一个反斜杠:
head -5 inputfile | sed '
1i\
# List of user accounts\
# (users 1 through 5)
$a\
# end
'
一些 Sed 实现,比如 GNU Sed,在初始的反斜杠后面的换行符是可选的,即便是在 --posix 模式下仍旧如此。我在标准中并没有找到任何关于该替代语法的解释(如果是由于我没有在标准中找到那个特性,请在评论区留言见告我!
)。因此,如果对可移植性哀求很高,请把稳利用它的风险:
# 非 POSIX 语法:
head -5 inputfile | sed -e '
1i\# List of user accounts
$a\# end
'
也有一些 Sed 的实现,让初始的反斜杠完备是可选的。因此毫无疑问,它是一个厂商对 POSIX 标准进行扩展的特定版本,它是否支持那个语法,你须要去查看那个 Sed 版本的手册。
在大略概述之后,我们现在来回顾一下这些命令的更多细节,从我还没有先容的改变命令开始。
改变命令改变命令(c\)就像 d 命令一样删除模式空间的内容并开始一个新的循环。唯一的不同在于,当命令运行之后,用户供应的文本是写往输出的。
The Sed change command
cat -n inputfile | sed -e '
/systemd/c\
# :REMOVED:
s/:.// # This will NOT be applied to the \"大众changed\公众 text
'
如果改变命令与一个地址范围关联,当到达范围的末了一行时,这个文本将仅输出一次。这在某种程度上成为 Sed 命令将被重复运用在地址范围内所有行这一老例的一个例外情形:
cat -n inputfile | sed -e '
19,22c\
# :REMOVED:
s/:.// # This will NOT be applied to the \公众changed\公众 text
'
因此,如果你希望将改变命令重复运用到地址范围内的所有行上,除了将它封装到一个块中之外,你将没有其它的选择:
cat -n inputfile | sed -e '
19,22{c\
# :REMOVED:
}
s/:.// # This will NOT be applied to the \"大众changed\"大众 text
'
插入命令插入命令(i\)将立即在输出中给出用户供应的文本。它并不以任何办法修处死式流或缓冲区的内容。
The Sed insert command
# display the first five user names with a title on the first row
sed < inputfile -e '
1i\
USER NAME
s/:.//
5q
'
追加命令当输入的下一行被读取时,追加命令(a\)将一些文本追加到显示行列步队。文本在当前循环的结束部分(包含程序结束的情形)或当利用 n 或 N 命令从输入中读取一个新行时被输出。
The Sed append command
与上面相同的一个示例,但这次是插入到底部而不是顶部:
sed < inputfile -e '
5a\
USER NAME
s/:.//
5q
'
读取命令这是插入一些文本内容到输出流的第四个命令:读取命令(r)。它的事情办法与追加命令完备一样,但不同的,它不从 Sed 脚本中取得硬编码到脚本中的文本,而是把一个文件的内容写入到一个输出上。
读取命令只调度要读取的文件。当清理追加行列步队时,后者才被高效地读取,而不是在读取命令运行时。如果这时候对这个文件有并发的访问读取,或那个文件不是一个普通的文件(比如,它是一个字符设备或命名管道),或文件在读取期间被修正,这时可能会产生严重的后果。
作为一个例证,如果你利用我们将不才一节详细讲述的写入命令,它与读取命令共同合营从一个临时文件中写入并重新读取,你可能会得到一些创造性的结果(利用法语版的 Shiritori 游戏作为一个例证):
printf \"大众%s\n\"大众 \"大众Trois p'tits chats\公众 \"大众Chapeau d' paille\"大众 \"大众Paillasson\"大众 |
sed -ne '
r temp
a\
----
w temp
'
现在,在流输出中专门用于插入一些文本的 Sed 命令清单结束了。我的末了一个示例纯属好玩,但是由于我前面提到过有一个写入命令,这个示例将我们完美地带到下一节,不才一节我们将看到在 Sed 中如何将数据写入到一个外部文件。
替代的输出Sed 的设计思想是,所有的文本转换都将写入到进程的标准输出上。但是,Sed 也有一些特性支持将数据发送到替代的目的地。你有两种办法去实现上述的输出目标更换:利用专门的写入命令(w),或者在一个更换命令(s)上添加一个写入标志。
写入命令写入命令(w)会追加模式空间的内容到给定的目标文件中。POSIX 哀求在 Sed 处理任何数据之前,目标文件能够被 Sed 所创建。如果给定的目标文件已经存在,它将被覆写。
The Sed write command
因此,即便是你从未真的写入到该文件中,但该文件仍旧会被创建。例如,下列的 Sed 程序将创建/覆写这个 output 文件,那怕是这个写入命令从未被运行过:
echo | sed -ne '
q # 急速退出
w output # 这个命令从未被运行
'
你可以将几个写入命令指向到同一个目标文件。指向同一个目标文件的所有写入命令将追加那个文件的内容(事情办法险些与 shell 的重定向符 >> 相同):
sed < inputfile -ne '
/:\/bin\/false$/w server
/:\/usr\/sbin\/nologin$/w server
w output
'
cat server
更换命令的写入标志在前面,我们已经学习了更换命令(s),它有一个 p 选项用于在更换之后输出模式空间的内容。同样它也供应一个类似功能的 w 选项,用于在更换之后将模式空间的内容输出到一个文件中:
sed < inputfile -ne '
s/:.\/nologin$//w server
s/:.\/false$//w server
'
cat server
注释我无数次利用过它们,但我从未花韶光正式先容过它们,因此,我决定现在来正式地先容它们:就像大多数编程措辞一样,注释是添加软件不去解析的自由格式文本的一种方法。Sed 的语法很晦涩,我不得不强调在脚本中须要的地方添加足够的注释。否则,除了作者外其他人将险些无法理解它。
The Sed comment command
不过,和 Sed 的其它部分一样,注释也有它自己的奇妙之处。首先并且是最主要的,注释并不是语法构造,但它是真正意义的 Sed 命令。注释虽然是一个“什么也不做”的命令,但它仍旧是一个命令。至少,它是在 POSIX 中定义了的。因此,严格地说,它们只许可利用在其它命令许可利用的地方。
大多数 Sed 实现都通过许可行内命令来放松了那种哀求,就像在那个文章中我到处都利用的那样。
结束那个主题之前,须要说一下 #n 注释(# 后面紧跟一个字母 n,中间没有空格)的分外情形。如果在脚本的第一行找到这个精确注释,Sed 将切换到静默模式(即:打消自动输出标志),就像在命令行上指定了 -n 选项一样。
很少用得到的命令现在,我们已经学习的命令能让你写出你所用到的 99.99% 的脚本。但是,如果我没有提到剩余的 Sed 命令,那么本教程就不能称为完备指南。我把它们留到末了是由于我们很少用到它。但或许你有实际利用案例,那么你就会创造它们很有用。如果是那样,请不要犹豫,不才面的评论区中把它分享给我们吧。
行数命令这个 = 命令将向标准输出上显示当前 Sed 正在读取的行数,这个行数便是行计数器(LC)的内容。没有任何办法从任何一个 Sed 缓冲区中捕获那个数字,也不能对它进行输出格式化。由于这两个限定使得这个命令的可用性大大降落。
The Sed line number command
请记住,在严格的 POSIX 兼容模式中,当在命令行上给定几个输入文件时,Sed 并不重置那个计数器,而是连续地增长它,就像所有的输入文件是连接在一起的一样。一些 Sed 实现,像 GNU Sed,它就有一个选项可以在每个输入文件读取结束后去重置计数器。
明确打印命令这个 l(小写的字母 l)浸染类似于打印命令(p),但它因此精确的格式去输出模式空间的内容。以下引用自 POSIX 标准 :
在 XBD 转义序列中列出的字符和干系的动作(\\、\a、\b、\f、\r、\t、\v)将被写为相应的转义序列;在那个表中的 \n 是不适用的。不在那个表中的不可打印字符将被写为一个三位八进制数字(在前面利用一个反斜杠 \),表示字符中的每个字节(最主要的字节在前面)。长行该当被换行,通过写一个反斜杠后跟一个换行符来表示换行位置;发生换行时的长度是不愿定的,但该当适宜输出设备的详细情形。每个行该当以一个 $ 标记结束。
The Sed unambiguous print command
我疑惑这个命令是在非 8 位规则化信道 上交流数据的。就我本人而言,除了调试用场以外,也从未利用过它。
移译命令移译(transliterate)(y)命令许可从一个源集到一个目标集映射模式空间的字符。它非常类似于 tr 命令,但是限定更多。
The Sed transliterate command
# The `y` c0mm4nd 1s for h4x0rz only
sed < inputfile -e '
s/:.//
y/abcegio/48<3610/
'
虽然移译命令语法与更换命令的语法有一些相似之处,但它在更换字符串之后不接管任何选项。这个移译总是全局的。
请把稳,移译命令哀求源集和目标集之间要逐一对应地转换。这意味着下面的 Sed 程序可能所做的事情并不是你乍一看所想的那样:
# 把稳:这可能并不如你想的那样事情!
sed < inputfile -e '
s/:.//
y/[a-z]/[A-Z]/
'
写在末了的话# 它要做什么?
# 提示:答案就在不远处...
sed -E '
s/.\W(.)/\1/
h
${ x; p; }
d' < inputfile
我们已经学习了所有的 Sed 命令,真不敢相信我们已经做到了!
如果你也读到这里了,该当恭喜你,尤其是如果你花费了一些韶光,在你的系统上考试测验了所有的不同示例!
正如你所见,Sed 是非常繁芜的,不仅由于它的语法比较零乱,也由于许多极度案例或命令行为之间的细微差别。毫无疑问,我们可以将这些归结于历史的缘故原由。只管它有这么多缺陷,但是 Sed 仍旧是一个非常强大的工具,乃至到现在,它仍旧是 Unix 工具箱中为数不多的大量利用的命令之一。是时候总结一下这篇文章了,没有你们的支持我将无法做到:请节选你对喜好的或最具创意的 Sed 脚本,并共享给我们。如果我网络到的你们共享出的脚本足够多了,我将会把这些 Sed 脚本结集发布!
via: https://linuxhandbook.com/sed-reference-guide/
作者: Sylvain Leroux 选题: lujun9972 译者: qhwdw 校正: wxy
本文由 LCTT 原创编译, Linux中国 名誉推出
点击“理解更多”可访问文内链接