第3章 查找与替换

3.1 grep 查找文本

三个历史版本

旧命令 等价POSIX写法 说明
grep grep BRE基础正则
egrep grep -E ERE扩展正则
fgrep grep -F 纯字面字符串,不解析正则

POSIX标准推荐使用选项形式,不建议直接写egrep/fgrep,但现实环境依旧兼容。

grep常用选项

选项 作用
-E 使用ERE扩展正则,替代egrep
-F 固定字符串匹配,不解析正则,替代fgrep
-e pattern 指定模式;模式以-开头必须用‑e
-f file 从文件读取多条匹配模式
-i 忽略大小写
-l 只打印匹配的文件名,不打印匹配行
-q 静默模式,只看退出状态,无输出;脚本判断用,屏幕什么都不打印,只返回成功 / 失败状态。
-s 屏蔽错误,常和‑q一起用
-v 反向匹配:输出不匹配的行

示例

1
2
3
4
who | grep -F austen       # 纯字符串匹配austen
who | grep austen          # 无元字符,效果和-F一样
grep -q root /etc/passwd   # 静默,$?看是否匹配成功
grep -v '^$' file.txt      # 过滤掉空行

3.2 正则表达式

两套标准:

  1. BRE基础正则grep默认、sed、ed;很多符号需要\开启特殊能力
  2. ERE扩展正则grep -E、awk;() {} + ? |直接写,不用反斜杠

BRE与ERE元字符对照表

符号 BRE(grep默认) ERE(grep -E) 含义
\ 转义符;消除元字符特殊含义;BRE给普通符号加\激活特殊能力
. 匹配任意1个字符(不匹配NUL)
* 前面单元重复0次或多次
^ 行开头锚点;BRE仅在正则开头生效
$ 行结尾锚点;BRE仅在正则末尾生效
[...] 字符集,匹配括号中任意一个字符;[^...]取反
\{n,m\} 次数区间,必须带反斜杠
\( \) 捕获分组,反向引用\1\(内容\)捕获分组:把括号里面匹配到的文本 “抓起来、存起来”。\1\2\9 叫做反向引用:把刚才抓出来的文本,再拿出来复用
\1~\9 反向引用;ERE不支持反向引用
{n,m} 次数区间,直接写,不需要\
+ 前面单元至少1次(1次或多次)
? 前面单元0次或1次,可选
` `
() 分组,直接写括号

⚠重要:ERE没有反向引用,如果要\1捕获,必须切回BRE。

POSIX方括号字符集(可移植优先用这个,不要写[a‑z]

必须套两层方括号 [[:xxx:]]

字符集 含义
[[:alnum:]] 字母+数字
[[:alpha:]] 大小写字母
[[:digit:]] 数字0‑9
[[:lower:]] 小写字母
[[:upper:]] 大写字母
[[:blank:]] 空格、tab
[[:space:]] 全部空白字符
[[:punct:]] 标点符号

注意:[:digit:]不能单独用!外面还要包一层[],写成[[:digit:]]

简单正则示例

表达式 匹配说明
^tolstoy 行开头是tolstoy
tolstoy$ 行结尾是tolstoy
^tolstoy$ 整行恰好等于tolstoy
tol.*toy tol后面任意字符(0个及以上),后面接toy

单词边界(GNU扩展,非POSIX标准,可移植脚本慎用)

写法 含义
\<word 单词开头
word\> 单词结尾
\<word\> 完整独立单词

示例

1
grep '\<chop\>' test.txt   # 匹配完整单词 chop

注意:awk、POSIX标准正则没有\< \>;属于GNU grep/sed扩展。

3.3 sed流编辑器

sed:读取输入流,按脚本指令处理,输出到标准输出;默认不修改原文件

sed语法

1
2
sed [-n] -e '命令1' -e '命令2' inputfile
sed [-n] -f script.sed inputfile
选项 作用
‑n 关闭自动打印;只打印p命令显式输出的行,p命令是 sed 编辑指令
‑e cmd 一条编辑脚本,多条命令写多个‑e
‑f file 从文件读取sed脚本

区分:

  • -n -e -fshell传给sed的启动参数,写在单引号外面
  • 单引号' '内部:sed脚本语言,s///pd、地址匹配

sed替换命令 s/查找/替换/标记

  1. 定界符:默认/;遇到路径大量斜杠,可以换别的符号如s:aaa:bbb:,不需要转义大量/
  2. 替换标记:
    • g:global全局,一行内全部匹配替换;不加g只替换该行第一个匹配
    • 数字:s/a/b/2只替换该行第2处匹配
  3. 替换文本特殊符号:
    • &:代表整个正则匹配到的全部内容
    • \1 \2 ... \9:BRE捕获分组反向引用

示例

1
2
3
4
5
# 在Atlanta后面追加文字
echo "Atlanta" | sed 's/Atlanta/&, the capital of the South/'

# 只替换第二个Tolstoy
echo "Tolstoy aa Tolstoy bb" | sed 's/Tolstoy/Camus/2'

sed 地址(筛选哪些行执行命令)

格式:地址 命令;没有地址,命令作用全部行
sed 的地址:就是筛选器:用来指定,哪些行要执行后面的 sed 命令

地址写法 说明
/regex/ 匹配正则的行
n 第n行;$最后一行
n,m 从第n行到第m行(行号区间)
/re1/,/re2/ 从匹配re1的行开始,直到匹配re2的行结束
/re/! cmd 不匹配re的行才执行cmd;!紧贴前面,不要空格
N q 读到N行执行q退出;模拟head

示例

1
2
3
sed -n '5p' file.txt        # 只打印第5行
sed -n '/root/p' passwd     # 打印匹配root的行
sed '10q' file.txt          # 读到第10行直接退出,输出前10行

空模式s//xxx/:复用最近一次成功的正则匹配。
例如:sed ‘/Tom/ s/Tom/TOM/’ test.txt,/Tom/地址筛选器,不属于 s 命令内部s//TOM/一条完整独立的 s 替换命令,只是 s 里面查找位置写空,复用上边地址匹配到的内容。sed ’s/Tom/TOM/’ test.txt,这里没有写地址,所以s/Tom/TOM/这条替换命令,对文件所有行全部执行

sed重要规则

  1. 模式空间:sed读入每行放到模式空间做处理,处理完输出;
  2. 默认自动打印模式空间;‑n关闭自动打印,只有p才输出;
  3. q命令:直接终止sed程序,不再读后续输入;
  4. 正则是最长最左匹配T.*y会尽可能向后匹配到最后一个y。

⚠坑:*修饰紧靠左边那一个字符,不是“任意字符”;.才代表任意单个字符。
b*:匹配0个或多个b,ac也能匹配(中间0个b)。因为b*允许0 个 bac里面没有 b,刚好符合条件。例如:echo "ac" | grep 'b*'这条命令会匹配成功,输出ac

3.4 字段处理工具 cut / join / awk

记录:一行;字段:一行被分隔符切开的一列列数据。

cut:截取字符/截取字段

两种模式:

  1. -c:按字符截取
  2. -f:按字段截取,‑d设置单个字符分隔符;默认分隔符tab。
参数 说明
‑s 配合‑f:没有分隔符的行直接丢弃,不输出;不加‑s原样输出整行

范围写法(‑c‑f通用)

写法 含义
n 第n项
n,m 第n、m项
n‑m n到m
‑m 开头到m
n‑ n到行末尾

示例

1
2
cut -d: -f1,5 /etc/passwd     # 冒号分隔,取1、5字段
cut -c 1‑10 file.txt          # 取每行1‑10号字符

硬限制:‑d只能单个字符,多字符分隔不能用cut,改用awk。

join:按键合并两个文件(类似数据库内连接inner‑join)

硬性前提:两个输入文件,用于join的key必须提前排序!否则结果错乱,不会报错

语法:

join [-1 N] [-2 N] [-t 分隔符] file1 file2
选项 说明
‑1 n file1使用第n字段作为连接键
‑2 n file2使用第n字段作为连接键
‑t char 设置输入输出分隔符;默认任意空白(多个空格/tab视为一个分隔)
‑o 1.1 -o 2.2 指定输出哪些字段,1.1代表file1第1字段

默认行为:只输出两边都有相同key的记录,单边独有的丢弃(内连接)

emp.txt(文件 1:姓名:员工 ID: 岗位)

1
2
3
4
Charlie:3:Engineer
Alice:1:Manager
Bob:2:Ops
David:4:Sales

dept.txt(文件 2:员工 ID: 部门)

1
2
3
4
1:HR
2:Dev
3:Test
5:Finance
1
join -t : -1 2 -2 1 -o 1.1 -o 1.2 -o 2.2 <(sort -t: -k2 emp.txt) <(sort -t: -k1 dept.txt)

shell 把每一个 <(command) 替换成一个特殊文件名(不是磁盘普通文件,是内存管道文件,类似 /dev/fd/63/dev/fd/62)。展开之后,join 实际收到的参数等价于:

1
join -t : -1 2 -2 1 -o 1.1 -o 1.2 -o 2.2  /dev/fd/63  /dev/fd/62
  • /dev/fd/63 → 对应第一个 sort 的输出流
  • /dev/fd/62 → 对应第二个 sort 的输出流

join 程序拿到两个文件名参数,join 内部:打开第一个 fd 读 sort‑emp 的结果,打开第二个 fd 读 sort‑dept 的结果。

  1. <(...) 产生的是一个文件名参数,传给命令,不是输入重定向。
  2. 普通 < file 是重定向,改变标准输入
  3. <(...) 生成文件名,当做命令的位置参数(join 需要两个输入文件,正好需要两个文件名)。

输出结果

1
2
3
Alice:1:HR
Bob:2:Dev
Charlie:3:Test

awk基础(字段处理)

awk程序基础格式:pattern { action }

  • pattern:匹配条件;省略pattern,全部行执行action
  • action:处理动作;省略action等价于{print}打印整行

常用内置变量

变量 含义
$0 整行完整记录
$1 $2 ... 第1、第2字段……
NF 当前行字段总数量;$NF代表最后一个字段
‑F: 命令行选项,设置输入字段分隔符
FS 输入字段分隔符;BEGIN块设置
OFS 输出字段分隔符
1
2
3
4
awk -F: '{print $1, $5}' /etc/passwd
# -F: 冒号分割,打印第1、第5字段

awk 'BEGIN{FS=":";OFS="###"} {print $1,$5}' /etc/passwd

print里面逗号不可丢,逗号会插入OFS输出分隔符;直接写字符串拼接不会加分隔。

BEGIN { ... }:读取任何输入行之前执行一次END { ... }:全部输入读取完毕之后执行一次。

本章速查卡片

命令 核心用途 示例
grep -E ERE扩展正则匹配 grep -E 'cat|dog' file
grep -F 纯字面字符串匹配 grep -F "hello world" file
grep -v 反向,输出不匹配行 grep -v '^$' file
sed 's/a/b/g' 文本替换,g全局 sed 's/old/new/g' file
sed -n '/re/p' 模拟grep,只打印匹配行 sed -n '/root/p' /etc/passwd
cut -d: -f1,5 按分隔符截取字段 cut -d: -f1,5 /etc/passwd
join -t: f1 f2 按键合并两个已排序文件 join -t: user.txt info.txt
awk -F: '{print $1}' awk字段处理 awk -F: '{print $1}' /etc/passwd

本章小结

  1. BRE/ERE区分:ERE() {} + |直接写;BRE要加反斜杠\( \) \{ \};ERE不支持\1反向引用。
  2. 正则最长最左匹配:.*会尽可能吃掉更多字符。
  3. join必须按key排序,不排序输出错乱,不会报错
  4. cut -d只支持单个字符分隔符,复杂分隔交给awk。
  5. sed -n是外部参数,不能写进单引号脚本内部。
  6. $(( ))是shell算术;grep/sed/awk里面是正则,两套语法不要混淆。
  7. 脚本尽量优先用[[:digit:]]这类POSIX字符集,少用[a‑z],规避locale语言环境带来的匹配异常。