欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

Linux文本处理三剑客之sed

程序员文章站 2022-04-03 16:19:20
推荐新手阅读【酷壳】或【骏马金龙】开篇的教程作为入门。骏马兄后面的文章以及官方英文文档较难。 【酷壳】:https://coolshell.cn/articles/9104.html 【骏马金龙-博客园】:https://www.cnblogs.com/f-ck-need-u/p/7499471.h ......

推荐新手阅读【酷壳】或【骏马金龙】开篇的教程作为入门。骏马兄后面的文章以及官方英文文档较难。

【酷壳】:https://coolshell.cn/articles/9104.html

【骏马金龙-博客园】:https://www.cnblogs.com/f-ck-need-u/p/7499471.html

【sed官方】:https://www.gnu.org/software/sed/manual/sed.html

【sed的info手册】:# info sed

简介

sed是一款流编辑器,可以从stdin或者文件中读取数据流,进行文本的过滤与转换。

 

工作原理

在高级的sed命令用法中,需要对工作原理有比较深的理解,一般使用的话就不一定要知道那么详细了。

sed持有两段数据缓冲区:模式空间(pattern space)和保持空间(hold space)。

sed会对文件的每一行做以下循环:首先,sed从输入流中读取一行数据,移除任何结尾的换行符(newline),然后将其放入模式空间中。然后执行命令;每一个命令,可以有与之关联的地址:地址就是一种条件码,只有在条件符合的情况下,那么命令才会被执行。

当最后一个脚本(script)执行完毕的时候,除非使用了-n选项,否则模式空间中的内容会被打印至输出流,加回被移除的末尾换行符。然后读取下一行继续这样的循环操作。

除非使用一些特殊的命令(像d命令),否则在两次循环间都会删除模式空间中的数据。而保持空间则会在每次循环中保留它的数据。

 

基本使用

# sed [option]... {script} [input-file]...

option:表示选项。

script:脚本。脚本有两部分构成,地址+命令。地址就是地址定界,符合地址定界范围内的行,才会被命令所执行。常见的有替换、删除等。

input-file:输入文件,可以是一个文件或者stdin。

# sed 's/hello/world/' input.txt
# sed 's/hello/world/' < input.txt
# cat input.txt | sed 's/hello/world/' -
# cat input.txt | sed 's/hello/world/'

再次拆分如下。

# sed [option]... {[address][command]} [input-file]...

脚本是必须要有的,如果脚本为空的话,则必须以“''”来表示。【选项】中有示例。

 

选项

-n, --quiet, --silent:sed每次会自动打印模式空间的内容,想要阻止这种行为,就使用该选项。

我们先来看一个例子。

[root@c7 tmp]# cat my.txt
this is my cat, my cat's name is betty
this is my dog, my dog's name is frank
this is my fish, my fish's name is george
this is my goat, my goat's name is adam

在脚本的位置,我们使用空'',空脚本的话,表示没有限制地址范围并且没有命令,那么该文件中每一行进入模式空间,都不会被处理,然后最终每一行都会被自动打印出来。这是我们在【工作原理】中提到过的。

[root@c7 tmp]# sed '' my.txt 
this is my cat, my cat's name is betty
this is my dog, my dog's name is frank
this is my fish, my fish's name is george
this is my goat, my goat's name is adam

假设我们尝试打印第3行。3表示地址,明确是第三行,只有该行会被后面的命令p所作用;p表示打印模式空间的数据之意。

[root@c7 tmp]# sed '3p' my.txt 
this is my cat, my cat's name is betty
this is my dog, my dog's name is frank
this is my fish, my fish's name is george
this is my fish, my fish's name is george
this is my goat, my goat's name is adam

此时我们再加上-n选项,就可以抑制sed的自动输出,只输出我们所需要的。

[root@c7 tmp]# sed -n '3p' my.txt 
this is my fish, my fish's name is george

-e script, --expression=script:脚本,除了可以写在cli的引号中以外,还可以使用选项来指定。

-f script-file, --file=script-file:或者我们将脚本写入文本文件中,每行对应一个脚本,调用脚本文件来执行。

这种情况一般用于有多个脚本,多个脚本也可以使用分号分隔。

[root@c7 tmp]# sed -n '3p;4p' my.txt 
this is my fish, my fish's name is george
this is my goat, my goat's name is adam
[root@c7 tmp]# sed -n -e '3p' -e '4p' my.txt 
this is my fish, my fish's name is george
this is my goat, my goat's name is adam
[root@c7 tmp]# echo "3p" > script.txt
[root@c7 tmp]# echo "4p" >> script.txt
[root@c7 tmp]# sed -n -f script.txt my.txt 
this is my fish, my fish's name is george
this is my goat, my goat's name is adam

分号用于分隔脚本,容易和大括号的用法搞混,大括号主要用于命令分组。下文会描述。

-i[suffix], --in-place[=suffix]:表示就地修改(in-place)文件,可选的suffix表示在就地修改是可以做备份操作,用于指定备份文件扩展名。

默认情况下,sed的操作不会修改原文件,仅将修改结果输出至stdout。

[root@c7 tmp]# sed '1,3d' my.txt 
this is my goat, my goat's name is adam
[root@c7 tmp]# cat my.txt
this is my cat, my cat's name is betty
this is my dog, my dog's name is frank
this is my fish, my fish's name is george
this is my goat, my goat's name is adam

而就地修改,则类似于将修改结果输出至原文件。默认的备份文件位置位于被操作文件所在目录下。

[root@c7 tmp]# sed -i.bak '1,3d' my.txt
[root@c7 tmp]# cat my.txt
this is my goat, my goat's name is adam
[root@c7 tmp]# cat my.txt.bak 
this is my cat, my cat's name is betty
this is my dog, my dog's name is frank
this is my fish, my fish's name is george
this is my goat, my goat's name is adam

在备份的时候,可以指定suffix中可以使用目录+“*”的形式来指定将备份文件放置在其他目录下。

[root@c7 tmp]# sed -i/root/*.bak '1,3d' my.txt
[root@c7 tmp]# cat my.txt
this is my goat, my goat's name is adam
[root@c7 tmp]# cat /root/my.txt.bak 
this is my cat, my cat's name is betty
this is my dog, my dog's name is frank
this is my fish, my fish's name is george
this is my goat, my goat's name is adam

注意:-i选项后,要么不接内容表示就地修改不备份,要么接的内容就要是后缀或备份路径。例如下面这个例子。

# sed -ei '...' file
# sed -ie '...' file

-i选项隐含了-s选项。

-s, --separate:默认情况下,sed将参数中的文件,全部视为一条长的连续的数据流。这主要会影响sed的地址定界功能。

[root@c7 tmp]# cat a.txt 
a1
a2
a3
[root@c7 tmp]# cat b.txt 
b1
b2
b3
[root@c7 tmp]# cat c.txt 
c1
c2
c3
[root@c7 tmp]# sed -n '1p;$p' a.txt b.txt c.txt 
a1
c3

正常我们可能会以为它会打印每个文件的首行和尾行。而其实,它类似于将所有文件cat成一个大文件后再做脚本处理。

[root@c7 tmp]# cat {a,b,c}.txt 
a1
a2
a3
b1
b2
b3
c1
c2
c3

想要将每个文件单独分隔(separate)的话,就可以使用该选项了。

[root@c7 tmp]# sed -ns '1p;$p' a.txt b.txt c.txt 
a1
a3
b1
b3
c1
c3

-e, -r, --regexp-extended:启用扩展正则表达式(ere)。默认使用的是基础正则表达式(bre)。

启用ere后,与bre的区别在于正则元字符的书写方式不太一样,启用ere在正则的书写上,相对bre要简洁很多。

关于正则,网上资料很多,我自己也写了一篇,可供参考《linux文本处理三剑客之grep - 阿龙弟弟 - 博客园》,因此本文不会对正则及其元字符有过多介绍。

 

地址定界

地址定界,是sed是否执行命令的条件。由于sed是基于行的流编辑器,因此只有符合地址定界中的行,才会被执行命令。

空地址

即不指定地址,其表示对所有的行都执行操作。

单地址

单地址并不是说只匹配某一行,而是在地址定界中,只给出一个地址。例如通过模式匹配到的地址,可能就是多行。

m:这里的m是一个正整数,表示对第m行执行操作。其中“$”表示最后一行。

/regexp/:模式匹配,支持正则。表示匹配模式的行。

[root@c7 tmp]# sed -n '3p' my.txt
this is my fish, my fish's name is george
[root@c7 tmp]# sed -n '$p' my.txt
this is my goat, my goat's name is adam
[root@c7 tmp]# sed -n '/fish/p' my.txt
this is my fish, my fish's name is george

如果我们想匹配的是一个类unix下的路径的话,那么regexp中会包含许多斜线。此时我们可以对其中的斜线执行转义。

sed -n '/^\/home\/alice\/documents\//p'

或者只用该类型的语法。

\cregexpc

c并不是关键字,可使用其他字符代替,只要前后一致就行。语法就可以避免那些啰嗦的转义字符了。

sed -n '\%^/home/alice/documents/%p'
sed -n '\;^/home/alice/documents/;p'

/regexp/i:忽略大小写(case insensitive)匹配。注意,是大写的字母i,如果写成小写的i,那表示的是插入命令。

地址范围

m,n:第m行至第n行。m<n。若m>n,则表示第m行之意。

[root@c7 tmp]# sed -n '1,3p' /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
[root@c7 tmp]# sed -n '3,1p' /etc/passwd
daemon:x:2:2:daemon:/sbin:/sbin/nologin
[root@c7 tmp]# sed -n '3,2p' /etc/passwd
daemon:x:2:2:daemon:/sbin:/sbin/nologin

m,+n:第m行以及之后的n行。

[root@c7 tmp]# sed -n '1,+3p' /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin

m,/regexp/:第m行至第一次匹配到模式的行。

[root@c7 tmp]# sed -n '1,/daemon/p' /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin

0,/regexp/:如果正则匹配到的是首行(英文原文:very first)的话,那么它就会匹配第一行,否则就和“1,/regexp/”效果一样。

[root@c7 tmp]# seq 10 | sed -n '1,/[[:digit:]]/p'
1
2
[root@c7 tmp]# seq 10 | sed -n '0,/[[:digit:]]/p'
1
[root@c7 tmp]# seq 10 | sed -n '0,/[234]/p'
1
2
[root@c7 tmp]# seq 10 | sed -n '1,/[234]/p'
1
2

/regexp1/,/regexp2/:从第一次匹配到regexp1的行至第一次匹配到regexp2的行。

[root@c7 tmp]# sed -n '/halt/,/operator/p' /etc/passwd
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin

一般来说,regexp1出现的位置应该先于regexp2,如果不是的话,则等同于“/regexp2/,$”。

# sed -n '/operator/,/halt/p' /etc/passwd

该效果等同于operator所在行至行尾。

first~step:步进(stepping)。先定位first所在行,然后每次经过step行再定位一次。

最典型的示例就是显示偶数和奇数。

[root@c7 tmp]# seq 10 | sed -n '1~2p'
1
3
5
7
9
[root@c7 tmp]# seq 10 | sed -n '0~2p'
2
4
6
8
10

显示5的倍数。

[root@c7 tmp]# seq 30 | sed -n '0~5p'
5
10
15
20
25
30

addr1,~n:这个写法和【步进】以及之前的“m,+n”相似但不同,不要混淆。它的作用是从addr1所在的行至第一个可以被n整除的行。

[root@c7 tmp]# seq 10 | sed -n '5,~4p'
5
6
7
8

地址取反

在地址和命令中,可以插入一个感叹号“!”,用来表示取反操作。

打印1~4行和15~行尾的数据。

# sed -n '5,15!p' file

 

命令

当符合地址定界条件后,就可以执行命令了。命令即sed具体所做的事情。

p:打印模式空间中的数据。一般结合-n选项使用,打印符合地址范围的行。

d:删除模式空间中的数据,并立即开始下一次循环。

[root@c7 tmp]# sed '2d' my.txt
this is my cat, my cat's name is betty
this is my fish, my fish's name is george
this is my goat, my goat's name is adam
[root@c7 tmp]# sed '/goat/d' my.txt
this is my cat, my cat's name is betty
this is my dog, my dog's name is frank
this is my fish, my fish's name is george

a\

text

或者

a text:追加(append)文本。追加的意思是在加在指定行后面,即下一行。“a\”的后面,需要回车。

[root@c7 tmp]# sed '2a my name is alongdidi' my.txt
this is my cat, my cat's name is betty
this is my dog, my dog's name is frank
my name is alongdidi
this is my fish, my fish's name is george
this is my goat, my goat's name is adam
[root@c7 tmp]# sed '2a\
> my name is alongdidi' my.txt
this is my cat, my cat's name is betty
this is my dog, my dog's name is frank
my name is alongdidi
this is my fish, my fish's name is george
this is my goat, my goat's name is adam

i\

text

或者

i text:插入(insert)文本。插入的意思是在加在指定行前面,即上一行。

[root@c7 tmp]# sed '2i my name is alongdidi' my.txt
this is my cat, my cat's name is betty
my name is alongdidi
this is my dog, my dog's name is frank
this is my fish, my fish's name is george
this is my goat, my goat's name is adam
[root@c7 tmp]# sed '2i\                           
my name is alongdidi' my.txt
this is my cat, my cat's name is betty
my name is alongdidi
this is my dog, my dog's name is frank
this is my fish, my fish's name is george
this is my goat, my goat's name is adam

c\

text

或者

c text:替换(change)文本。将指定的行替换成text。这里的替换是整行替换,和s///命令的文本替换不一样。

[root@c7 tmp]# sed '2c my name is alongdidi' my.txt
this is my cat, my cat's name is betty
my name is alongdidi
this is my fish, my fish's name is george
this is my goat, my goat's name is adam
[root@c7 tmp]# sed '2c\                           
my name is alongdidi' my.txt
this is my cat, my cat's name is betty
my name is alongdidi
this is my fish, my fish's name is george
this is my goat, my goat's name is adam

如果被替换的行是多行的话,那么多行会被整体替换为text,而不是每一行都替换成text。

[root@c7 tmp]# sed '2,4c my name is alongdidi' my.txt
this is my cat, my cat's name is betty
my name is alongdidi

如果是多行的追加或者插入,则会在每个匹配的行上都执行追加或者插入操作。

追加、插入和替换的文本(text)中,都可以使用“\n”来表示换行,实现多行输入的功能。

[root@c7 tmp]# sed '2,4c along\ndi\ndi' my.txt
this is my cat, my cat's name is betty
along
di
di

=:打印行号,可惜不是在行首打印,而是在行位置的上一行打印,类似插入操作。

[root@c7 tmp]# sed '=' my.txt
1
this is my cat, my cat's name is betty
2
this is my dog, my dog's name is frank
3
this is my fish, my fish's name is george
4
this is my goat, my goat's name is adam

r filename:在指定的行后追加文本,文本内容来自一个文件。

[root@c7 tmp]# cat a.txt 
a1
a2
a3
[root@c7 tmp]# sed '2r a.txt' my.txt
this is my cat, my cat's name is betty
this is my dog, my dog's name is frank
a1
a2
a3
this is my fish, my fish's name is george
this is my goat, my goat's name is adam

w filename:将匹配到的行写入另一个文件中。

这里使用了-n选项,抑制了sed命令执行完毕后,打印整个/etc/fstab文件的内容。

不过即便不使用-n选项,这些输出的多余内容,也是输出至stdout,而不会输出至filename中,因为w命令的执行,必须满足前面的地址定界条件。

[root@c7 tmp]# grep "^/dev/" /etc/fstab 
/dev/mapper/centos-root /                       xfs     defaults        0 0
/dev/mapper/centos-swap swap                    swap    defaults        0 0
[root@c7 tmp]# sed -n '\c^/dev/cw /tmp/sed_write.txt' /etc/fstab
[root@c7 tmp]# cat sed_write.txt 
/dev/mapper/centos-root /                       xfs     defaults        0 0
/dev/mapper/centos-swap swap                    swap    defaults        0 0

{cmd1; cmd2; ...}:命令的分组(group),这种一般适用于针对同一地址执行多个命令。

[root@c7 tmp]# seq 3 | sed -n '2s/2/x/;p'
1
x
3
[root@c7 tmp]# seq 3 | sed -n '2{s/2/x/;p}'
x

删除第3行至第6行中包含“this”字符串的。

[root@c7 tmp]# cat -n pets.txt 
     1    this is my cat
     2      my cat's name is betty
     3    this is my dog
     4      my dog's name is frank
     5    this is my fish
     6      my fish's name is george
     7    this is my goat
     8      my goat's name is adam
[root@c7 tmp]# sed '3,6{/this/d}' pets.txt
this is my cat
  my cat's name is betty
  my dog's name is frank
  my fish's name is george
this is my goat
  my goat's name is adam
[root@c7 tmp]# sed '3,6{/this/{/fish/d}}' pets.txt
this is my cat
  my cat's name is betty
this is my dog
  my dog's name is frank
  my fish's name is george
this is my goat
  my goat's name is adam

文本替换

文本替换是sed命令中最常见的命令,sed官网甚至单独使用一个小节阐述它。语法如下。

s/regexp/replacement/[flags]

s命令在模式空间中尝试去匹配我们所给出的正则表达式regexp,如果匹配成功,则将匹配到的那部分(portion)替换为replacement。

[root@c7 tmp]# cat my.txt
this is my cat, my cat's name is betty
this is my dog, my dog's name is frank
this is my fish, my fish's name is george
this is my goat, my goat's name is adam
[root@c7 tmp]# sed 's/my/your/' my.txt
this is your cat, my cat's name is betty
this is your dog, my dog's name is frank
this is your fish, my fish's name is george
this is your goat, my goat's name is adam

replacement中的“\n”表示引用regexp中的“()”的值,n取值范围为1~9,“\3”表示引用第三个“()”中的值。

[root@c7 tmp]# sed -e 's/.* ([a-z]+),.*\<([[:alpha:]]+)$/\1:\2/' my.txt
cat:betty
dog:frank
fish:george
goat:adam

replacement中的“&”表示引用整个regexp所匹配的内容。

[root@c7 tmp]# sed 's/cat/tom&/' my.txt
this is my tomcat, my cat's name is betty
this is my dog, my dog's name is frank
this is my fish, my fish's name is george
this is my goat, my goat's name is adam

和地址匹配类似,斜线“/”也可以换成其他任意字符作为固定字符。如果regexp或者replacement中包含了固定字符的话,那么应该将固定字符转义。

s///
s@@@
sccc
s/\/etc\/fstab/\/root\/a.txt/
s@/etc/fstab@/root/a.txt@

sed其实有两种标准,一种是posix兼容模式,另一种是gnu扩展。我们使用的是gnu sed,因此是支持gnu扩展的。

gnu扩展的sed,支持特殊序列(special sequence),它由一个反斜线和一个字母组成:

  • \l:将replacement替换成小写直到遇到\u或者\e。
  • \l:将下一个字符替换成小写。
  • \u:将replacement替换成大写直到遇到\l或者\e。
  • \u:将下一个字符替换成大写。
  • \e:停止\u或者\l带来的大小写转换功能。
[root@c7 tmp]# sed 's/my/\uyour/' my.txt
this is your cat, my cat's name is betty
this is your dog, my dog's name is frank
this is your fish, my fish's name is george
this is your goat, my goat's name is adam
[root@c7 tmp]# sed 's/my/\uyo\uur/' my.txt
this is your cat, my cat's name is betty
this is your dog, my dog's name is frank
this is your fish, my fish's name is george
this is your goat, my goat's name is adam
[root@c7 tmp]# sed 's/my/\uyou\er/' my.txt
this is your cat, my cat's name is betty
this is your dog, my dog's name is frank
this is your fish, my fish's name is george
this is your goat, my goat's name is adam

flags:文本替换专属的标记,用于改变替换的一些行为。

部分标记和命令是同一个字母,要根据字母出现的位置来判断是标记或者命令。

g:global,全局替换,默认情况下,s命令只替换行内第一个匹配的regexp,使用该选项将替换所有。

[root@c7 tmp]# sed 's/my/your/g' my.txt
this is your cat, your cat's name is betty
this is your dog, your dog's name is frank
this is your fish, your fish's name is george
this is your goat, your goat's name is adam

number:标记可以是一个数字,表示替换第几个匹配的regexp,默认是1。

[root@c7 tmp]# sed 's/my/your/2' my.txt
this is my cat, your cat's name is betty
this is my dog, your dog's name is frank
this is my fish, your fish's name is george
this is my goat, your goat's name is adam

对于gnu sed来说,g和number可以结合使用,表示从number位置开始替换至最后一个。

[root@c7 tmp]# cat sed_replace_flags.txt
a a a a a a
[root@c7 tmp]# sed 's/a/b/3g' sed_replace_flags.txt
a a b b b b

p:替换成功则打印。默认情况下,替换成功后会打印所有模式空间中的数据,如果我们结合-n选项和p标记的话,就可以只显示替换成功后的行。

[root@c7 tmp]# sed -n 's/dog/gouzi/p' my.txt
this is my gouzi, my dog's name is frank

i或者i:在匹配regexp的时候,忽略大小写。

w filename:替换成功的时候,把结果输出到文件。支持2个特殊文件,/dev/stderr和/dev/stdout。

[root@c7 tmp]# sed -n 's/my/your/gw change.txt' my.txt
[root@c7 tmp]# cat change.txt 
this is your cat, your cat's name is betty
this is your dog, your dog's name is frank
this is your fish, your fish's name is george
this is your goat, your goat's name is adam

 

高级使用

高级使用主要涉及到sed的多行模式(multi-line mode),即模式空间中会存在多行的数据。需要理解上文提到的【工作原理】。

这块难度比较大,我自己也不太会用。平时使用中更多的还是善用正则实现sed的简单替换。

因此只这里列出多行模式相关的命令翻译。

g:使用保持空间的数据替换模式空间。

g:向模式空间追加一个换行,然后将保持空间的数据追加至模式空间。

h:使用模式空间的数据替换保持空间。

h:向保持空间追加一个换行,然后将模式空间的数据追加至保持空间。

x:将模式空间和保持空间的数据互换。