首页 > 代码库 > 流编辑器 SED 十分钟入门全教程

流编辑器 SED 十分钟入门全教程

这里借用一下酷壳网sed博文的图来开题,超赞的~~


1. sed 简介及原理简析

1.1 sed 简介

Sed 是什么?相信很多人都有所了解,sed 全称StreamEDitor 即流编辑器。生于1973年or 1974年by 贝尔实验室的 Lee E. McMahon(已故),是基于交互式编辑器ed("editor", 1971)的脚本功能及更早的qed(quick editor ,1965-1966)(Sed 比 awk 要大那么几岁,所以客官莫急,过几天我们再来详解 awk)。Sed 是最早支持正则表达式的工具之一,并且至今仍然被人们用做文本处理,特别是在其强大的替代命令(sed中最常用的s命令,下文会有详述!)。


Sed 是一个脚本型的编译器,是非交互式的编辑器,也就是说sed与常见的编译器不同(比如说vim),sed没有交互式的编辑界面以及光标移动或者庞大的快捷键/功能,sed 的使用就是很简单的一个脚本行,相当极客吧?



1.2 sed 原理简析

Sed 从系统的标准输入或者文本获取输入,经过处理之后输出到系统的标准输出(屏幕)

那么其处理过程是什么样的呢?Sed 是流编辑器,它一次只处理一行信息,也就是说sed 以行为处理单位,每次从标准输入/文本获取一行信息,存储到其“ 模式空间 ”(pattern space,实际上是一个临时缓冲区)中,在这个模式空间中,sed 就会将脚本中的处理命令做完,然后就将处理完的数据输出到标准输出(屏幕)。然后sed 再获取下一行至模式空间处理,这样循环直至文件末尾。


这里说的只是 sed 的一个简单的工作流程,也有可能有别的情况发生,比如限定行,重定向标准输出等。此外对于 sed 还有其他的讨论在第三节进阶中。


1.3 sed 的特点

sed 作为编辑器有如下特点让人难以拒绝:

(1) 非交互,基于模式匹配的过滤及修改文本

(2) 逐行处理,所以那些对舒适的交互式编辑而言太大的文件使用sed 会显得格外有优势

(3) 可实现对文本的输出、删除、替换、复制、剪切、导入、导出等各种编辑操作

(4) 脚本化,在shell 脚本编程中你总不能打开vim 吧?


2. 系统性讲解 sed 的常用函数

在UNIX 系操作系统,遇到不会的,你首先想到的不是Google,不是找书,最简单方便的方法是查找学习其manpage! 

那么我们来看一下 sed 使用:

                       sed [OPTION]... {script-only-if-no-other-script} [input-file]...

所以对于一个 sed 命令主要有四个字段:

第一个是sed (擦,你不废话么?);

第二是  [OPTION] 字段,用于指定后面的脚本字段的工作模式;

第三是  {script...} 字段,干哈的?当然是如何处理的命令部分;

第四个是输入文件字段!

下面我们对sed 命令行中最重要的第二和第三个字段展开讨论!


2.1 sed 的简单使用

上文介绍sed 运行原理时,我们说到 sed 的输入是标准输入 或 文本文件!

所以sed 命令的使用也是可以有两种方式的:

(1) $ echo "hello,world" | sed "s/hello/hi/"
      hi,world

(2) $ echo "hello,world" > file
      $ sed ‘s/hello/hi/‘ file
      hi,world


2.2 sed 命令之 OPTION 字段选项

在sed 的 manpage 中,对于OPTION字段的所有选项都简单地使用一句话描述,那么这显然是不够的,下文我们一个一个讨论:


2.2.1 选项 -n

manpage 中对于 -n 选项是这么描述的:

 -n, --quiet, --silent
              suppress automatic printing of pattern space  即关闭模式空间的自动打印

所以上文说到 sed 在模式空间中处理好一行文本之后,它就直接打印到屏幕上(再不赘述标准输出了阿),那么使用-n 选项后,sed 命令做完工作也不打印信息!那么这有什么用?在2.3 中我们介绍到 p 函数再结合它来介绍!


2.2.2 选项 -e

manpage 中对于 -e 选项是这么描述的:

-e script, --expression=script

              add the script to the commands to be executed   即添加将要执行的脚本命令

使用 -e 选项后接 将要执行的命令,这也是sed 的最常用的形式,即:  sed  -e   ‘ ... ‘  input_file


2.2.3 选项 -f

manpage 中对于 -f 选项是这么描述的:

-f script-file, --file=script-file
              add the contents of script-file to the commands to be executed   即添加脚本文件的内容来执行

作为脚本型编辑器,sed 当然也支持使用较本文件咯,下面的小例子我们也可以清晰的看出sed 的 -f 执行sed 脚本文件的方法:

$ echo "hello,world" > hi
$ cat a.sed
s/hello/hi/
$ sed -f a.sed hi 
hi,world



2.2.4 选项 -i

manpage 中对于 -i 选项是这么描述的:

-i[SUFFIX], --in-place[=SUFFIX]
              edit files in place (makes backup if extension supplied)   --  直接编辑原文件(尽量添加备份)

这里需要详述一下,前面提到的sed 工作原理:sed 获取一行--> 处理--> 打印,我们可以看到,sed 所作的只是将命令执行后打印,而没有保存到任何地方,所以如果我们需要保存处理后的内容,有两种方法:

(1) 重定向

使用Linux 的童鞋们别偷笑阿,就是这么简单,使用">" 将输出重定向到文件即可。

(2) 使用 -i 选项

使用 -i 选项,sed 所作的操作将会直接在原文件中操作!是的,你可以想象,这才是编辑器该干的事,但是如果你修改的是比较重要的文件,建议还是使用"-i.***" 格式的选项,比如使用sed -i.bak  "{...}" file,这样虽然会直接在file文件中进行修改,但是 -i.bak 选项在进行操作之前就将 file 文件做了一个 file.bak的备份!

但是还是尽量不要使用 -i 选项!


2.2.5 选项 -r

manpage 中对于 -r 选项是这么描述的:

-r, --regexp-extended
              use extended regular expressions in the script.   即在脚本中使用扩展正则表达式


sed 默认情况下是基础正规表示法语法,使用 -r 选项是支持扩张的正则表达式。说简单点就是,使用 -r 选项的话,一些元字符(比如:+,*,(,),{,})都不需要转义符\ 了!用一个简单的例子说明基础的正则表达式和扩展正则表达式的区别,下面的sed 语句很简单,就是将hello 中的多个l合并成一个:
long@zhouyl:/tmp$ echo "hello" | sed ‘s/l+/l/‘      -- 不填加 -r 选项,l+ 并没有匹配上多个l
hello   
long@zhouyl:/tmp$ echo "hello" | sed ‘s/l\+/l/‘     -- 给+ 添加上转义字符\ 后,l\+匹配了多个l并替换为一个l
helo
long@zhouyl:/tmp$ echo "hello" | sed -r ‘s/l+/l/‘   -- 添加-r选项,使用l+即可匹配上ll并成功替换为一个l
helo



2.3 sed 命令之 脚本字段函数使用方法

Sed 中的脚本字段才是重中之重,这里面包括sed函数、正则表达式等等,语法以及使用也都因人而异,也是编写/分析sed 脚本最难的部分!但是万变不离其宗,sed 函数的使用是确定的,所以本节围绕着sed  的常用函数展开分析,毕竟标题入门教程,如有遗漏也请见谅!


2.3.1 各种打印 p(print)

sed 的p 函数当然是用作打印的了!但是sed 支持的打印方法比较灵活:

*    sed "np" test             打印test文件的第n行

*    sed "1,3p" test          打印test文件的第1,3行

*    sed "/pattern/p" test         /pattern/是sed的匹配语法,sed 读一行信息到模式空间,如此行匹配到pattern,执行后面的操作p即打印到屏幕,即打印匹配行

这里使用一个小例子我们看一下执行效果:

long@zhouyl:/tmp$ sed "2p" test 
hello ,   world
my name is zhouyunlong
my name is zhouyunlong
what‘s   your name?


我们可以看到,sed 语句中为打印第二行,可是执行结果中每行都打印了,而第二行打印了两次,这是为什么?

这还得从sed 的执行原理来看,上面一直重复,sed 是读一行到模式空间,执行脚本,然后把模式空间的内容打印到屏幕,所以上面的测试是,先读第一行到模式空间,发现脚本不是对此行的操作,打印到屏幕。sed再读第二行到模式空间,执行脚本发现脚本是打印第二行,于是打印了到屏幕 ,脚本执行完,再把模式空间的打印到屏幕,所以第二行会打印2 遍! sed再继续读后面的行直至文件结束!


那么我想只打印第二行怎么办? 对,上面说到的 -n 选项!添加 -n 选项后,sed 先读一行到模式空间,执行脚本,本来执行完脚本后应该是把模式空间的内容再打印到屏幕上,而此时打印被 -n 选项屏蔽了!所以不打印接着读第二行,执行脚本,脚本中的p 函数的打印不会被 -n 选项屏蔽,所以会打印,然后再读第三行...

long@zhouyl:/tmp$ sed -n "2p" test 
my name is zhouyunlong
long@zhouyl:/tmp$ sed -n "1,2p" test 
hello ,   world
my name is zhouyunlong
long@zhouyl:/tmp$ sed -n "/zhou/p" test 
my name is zhouyunlong


但是有时候匹配打印时会打印多个值,比如上面我们匹配 o ,打印结果:

long@zhouyl:/tmp$ sed -n "/o/p" test 
hello ,   world
my name is zhouyunlong
what‘s   your name?

这时候我们该怎么办? 对,使用行号加匹配双限定!语法要求为‘linenumber,/pattern/p‘ ,执行效果为:如果给定行没有此匹配就往下找,直到找到需要的匹配位置;而如果给定的行直接就找到了匹配,sed 会打印接下来的所有行:

long@zhouyl:/tmp$ sed -n ‘1,/long/p‘ test
hello ,   world                     
my name is zhouyunlong                          -- 第一行没有匹配long,在第二行找到。脚本中的"1,/long/" 的效果则类似于从第一行到匹配到long 的行! 
long@zhouyl:/tmp$ sed -n ‘1,/hello/p‘ test
hello ,   world                                 -- 给定行找到匹配,sed 会打印接下来的所有行!
my name is zhouyunlong
what‘s   your name?


打印第一行很简单 sed -n ‘1p‘ test即可,这我们都知道,那么打印文件的最后一行呢?我总不能先查找一下文件共多少行,然后使用 sed -n ‘1,np‘ test 来打印吧?放心,sed 设定 $ 为最后一行。所以,sed -n "$p" test 即为打印最后一行,而sed -n "1,$p" test 为打印全部文件

long@zhouyl:/tmp$ sed -n ‘1p‘ test
hello ,   world
long@zhouyl:/tmp$ sed -n ‘$p‘ test 
what‘s   your name?
long@zhouyl:/tmp$ sed -n ‘1,$p‘ test 
hello ,   world
my name is zhouyunlong
what‘s   your name?


上面的$ 被预设为最后一行,与此类似,还有其他关于正则表达式的元字符,对于这些元字符我们该如何查找?聪明的你肯定知道使用转义符‘ \ ‘的,对吧?

long@zhouyl:/tmp$ sed -n ‘/\$/p‘ test 
$what‘s   your name?


注:上文介绍了 -n 选项与 p 共用,可以只打印匹配行做操作后的输出,但是在这罗嗦一句,请客官你在平时使用时切记 -n + p 不要和 -i 共用
我们都知道 -i 选项会直接将输出保存到原文件,而 -n +p 只打印匹配行操作后的输出,那么会发生什么? 原文件将只剩下修改的部分!下面我们模拟一个场景,比如说我们想通过sed 修改 /etc/network/interfaces 文件,比如我想修改eth0 为eth1 ,那么如果我们使用 -n -i "...p" 会发生什么?更不幸的是,如果你的命令写错了,会发生什么?
long@zhouyl:/tmp$ cat interfaces        -- 当前我们的interfaces文件
# The loopback network interface
auto lo
iface lo inet loopback


auto eth0
iface eth0 inet static
address 192.168.2.239
netmask 255.255.255.0
gateway 192.168.2.1


long@zhouyl:/tmp$ sed -i -n "s/eth0/eth1/gp" interfaces 
long@zhouyl:/tmp$ cat interfaces    -- 使用-n -i p后,interfaces文件只剩下我们修改的内容!
auto eth1
iface eth1 inet static 
long@zhouyl:/tmp$ sed -i -n "s/eth0/eth1/gp" interfaces -- 此时文件中已全是eth1,我们用"s/eth0/eth1/gp" 来模拟错误的匹配
long@zhouyl:/tmp$ cat interfaces            -- 经过错误的匹配之后,因为没有匹配任何内容,interfaces文件已为空!!!


所以,即使你想用 -n -i p三个操作的组合,你也要使用"i.bak"模式来进行:
long@zhouyl:/tmp$ sed -i.bak -n "s/eth0/eth1/gp" interfaces
long@zhouyl:/tmp$ cat interfaces.bak    -- 即使此时interfaces 已经出错了,但是我们仍然有个备份文件
# The loopback network interface
auto lo
iface lo inet loopback


auto eth0
iface eth0 inet static
address 192.168.2.239
netmask 255.255.255.0
gateway 192.168.2.1

所以,尽量避免使用sed  -n  -i ‘...p‘  file 这种 -n -i p三者共存的情况!还要牢记:每次使用 -i 选项时,都要使用 "-i.***" 形式


2.3.2 打印行号 =

如上面的例子匹配时,如果文件很大,我想知道在第几行匹配的该怎么办? 别急, sed 预定了 = 来完成此重任!

long@zhouyl:/tmp$ sed -n ‘/\$/=‘ test 
3
long@zhouyl:/tmp$ sed -n ‘/long/=‘ test 
2


2.3.3 附加文本 a(append)

有时候我们想找到一行信息,然后在其后添加一句该怎么办? 别急 sed 提供了附加信息 a 方法,它会在匹配行之后添加上你想要添加的信息:

long@zhouyl:/tmp$ sed "/long/a\I\‘m glad to see you" test 
hello ,   world
my name is zhouyunlong          --- 匹配上long,并在此行之后附加了脚本中 a\ 之后的字符串
I‘m glad to see you               
$what‘s   your name?


注: 为什么我使用双引号? 因为有时候使用单引号会不支持转义,这个好像在shell 中也有类似效果:

long@zhouyl:/tmp$ echo "my name is $name"
my name is zhou
long@zhouyl:/tmp$ echo ‘my name is $name‘
my name is $name

所以你只要记住,以后统一使用双引号即可!


2.3.4 插入文本 i(insert)

插入文本的使用格式与附加文本一样,只不过执行效果是在匹配行之前添加信息:

long@zhouyl:/tmp$ sed "/long/i\I\‘m glad to see you" test 
hello ,   world
I‘m glad to see you
my name is zhouyunlong       --- 匹配上long,并在此行之前附加了脚本中 i\ 之后的字符串
$what‘s   your name?


2.3.5 修改文本 c(change)

插入文本的使用格式与附加文本一样,只不过执行效果是直接替换匹配行:

long@zhouyl:/tmp$ sed "/long/c\I\‘m glad to see you" test 
hello ,   world
I‘m glad to see you                --- 匹配上long,并直接使用脚本中 c\ 之后的字符串 替换了改行
$what‘s   your name?


注: 因为附加文本,插入文本以及修改文本的使用方法一样,所以在此添加的注释是对这三条全体!

(1) 除了使用"/pattern/a\string" 这样的格式对匹配行做操作外,sed还支持 "linenumber a\string" 这样直接对指定行进行操作!

long@zhouyl:/tmp$ sed "$ a\I\‘m glad to see you" test 
hello ,   world
my name is zhouyunlong
$what‘s   your name?
I‘m glad to see you          ---这里是在最后行 $ 之后追加文本!


(2) 如果不为 a/i/c 操作指定匹配内容或者行号,它会应用到每一行!!!(这种情况在sed 中比比皆是,如 d ,p 等~)

long@zhouyl:/tmp$ sed "a\For play" test 
hello ,   world
For play
my name is zhouyunlong
For play
$what‘s   your name?
For play


2.3.6 删除文本 d(delete)

sed 的删除文本使用更简单,一种是使用 "/pattern/d" 删除匹配行,一种是使用 "linenumber d" 删除指定某一行!

long@zhouyl:/tmp$ sed "1d" test         -- 删除第一行
my name is zhouyunlong
$what‘s   your name?
long@zhouyl:/tmp$ sed "/long/d" test    -- 删除匹配long 的行
hello ,   world
$what‘s   your name?    
long@zhouyl:/tmp$ sed "d" test          -- 不指定全部删除!

注: 还记得上面的行号和匹配混合模式么? 还记得匹配后打印的内容么?对d 方法同样管用哦:

long@zhouyl:/tmp$ sed "1,/long/d" test     --- 上面使用"1,/long/p" 会打印第一行第二行,而此处第一行正常打印,第二行被d 操作删除!
$what‘s   your name?
long@zhouyl:/tmp$ sed "1,/hello/d" test    ---- 上面使用"1,/hello/p" 打印了全文,此处全文都被d 操作删除!



2.3.7 替换文本 s(substitute)

如同简介中所说,sed 至今接近40余载仍然活跃在脚本中,与之有着强大的替换文本方法有着莫大的关系!此处我们着重讲解:

替换文本的使用格式为    [address[,address]]   s   /pattern-to-find/replace-pattern/[gpwn]   即使用 replace-pattern 去替换超找到的 pattern-to-find 字段!

简而言之就是 s/old/new/flag ,下面我们将根据此处的几段分别展开介绍:

*    old部分: 可以使用正则表达式,此处不赘述!如果你还没有正则表达式的概念,建议你想去看看正则表达式!


*    new部分: 可以使用\U,\u,\L,\l,&,\E以及正则表达式的向后引用
        >>>  \U: \U后接的部分全部改为大写

long@zhouyl:/tmp$ sed -n "s/zhou/\Uzhou/gp" test 
my name is ZHOUyunlong

        >>>  \u: \U后接的部分仅首字母改为大写
long@zhouyl:/tmp$ sed -n "s/zhou/\uzhou/gp" test 
my name is Zhouyunlong

        >>>  \L: \L后接的部分全部改为小写
        >>>  \l: \l后接的部分仅首字母改为小写
        >>>  &:   这里用来替换old部分被匹配的文字
long@zhouyl:/tmp$ sed -n "s/zhou/Mr.\u&/gp" test 
my name is Mr.Zhouyunlong

        >>>  \E:  以\E后接的部分终止
long@zhouyl:/tmp$ sed -n "s/zhou/Mr.\u&\E\!/gp" test 
my name is Mr.Zhou!yunlong

        >>>  向后引用,即用\加上前面正则表达式分组,如\1,\2 
long@zhouyl:/tmp$ sed -n -r "s/(zhou)(yunlong)/\u\1 \u\2/gp" test 
my name is Zhou Yunlong 


*    flag部分:flag部分可以使用g,p,w,数字,n,N
        >>>  g: global 全局替换,flag部分使用g,sed会替换所有文本中的匹配部分
        >>>  p: 打印,上文已详细说了,如上面的例子,p经常与-n 选项一起使用
        >>>  w: 保存文档,如果flag 部分使用 w file,会将匹配内容写入file文件中
long@zhouyl:/tmp$ sed -n -r "s/(zhou)(yunlong)/\u\1 \u\2/gpw file" test
my name is Zhou Yunlong
long@zhouyl:/tmp$ cat file
my name is Zhou Yunlong

        >>>  数字: 如果flag 部分使用的是数字,表示对匹配行中的第几次匹配进行替换
long@zhouyl:/tmp$ sed -n -r "s/o/O/p2" test
hello ,   wOrld
my name is zhouyunlOng



注:在替换命令的格式 ‘s/.../.../‘ 中,使用 “/” 作为分隔符! 有时候,无论是在匹配项中需要匹配 / 还是在替换内容中要添加/ ,使用 ‘s/.../.../‘格式都显得非常混乱,而且可读性变得极弱!所以sed 可以很方便快捷地指定使用其他分隔符!只要将你想要替换的分隔符紧接着 s 即可,sed 即可自动识别!

比如:

long@zhouyl:/tmp$ echo "/home/long/hello/test/abc" | sed -e  "s/\//\:/g"
:home:long:hello:test:abc
long@zhouyl:/tmp$ echo "/home/long/hello/test/abc" | sed -e  "s#\/#\:#g"
:home:long:hello:test:abc


在上面的例子中你觉得使用# 替代 / 作为分隔符是不是清晰许多? 再来看个恶心的,"s/\//\\\/g"整个看花了
long@zhouyl:/tmp$ echo "/home/long/hello/test/abc" | sed -e  "s/\//\\/g"
sed: -e expression #1, char 8: unterminated `s‘ command
long@zhouyl:/tmp$ echo "/home/long/hello/test/abc" | sed -e  "s/\//\\\/g"
\home\long\hello\test\abc
long@zhouyl:/tmp$ echo "/home/long/hello/test/abc" | sed -e  "s#\/#\\\#g"
\home\long\hello\test\abc



2.3.8 从文件中读文本 r(read)

sed 还支持从文件中读取内容并将读出来的文本添加到匹配内容行之后

long@zhouyl:/tmp$ cat hi
hello,world
long@zhouyl:/tmp$ sed  -e "$ r hi" test         -- 将从文件中读出的文本行放在最后一行之后
hello ,   world
my name is zhouyunlong
$what‘s   your name?
hello,world
long@zhouyl:/tmp$ sed  -e "/zhou/r hi" test     -- 将从文件中读出的文本行放在匹配zhou字段行之后
hello ,   world
my name is zhouyunlong
hello,world
$what‘s   your name?



2.3.9 读取下一行 n/N(next)

n和N都是读下一行的函数,但是n 与N 的区别是:使用n 读取下一行,会直接替换当前模式空间的内容,而N为读取下一行并追加在模式空间内容之后。

long@zhouyl:/tmp$ sed -n 1"p" test      -- 只打印第一行
hello ,   world
long@zhouyl:/tmp$ sed -n 1"{n;p}" test  -- 当读取第一行时,脚本命令为读取下一行并打印!所以打印出来的为第二行
my name is zhouyunlong
long@zhouyl:/tmp$ sed -n 1"{N;p}" test  -- 当读取第一行时,脚本命令为读取下一行并追加到当前模式空间之后,打印!
hello ,   world
my name is zhouyunlong


注意: sed 的追加命令(包括下面进阶用法中的保持空间中的追加),如上虽然打印出来的为两行,但是其实在sed看来只有一行,以上面的测试用例为例, 虽然打印出来为两行,但是此时对于sed来说是 : “ ^hello ,   world\nmy name is zhouyunlong$ ”。也就是说本来第一行行末的$ 被追加操作替换成换行符\n, 而第二行首^也被清除了!不信?用事实说话,下面我们在打印之前使用替换命令将换行符 \n 替换成 --- !

long@zhouyl:/tmp$ sed -n 1"{N;l;s/\n/---/p}" test 
hello ,   world\nmy name is zhouyunlong$
hello ,   world---my name is zhouyunlong


2.3.10 匹配后退出 q(quit)

有时候需要在模式匹配之后退出sed命令,以便执行其他处理脚本,所以q 方法就可以用来帮你完成这个使命!

long@zhouyl:/tmp$ sed  -e "2q" test 
hello ,   world
my name is zhouyunlong
long@zhouyl:/tmp$ sed  -e "/zhou/q" test 
hello ,   world
my name is zhouyunlong


2.3.11 更多的打印 l (list)

上面已经提到打印使用p,那这个l 函数打印什么?manpage上说它是以一种视觉上更明确的方式打印,也就是说不是标准输出,l 会打印比p 更多的内容,有一些在p 标准输出的情况下会被系统掩盖,比如"$""\n":

long@zhouyl:/tmp$ sed ‘1,$l‘ test 
hello ,   world$          --- 这是l 打印的信息!
hello ,   world            --- 这是执行完操作后系统把模式空间的内容打印到标准输出!
my name is zhouyunlong$
my name is zhouyunlong
$what‘s   your name?$
$what‘s   your name?



3. sed 进阶

3.1 Sed 进阶之执行多语句

sed 中有时候你需要一次执行较多的语句,这里有两种方法:

(1) sed [OPTION]  "{cmd1; cmd2; cmd3 ... }"  input_file

(2) sed -e "cmd1" -e "cmd2" -e "cmd3"  ... input_file 

那么这两种方法有什么区别呢?其实很简单,方法一中的 cmd1; cmd2; cmd3 ... 让你想到了什么?对,C语言中的一个一个语句,事实是这样,方法一中有着执行顺序,也就是说,先执行 cmd1 再执行cmd2 ...

方法二中的每个cmd 都以 -e 链接,所以他们都处于同一级别

long@zhouyl:/tmp$ sed -n -e 1"{p;n;p;n;p}" test
hello ,   world
my name is zhouyunlong
$what‘s   your name?

我们可以利用这种顺序性,完成上述这样的操作,虽然限定了第一行,但是语句中使用n读取下一行然后打印,再在第二行之上读下一行再打印,我们就这样打印了三行~在下一个进阶用法中,我们再深入使用这种顺序性!


long@zhouyl:/tmp$ sed -n -e "1p" -n -e "2p" test
hello ,   world
my name is zhouyunlong
long@zhouyl:/tmp$ sed -n -e "2p" -n -e "1p" test
hello ,   world
my name is zhouyunlong


我们可以看到,-e 链接的命令并没有顺序性,"1p"和"2p"的顺序颠倒并没有对结果有任何影响!


3.2 Sed 进阶之保持空间及其函数

前面介绍 sed 的 运行原理以及介绍其他一些方法时,我们不断提及sed 的模式空间(Pattern Space)sed 读取文件的一行,放入模式空间,并在模式空间做命令对应的操作,再将模式空间的内容打印到屏幕上。所以,模式空间整个过程中就相当于一个车间!但是有时候,车间当前所制作的东西还不足以打印出来,我们还需要后面的其他原料制作的东西与这个东西一组合才能生产出我们想要的东西!所以,还需要一个仓库来存储一些当前还不足以打印的内容!

实际上,sed 总共是有两个缓冲区的,一个叫做模式空间,还有一个叫做保持空间(Hold Space),而这个保持空间就是我们上面说到的仓库! 有了一个车间一个仓库,我们就能制作出更加丰富更加多元的东西!当然,仓库和车间之间需要交流,所以sed 提供了几组命令用来完成 模式空间和保持空间的内容复制工作:

(1) h / H(hold): 保存命令,用于将模式空间的内容复制/追加到保持空间
(2) g / G(get): 取回命令,用于将保持空间的内容复制/追加到模式空间
(3) x (exchange): 交换命令,用于将模式空间和保持空间的内容做交换


那么,有了保持空间,我们可以完成什么?结合上面的命令,我们可以完成很多给力的操作:

long@zhouyl:/tmp$ cat abc 
Tom
male
Jerry
male
Marry
female
Linux
penguin
long@zhouyl:/tmp$ sed -n -e "{h;n;G;p}" abc        -- 我们可以隔两行掉转顺序,想打印性别再打应名字
male
Tom
male
Jerry
female
Marry
penguin
Linux
long@zhouyl:/tmp$ sed -n -e "{h;n;H;g;s/\n/:/g;p}" abc      ---  我们还可以将名字和性别读到一行并改用: 链接
Tom:male
Jerry:male
Marry:female
Linux:penguin
long@zhouyl:/tmp$ sed -n -e "{h;n}" -e ‘/male/{g;p}‘ abc  --- 我们可以打印男性的名字
Tom
Jerry
Marry       
long@zhouyl:/tmp$ sed -n -e "{h;n}" -e "/^male/{g;p}" abc   ---上面的匹配中不幸的是,female也匹配到了。我们只能改用"^male"进行匹配,结果正确!
Tom
Jerry
long@zhouyl:/tmp$ sed  -e ‘{1!G;h;$!d}‘ abc      -- 我们还可以掉转整个文件的顺序!
penguin
Linux
female
Marry
male
Jerry
male
Tom


对于最后一个关于掉转文件中行的顺序的sed 命令,可能还需要详述一下,使用的命令为 sed  -e ‘{1!G;h;$!d}‘  input_file。 此句的功能与Linux下的 tac 命令比较接近!在命令脚本中也只有三句!  1!G; h ;$!d 。其中每一个函数我们都已经在上面有所讲解了,那么为什么这三句命令能够完成这么好玩的任务呢?简单分析一下:

首先第一句是 1!G,G我们在上面说过,是将当前的保持空间的内容取回并追加在当前的模式空间内容之后,1!G表示除了第一行不用取回之外每行都需要取回追加!

第二句是h ,我们都知道是将当前的模式空间的内容复制到保持空间,因为是 h 所以是覆盖的。

第三句是 $!d ,也就是说除了最后一行,每行在第三句时会删除模式空间中的内容,也就是说不打印到屏幕。而到最后一行不删除时,sed会打印到屏幕!

所以整个sed 命令的流程就是首先读第一行到模式空间,然后赋值到保持空间,然后删除模式空间的内容;再读第二行到模式空间,此时就需要先将保持空间的内容(即第一行)追加到此时模式空间的内容之后,也就是说此时的模式空间内容为“^line2\nline1$”,再将此内容复制到保持空间中,最后删除模式空间的内容;再读第三行到模式空间,还是先将保持空间的内容取回并追加到当前模式空间的内容之后,所以此时是“^line3\nline2\nline1$”,此后将此内容复制到保持空间后删除模式空间;...此时我们也可以看出来,每一次执行G时,就是把当前读到的行顺序都颠倒了!直到最后一行时,不删除模式空间的内容,所以打印出来的也就是颠倒完顺序的状态!


3.3 Sed 进阶之标签

为了使 sed脚本真正的"自由",sed还允许在脚本中用":"设置标签,然后使用"b"和"t"命令进行流程控制。其中,"b"表示"branch",为分支命令使用起来很类似C语言中的goto 语句, "t/T"表示"test",是测试后跳转的命令。


同样,我们先来看看 sed 的manpage中对这三个函数的定义:
       b label
              Branch to label; if label is omitted, branch to end of script.   即跳转到标签;如果标签未定义,则分支会结束脚本。
       t label
              If a s/// has done a successful substitution since the last input line was read and since the last t or T command, then branch to label; if label is omitted, branch to end of script.    即自从上一行读或者上一次执行t/T命令后,如果s/// 成功替换,那么 t 会跳转到标签;如果标签未定义,则分支会结束脚本。
       T label
              If  no  s///  has  done a successful substitution since the last input line was read and since the last t or T command, then branch to label; if label is omitted, branch to end of script.  This is a GNU extension.  与上面的t 命令刚好相反,如果 s/// 未成功替换,T会跳转到标签;如果标签未定义,则分支会结束脚本。



那么标签是如何定义的呢?标签以冒号开始,冒号与标签名之间不允许有空格或者制表符,标签最后如果有空格的话,也会被认为是标签的一部分

再来说b命令。它的格式是这样的:
[address]  b  [label]
它的含意是,如果满足address,则sed流程跟随标签跳转:如果标签指明的话,脚本首先假设这个标签在b命令以下的某行,然后转入该行执行相应的命令;如果这个标签不存在的话,控制流程就直接跳到脚本的末尾。如果不满足则继续执行后续的命令。


在某些情况下,b命令和!命令有些相似,但是!命令只能对紧挨它的{}中的内容起作用,而b命令则给予使用者足够的自由在sed脚本中选择哪些命令应该被执行,哪些命令不应该被执行。下面提供几种b命令的经典用法:

(1) 创建循环:
:top
command1
command2
/pattern/b top
command3
(2) 忽略某些不满足条件的命令:
command1
/patern/b end
command2
:end
command3


t命令的格式和b命令是一样的:
[address] t [label]
它表示的是如果满足address的话,sed脚本就会根据t命令指示的标签进行流程转移。而标签的规则和上面讲的b命令的规则是一样的。下面也给出
一个例子:
s/pattern/replacement/
t break
command
:break

而上面已经说过,T命令和t 命令的使用基本上一模一样,只不过 t 对应的是在s/// 匹配时跳转,而 T 在 s/// 不匹配时才跳转

那么使用起来如何?我们看个小例子,但是不做那么复杂了阿:

long@zhouyl:/tmp$ sed -e ":la s/o/O/; /o/b la" test 
hellO ,   wOrld
my name is zhOuyunlOng
$what‘s   yOur name?
long@zhouyl:/tmp$ sed -e ":la s/o/O/;s/zhou/Zhou/ ; t la" test 
hellO ,   wOrld
my name is zhOuyunlOng
$what‘s   yOur name?
long@zhouyl:/tmp$ sed -e ":la s/o/O/;s/zhou/Zhou/ ; T la" test 
hellO ,   world
my name is zhOuyunlong
$what‘s   yOur name?



4. 小结

Sed 是学习Shell 时不得不学的利器,在各处的脚本中,你也难免会看到她的身影。本文虽然不能全面介绍Sed,但是我力求详尽,希望对你有用!

看了这么多,终于快结束了,我承认你不可能只花了十分钟,这是我的错,但我不是标题党,我只是希望你有信心能够学习好Sed!既然你都看到这了,说明你和我都成功了!谢谢,如果你还想深入,欢迎学习最下方的课外阅读!


看文容易,写文难,且看且点赞~谢谢。


下期我们介绍sed 的兄弟 AWK,敬请期待。转载请注明出处,谢谢~


==========================

引用:

【1】《Shell基础十二篇》

【2】酷壳网:《sed 简明教程》

【3】关于一些概念之类的可能有部分参考百度百科OR wiki!

【4】 http://club.topsage.com/thread-2372858-1-1.html

==========================

课外阅读:

【1】 sed的GNU官方使用文档

【2】 SED单行脚本快速参考 / sed1line (很多大神的sed命令哦)

【3】 Sed 手册



流编辑器 SED 十分钟入门全教程