使用

创建file.txt,作为下面示例的输入。后面的命令默认独立演示,直接修改文件后,可重新执行下面的命令恢复示例。

1
2
3
4
5
6
7
cat > file.txt <<'EOF'
old_text old_text
hello shell
old_text1 old_text2
123456 abcde
old_text at end
EOF

1.在行前或行后插入内容

行前插入

1
2
3
sed '/hello/i\
new_line
' file.txt
1
2
3
4
5
6
old_text old_text
new_line
hello shell
old_text1 old_text2
123456 abcde
old_text at end

行后追加

1
2
3
sed '/hello/a\
new_line
' file.txt
1
2
3
4
5
6
old_text old_text
hello shell
new_line
old_text1 old_text2
123456 abcde
old_text at end
  • /hello/选择包含hello的行,也可以换成行号
  • i在行前插入,a在行后追加
  • 匹配多行时,每个匹配行都会执行一次

💡提示

GNU sed还支持sed '/hello/i new_line' file.txt这样的单行写法。这里使用带实际换行的形式,兼顾macOS自带的BSD sed。

2.替换整行内容

1
2
3
sed '/hello/c\
new_line
' file.txt
1
2
3
4
5
old_text old_text
new_line
old_text1 old_text2
123456 abcde
old_text at end

c替换选中的整行,s替换行中匹配到的字符串,两者不要混淆。

3.修改源文件

直接修改

1
2
3
4
5
# GNU sed
sed -i 's/old_text/new_text/g' file.txt

# macOS / BSD sed,同样的操作
sed -i '' 's/old_text/new_text/g' file.txt

上述两种写法选择一种即可。命令正常完成时没有终端输出,可以通过cat file.txt查看修改结果。

先备份后修改

1
sed -i.bak 's/old_text/new_text/g' file.txt

执行后得到两个文件。

1
2
file.txt       修改后的文件
file.txt.bak 修改前的文件

💡提示

建议先不加-i预览结果,再使用-i.bak修改。重复使用同一个备份后缀时,旧备份可能被覆盖。

不要使用sed '命令' file.txt > file.txt,Shell会先清空输出文件,sed可能读不到原来的内容。

4.从文件读取内容

1
2
printf '%s\n' 'read from another file' > extra.txt
sed '/123456/r extra.txt' file.txt
1
2
3
4
5
6
old_text old_text
hello shell
old_text1 old_text2
123456 abcde
read from another file
old_text at end

r extra.txt读取另一个文件,并在匹配行后输出它的内容。匹配多行时,会插入多次。

5.写入文件

1
2
sed -n '/123/w result.txt' file.txt
cat result.txt
1
123456 abcde
  • w result.txt把选中的模式空间内容写入文件
  • -n关闭默认输出,所以sed本身不会在屏幕上显示这些行
  • w不是Shell重定向;同一次运行中,多次匹配的结果会依次写入,原有目标文件内容会被覆盖

6.字符转换

1
sed '1,2y/abcde/ABCDE/' file.txt
1
2
3
4
5
olD_tExt olD_tExt
hEllo shEll
old_text1 old_text2
123456 abcde
old_text at end

只转换第1~2行,a对应A、b对应B,依此类推。

y按字符逐个转换,不使用正则表达式;两边的字符数量必须相同。

7.打印奇偶行

打印奇数行

1
sed -n 'p;n' file.txt
1
2
3
old_text old_text
old_text1 old_text2
old_text at end

打印偶数行

1
sed -n 'n;p' file.txt
1
2
hello shell
123456 abcde

这里的n读取当前输入的下一行,替换模式空间,再从下一条命令继续执行;它不会自动读取另一个文件。

GNU sed也支持步长地址。

1
2
sed -n '1~2p' file.txt    # 从第1行开始,每隔2行选中一行
sed -n '2~2p' file.txt # 从第2行开始,每隔2行选中一行

8.保持空间

除了模式空间,sed还有一个用于暂存内容的保持空间(Hold Space)。

flowchart LR P["模式空间:当前处理的内容"] -->|"h:复制并覆盖"| H["保持空间:暂存的内容"] P -->|"H:追加换行和内容"| H H -->|"g:复制并覆盖"| P H -->|"G:追加换行和内容"| P

这张图是内容的传递方向,不是处理循环;只有执行对应命令时,两个空间之间才会传递内容。x则直接交换两边的内容。

指令 说明
h 将模式空间复制到保持空间,覆盖原有内容
H 在保持空间后追加换行符和模式空间内容
g 将保持空间复制到模式空间,覆盖原有内容
G 在模式空间后追加换行符和保持空间内容
x 交换两个空间的内容

举个例子

1
sed -e '/123/h' -e '$G' file.txt
1
2
3
4
5
6
old_text old_text
hello shell
old_text1 old_text2
123456 abcde
old_text at end
123456 abcde
  • 遇到包含123的行时,h将这一行复制到保持空间
  • 遇到最后一行时,G将保存的内容追加到模式空间
  • 最后默认输出模式空间,因此看起来像在文件末尾追加了一行;原文件没有被修改

💡提示

多行匹配/123/时,后一次h会覆盖前一次,最终只追加最后一个匹配行。可以把它理解为只有一个格子的剪贴板,不是历史记录列表。

如果一直没有匹配,保持空间为空,G仍会追加一个换行符,输出中会多出一个空行。

总结

(1)i、a、c分别用于行前插入、行后追加、替换整行

(2)r和w用于读写文件,-i用于直接修改输入文件

(3)模式空间用于当前处理,保持空间用于跨处理轮次保存内容

(4)小写h、g是覆盖,大写H、G是追加

练习

基础

先复制一份系统用户文件,在副本上练习。

1
cp /etc/passwd user.txt

系统用户信息因环境而异,下面只列出命令,不写固定输出。除第13题外,这些命令只输出处理结果。

1.打印所有行

1
sed -n 'p' user.txt

2.打印第1~8行

1
sed -n '1,8p' user.txt

3.打印包含bash的行

1
sed -n '/bash/p' user.txt

4.删除第6行及其后面的所有行

1
sed '6,$d' user.txt

5.打印包含nologin的行

1
sed -n '/nologin/p' user.txt

6.将bash替换成zsh,显示替换后的相关行

1
sed 's/bash/zsh/g' user.txt | grep 'zsh'

7.将/sbin/nologin替换成/bin/login

1
sed 's#/sbin/nologin#/bin/login#g' user.txt | grep 'login'

路径中本来就有/,换成#作为替换命令的分隔符,可以少写一些转义符。

8.输出行号和内容

1
sed -n '=;p' user.txt

行号和内容会分别占一行。

9.删除第5~10行中的数字,并只输出这一范围

1
sed -n -e '5,10s/[0-9]//g' -e '5,10p' user.txt

10.只保留ASCII数字和大小写字母,输出第6~14行

1
LC_ALL=C sed -n -e 's/[^0-9a-zA-Z]//g' -e '6,14p' user.txt

这里也会删除空格、冒号和下划线,并不是只删除标点。

11.交换第一个字段和最后一个字段,输出最后5行

1
sed -E 's/^([^:]*):(.*):([^:]*)$/\3:\2:\1/' user.txt | tail -n 5

passwd文件使用冒号分隔字段。注意,^(.)(.*)(.)$交换的是首尾字符;交换字段时,需要匹配到分隔符的位置。

12.给日志的第4行及以后加上待检查标记,输出第2~6行

1
2
3
4
5
6
7
8
9
cat > deploy.log <<'EOF'
INFO start deploy
INFO build complete
INFO upload complete
WARN health check slow
ERROR health check failed
INFO rollback complete
EOF
sed -n -e '4,$s/$/ [review]/' -e '2,6p' deploy.log
1
2
3
4
5
INFO build complete
INFO upload complete
WARN health check slow [review]
ERROR health check failed [review]
INFO rollback complete [review]

使用$在行尾追加;.$会连同最后一个字符一起替换。这里仅给输出加标记,方便排查,不修改原日志。

13.修改本地配置中的端口,直接修改并备份

1
2
3
printf '%s\n' 'APP_PORT=3000' 'APP_HOST=127.0.0.1' > app.env
sed -i.bak 's/^APP_PORT=3000$/APP_PORT=8080/' app.env
cat app.env
1
2
APP_PORT=8080
APP_HOST=127.0.0.1

app.env.bak保留修改前的配置。这条命令只修改文本,应用是否读取新端口还要看启动方式。

14.显示奇数行

1
sed -n 'p;n' user.txt

15.显示偶数行

1
sed -n 'n;p' user.txt

小应用

1.提取最后一个目录名

1
echo '/etc/rc.d/init.d/' | sed -E 's#/*$##;s#^.*/##'
1
init.d
  • 第一条命令去掉末尾的斜线
  • 第二条命令去掉最后一个斜线及其前面的内容
  • 对/etc/rc.d/init.d也可以使用;如果输入只有根目录/,结果会为空

2.修改应用日志级别

开发时开着调试日志很方便,处理完问题后,可以把本地配置中的日志级别改回来。

1
2
3
4
5
6
7
cat > app-demo.conf <<'EOF'
# LOG_LEVEL=DEBUG
LOG_LEVEL=DEBUG
LOG_PATH=./logs/app.log
EOF
sed -i.bak 's/^LOG_LEVEL=.*/LOG_LEVEL=INFO/' app-demo.conf
cat app-demo.conf
1
2
3
# LOG_LEVEL=DEBUG
LOG_LEVEL=INFO
LOG_PATH=./logs/app.log

^LOG_LEVEL=锚定行首,避免修改注释或其他配置项。这里只修改文件,是否需要重启或重新加载配置,取决于应用本身。

日志可以少一点,备份还是要有的。毕竟“我就改一行”也是不少事故故事的开场白。

3.提取网卡的IPv4地址

Linux中可以使用ip命令,避免依赖ifconfig输出的固定行号。

1
ip -4 -o addr show dev ens33 | sed -nE 's/.* inet ([0-9.]+)\/.*/\1/p'

ens33需要替换为实际网卡名称。下面用模拟输入演示同一个表达式。

1
printf '%s\n' '2: ens33    inet 192.168.10.20/24 brd 192.168.10.255 scope global ens33' | sed -nE 's/.* inet ([0-9.]+)\/.*/\1/p'
1
192.168.10.20

一个网卡存在多个IPv4地址时,会分别输出。

参考

GNU sed保持空间命令
POSIX sed说明