环境
- Ubuntu 22.04
概述
sed 是“stream editor”的意思,用来处理文本,比如做文本查找、替换。
处理单行文本
用法1
语法: sed 's/aaaa/bbbb/'
aaaa:用正则表达式来匹配字符串,并用圆括号括起来需要获取的内容,如xx\(yy\)zzbbbb:获取第几个圆括号里的内容,如\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