正则表达式 (Regular Expression, RE) ≠ bash 通配符 (wildcard)! RE 是字串处理的标准表示法,以行为单位。支持 RE 的工具:vi、grep、awk、sed;不支持 RE 的(cp、ls)只能用 bash 通配符。学本章前先把第十章的通配符忘掉!

11.1 开始之前:什么是正则表达式

RE 就是处理字串的方法——通过特殊符号的排列,达成”搜寻/删除/取代”特定字串。

为什么要学? 系统每天产生海量日志,用肉眼从千百行中找问题是不可能的。正则表达式一行指令就能提取出”有问题的信息”——这是系统管理员的必备技能。邮件服务器过滤垃圾信、vi 搜索替换、shell script 分析日志,都离不开 RE。

RE 的两种类型

类型说明工具
基础正则 (BRE)最基础的符号集grep, sed
延伸正则 (ERE)支持群组、或运算egrep, grep -E

11.2 基础正则表达式

练习前提:export LANG=C; export LC_ALL=C

11.2.1 语系对正则表达式的影响

不同语系的字符编码顺序不同,直接影响 [A-Z] 这类范围匹配。练 RE 前务必切到 C 语系

LANG=C     : A B C D ... Z a b c ... z (连续)
LANG=zh_TW : a A b B c C ... z Z (大小写穿插!)

POSIX 字符类(跨语系可靠,推荐):

匹配匹配
[:alnum:]A-Z a-z 0-9[:lower:]a-z
[:alpha:]A-Z a-z[:upper:]A-Z
[:digit:]0-9[:space:]空白、Tab、CR
[:punct:]标点符号[:xdigit:]十六进制

11.2.2 grep 的一些进阶选项

grep -n 'keyword' file           # 带行号
grep -vn 'keyword' file           # 反向:不含 keyword
grep -A3 -B2 'keyword' file      # 匹配行 + 前2行 + 后3行
grep -i 'keyword' file            # 忽略大小写
grep --color=auto                 # 关键字高亮(CentOS 7 已设 alias)

grep 以整行为单位——一行中任意位置匹配到就输出整行。

11.2.3 基础正则表达式练习

练习文件:wget http://linux.vbird.org/linux_basic/0330regularex/regular_express.txt

BRE 特殊字符详解

符号含义示例
^word行首^# → 以 # 开头
word$行尾!$ → 以 ! 结尾
.恰好一个任意字符(不能为零)g..d → good, glad;不匹配 god
\跳脱特殊字符\. → 字面上的点;\' → 字面上的单引号
*重复前一个 RE 字符 0~∞ 次go*d → gd, god, good, goood…
[abc]一个字符,任选其一[Tt]est → Test 或 test
[^abc]反向——不在集合中[^a-z] → 非小写字母
[a-z]范围(与 ASCII 码有关)[0-9] = 所有数字
\{n,m\}前一个字符重复 n~m 次go\{2,5\}d → good~gooood
\{n,\}至少 n 次go\{2,\}d → 2个o以上
\{n\}恰好 n 次o\{2\} → 刚好2个o

⚠️ BRE 中 { } 必须用 \ 跳脱!( ) 也要跳脱。还有 ^[] 是反向选择,在 [] 是行首定位——两回事!

经典组合

grep -n 't[ae]st' file          # tast 或 test ——[ ] 只占一个字符位
grep -n '[^g]oo' file           # oo 前不能是 g(但一行中只要有一处满足就行)
grep -n '[^a-z]oo' file         # oo 前不能是小写字母
grep -n '^[a-z]' file           # 行首是小写字母
grep -n '^[^a-zA-Z]' file       # 行首不是英文字母
 
grep -n '\.$' file              # 行尾是小数点(仅 Linux 的 LF 换行,Windows 的 CRLF 不行)
grep -n '^$' file               # 空白行
 
# 去噪利器——去空白行和注释行
grep -v '^$' /etc/rsyslog.conf | grep -v '^#'
 
# .* 组合——零个或多个任意字符(RE 中最常用组合!)
grep -n 'g.*g' file             # g 开头 g 结尾,中间任意
 
# {} 限定次数(BRE 中必须跳脱)
grep -n 'go\{2,5\}g' file       # goog ~ goooog
grep -n 'go\{2,\}g' file        # 2个o以上

11.2.4 基础正则表达式字符汇整

RE 字符意义范例
^word行首grep -n '^#' file
word$行尾grep -n '!$' file
.恰好一个任意字符grep -n 'e.e' file
\跳脱grep -n \' file
*重复前一个字符 0~∞ 次grep -n 'ess*' file
[list]集合中任一字符grep -n 'g[ld]' file
[n1-n2]范围grep -n '[A-Z]' file
[^list]反向集合grep -n 'oo[^t]' file
\{n,m\}重复 n~m 次grep -n 'go\{2,3\}g' file

ls | grep '^a.*'(正则)≠ ls -l a*(通配符)——RE 和 wildcard 是完全不同的两套系统!

11.2.5 sed 工具

sed 是管线命令,以行为单位处理,能取代、删除、新增、撷取。

sed [-nefri] [动作]
# -n: 安静模式(只输出处理的行)
# -e: 直接在命令行执行动作
# -r: 使用延伸正则(ERE)
# -i: 直接修改文件(危险!)
动作说明
a新增——在指定行加一行
i插入——在指定行加一行
c取代——替换整行
d删除
p打印(配合 -n
s搜寻并取代
# 删除
nl /etc/passwd | sed '2,5d'                    # 删第2~5行
nl /etc/passwd | sed '3,$d'                    # 删第3~最后($ = 最后一行)
 
# 新增(多行用 \ 换行)
nl /etc/passwd | sed '2a drink tea'            # 第2行后加一行
nl /etc/passwd | sed '2a Line1\nLine2'         # 加多行
 
# 整行取代
nl /etc/passwd | sed '2,5c No 2-5 number'
 
# 打印指定行
nl /etc/passwd | sed -n '5,7p'
 
# 搜寻并取代(最核心!)
sed 's/要被取代的字串/新的字串/g'               # g=全局,不加g只替换每行第一个

实战——提取 IP 地址

# 步骤1: 取出包含 IP 的 inet 行
/sbin/ifconfig eth0 | grep 'inet '
# inet 192.168.1.100  netmask 255.255.255.0  broadcast 192.168.1.255
 
# 步骤2: 删除 IP 前面的部分(^.*inet 匹配从行首到 "inet ")
/sbin/ifconfig eth0 | grep 'inet ' | sed 's/^.*inet //g'
# 192.168.1.100  netmask 255.255.255.0  broadcast 192.168.1.255
 
# 步骤3: 删除 IP 后面的部分
/sbin/ifconfig eth0 | grep 'inet ' | sed 's/^.*inet //g' | sed 's/ *netmask.*$//g'
# 192.168.1.100

实战——去注释和空白行(配合 RE)

# 提取 MAN 相关设置,去掉注释(#开头部分)和空白行
cat /etc/man_db.conf | grep 'MAN' | sed 's/#.*$//g' | sed '/^$/d'

-i 直接修改文件

sed -i 's/\.$/!/g' file            # 所有行尾的 . 改为 !
sed -i '$a # This is a test' file  # 文件最后加一行

11.3 延伸正则表达式

ERE 的符号无需反斜线跳脱,使用 egrep(或 grep -E)。

BREERE含义
\++1~∞ 次(至少一次)
\??0 或 1 次
|``
\(\)()群组
\{\}{}次数范围
egrep -n 'go+d' file               # god, good, goood...(至少一个 o)
egrep -n 'go?d' file               # gd 或 god(0~1 个 o)
egrep -n 'gd|good|dog' file        # 或运算
egrep -n 'g(la|oo)d' file          # glad 或 good(群组+或)
egrep -v '^$|^#' file              # 一行去空白+注释!vs BRE 需要两次 grep
echo 'AxyzxyzxyzxyzC' | egrep 'A(xyz)+C'   # A + 一个以上xyz + C

+ vs * vs ?+=1个以上,*=0个以上,?=0或1个。所以 go*dgo+dgo?d 三者取并集等于 go*d

11.4 文件的格式化与相关处理

11.4.1 格式化打印:printf

printf '打印格式' 实际内容
格式说明
%nsn 个字符宽度的字串
%nin 个字符宽度的整数
%N.nf总宽度 N,n 位小数
\tTab
\n换行(必须手动加!)
\xNN16进制的 ASCII 字符
printf '%10s %5i %5i %5i %8.2f \n' $(cat data.txt | grep -v Name)
#    DmTsai    80    60    92    77.33
# 字段宽度:%8.2f = 8位总宽,其中整数5位 + 小数点1位 + 小数2位

printf 不是管线命令,需用 $(cmd) 或反引号传递数据给它。

11.4.2 awk:好用的数据处理工具

awk 以行为单位处理,每行以字段为最小处理单位。默认分隔符为空白/Tab。

awk '条件{动作}' filename
内置变量含义
$0整行
$1, $2...第1列,第2列…
NF每行的字段总数
NR当前行号
FS字段分隔符(默认空白,可设为 FS=":"
# 取第1列和第3列
last -n 5 | awk '{print $1 "\t" $3}'
 
# 行号 + 字段数
last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}'
 
# 条件——UID < 10 的行,并指定分隔符为冒号(用 BEGIN 保证第一行也生效)
cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t" $3}'
 
# 计算总和——格式对齐
cat pay.txt | awk 'NR==1{printf "%10s %10s %10s %10s %10s\n",$1,$2,$3,$4,"Total"}
NR>=2{total=$2+$3+$4; printf "%10s %10d %10d %10d %10.2f\n",$1,$2,$3,$4,total}'

awk 逻辑运算> < >= <= == !=

awk 变量不加 $total 而非 $total)。多指令用分号或换行分隔。if 条件也可放在 {} 内。

11.4.3 文件比对工具

diff — 逐行比对文本文件(两个版本之间的差异):

diff [-bBi] from-file to-file
# -b: 忽略多余空白;-B: 忽略空白行;-i: 忽略大小写
diff passwd.old passwd.new
# 4d3    ← 左边第4行被删(d),基准是右边第3行
# 6c5    ← 左边第6行被替换(c)为右边第5行
 
diff -Naur passwd.old passwd.new > passwd.patch  # 生成补丁文件
diff /etc/rc0.d/ /etc/rc5.d/                      # 比对整个目录

cmp — 逐字节比对(可比较二进制文件),默认只报告第一个不同点:

cmp passwd.old passwd.new
# passwd.old passwd.new differ: char 106, line 4

patch — 应用补丁:

patch -pN < patch_file      # 升级(-pN=去掉N层目录)
patch -R -pN < patch_file   # 还原

11.4.4 文件打印准备: pr

自动添加页标题(文件时间、文件名、页码):

pr /etc/man_db.conf
# 2014-06-10 05:35    /etc/man_db.conf    Page 1

命令速查表

命令用途
grep -n/-v/-A/-B/-i 'RE' file正则搜索
grep -v '^$' | grep -v '^#'去空白行和注释行
sed 's/old/new/g'全域替换
sed 'Nd' / sed 'N,Md'删除指定行
sed -n 'N,Mp'只打印指定行
sed '/^$/d'删除空白行
sed -i 's/a/b/g' file直接修改文件
egrep 'a|b' / egrep 'g(la|oo)d'延伸正则(或/群组)
printf '%10s %5i %8.2f \n'格式化输出
awk '{print $1 "\t" $3}'取指定字段
awk 'BEGIN{FS=":"} $3<10{...}'条件+自定义分隔符
diff -Naur old new > p.patch生成补丁
patch -pN < p.patch应用补丁
cmp file1 file2逐字节比对
pr file打印格式化