Sed 命令完全指南

编译自:
https://linuxhandbook.com/sed-reference-guide/

译者: qhwdw

关于 Sed 的一点点理论知识

首先我们看一下 sed 的运行模式

要准确理解 Sed 命令,你必须先了解工具的运行模式。

默认情况下,Sed 在结束每个处理循环后输出模式空间中的内容,也就是说,输出发生在输入的下一个行覆盖模式空间之前。我们可以将这种运行模式总结如下:

  1. 尝试将下一个行读入到模式空间中
  2. 如果读取成功:
  3. 按脚本中的顺序将所有命令应用到与那个地址匹配的当前输入行上
  4. 如果 sed 没有以静默模式(-n)运行,那么将输出模式空间中的所有内容(可能会是修改过的)。
  5. 重新回到 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 # 空的“什么都不做的”程序,隐式输出

    地址

    显而易见,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 “%sn” {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 “%sn” {a,b,c}{d,e,f} | cat -n | sed -ne ‘4,4p’

    4 bd

    printf “%sn” {a,b,c}{d,e,f} | cat -n | sed -ne ‘4,3p’

    4 bd

    下面有点难了,但是在前面的段落中给出的规则也适用于起始地址是正则表达式的情况。在那种情况下,Sed 将对正则表达式匹配的第一个行的行 和给定的作为结束地址的显式的行 进行比较。再强调一次,如果结束行 小于或等于起始行 ,那么这个范围将缩小为一行:

    # 这个 /b/,4 地址将匹配三个单行

    # 因为每个匹配的行有一个行 >= 4

    #(LCTT 译注:结果正确,但是说明不正确。4、5、6 行都会因为匹配开始正则表达式而通过,第 7 行因为不匹配开始正则表达式,所以开始比较行数: 7 > 4,遂停止。)

    printf “%sn” {a,b,c}{d,e,f} | cat -n | sed -ne ‘/b/,4p’

    4 bd

    5 be

    6 bf

    # 你自己指出匹配的范围是多少

    # 第二个例子:

    printf “%sn” {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 “%sn” {a,b,c}{d,e,f} | cat -n | sed -ne ‘/b/,/d/p’

    4 bd

    5 be

    6 bf

    7 cd

    printf “%sn” {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

    替换命令

    The Sed `substitution` command

    在前一篇文章 中我们已经讲过它了,因此,在这里就不再重复了。但是,如果你对它的使用不是很熟悉,那么你需要记住下面的这些关键点:

  • 替换命令有两个参数:查找模式和替换字符串:sed s/:/—–/ inputfile
  • s 命令和它的参数是用任意一个字符来分隔的。这主要看你的习惯,在 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/(.)(.)/21/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 看到是一个功能有限的汇编语言。因此,你不会找到在高级语言中常见的 “for” 或 “while” 循环,或者 “if … else” 语句,但是你可以使用分支来实现同样的功能。

    The Sed branch command

    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 /

    :print

    s/:.*//p

    声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2018年10月9日
    下一篇 2018年10月9日

    相关推荐