正则表达式 (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)。
| BRE | ERE | 含义 |
|---|---|---|
\+ | + | 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*d、go+d、go?d三者取并集等于go*d。
11.4 文件的格式化与相关处理
11.4.1 格式化打印:printf
printf '打印格式' 实际内容| 格式 | 说明 |
|---|---|
%ns | n 个字符宽度的字串 |
%ni | n 个字符宽度的整数 |
%N.nf | 总宽度 N,n 位小数 |
\t | Tab |
\n | 换行(必须手动加!) |
\xNN | 16进制的 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 4patch — 应用补丁:
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 | 打印格式化 |