sed & awk 第二版学习(四)—— 基本 sed 命令

2024-09-10 08:37:47 浏览数 (2)

sed 命令集由 25 个命令组成。

1. sed 命令的语法

代码语言:javascript复制
# 双地址命令语法
[address]command

# 单地址命令语法
[line-address]command

# 同一地址多个命令语法
address {
command1
command2
command3
}

第一个命令可以和左大括号放置在同一行,但是右大括号必须自己单独处于一行。每个命令都可以有自己的地址并允许有多层分组。而且就像命令在大括号内的缩进方式一样,允许在行的开始处插入空格或制表符。要确保在大括号后没有空格。

2. 注释

使用注释作为脚本文档往往非常有效。

代码语言:javascript复制
# 注释语法
#[n]

可以在脚本的任何地方放置注释,甚至是跟在命令行的后面。注释行的第一个字符必须是“#”号。如果跟在 # 后面的第一个字符是 n,那么脚本不会自动产生输出,这和指定命令行选项 -n 是等价的。跟在 n 后面的其余的内容被看做是注释。在 POSIX 标准中,采用这种方式的 #n 必须是文件的前两个字符。

3. 替换

代码语言:javascript复制
# 语法
[address]s/pattern/replacement/flags

替换命令应用于与 address 匹配的行。如果没有指定地址,那么就应用于与 pattern 匹配的所有行。正则表达式可以使用“n”来匹配嵌入的换行符。

在 replacement 部分,只有下列字符有特殊含义:

  • &:用正则表达式匹配的内容进行替换。
  • n:匹配第 n 个子串(n 是一个数字),这个子串以前在 pattern 中用“(”和“)”指定。
  • :当在替换部分包含“与”符号(&),反斜杠()或替换命令的定界符时可以用 转义它们。另外,它用于转义换行符并创建多行 replacement 字符串。

修饰替换的标志 flags 是:

  • n:1 到 512 之间的一个数字,表示对模式的第 n 次出现进行替换。
  • g:对模式空间的所有匹配进行全局替换。没有 g 时只替换第一个匹配。
  • p:打印模式空间的内容。
  • w file:将模式空间的内容写到文件 file 中。

flag 可以组合使用,例如 gp 表示进行全局替换并打印这一行。

看一个替换元字符的例子:将 .Ah "Major Heading" 替换为:

代码语言:javascript复制
@A HEAD = Major Heading

这个问题中的难点是这一行需要前后都有空行,这是一个编写多行替换字符串的问题。命令脚本 sedscr 如下:

代码语言:javascript复制
/^.Ah/{
s/.Ah */
@A HEAD = /
s/"//g
s/$/
/
}

第一个替换命令用一个换行符和“@A HEAD =”取代“.Ah”,在行结尾处有必要用反斜杠转义换行符。第二个替换删除了引号。最后一个命令匹配模式空间中的行的结尾(不是嵌入的换行符),并在它后面添加一个换行符。执行结果如下:

代码语言:javascript复制
$ echo .Ah "Major Heading" | sed -f sedscr 

@A HEAD = Major Heading

下一个例子是将 ORA 替换为 O'Reilly & Associates, Inc.:

代码语言:javascript复制
$ echo ORA | sed "s/ORA/O'Reilly & Associates, Inc./g"
O'Reilly & Associates, Inc.

如果不对 & 转义,那么输出结果为 O'Reilly ORA Associates, Inc.:

代码语言:javascript复制
$ echo ORA | sed "s/ORA/O'Reilly & Associates, Inc./g"
O'Reilly ORA Associates, Inc.

再看一个转义反斜杠的例子,将 on the UNIX Operating System. 替换为 on the s-2UNIXs0 Operating System.:

代码语言:javascript复制
$ echo "on the UNIX Operating System." | sed 's/UNIX/\s-2&\s0/g'
on the s-2UNIXs0 Operating System.

因为反斜杠也是替换字符串的元字符,所以需要用两个反斜杠输出一个反斜杠。替换字符串中的“&”表示“UNIX”。& 允许指定一个可变的替换字符串,该字符串是与实际内容匹配的字符串。下面是两个应用场景。

在行头加字符串:

代码语言:javascript复制
$ echo "keys" | sed 's/^/unlink &/g'
unlink keys

给匹配字符串加括号:

代码语言:javascript复制
$ echo "See Section 12.9" | sed 's/See Section [1-9][0-9]*.[1-9][0-9]*/(&)/'
(See Section 12.9)

在 sed 中转义的圆括号括住正则表达式的任意部分并且保存它以备回调,一行最多允许“保存” 9 次。“n”用于回调被保存的匹配部分,n 是 1 到 9 的数字,用于引用特殊的“保存的”备用字符串。可以使用这种技术匹配行的内容并交换它们。

代码语言:javascript复制
$ cat test1
first:second
one:two
$ sed 's/(.*):(.*)/2:1/' test1
second:first
two:one

书中还举了一个校正索引条目的例子,实际上是用内容如下的 index.edit 脚本生成一系列 sed 命令,然后在根据需要再进行手工加工。

代码语言:javascript复制
#! /bin/sh
# index.edit -- compile list of index entries for editing.
grep "^.XX" $* | sort -u | sed 's/^.XX (.*)$//^\.XX /s/1/1//'

shell 脚本 index.edit 使用 grep 从命令行上指定的任意数量的文件中,提取包含索引条目(以 .XX 开头)的所有行。它将列表传递给 sort,sort 使用 -u 选项来排序和删除重复的条目。然后这个列表被输送到 sed,这行 sed 脚本则构建一个替换命令。

sed 脚本的模式正则表达式:

代码语言:javascript复制
^.XX (.*)$

它匹配整个行,并保存索引条目以备回调。下面是替换字符串:

代码语言:javascript复制
/^\.XX /s/1/1/

它产生以地址开头的替换命令:地址开始为斜杠,然后是两个反斜杠以输出一个反斜杠,转义跟在后面的“.XX”中的句点。然后是一个空格,接着是另一个反斜杠以结束地址。接下来输出后面跟有斜杠的“s”,然后回调被保存的部分用来作为正则表达式。这后面跟着一个斜杠并且再次调用保存的子串并将它作为替换字符串。最后用一个斜杠结束这个命令。

当 index.edit 脚本在文件上运行时,创建类似下面的一个清单:

代码语言:javascript复制
$ index.edit ch05
/^.XX /s/"append command(a)"/"append command(a)"/
/^.XX /s/"change command"/"change command"/
/^.XX /s/"change command(c)"/"change command(c)"/
/^.XX /s/"commands:sed, summary of"/"commands:sed, summary of"/
/^.XX /s/"delete command(d)"/"delete command(d)"/
/^.XX /s/"insert command(i)"/"insert command(i)"/
/^.XX /s/"line numbers:printing"/"line numbers:printing"/
/^.XX /s/"list command(l)"/"list command(l)"/

这个输出可以被捕获到一个文件中,然后可以删除不需要改变的条目,或通过编辑替换字符串来完成修改。最终可以将这个文件作为 sed 脚本来纠正所有文档中的索引条目。此程序还应该在索引中的普通文字中查找元字符并将其转义为普通字符,这需要使用下篇介绍的高级命令。

4. 删除

删除命令采用一个地址,如果行匹配这个地址就删除模式空间的内容。删除命令还是一个可以改变脚本中的控制流的命令。这是因为一旦执行这个命令,那么在“空的”模式空间中就不会再有命令执行,即不允许在被删除的行上进行进一步操作。删除命令会导致读取新的输入行,而编辑脚本则从头开始新的一轮。

d 命令删除整行,而不只是删除行中匹配的部分。要删除行的一部分,可以使用替换命令并制定一个空的替换。

删除空行:

代码语言:javascript复制
/^$/d

删除某些 troff 请求:

代码语言:javascript复制
/^.sp/d
/^.bp/d
/^.nf/d
/^.fi/d

5. 追加、插入和更改

插入(i)命令将所提供的文本放置在模式空间的当前行之前。追加(a)命令将文本放置在当前行之后。更改(c)命令用所提供的文本取代模式空间的内容。

在 SQL 文件第一行前插入两行设置文本和一个空行,在最后追加一个空行和一行提交命令:

代码语言:javascript复制
$ cat insert.sql
insert into t1 values(1);
insert into t1 values(2);
insert into t1 values(3);
$ sed -e '1i set names utf8mb4;nset autocommit=0;n' -e '$a \ncommit;' insert.sql
set names utf8mb4;
set autocommit=0;

insert into t1 values(1);
insert into t1 values(2);
insert into t1 values(3);

commit;

追加命令和插入命令只应用于单个地址,而更改命令可以处理一个范围内的行。在这种情况下,它删除这个范围中的所有行,但只输出一次提供的文本。

代码语言:javascript复制
$ cat -n a.txt
     1    From aaa
     2    bbb
     3    ccc
     4    
$ sed '/^From /,/^$/c <Mail Header Removed>' a.txt
<Mail Header Removed>

更改命令用所提供的文本取代模式空间的内容。实际上,它删除当前行并且在该位置放置所提供的文本。当想要匹配行并且整体取代它时可以使用这个命令。例如有如下文本:

代码语言:javascript复制
.sp 1.5
.sp
.sp 1
.sp 1.5v
.sp .3v
.sp 3

想要将所有的参数都换成“.5”:

代码语言:javascript复制
#echo -e ".sp 1.5n.spn.sp 1n.sp 1.5vn.sp .3vn.sp 3" | sed '/^.sp/c .sp 0.5'
.sp 0.5
.sp 0.5
.sp 0.5
.sp 0.5
.sp 0.5
.sp 0.5

当更改命令作为一组命令之一被封闭在大括号中,并作用于一个范围内的行时,它将对这个范围内的每一行输出。

代码语言:javascript复制
$ sed '/^From /,/^$/{c <Mail Header Removed>
> }' a.txt
<Mail Header Removed>
<Mail Header Removed>
<Mail Header Removed>
<Mail Header Removed>

更改命令清除模式空间,它在模式空间中与删除命令有相同的效果。脚本中在更改命令之后的其它命令不被应用。插入命令和追加命令不影响模式空间的内容,后续命令不影响该文本,所提供的文本也不影响 sed 的内部行计数器。

6. 列表

列表命令(l)用于显示模式空间的内容,将非打印字符显示为两个数字的 ASCII 代码。可以使用该命令检测输入中的“不可见”字符。在 sed 中不能用 ASCII 值匹配字符,也不能匹配八进制数值,但 awk 可以完成这些。

代码语言:javascript复制
$ cat test/spchar
Here is a string of special characters: ^A ^B
^M ^G
$ sed -n -e "l" test/spchar
Here is a string of special characters: 1 2
15 7
$ # test with GNU sed too
$ gsed -n -e "l" test/spchar
Here is a string of special characters: 1 2
r a

(另外,sed 能匹配十六进制数表示的 ASCII 字符:)

代码语言:javascript复制
$ echo 'aaa' | sed 's/x61/b/g'
bbb

7. 转换

转换命令(y)将源中出现的模式空间中的字符转换为目标中的相应字符,语法如下:

代码语言:javascript复制
[address]y/source/dest/

替换根据字符的位置来进行。这个命令一个可能的应用是全转大写或小写字母:

代码语言:javascript复制
$ echo "1a2b3c" | sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/'
1A2B3C

这个命令影响整个模式空间的所有内容。如果想在输入行上转换单个单词,那么通过使用保持空间可以完成。大致过程是:输出要更改单词的那一行之前的所有行,删除这些行,将单词后面的行复制到保持空间,转换这个单词,然后将保持空间的内容追加到模式空间。

8. 打印

打印命令(p)输出模式空间的内容,它既不清除模式空间也不改变脚本中的控制流。可以在行改变之前和之后打印行来进行调试。

代码语言:javascript复制
$cat sed.debug 
#n Print line before and after changes.
/^.Ah/{
p
s/"//g
s/^.Ah //p
}

#n 和指定命令行选项 -n 等价,抑制行的默认输出。打印标志被提供给替换命令。替换命令的打印标志不同于打印命令,因为它以成功的替换为条件。下面是运行该脚本的一个例子,每个受影响的行都被打印了两次,分别是行改变前和改变后的内容:

代码语言:javascript复制
$ cat ch05 
.Ah "Comment"
.Ah "Substitution"
.Ah "Delete"
.Ah "Append, Insert and Change"
.Ah "List"

$ sed -f sed.debug ch05
.Ah "Comment"
Comment
.Ah "Substitution"
Substitution
.Ah "Delete"
Delete
.Ah "Append, Insert and Change"
Append, Insert and Change
.Ah "List"
List

9. 打印行号

跟在地址后面的等号(=)打印被匹配的行的行号。除非抑制行的自动输出(-n),行号和行本身将被打印。这个命令不能对一个范围内的行进行操作。程序员也许用该命令打印源文件中的某些行,例如打印制表符后跟有“if”的行号和行本身:

代码语言:javascript复制
$ sed '/tif/=' random.c
192
    if( rand_type == TYPE_0 ) {
234
    if( rand_type == TYPE_0 ) state[ -1 ] = rand_type;
236
    if( n < BREAK_1 ) {
252
    if( n < BREAK_3 ) {
274
    if( rand_type == TYPE_0 ) state[ -1 ] = rand_type;
303
    if( rand_type == TYPE_0 ) state[ -1 ] = rand_type;

在寻找由编译器报告的问题时,行号非常有用,因为编译的错误信息通常会列出行号。

10. 下一步

下一步(n)命令输出模式空间的内容,然后读取输入的下一行。它总是在读入新行之后从脚本的顶端开始。next 命令改变了正常的流控制(直到到达脚本的底部才会输出模式空间的内容)。实际上,next 命令导致输入的下一行取代模式空间的当前行。脚本中的后续命令应用于替换后的行。如果没有抑制默认输出,那么在替换发生之前会打印当前行。 在下面的例子中,当空行跟随一个匹配模式的行时,则删除该空行。

示例文件:

代码语言:javascript复制
.H1 "On Egypt"

Napoleon, pointing to the Pyramids, said to his troops:
"Soldiers, forty centuries have their eyes upon you."

删除脚本:

代码语言:javascript复制
/^.H1/{
n
/^$/d
}

该脚本匹配任何以字符串“.H1”开始的行,然后打印该行并读入下一行。如果那一行为空则删除它。大括号用于在同一地址应用多个命令。

执行结果:

代码语言:javascript复制
$ sed '/^.H1/{
n
/^$/d
}' sample
.H1 "On Egypt"
Napoleon, pointing to the Pyramids, said to his troops:
"Soldiers, forty centuries have their eyes upon you."

在较长的脚本中,必须记住出现在 n 命令之前的命令不会应用于新的输入行,而且出现在 n 后面的命令不应用于旧的输入行。

11. 读、写文件 读(r)和写(w)命令用于直接处理文件。这两个命令都只有文件名一个参数。语法如下:

代码语言:javascript复制
[line-address]r file
[address]w file

读命令将由 file 指定的文件内容在确定的行之后读入模式空间,它不能对一个范围内的行进行操作。写命令将模式空间的内容写到 file 中。 如果文件不存在,读命令也不会报错。如果写命令中指定的文件不存在,将创建一个文件;如果文件已存在,那么写命令将在每次调用脚本时改写它。如果一个脚本中有多个指令写到同一个文件,那么每个写命令都将内容追加到这个文件中。而且,每个脚本最多只能打开 10 个文件。

读命令对于将一个文件的内容插入到另一个文件中的特定位置很有用。看下面的命令:

代码语言:javascript复制
/^<Company-list>/r company.list

当 sed 匹配字符串“<Company-list>”开始的行时,它将文件 company.list 的内容附加在被匹配行的末尾。

代码语言:javascript复制
$ cat a.txt 
For service, contact any of the following companies:
<Company-list>
Thank you.
$ cat company.list 
        Allied
        Mayflower
        United
$ sed '/^<Company-list>/r company.list' a.txt 
For service, contact any of the following companies:
<Company-list>
        Allied
        Mayflower
        United
Thank you.

后面的命令不会影响从这个文件中读取的行。然而,寻址初始行的命令将会起作用:

代码语言:javascript复制
$ sed -e '/^<Company-list>/r company.list' -e '/^<Company-list>/d' a.txt
For service, contact any of the following companies:
        Allied
        Mayflower
        United
Thank you.

使用 -n 选项或 #n 脚本语法可以取消自动输出,阻止模式空间的厨师帽行被输出,但是读命令的结果仍然转到标准输出。

代码语言:javascript复制
$ sed -n -e '/^<Company-list>/r company.list' a.txt
        Allied
        Mayflower
        United

下面看个写命令的例子。示例文件 sample 内容如下:

代码语言:javascript复制
Adams, Henrietta    Northeast
Banks, Freda        South
Dennis, Jim         Midwest
Garvey, Bill        Northeast
Jeffries, Jane      West
Madison, Sylvia     Midwest
Sommes, Tom         South

使用 sed 一步将文件按地区分成 4 个独立的文件。脚本 sedscr 如下:

代码语言:javascript复制
/Northeast$/w region.northeast
/South$/w region.south
/Midwest$/w region.midwest
/West$/w region.west

执行结果如下:

代码语言:javascript复制
$ sed -n -f sedscr sample
$ cat region.midwest 
Dennis, Jim        Midwest
Madison, Sylvia    Midwest
$ cat region.northeast 
Adams, Henrietta   Northeast
Garvey, Bill       Northeast
$ cat region.south 
Banks, Freda       South
Sommes, Tom        South
$ cat region.west 
Jeffries, Jane     West

写命令在被调用时就写出模式空间的内容,而不是等到到达脚本的结尾时才进行写操作。修改脚本如下,在写到文件之前删除地区名字:

代码语言:javascript复制
/Northeast$/{
s///
w region.northeast
}
/South$/{
s///
w region.South
}
/Midwest$/{
s///
w region.Midwest
}
/West$/{
s///
w region.West
}

替换命令匹配与地址相同的模式并删除它。写命令的应用之一是可以在脚本中使用它来生成同一源文件的几个自定义版本。

12. 退出

退出(q)命令会使 sed 停止读取新的输入行,并停止将它们发送到输出。它只适用于单行地址,一旦找到和地址匹配的行,脚本就结束。在将编辑操作写回到原始文件的任何程序中不要使用 q 命令。在执行 q 命令后,就不会再产生输出。在想要编辑文件的前一部分并保存剩余部分不改变的情况下,不要使用 q 命令。

使用退出命令从文件中打印前 100 行:

代码语言:javascript复制
sed '100q' test

它打印每一行,直到到达行 100 并且退出。在这点上,该命令的功能与 UNIX 的 head 命令类似。q 命令的另一个可能得用法是在从文件中提取了想要的内容后退出脚本。在 sed 已经找到它寻找的东西之后继续扫描庞大的文件是相当低效的。因此可以按照下面的方式在 getmac shell 脚本中修订这个 sed 脚本:

代码语言:javascript复制
sed -n "
/^.de *$mac/,/^..$/{
p
/^..$/q
}" $file

当 sed 找到了要寻找的宏的结尾(这一行本身在第一个宏定义结束的地方终止脚本),程序当即退出,并且不再继续遍历文件的剩余部分寻找其它可能的匹配。如果比较下面的两个 shell 脚本,就会发现第一个脚本比第二个更有效率。下面这个简单的 shell 程序打印文件的前 10 行,然后退出:

代码语言:javascript复制
for file
do
    sed 10q $file
done

下面的脚本也打印前 10 行,它采用打印命令并抑制默认输出:

代码语言:javascript复制
for file
do
    sed -n 1,10p $file
done

0 人点赞