Linux shell 脚本编程-高级篇 (五)

继: Linux shell 脚本编程-高级篇 (四)

5. sed 进阶


sed 编辑器的基础命令能满足大多数日常文本编辑需求。本节介绍 sed 编辑器提供的更多高级特性。这些功能未必会经常用到,但当需要时,知道这些功能
的存在以及如何使用它们是必要的。


5.1 多行命令
-----------------------------------------------------------------------------------------------------------------------------------------
在使用 sed 编辑器的基础命令时,可能注意到了一个局限。所有的 sed 编辑器命令都是针对单行数据执行操作的。在 sed 读取数据流时,它会基于换行符
的位置将数据分成行。sed 根据定义好的脚本命令一次处理一行数据,然后移到下一行重复这个过程。

有时需要对跨多行的数据执行特定操作。例如,如果正在数据中查找短语 Linux System Administrators Group,它很有可能出现在两行中,每行各包含其中
一部分短语。如果用普通的 sed 命令来处理文本,就不可能发现这种被分开的短语。

sed 编辑器包含了三个可用来处理多行文本的特殊命令:

    □ N:将数据流中的下一行加进来创建一个多行组(multiline group)来处理。
    □ D:删除多行组中的一行。
    □ P:打印多行组中的一行。


5.1.1 next 命令
-----------------------------------------------------------------------------------------------------------------------------------------


■ 单行的 next 命令
-----------------------------------------------------------------------------------------------------------------------------------------
小写的 n 命令会告诉 sed 编辑器移动到数据流中的下一文本行,而不用重新回到命令的最开始再执行一遍。通常 sed 在移动到数据流中的下一文本行之前,
会在当前行上执行完所有定义好的命令。单行 next 命令改变了这个流程。

下面的例子中,有个数据文件,共有 5行内容,其中的两行是空的。目标是删除首行之后的空白行,而留下最后一行之前的空白行。如果写一个删掉空白行的
sed 脚本,会删掉两个空白行:

    [devalone@devalone 21]$ cat data1.txt
    This is the header line

    This is a data line

    This is the last line
    [devalone@devalone 21]$ sed '/^$/d' data1.txt
    This is the header line
    This is a data line
    This is the last line

由于要删除的行是空行,没有任何能够标示这种行的文本可供查找。解决办法是用 n 命令。

在这个例子中,脚本要查找含有单词 header 的那一行。找到之后,n 命令会让 sed 编辑器移动到文本的下一行,也就是那个空行。

    [devalone@devalone 21]$ sed '/header/{n;d}' data1.txt
    This is the header line
    This is a data line

    This is the last line

这时,sed 编辑器会继续执行命令列表,该命令列表使用 d 命令来删除空白行。sed 编辑器执行完命令脚本后,会从数据流中读取下一行文本,并从头开始
执行命令脚本。因为 sed 编辑器再也找不到包含单词 header 的行了。所以也不会有其他行会被删掉。


■ 合并文本行
-----------------------------------------------------------------------------------------------------------------------------------------
单行 next 命令会将数据流中的下一文本行移动到 sed 编辑器的工作空间(称为模式空间)。多行版本的 next 命令(用大写 N )会将下一文本行添加到
模式空间中已有的文本后。这样的作用是将数据流中的两个文本行合并到同一个模式空间中。文本行仍然用换行符分隔,但 sed 编辑器现在会将两行文本当
成一行来处理。

下面的例子演示了 N 命令的工作方式:

    [devalone@devalone 21]$ cat data2.txt
    This is the header line.
    This is the first data line.
    This is the second data line.
    This is the last line.
    [devalone@devalone 21]$
    
    [devalone@devalone 21]$ sed '/first/{N; s/\n/ /}' data2.txt
    This is the header line.
    This is the first data line. This is the second data line.
    This is the last line.

sed 编辑器脚本查找含有单词 first 的那行文本。找到该行后,它会用 N 命令将下一行合并到那行,然后用替换命令 s 将换行符替换成空格。结果是,
文本文件中的两行在 sed 编辑器的输出中成了一行。

如果要在数据文件中查找一个可能会分散在两行中的文本短语的话,这是个很实用的应用程序。

示例:
    [devalone@devalone 21]$ cat data3.txt
    On Tuesday, the Linux System
    Administrator's group meeting will be held.
    All System Administrators should attend.
    Thank you for your attendance.
    
    [devalone@devalone 21]$ sed 'N; s/System Administrator/Desktop User/' data3.txt
    On Tuesday, the Linux System
    Administrator's group meeting will be held.
    All Desktop Users should attend.
    Thank you for your attendance.

替换命令会在文本文件中查找特定的双词短语 System Administrator。如果短语在一行中的话,事情很好处理,替换命令可以直接替换文本。但如果短语
分散在两行中的话,替换命令就没法识别匹配的模式了。

这时N命令就可以派上用场了:

    [devalone@devalone 21]$ sed 'N; s/System.Administrator/Desktop User/' data3.txt
    On Tuesday, the Linux Desktop User's group meeting will be held.
    All Desktop Users should attend.
    Thank you for your attendance.

用 N 命令将发现第一个单词的那行和下一行合并后,即使短语内出现了换行,仍然可以找到它。

注意,替换命令在 System 和 Administrator之间用了通配符模式(.)来匹配空格和换行符这两种情况。但当它匹配了换行符时,它就从字符串中删掉换行符,
导致两行合并成一行。这可能不是我们想要的。

要解决这个问题,可以在 sed 编辑器脚本中用两个替换命令:一个用来匹配短语出现在多行中的情况,一个用来匹配短语出现在单行中的情况:

    [devalone@devalone 21]$ sed 'N
    > s/System\nAdministrator/Desktop\nUser/
    > s/System Administrator/Desktop User/
    > ' data3.txt
    On Tuesday, the Linux Desktop
    User's group meeting will be held.
    All Desktop Users should attend.
    Thank you for your attendance.

第一个替换命令专门查找两个单词间的换行符,并将它放在了替换字符串中。这样就能在第一个替换命令专门在两个检索词之间寻找换行符,并将其纳入替换
字符串。这样就允许在新文本的同样位置添加换行符了。

但这个脚本中仍有个小问题。这个脚本总是在执行 sed 编辑器命令前将下一行文本读入到模式空间。当它到了最后一行文本时,就没有下一行可读了,所以
N 命令会叫 sed 编辑器停止。如果要匹配的文本正好在数据流的最后一行上,命令就不会发现要匹配的数据。

示例:
    [devalone@devalone 21]$ cat data4.txt
    On Tuesday, the Linux System
    Administrator's group meeting will be held.
    All System Administrators should attend.
    [devalone@devalone 21]$
    
    [devalone@devalone 21]$ sed 'N
    s/System\nAdministrator/Desktop\nUser/
    s/System Administrator/Desktop User/
    ' data4.txt
    On Tuesday, the Linux Desktop
    User's group meeting will be held.
    All System Administrators should attend.

由于 System Administrator 文本出现在了数据流中的最后一行,N 命令会错过它,因为没有其他行可读入到模式空间跟这行合并。

可以轻松地解决这个问题,将单行命令放到 N 命令前面,并将多行命令放到 N 命令后面,像这样:

    [devalone@devalone 21]$ sed '
    > s/System Administrator/Desktop User/
    > N
    > s/System\nAdministrator/Desktop\nUser/
    > ' data4.txt
    On Tuesday, the Linux Desktop
    User's group meeting will be held.
    All Desktop Users should attend.

现在,查找单行中短语的替换命令在数据流的最后一行也能正常工作,多行替换命令则会负责短语出现在数据流中间的情况。


5.1.2 多行删除命令
-----------------------------------------------------------------------------------------------------------------------------------------
sed 编辑器用单行删除命令(d)来删除模式空间中的当前行。但和 N 命令一起使用时,使用单行删除命令就要小心了。

示例:
    [devalone@devalone 21]$ cat data4.txt
    On Tuesday, the Linux System
    Administrator's group meeting will be held.
    All System Administrators should attend.

    [devalone@devalone 21]$ sed 'N; /System\nAdministrator/d' data4.txt
    All System Administrators should attend.
    [devalone@devalone 21]$

删除命令会在不同的行中查找单词 System和 Administrator,然后在模式空间中将两行都删掉。这未必是我们想要的结果。

sed 编辑器提供了多行删除命令 D,它只删除模式空间中的第一行。该命令会删除到换行符(含换行符)为止的所有字符。

示例:
    [devalone@devalone 21]$ sed 'N; /System\nAdministrator/D' data4.txt
    Administrator's group meeting will be held.
    All System Administrators should attend.

文本的第二行被 N 命令加到了模式空间,但仍然完好。如果需要删掉目标数据字符串所在行的前一文本行,它能派得上用场。

示例: 删除数据流中出现在第一行前的空白行

    [devalone@devalone 21]$ cat data5.txt

    This is the header line.
    This is a data line.

    This is the last line.
    [devalone@devalone 21]$

    [devalone@devalone 21]$ sed '/^$/{N; /header/D}' data5.txt
    This is the header line.
    This is a data line.

    This is the last line.

sed 编辑器脚本会查找空白行,然后用 N 命令来将下一文本行添加到模式空间。如果新的模式空间内容含有单词 header,则 D 命令会删除模式空间中的
第一行。如果不结合使用 N 命令和 D 命令,就不可能在不删除其他空白行的情况下只删除第一个空白行。


5.1.3 多行打印命令
-----------------------------------------------------------------------------------------------------------------------------------------
多行打印命令(P)沿用了同样的方法。它只打印多行模式空间中的第一行。这包括模式空间中直到换行符为止的所有字符。当用 -n 选项来阻止脚本输出时,
它和显示文本的单行 p 命令的用法大同小异。

示例:
    [devalone@devalone 21]$ cat data3.txt
    On Tuesday, the Linux System
    Administrator's group meeting will be held.
    All System Administrators should attend.
    Thank you for your attendance.
    
    [devalone@devalone 21]$ sed -n 'N; /System\nAdministrator/P' data3.txt
    On Tuesday, the Linux System

当多行匹配出现时,P 命令只会打印模式空间中的第一行。多行 P 命令的强大之处在和 N 命令及 D 命令组合使用时才能显现出来。

D 命令的独特之处在于强制 sed 编辑器返回到脚本的起始处,对同一模式空间中的内容重新执行这些命令(它不会从数据流中读取新的文本行)。在命令脚
本中加入 N 命令,就能单步扫过整个模式空间,将多行一起匹配。

接下来,使用 P 命令打印出第一行,然后用 D 命令删除第一行并绕回到脚本的起始处。一旦返回,N 命令会读取下一行文本并重新开始这个过程。这个循环
会一直继续下去,直到数据流结束。


5.2 保持空间
-----------------------------------------------------------------------------------------------------------------------------------------
模式空间(pattern space)是一块活跃的缓冲区,在 sed 编辑器执行命令时它会保存待检查的文本。但它并不是 sed 编辑器保存文本的唯一空间。

sed 编辑器有另一块称作保持空间(hold space)的缓冲区域。在处理模式空间中的某些行时候,可以用保持空间来临时保存一些行。有 5 条命令可用来操作
保持空间,如下表所示:

    sed 编辑器的保持空间命令
    +-------+---------------------------------------------------------------------
    | 命 令    | 描 述
    +-------+---------------------------------------------------------------------
    | h        | 将模式空间复制到保持空间
    +-------+---------------------------------------------------------------------
    | H        | 将模式空间附加到保持空间
    +-------+---------------------------------------------------------------------
    | g        | 将保持空间复制到模式空间
    +-------+---------------------------------------------------------------------
    | G        | 将保持空间附加到模式空间
    +-------+---------------------------------------------------------------------
    | x        | 交换模式空间和保持空间的内容
    +-------+---------------------------------------------------------------------

这些命令用来将文本从模式空间复制到保持空间。这可以清空模式空间来加载其他要处理的字符串。

通常,在使用 h 或 H 命令将字符串移动到保持空间后,最终还要用 g、G 或 x 命令将保存的字符串移回模式空间(否则就不用在一开始考虑保存它们了)。

由于有两个缓冲区域,弄明白哪行文本在哪个缓冲区域有时会比较麻烦。

示例: 用 h 和 g 命令来将数据在 sed 编辑器缓冲空间之间移动

    [devalone@devalone 21]$ cat data2.txt
    This is the header line.
    This is the first data line.
    This is the second data line.
    This is the last line.
    [devalone@devalone 21]$
    
    [devalone@devalone 21]$ sed -n '/first/{h; p; n; p; g; p}' data2.txt
    This is the first data line.
    This is the second data line.
    This is the first data line.
    [devalone@devalone 21]$

分析:
    (1) sed 脚本在地址中用正则表达式来过滤出含有单词first的行;
    (2) 当含有单词 first 的行出现时,h 命令将该行放到保持空间;
    (3) p 命令打印模式空间也就是第一个数据行的内容;
    (4) n 命令提取数据流中的下一行(This is the second data line),并将它放到模式空间;
    (5) p 命令打印模式空间的内容,现在是第二个数据行;
    (6) g 命令将保持空间的内容(This is the first data line)放回模式空间,替换当前文本;
    (7) p 命令打印模式空间的当前内容,现在变回第一个数据行了。

通过使用保持空间来回移动文本行,可以强制输出中第一个数据行出现在第二个数据行后面。

示例:
    [devalone@devalone 21]$ sed -n '/first/ {h; n; p; g; p }' data2.txt
    This is the second data line.
    This is the first data line.


5.3 排除命令
-----------------------------------------------------------------------------------------------------------------------------------------
感叹号命令(!)用来排除(negate)命令,也就是让原本会起作用的命令不起作用。

示例:
    [devalone@devalone 21]$ sed -n '/header/!p' data2.txt
    This is the first data line.
    This is the second data line.
    This is the last line.
    
    [devalone@devalone 21]$ sed -n '/header/p' data2.txt
    This is the header line.


普通 p 命令只打印 data2.txt 文件中包含单词 header 的那行。加了感叹号之后,情况就相反了:除了包含单词 header 那一行外,文件中其他所有的行都
被打印出来了。

感叹号在有些应用中用起来很方便。之前演示了一种情况:sed 编辑器无法处理数据流中最后一行文本,因为之后再没有其他行了。可以用感叹号来解决这个
问题。

示例:
    [devalone@devalone 21]$ sed 'N
    > s/System\nAdministrator/Desktop\nUser/
    > s/System Administrator/Desktop User/
    > ' data4.txt
    On Tuesday, the Linux Desktop
    User's group meeting will be held.
    All System Administrators should attend.

    [devalone@devalone 21]$ sed '$!N
    s/System\nAdministrator/Desktop\nUser/
    s/System Administrator/Desktop User/
    ' data4.txt
    On Tuesday, the Linux Desktop
    User's group meeting will be held.
    All Desktop Users should attend.

这个例子演示了如何配合使用感叹号与 N 命令以及与美元符特殊地址。美元符表示数据流中的最后一行文本,所以当 sed 编辑器到了最后一行时,它没有
执行 N 命令,但它对所有其他行都执行了这个命令。

使用这种方法,可以反转数据流中文本行的顺序。要实现这个效果(先显示最后一行,最后显示第一行),需要利用保持空间做一些特别的铺垫工作。

    (1) 在模式空间中放置一行;
    (2) 将模式空间中的行放到保持空间中;
    (3) 在模式空间中放入下一行;
    (4) 将保持空间附加到模式空间后;
    (5) 将模式空间中的所有内容都放到保持空间中;
    (6) 重复执行第(3)~(5)步,直到所有行都反序放到了保持空间中;
    (7) 提取并打印行。

在使用这种方法时,不想在处理时打印行。这意味着要使用 sed 的 -n 命令行选项。下一步是决定如何将保持空间文本附加到模式空间文本后面。这可以用
G 命令完成。唯一的问题是不想将保持空间附加到要处理的第一行文本后面。这可以用感叹号命令轻松解决:

    1!G

下一步就是将新的模式空间(含有已反转的行)放到保持空间。这也非常简单,只要用 h 命令就行。

将模式空间中的整个数据流都反转了之后,要做的就是打印结果。当到达数据流中的最后一行时,就知道已经得到了模式空间的整个数据流。打印结果要用
下面的命令:

    $p

示例:
    [devalone@devalone 21]$ cat data2.txt
    This is the header line.
    This is the first data line.
    This is the second data line.
    This is the last line.
    [devalone@devalone 21]$ sed -n '{1!G; h; $p}' data2.txt
    This is the last line.
    This is the second data line.
    This is the first data line.
    This is the header line.


5.4 改变流
-----------------------------------------------------------------------------------------------------------------------------------------
通常,sed 编辑器会从脚本的顶部开始,一直执行到脚本的结尾(D 命令是个例外,它会强制sed编辑器返回到脚本的顶部,而不读取新的行)。sed 编辑器
提供了一个方法来改变命令脚本的执行流程,其结果与结构化编程类似。


5.4.1 分支
-----------------------------------------------------------------------------------------------------------------------------------------
sed 编辑器提供了一种方法,可以基于地址、地址模式或地址区间排除一整块命令。这允许只对数据流中的特定行执行一组命令。

分支(branch)命令 b 的格式如下:

    [address]b [label]

address 参数决定了哪些行的数据会触发分支命令。label 参数定义了要跳转到的位置。如果没有加 label 参数,跳转命令会跳转到脚本的结尾。

示例:
    [devalone@devalone 21]$ cat data2.txt
    This is the header line.
    This is the first data line.
    This is the second data line.
    This is the last line.
    [devalone@devalone 21]$ sed '{2,3b; s/This is/Is this/; s/line./test?/}' data2.txt
    Is this the header test?
    This is the first data line.
    This is the second data line.
    Is this the last test?

分支命令在数据流中的第 2行和第 3行处跳过了两个替换命令。

要是不想直接跳到脚本的结尾,可以为分支命令定义一个要跳转到的标签。标签以冒号开始,最多可以是7个字符长度。

    :label2

要指定标签,将它加到 b 命令后即可。使用标签允许跳过地址匹配处的命令,但仍然执行脚本中的其他命令。

示例:
    [devalone@devalone 21]$ sed '{/first/b jump1; s/This is the/No jump on/
    > :jump1
    > s/This is the/Jump here on/}' data2.txt
    No jump on header line.
    Jump here on first data line.
    No jump on second data line.
    No jump on last line.

跳转命令指定如果文本行中出现了 first,程序应该跳到标签为 jump1的脚本行。如果分支命令的模式没有匹配,sed编辑器会继续执行脚本中的命令,包括
分支标签后的命令(因此,所有的替换命令都会在不匹配分支模式的行上执行)。

如果某行匹配了分支模式, sed 编辑器就会跳转到带有分支标签的那行。因此,只有最后一个替换命令会执行。

下面例子演示了跳转到 sed 脚本后面的标签上。也可以跳转到脚本中靠前面的标签上,这样就达到了循环的效果:

    [devalone@devalone 21]$ echo "This, is, a, test, to, remove, commas." | sed -n '{
    > :start
    > s/,//1p
    > b start
    > }'
    This is, a, test, to, remove, commas.
    This is a, test, to, remove, commas.
    This is a test, to, remove, commas.
    This is a test to, remove, commas.
    This is a test to remove, commas.
    This is a test to remove commas.

脚本的每次迭代都会删除文本中的第一个逗号,并打印字符串。这个脚本有个问题:它永远不会结束。这就形成了一个无穷循环,不停地查找逗号,直到使用
Ctrl+C 组合键发送一个信号,手动停止这个脚本。

要防止这个问题,可以为分支命令指定一个地址模式来查找。如果没有模式,跳转就应该结束:

    [devalone@devalone 21]$ echo "This, is, a, test, to, remove, commas." | sed -n '{
    :start
    s/,//1p
    /,/b start
    }'
    This is, a, test, to, remove, commas.
    This is a, test, to, remove, commas.
    This is a test, to, remove, commas.
    This is a test to, remove, commas.
    This is a test to remove, commas.
    This is a test to remove commas.

现在分支命令只会在行中有逗号的情况下跳转。在最后一个逗号被删除后,分支命令不会再执行,脚本也就能正常停止了。


5.4.2 测试
-----------------------------------------------------------------------------------------------------------------------------------------
类似于分支命令,测试(test)命令(t)也可以用来改变 sed 编辑器脚本的执行流程。测试命令会根据替换命令的结果跳转到某个标签,而不是根据地址
进行跳转。

如果替换命令成功匹配并替换了一个模式,测试命令就会跳转到指定的标签。如果替换命令未能匹配指定的模式,测试命令就不会跳转。

测试命令使用与分支命令相同的格式:

    [address]t [label]

跟分支命令一样,在没有指定标签的情况下,如果测试成功,sed 会跳转到脚本的结尾。

测试命令提供了对数据流中的文本执行基本的 if-then 语句的一个低成本办法。例如,如果已经做了一个替换,不需要再做另一个替换,可以用测试命令。

示例:
    [devalone@devalone 21]$ sed '{
    > s/first/matched/
    > t
    > s/This is the/No match on/
    > }' data2.txt
    No match on header line.
    This is the matched data line.
    No match on second data line.
    No match on last line.

第一个替换命令会查找模式文本 first。如果匹配了行中的模式,它就会替换文本,而且测试命令会跳过后面的替换命令。如果第一个替换命令未能匹配模式,
第二个替换命令就会被执行。

有了测试命令,就能结束之前用分支命令形成的无限循环。

示例:
    [devalone@devalone 21]$ echo "This, is, a, test, to, remove, commas. " | sed -n '{
    > :start
    > s/,//p
    > t start
    > }'
    This is, a, test, to, remove, commas.
    This is a, test, to, remove, commas.
    This is a test, to, remove, commas.
    This is a test to, remove, commas.
    This is a test to remove, commas.
    This is a test to remove commas.

当无需替换时,测试命令不会跳转而是继续执行剩下的脚本。

 

5.5 模式替代
-----------------------------------------------------------------------------------------------------------------------------------------
已经知道如何在 sed命令中使用模式来替代数据流中的文本。然而在使用通配符时,很难知道到底哪些文本会匹配模式。

举个例子,假如想在行中匹配的单词两边上放上引号。如果只是要匹配模式中的一个单词,那就非常简单:

    [devalone@devalone 21]$ echo "The cat sleeps in his hat." | sed 's/cat/"cat"/'
    The "cat" sleeps in his hat.

但如果在模式中用通配符(.)来匹配多个单词呢?

    [devalone@devalone 21]$ echo "The cat sleeps in his hat." | sed 's/.at/".at"/g'
    The ".at" sleeps in his ".at".

模式字符串用点号通配符来匹配at前面的一个字母。遗憾的是,用于替代的字符串无法匹配已匹配单词中的通配符字符。


5.5.1 & 符号
-----------------------------------------------------------------------------------------------------------------------------------------
sed 编辑器提供了一个解决办法。& 符号可以用来代表替换命令中的匹配的模式。不管模式匹配的是什么样的文本,都可以在替代模式中使用 & 符号来使用
这段文本。这样就可以操作模式所匹配到的任何单词了。

示例:
    [devalone@devalone 21]$ echo "The cat sleeps in his hat." | sed 's/.at/"&"/g'
    The "cat" sleeps in his "hat".

当模式匹配了单词c at,"cat" 就会出现在了替换后的单词里。当它匹配了单词 hat,"hat" 就出现在了替换后的单词中。

5.5.2 替代单独的单词
-----------------------------------------------------------------------------------------------------------------------------------------
& 符号会提取匹配替换命令中指定模式的整个字符串。有时只想提取这个字符串的一部分。

sed 编辑器用圆括号来定义替换模式中的子模式。可以在替代模式中使用特殊字符引用每个子模式。替代字符由反斜线和数字组成。数字表明子模式的位置。
sed 编辑器会给第一个子模式分配字符 \1,给第二个子模式分配字符 \2,依此类推。


    NOTE:
    -------------------------------------------------------------------------------------------------------------------------------------
    当在替换命令中使用圆括号时,必须用转义字符将它们标示为分组字符而不是普通的圆括号。这跟转义其他特殊字符正好相反。

示例:
    [devalone@devalone 21]$ echo "The System Administrator manual" | sed '
    > s/\(System\) Administrator/\1 User/'
    The System User manual

这个替换命令用一对圆括号将单词 System 括起来,将其标示为一个子模式。然后它在替代模式中使用 \1 来提取第一个匹配的子模式。这没什么特别的,
但在处理通配符模式时却特别有用。

如果需要用一个单词来替换一个短语,而这个单词刚好是该短语的子字符串,但那个子字符串碰巧使用了通配符,这时使用子模式会方便很多。

示例:
    [devalone@devalone 21]$ echo "That furry cat is pretty" | sed 's/furry \(.at\)/\1/'
    That cat is pretty

    [devalone@devalone 21]$ echo "That furry hat is pretty" | sed 's/furry \(.at\)/\1/'
    That hat is pretty

在这种情况下,不能用 & 符号,因为它会替换整个匹配的模式。子模式提供了方案,允许将模式中的某部分作为替代模式。

需要在两个或多个子模式间插入文本时,这个特性尤其有用。

示例: 使用子模式在大数字中插入逗号

    [devalone@devalone 21]$ echo "1234567" | sed '{
    > :start
    > s/\(.*[0-9]\)\([0-9]\{3\}\)/\1,\2/
    > t start
    > }'
    1,234,567
    [devalone@devalone 21]$

这个脚本将匹配模式分成了两部分。

    .*[0-9]
    [0-9]{3}

这个模式会查找两个子模式。第一个子模式是以数字结尾的任意长度的字符。第二个子模式是若干组三位数字。如果这个模式在文本中找到了,替代文本会
在两个子模式之间加一个逗号,每个子模式都会通过其位置来标示。这个脚本使用测试命令来遍历这个数字,直到放置好所有的逗号。


5.6 在脚本中使用 sed
-----------------------------------------------------------------------------------------------------------------------------------------


5.6.1 使用包装脚本
-----------------------------------------------------------------------------------------------------------------------------------------
实现 sed 编辑器脚本的过程很烦琐,尤其是脚本很长的话。可以将 sed 命令放到 shell 包装脚本(wrapper)中,不用每次使用时都重新键入整个脚本。
包装脚本充当 sed 编辑器脚本和命令行之间的中间人角色。

在 shell 脚本中,可以将普通的 shell 变量及参数和 sed 编辑器脚本一起使用。

示例:
    [devalone@devalone 21]$ cat reverse.sh
    #!/bin/bash
    # shell wrapper for sed editor script
    # to reverse text file lines.

    sed -n '{ 1!G; h; $p}' $1

脚本用 sed 编辑器脚本来反转数据流中的文本行。它使用 shell 参数 $1 从命令行中提取第一个参数,作为要进行反转的文件名。

运行:
    [devalone@devalone 21]$ ./reverse.sh data2.txt
    This is the last line.
    This is the second data line.
    This is the first data line.
    This is the header line.

现在能在任何文件上轻松使用这个 sed 编辑器脚本,再不用每次都在命令行上重新输入了。


5.6.2 重定向sed 的输出
-----------------------------------------------------------------------------------------------------------------------------------------
默认情况下,sed 编辑器会将脚本的结果输出到 STDOUT 上。可以在 shell 脚本中使用各种标准方法对 sed 编辑器的输出进行重定向。

可以在脚本中用 $() 将 sed 编辑器命令的输出重定向到一个变量中,以备后用。

示例: 使用 sed 脚本来向数值计算结果添加逗号

    [devalone@devalone 21]$ cat fact.sh
    #!/bin/bash
    # Add commas to number in factorial answer
    #
    factorial=1
    counter=1
    number=$1
    #
    while [ $counter -le $number ]
    do
            factorial=$[ $factorial * $counter ]
            counter=$[ $counter + 1 ]
    done
    #
    result=$(echo $factorial | sed '{
    :start
    s/\(.*[0-9]\)\([0-9]\{3\}\)/\1,\2/
    t start
    }')
    #
    echo "The result is $result"

运行:
    [devalone@devalone 21]$ ./fact.sh 20
    The result is 2,432,902,008,176,640,000


在使用普通的阶乘计算脚本后,脚本的结果会被作为 sed 编辑器脚本的输入,它会给结果加上逗号。然后 echo 语句使用这个值产生最终结果。


5.7 创建 sed 实用工具
-----------------------------------------------------------------------------------------------------------------------------------------


5.7.1 加倍行间距
-----------------------------------------------------------------------------------------------------------------------------------------
示例: 向文本文件的行间插入空白行

    [devalone@devalone 21]$ sed 'G' data2.txt
    This is the header line.

    This is the first data line.

    This is the second data line.

    This is the last line.

    [devalone@devalone 21]$
    
这个技巧的关键在于保持空间的默认值。G 命令会简单地将保持空间内容附加到模式空间内容后。当启动 sed 编辑器时,保持空间只有一个空行。将它附加
到已有行后面,就在已有行后面创建了一个空白行。

这个脚本在数据流的最后一行后面也加了一个空白行,使得文件的末尾也产生了一个空白行。如果不想要这个空白行,可以用排除符号(!)和尾行符号 ($)
来确保脚本不会将空白行加到数据流的最后一行后面。

示例:
    [devalone@devalone 21]$ sed '$!G' data2.txt
    This is the header line.

    This is the first data line.

    This is the second data line.

    This is the last line.
    [devalone@devalone 21]$

只要该行不是最后一行,G 命令就会附加保持空间内容。当 sed 编辑器到了最后一行时,它会跳过 G 命令。


5.7.2 对可能含有空白行的文件加倍行间距
-----------------------------------------------------------------------------------------------------------------------------------------
如果文本文件已经有一些空白行,但想给所有行加倍行间距要怎么办呢?如果用前面的脚本,有些区域会有太多的空白行,因为每个已有的空白行也会被加倍。

示例:
    [devalone@devalone 21]$ cat data6.txt
    This is line one.
    This is line two.

    This is line three.
    This is line four.
    [devalone@devalone 21]$ sed '$!G' data6.txt
    This is line one.

    This is line two.

 

    This is line three.

    This is line four.

现在,在原来空白行的位置有了三个空白行。

这个问题的解决办法是,首先删除数据流中的所有空白行,然后用 G 命令在所有行后插入新的空白行。要删除已有的空白行,需要将 d 命令和一个匹配
空白行的模式一起使用:

    /^$/d

这个模式使用了行首符号(^)和行尾符号($)。将这个模式加到脚本中会生成想要的结果。

示例:
    [devalone@devalone 21]$ sed '/^$/d; $!G' data6.txt
    This is line one.

    This is line two.

    This is line three.

    This is line four.


5.7.3 给文件中的行编号
-----------------------------------------------------------------------------------------------------------------------------------------
之前演示了如何用等号来显示数据流中行的行号:

    [devalone@devalone 21]$ sed '=' data2.txt
    1
    This is the header line.
    2
    This is the first data line.
    3
    This is the second data line.
    4
    This is the last line.

行号是在数据流中实际行的上方。比较好的解决办法是将行号和文本放在同一行。

N 命令用来合并行,在 sed 脚本中使用这个命令应该不难。这个工具的技巧在于不能将两个命令放到同一个脚本中。

在获得了等号命令的输出之后,可以通过管道将输出传给另一个 sed 编辑器脚本,它会使用 N 命令来合并这两行。还需要用替换命令将换行符更换成空格
或制表符。最终的解决办法看起来如下:

    [devalone@devalone 21]$ sed '=' data2.txt | sed 'N; s/\n/ /'
    1 This is the header line.
    2 This is the first data line.
    3 This is the second data line.
    4 This is the last line.

有些 bash shell 命令也可以添加行号,但它们会另外加入一些东西(有可能是不需要的间隔)。

示例:

    [devalone@devalone 21]$ nl data2.txt
         1  This is the header line.
         2  This is the first data line.
         3  This is the second data line.
         4  This is the last line.
    [devalone@devalone 21]$ cat -n data2.txt
         1  This is the header line.
         2  This is the first data line.
         3  This is the second data line.
         4  This is the last line.


5.7.4 打印末尾行
-----------------------------------------------------------------------------------------------------------------------------------------
如果只需处理一个长输出(比如日志文件)中的末尾几行,要怎么办呢?

美元符代表数据流中最后一行,所以只显示最后一行很容易:

    [devalone@devalone 21]$ sed -n '$p' data2.txt
    This is the last line.

如何用美元符来显示数据流末尾的若干行呢?答案是创建滚动窗口。

滚动窗口是检验模式空间中文本行块的常用方法,它使用 N 命令将这些块合并起来。N 命令将下一行文本附加到模式空间中已有文本行后面。一旦在模式
空间有了一个10行的文本块,可以用美元符来检查是否已经处于数据流的尾部。如果不在,就继续向模式空间增加行,同时删除原来的行(D 命令会删除模
式空间的第一行)。

通过循环 N 命令和 D 命令,在向模式空间的文本行块增加新行的同时也删除了旧行。分支命令非常适合这个循环。要结束循环,只要识别出最后一行并用
q 命令退出就可以了。

示例:
    [devalone@devalone 21]$ cat data7.txt
    This is line 1.
    This is line 2.
    This is line 3.
    This is line 4.
    This is line 5.
    This is line 6.
    This is line 7.
    This is line 8.
    This is line 9.
    This is line 10.
    This is line 11.
    This is line 12.
    This is line 13.
    This is line 14.
    This is line 15.
    [devalone@devalone 21]$ sed '{
    > :start
    > $q; N; 11, $D
    > b start
    > }' data7.txt
    This is line 6.
    This is line 7.
    This is line 8.
    This is line 9.
    This is line 10.
    This is line 11.
    This is line 12.
    This is line 13.
    This is line 14.
    This is line 15.

这个脚本会首先检查这行是不是数据流中最后一行。如果是,退出(quit)命令会停止循环。N 命令会将下一行附加到模式空间中当前行之后。如果当前行
在第 10 行后面,11,$D 命令会删除模式空间中的第一行。这就会在模式空间中创建出滑动窗口效果。因此,这个 sed 程序脚本只会显示出 data7.txt文件
最后10行。


5.7.5 删除行
-----------------------------------------------------------------------------------------------------------------------------------------
另一个有用的 sed 编辑器工具是删除数据流中不需要的空白行。删除数据流中的所有空白行很容易,但要选择性地删除空白行则需要一点创造力。


■ 删除连续的空白行
-----------------------------------------------------------------------------------------------------------------------------------------
删除连续空白行的最简单办法是用地址区间来检查数据流。sed 编辑器会对所有匹配指定地址区间的行执行该命令。

删除连续空白行的关键在于创建包含一个非空白行和一个空白行的地址区间。如果 sed 编辑器遇到了这个区间,它不会删除行。但对于不匹配这个区间的行
(两个或更多的空白行),它会删除这些行。

下面是完成这个操作的脚本:

    /./,/^$/!d

区间是 /./ 到 /^$/。区间的开始地址会匹配任何含有至少一个字符的行。区间的结束地址会匹配一个空行。在这个区间内的行不会被删除。

示例:
    [devalone@devalone 21]$ cat data8.txt
    This is line one.

 

    This is line two.

    This is line three.

 


    This is line four.
    [devalone@devalone 21]$ sed '/./,/^$/!d' data8.txt
    This is line one.

    This is line two.

    This is line three.

    This is line four.

无论文件的数据行之间出现了多少空白行,在输出中只会在行间保留一个空白行。


■ 删除开头的空白行
-----------------------------------------------------------------------------------------------------------------------------------------
删除数据流顶部的空白行不难。下面是完成这个功能的脚本:

    /./,$!d

这个脚本用地址区间来决定哪些行要删掉。这个区间从含有字符的行开始,一直到数据流结束。在这个区间内的任何行都不会从输出中删除。这意味着含有
字符的第一行之前的任何行都会删除。

示例:
    [devalone@devalone 21]$ cat data9.txt

 

    This is line one.


    This is line two.
    [devalone@devalone 21]$ sed '/./,$!d' data9.txt
    This is line one.


    This is line two.

测试文件在数据行之前有两个空白行。这个脚本成功地删除了开头的两个空白行,保留了数据中的空白行。


■ 删除结尾的空白行
-----------------------------------------------------------------------------------------------------------------------------------------
删除结尾的空白行并不像删除开头的空白行那么容易。就跟打印数据流的结尾一样,删除数据流结尾的空白行也需要花点心思,利用循环来实现。

命令:
    sed '{
    :start
    /^\n*$/{$d; N; b start }
    }'

注意,在正常脚本的花括号里还有花括号。这允许在整个命令脚本中将一些命令分组。该命令组会被应用在指定的地址模式上。

地址模式能够匹配只含有一个换行符的行。如果找到了这样的行,而且还是最后一行,删除命令会删掉它。如果不是最后一行,N 命令会将下一行附加到它
后面,分支命令会跳到循环起始位置重新开始。

示例:
    [devalone@devalone 21]$ cat data10.txt
    This is the first line
    This is the second line

 


    [devalone@devalone 21]$ sed '{
    > :start
    > /^\n*$/{$d; N; b start }
    > }' data10.txt
    This is the first line
    This is the second line

这个脚本成功删除了文本文件结尾的空白行。


5.7.6 删除 HTML 标签
-----------------------------------------------------------------------------------------------------------------------------------------
标准的 HTML Web 页面包含一些不同类型的 HTML 标签,标明了正确显示页面信息所需要的格式化功能。下面是个 HTML 文件的例子:

    [devalone@devalone 21]$ cat data11.txt
    <html>
    <head>
    <title>This is the page title</title>
    </head>
    <body>
    <p>
    This is the <b>first</b> line in the Web page.
    This should provide some <i>useful</i>
    information to use in our sed script.
    </body>
    </html>


HTML 标签由小于号和大于号来识别。大多数 HTML 标签都是成对出现的:一个起始标签(比如<b>用来加粗),以及另一个结束标签(比如</b>用来结束加粗)。

但如果不够小心的话,删除HTML标签可能会带来问题。乍一看,可能认为删除 HTML 标签的办法就是查找以小于号(<)开头、大于号(>)结尾且其中有数据
的文本字符串:

    s/<.*>//g

很遗憾,这个命令会出现一些意料之外的结果:

    [devalone@devalone 21]$ sed 's/<.*>//g' data11.txt

 

 


    This is the  line in the Web page.
    This should provide some
    information to use in our sed script.


    [devalone@devalone 21]$

注意,标题文本以及加粗和倾斜的文本都不见了。sed 编辑器将这个脚本忠实地理解为小于号和大于号之间的任何文本,且包括其他的小于号和大于号。
每次文本出现在 HTML 标签中(比如<b>first</b>),这个 sed 脚本都会删掉整个文本。

这个问题的解决办法是让 sed 编辑器忽略掉任何嵌入到原始标签中的大于号。要这么做的话,可以创建一个字符组来排除大于号。脚本改为:

    s/<[^>]*>//g
    
这个脚本现在能够正常工作了,它会显示要在 Web 页面 HTML 代码里看到的数据:

    [devalone@devalone 21]$ sed 's/<[^>]*>//g' data11.txt


    This is the page title

 

    This is the first line in the Web page.
    This should provide some useful
    information to use in our sed script.


    [devalone@devalone 21]$

要想看起来更清晰一些,可以加一条删除命令来删除多余的空白行:

    [devalone@devalone 21]$ sed 's/<[^>]*>//g; /^$/d' data11.txt
    This is the page title
    This is the first line in the Web page.
    This should provide some useful
    information to use in our sed script.

现在紧凑多了,只有想要看的数据。

 

系列目录:

Linux shell 脚本编程-高级篇 (一)

Linux shell 脚本编程-高级篇 (二)

Linux shell 脚本编程-高级篇 (三)

Linux shell 脚本编程-高级篇 (四)

Linux shell 脚本编程-高级篇 (五)

Linux shell 脚本编程-高级篇 (六)

Linux shell 脚本编程-高级篇 (七)

 

 

-----------------------------------------------------------------------------------------------------------------------------------------
参考:

    《Linux 命令行与 shell 脚本编程大全》 第 3 版 —— 2016.8(美)Richard Blum  Cristine Bresnahan

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章