环境
- Ubuntu 22.04
概述
sed
是“stream editor”的意思,用来处理文本,比如做文本查找、替换。
处理单行文本
用法1
语法: sed 's/aaaa/bbbb/'
aaaa
:用正则表达式来匹配字符串,并用圆括号括起来需要获取的内容,如xx\(yy\)zz
bbbb
:获取第几个圆括号里的内容,如\1
比如,有一个由3个冒号“ :
”所分隔的字符串,比如 a:b:c:d
。想要获取第一个冒号之前的内容。
从正则表达式角度,该字符串可以看做:
.*:.*:.*:.*
分别用圆括号 ()
把两个冒号之间的内容括起来,即:
(.*):(.*):(.*):(.*)
注意,圆括号需要转义,所以是:
\(.*\):\(.*\):\(.*\):\(.*\)
然后指定获取第几部分,比如第一部分就是 \1
(注意需要转义)。
完整的例子如下:
➜ ~ echo "a:b:c:d" | sed 's/\(.*\):\(.*\):\(.*\):\(.*\)/\1/'
a
➜ ~ echo "a:b:c:d" | sed 's/\(.*\):\(.*\):\(.*\):\(.*\)/\2/'
b
➜ ~ echo "a:b:c:d" | sed 's/\(.*\):\(.*\):\(.*\):\(.*\)/\3/'
c
➜ ~ echo "a:b:c:d" | sed 's/\(.*\):\(.*\):\(.*\):\(.*\)/\4/'
d
贪婪匹配
如果我们把 a:b:c:d
用 .*:.*
来通配,即分成两部分,那么sed会怎么来划分呢?
答案:第一部分是 a:b:c
,第二部分是 d
。
➜ ~ echo "a:b:c:d" | sed 's/\(.*\):\(.*\)/\1/'
a:b:c
➜ ~ echo "a:b:c:d" | sed 's/\(.*\):\(.*\)/\2/'
d
这是因为,在正则表达式里, .*
是“贪婪式”的,会尽可能多的向右匹配。
所以,如果我们只想获取最后一个冒号右边的内容,并不需要把整个字符串按每个冒号划分为多个部分,而只需用 .*:.*
来通配,参见上面的例子。
sed貌似无法指定非贪婪式匹配。
如果我们想要获取第一个冒号前面的内容,可以像前面所介绍的,按照每个冒号分隔为多个部分,然后取第一部分。但是这种做法其实不太方便,比如如果字符串发生变化,多了一个冒号,那么正则表达式也得随之变化,否则就会取错内容。
一个变通的方法是:从左到右,把所有非冒号的字符( [^:]*
)作为第一部分( ^
表示“非”),后面就是 :.*
,即冒号以及随后的所有内容。
➜ ~ echo "a:b:c:d" | sed 's/\([^:]*\):.*/\1/'
a
用法2
sed还有一种用法,获取正则表达式匹配之外的内容。
语法: sed 's/aaaa//'
aaaa
:是一个正则表达式
例如:
➜ ~ echo "abcdefg" | sed 's/cde//'
abfg
正则表达式匹配了 cde
,则获取的是前面和后面没有匹配上的内容,即 ab
和 fg
。
按此方法,对于 a:b:c:d
,要获取最后一个冒号右边的内容,可以简化如下:
➜ ~ echo "a:b:c:d" | sed 's/\(.*\)://'
d
若正则没有匹配成功则获取整个字符串
注意:sed的这两种用法,有一个共同的行为特点:如果正则表达式没有匹配成功,则会获取整个字符串。
比如:
➜ ~ echo "abcd" | sed 's/ab\(c\)d/\1/'
c
这是正确的匹配。如果正则没有匹配上:
➜ ~ echo "abcd" | sed 's/zzzab\(c\)d/\1/'
abcd
则获取了整个字符串。
第二种用法也同理:
➜ ~ echo "abcd" | sed 's/abc//'
d
这是正确的匹配。如果正则没有匹配上:
➜ ~ echo "abcd" | sed 's/zzzabc//'
abcd
则获取了整个字符串。
处理多行文本(比如文件)
查找内容
假设在 ~/.zshrc
里包含了 JAVA_HOME
设置:
➜ ~ grep JAVA_HOME ~/.zshrc
export JAVA_HOME=/home/ding/Downloads/jdk-17.0.3.1
export PATH=${JAVA_HOME}/bin:$PATH
现在需要通过sed获取 JAVA_HOME
的设置,即 /home/ding/Downloads/jdk-17.0.3.1
。
一种方法是,先通过grep命令找到那一行(假设只有一行满足条件),然后就只需处理单行文本:
➜ ~ cat ~/.zshrc | grep JAVA_HOME= | sed 's/.*JAVA_HOME=\(.*\)/\1/'
/home/ding/Downloads/jdk-17.0.3.1
修改文件内容
比如,要把 JAVA_HOME
设置为 myPath
。
语法: sed -i '/aaaa/s/bbbb/cccc/'
aaaa
:正则表达式,用来匹配行s
:表示替换bbbb
:正则表达式,表示旧内容cccc
:新内容
其中, -i
表示修改文件,否则只会把结果输出。
注:可以在最后加一个 g
,表示对本行做全局替换,否则只替换第一处。本例中只有一处,所以无需加 g
。
sed -i '/JAVA_HOME=/s/=.*/=myPath/' ~/.zshrc
本例中:
aaaa
:为JAVA_HOME=
,即查找符合条件的行bbbb
:为=.*
,即等号以及后面所有内容cccc
:为目标字符串
运行后,文件内容被修改为:
......
export JAVA_HOME=myPath
......
注意:这里的 bbbb
无需匹配全部内容,而只是需被替换的内容,而前面介绍的,从字符串获取文本时,正则表达式必须要匹配全部内容。
如果目标字符串需要包含 bbbb
的内容,则可用 &
来代表。
比如,要把 export JAVA_HOME=xxxxx
改为 export JAVA_HOME=xxxxx/jdk
sed -i '/JAVA_HOME=/s/=.*/=&\/jdk/' ~/.zshrc
运行后,文件内容被修改为:
......
export JAVA_HOME==/home/ding/Downloads/jdk-17.0.3.1/jdk
......
获取一部分内容
语法: sed -n '/xxxx/,/yyyy/p'
表示输出从 xxxx
到 yyyy
的行。其中 yyyy
可以为空,表示输出到最末。
比如,创建文件 test1.txt
如下:
abcdefg
hijklmn
opq
rst
uvw
xyz
则:
➜ ~ sed -n "/jk/,/st/p" test1.txt
hijklmn
opq
rst
➜ ~ sed -n "/cde/,/uv/p" test1.txt
abcdefg
hijklmn
opq
rst
uvw
➜ ~ sed -n "/w/,//p" test1.txt
uvw
xyz
在一部分内容里做替换
语法: sed -i '/xxxx/,/yyyy/s/aaaa/bbbb/'
xxxx
和yyyy
:开始行和结束行,也就是指定的部分内容aaaa
:旧内容bbbb
:新内容
也就是说,在 xxxx
和 yyyy
所指定的范围内,把 aaaa
替换为 bbbb
。
比如, test1.txt
内容如下:
rst
abcdefg
hijklmn
opq
rst
uvw
xyz
要把所有的 rst
里的 rs
替换为 123
:
sed -i '/.*/s/rs/123/' test1.txt
而如果只把下方的 rst
里的 rs
替换为 123
:
sed -i '/pq/,/u/s/rs/123/' test1.txt
其中, pq
和 u
划定了处理内容的范围。
分隔符
如果字符串里本身含有 /
,则匹配时,需要用 \
来转义。
比如,要想获取字符串 /etc/ansible/hosts
里的 ansible
:
➜ ~ echo "/etc/ansible/hosts" | sed 's/\/etc\/\(.*\)\/hosts/\1/'
ansible
这里的正则表达式为 \/etc\/\(.*\)\/hosts
,较为复杂。
为了方便,我们可以换一个分隔符,比如换成 |
:
➜ ~ echo "/etc/ansible/hosts" | sed 's|/etc/\(.*\)/hosts|\1|'
ansible
这里的正则表达式为 /etc/\(.*\)/hosts
,相对简单一些。
考一考
1
求输出结果:
echo "a:b:c:d" | sed 's/\(.*\):\(.*\):\(.*\)/\1/'
答:贪婪式匹配
➜ ~ echo "a:b:c:d" | sed 's/\(.*\):\(.*\):\(.*\)/\1/'
a:b
2
求输出结果:
echo "a:b:c" | sed 's/\(.*\):\(.*\):\(.*\):\(.*\)/\1/'
答:没有匹配成功,则获取整个字符串
➜ ~ echo "a:b:c" | sed 's/\(.*\):\(.*\):\(.*\):\(.*\)/\1/'
a:b:c
3
已知字符串 {"key1":"<value1>","key2":"<value2>",......}
,从中获取 <value2>
的值( key2
是已知字符串,且 <value2>
中不含引号 "
)。
答:
➜ ~ echo '{"key1":"value1","key2":"value2","key3":"value3"}' | sed 's/.*"key2":"\([^"]*\)".*/\1/'
value2