概念
一种流编辑器(stream editor),对标准输出或文件逐行进行处理。
语法格式(参数 + 动作):
stdout | sed [option] 'function'
sed [option] 'function' file
鸟哥建议,function 总是用引号括起来。
option
选项 | 含义 |
---|---|
-n | 静默模式,只打印模式匹配行(sed 默认打印原文件的所有数据) |
-e | 默认选项,直接在命令行进行sed编辑(如果存在多个动作,-e就不能省略) |
-f | 将文件中的内容作为编辑动作,用法:-f file |
-r | 支持扩展正则表达式,如果没有 -r,则只支持基础正则 |
-i | 直接修改文件内容 |
function
编辑命令 | 含义 |
---|---|
p | 打印,通常与 sed -n 一起使用 |
a | 追加 |
c | 替换 |
i | 插入 |
d | 删除 |
s | 替换,与正则搭配使用:'1,20s/old/new/g' |
r | 读取外部文件,行后追加(用法:'r filename' ) |
w | 将匹配的内容,写入到外部文件(用法:'w filename' ) |
事实上,sed 玩儿得 6 不 6 全看对正则是否熟悉。这是因为,除了 s 外,其他的动作也可以搭配正则使用,就连行号都可以用正则代替。比如有以下文本,我希望输出所有数字开头的行内容。
1 Hello World
Hello World
2 Hello world
Hello World
Hello World
那么:cat txt | sed -n '/^[1-9]/p'
,其效果等同于:cat txt | sed -n '1p;3p'
。显然,在前提条件下,第一种处理方式要比第二种灵活、强大许多。
s///g
替换语句是对行内操作,c 则是以行为最小处理单位做处理。s 的前面可以放参数,代表需要处理哪行,参数可以是正则(要求:将数字开头的行中的 Hello 用 # 替换):
root@hui:shell-study$ cat txt | sed '/^[1-9]/s/Hello/#/g'
1 # World
Hello World
2 # world
Hello World
Hello World
g 表示需要处理行内所有符合条件的关键字。现实中可能不需要全部处理,有时只需要处理行内第二个符合条件关键字,那就不用 g,直接用数字(要求:将数字开头的行中的第二个 l ,用 # 替换):
root@hui:shell-study$ cat txt | sed '/^[1-9]/s/l/#/2'
1 Hel#o World
Hello World
2 Hel#o world
Hello World
Hello World
正则补充
由于正则相关的内容很多,所以在此并非要补充正则的知识,而是补充一些我的知识体系里需要的那部分。
圆括号匹配
现在我需要将 /etc/passwd 文件中每一行的第一个冒号之前的内容用符号 “【】” 括起来,那么:
$ nl passwd | sed -rn 's/([a-z-]+):/【\1】:/1;p'
1 【root】:x:0:0:root:/root:/bin/bash
2 【bin】:x:1:1:bin:/bin:/sbin/nologin
3 【daemon】:x:2:2:daemon:/sbin:/sbin/nologin
4 【adm】:x:3:4:adm:/var/adm:/sbin/nologin
5 【lp】:x:4:7:lp:/var/spool/lpd:/sbin/nologin
...
也就是说,用圆括号括起来的部分,可以在后面用 \1 代表匹配出来的内容;如果前面有两个圆括号,则第二个括号中匹配出来的内容可用 \2 代替;依次类推。
占位符 &
现在我想标记出所有的 root 和 bin ,则可以:
root@hui:shell-study$ nl passwd | sed -r 's/root|bin/【&】/g'
1 【root】:x:0:0:【root】:/【root】:/【bin】/bash
2 【bin】:x:1:1:【bin】:/【bin】:/s【bin】/nologin
3 daemon:x:2:2:daemon:/s【bin】:/s【bin】/nologin
4 adm:x:3:4:adm:/var/adm:/s【bin】/nologin
5 lp:x:4:7:lp:/var/spool/lpd:/s【bin】/nologin
...
也就是说,占位符 & 用来表示前面匹配到的内容本身。
美元符 $
美元符 $ 表示“末尾”,如果我想在每一行的末尾添加感叹号,可以:nl passwd | sed 's/$/!/g'
;如果我想在整个文本的末尾添加 “over” ,可以:nl passwd | sed '$a over'
。
相对应的, ^ 表示开头,用法与 $ 相似。
练习
下面以 /etc/passwd 文件作为演示文件。
1.输出文件中2到5行的内容:
root@hui:shell-study$ nl passwd | sed -n '2,5p'
2 bin:x:1:1:bin:/bin:/sbin/nologin
3 daemon:x:2:2:daemon:/sbin:/sbin/nologin
4 adm:x:3:4:adm:/var/adm:/sbin/nologin
5 lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
2.在第 2 行后面追加 “hello world” ,同时只输出前5行的内容:
root@hui:shell-study$ nl passwd | sed -n -e '2a hello world' -e '1,5p'
1 root:x:0:0:root:/root:/bin/bash
2 bin:x:1:1:bin:/bin:/sbin/nologin
hello world
3 daemon:x:2:2:daemon:/sbin:/sbin/nologin
4 adm:x:3:4:adm:/var/adm:/sbin/nologin
5 lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
此时有两个操作,因此 -e 选项不可省略。
3.替换第 2 到 5 行的内容,同时只输出前 6 行的内容:
root@hui:shell-study$ nl passwd | sed -n -e '2,5c No 2-5 number' -e '1,6p'
1 root:x:0:0:root:/root:/bin/bash
No 2-5 number
6 sync:x:5:0:sync:/sbin:/bin/sync
动作 i(插入) 与 动作 d(删除) 的用法与 a、c 一样。
4.将 1 到 3 行中的冒号 “:” 用空格替换,并输出前 4 行:
root@hui:shell-study$ nl passwd | sed -n -e '1,3s/:/ /g' -e '1,4p'
1 root x 0 0 root /root /bin/bash
2 bin x 1 1 bin /bin /sbin/nologin
3 daemon x 2 2 daemon /sbin /sbin/nologin
4 adm:x:3:4:adm:/var/adm:/sbin/nologin
此时,多个动作可以通过 “;” 连接起来,合成一句话:
root@hui:shell-study$ nl passwd | sed -n '1,3s/:/ /g;1,4p'
1 root x 0 0 root /root /bin/bash
2 bin x 1 1 bin /bin /sbin/nologin
3 daemon x 2 2 daemon /sbin /sbin/nologin
4 adm:x:3:4:adm:/var/adm:/sbin/nologin
最后来一个综合性的例子。
5.打印所有网卡的 ipv4:
因我的环境没有安装 ifconfig ,所以这里用 ip addr 来代替,因此输出为:
root@hui:shell-study$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:d5:d9:d4 brd ff:ff:ff:ff:ff:ff
inet 192.168.20.131/24 brd 192.168.20.255 scope global noprefixroute dynamic ens33
valid_lft 1765sec preferred_lft 1765sec
inet6 fe80::bbfb:8164:68e9:75c2/64 scope link noprefixroute
valid_lft forever preferred_lft forever
显然,此时要获取 ipv4 的值,那么得先挑出 inet 这一行来:
root@hui:shell-study$ ip addr | grep inet
inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host
inet 192.168.20.131/24 brd 192.168.20.255 scope global noprefixroute dynamic ens33
inet6 fe80::bbfb:8164:68e9:75c2/64 scope link noprefixroute
此时有 ipv6 的内容,尽管可以通过 grep -v 干掉它,但这里我想用 sed 实现目的。于是:
root@hui:shell-study$ ip addr | grep inet | sed 's/inet6.*//g'
inet 127.0.0.1/8 scope host lo
inet 192.168.20.131/24 brd 192.168.20.255 scope global noprefixroute dynamic ens33
接下来需要干掉第二个ipv4 brd 后面的内容:
root@hui:shell-study$ ip addr | grep inet | sed 's/inet6.*//g' | sed 's/brd.*//g'
inet 127.0.0.1/8 scope host lo
inet 192.168.20.131/24
接下来我们干掉有所得字母和空格就好了:
root@hui:shell-study$ ip addr | grep inet | sed 's/inet6.*//g' | sed 's/brd.*//g' | sed -e 's/[a-z]*//g' -e 's/\s//g'
127.0.0.1/8
192.168.20.131/24
此时发现两个 ipv4 下面都有空行,空行也要干掉(^$
表示空行),因此:
root@hui:shell-study$ ip addr | grep inet | sed 's/inet6.*//g' | sed 's/brd.*//g' | sed -e 's/[a-z]*//g' -e 's/\s//g' | sed '/^$/d'
127.0.0.1/8
192.168.20.131/24
最后得到了我们想要得的结果,但是观察发现,我们使用了很多 s 操作,从上面也可以看出,这些动作可以通过管道连接起来,也可以用 -e 组合起来。事实上,我们也可以使用扩展正则表达式来合并它们:
root@hui:shell-study$ ip addr | grep inet | sed -r 's/inet6.*|brd.*|[a-z]*|\s//g' | sed '/^$/d'
127.0.0.1/8
192.168.20.131/24
用 sed
代替 grep
也并非不可以:
root@hui:shell-study$ ip addr | sed -n '/inet/p' | sed -r 's/inet6.*|brd.*|[a-z]*|\s//g' | sed '/^$/d'
127.0.0.1/8
192.168.20.131/24
还可以:
ip addr | sed -n '/inet/p' | sed -r '/inet6/d;s/brd.*|[a-z]*|\s+//g'
事实上,相关的解法有很多,并不存在标准答案,如何达到目的是看你心情。
感谢
- 《鸟哥的Linux私房菜(第三版)》
- 耗子叔的 SED 简明教程
还不快抢沙发