《鸟哥的 Linux 私房菜》——正则表达式与文件格式化处理

408 阅读10分钟

正则表达式(Regular Expression)是通过一些特殊字符的排列,用来『查找/替换/删除』一行或多行字符串文本。简而言之,正则表达式就是用在字符串文本处理上的一项『表达式』。正则表达式不是一个程序,而是一个字符串处理的标准规范,如果想要使用正则表达式处理文本,就要使用支持正则表达式的工具程序。这类工具程序有 vi, sed, awk 等。

系统管理员可以通过正则表达式从系统产生的大量信息中攫取出重要的信息,并产生便于分析和查看的报表来简化管理流程。

3.1. 什么是正则表达式

  • 正则表达式是用来处理字符串的方法,以行为单位来进行处理,通过一些特殊符号的排列,以实现对某个字符串文本的『查找/替换/删除』。
  • 正则表达式基本上是一种『表达式』,只要工具程序支持这种表达式,那么该工具程序就可使用正则表达来进行字符串/文本的处理。如 vi, grep, awk, aed;但是例如 cp, ls 等指令并未支持正则表达式,所以只能使用 bash 本身的通配符。
  • 通配符(wildcard)代表的是 bash 操作接口的一种功能,但正则表达式是一种字符串处理的方式,两者是完全不同。

3.2 基础正则表达式

语系对正则表达式的影响

  • 使用正则表达式时需要注意当时环境的语系,因为不同的语系字符的排列顺序不一样,推荐使用 LANG=C

为了避免编码所造成的英文与数字攫取出现问题,需要了解下面这些特殊的符号所代表的含义:

特殊符号代表含义
[:alnum:]代表英文大小写字母及数字,即 0-9,A-Z,a-z
[:alpha:]代表任何英文大小写字母,即 A-Z,a-z
[:blank:]代表空白键与 [Tab]
[:cntrl:]代表键盘上的控制按键,即包括 CR,LF,Tab,Del...
[:digit:]代表数字,即 0-9
[:graph:]除了空白字符(空白键与 Tab 键)外的所有按键
[:lower:]代表小写字母,即 a-z
[:print:]代表任何可以被打印出来的字符
[:punct:]代表标点符号(punctuation symbol),即 '?!;:#$...
[:upper:]代表大写字母,即 A-Z
[:space:]任何会产生空白的字符,包括空白键,Tab, CR 等
[:xdigit:]代表 16 进制的数字,即 0-9,A-F,a-f

grep 的进阶选项

$ grep [-A] [-B] [--color=auto] '查找字符串' filename
    -B, --before-context=NUM  print NUM lines of leading context
    -A, --after-context=NUM   print NUM lines of trailing context
    --color[=WHEN],
    --colour[=WHEN]           use markers to highlight the matching strings;
                              WHEN is 'always', 'never', or 'auto'

# 使用 dmesg 列出内核信息,再使用 grep 查找除包含 journald 的行
# dmesg 可列出内核产生的信息,包括硬件检测的流程也是显示出来
$ dmesg | grep 'journald'

# 将查找到的关键字显色,并加上行号来表示(颜色显示已经默认设置在 alias 中了)
$ dmesg | grep -n --color=auto 'journald'

# 将关键字的前两行和后三行也一起攫取出来显示
$ dmesg | grep -n -A3 -B2 --color=auto 'journald'

grep 在数据中查找一个字符串时,是以『整行』为单位进行数据的攫取的。

基础正则表达式的练习

练习前提:

  • 语系设置为 export LANG=C; export LC_ALL=c
  • grep 已经使用 alias 设置为 grep --color=auto

例题一:查找特定字符串

# 1. 查找包含字符串 the 的行
$ grep -n 'the' regx.txt

# 反向选择,当该行没有 the 时
$ grep -vn 'the' regx.txt

# 不区分大小写查找含有 the 的行
$ grep -in 'the' regx.txt

例题二:利用 [] 来查找集合字符

# 查找 test 或 taste 两个单词
$ grep -n 't[ae]st' regx.txt
# [] 表示符合 [] 里的『某个』字符

# 查找含有 oo 的字符串
$ grep -n 'oo' regx.txt

# 如果不想让 oo 前面有 g,可以使用集合字符的反向选择 [^] 来完成
$ grep -n '[^g]oo' regx.txt

# oo 前不想有小写字母
# 在一组集合字符中,如果该字符组是连续的,如大写英文/小写英文/数字等
# 就可以使用 [a-z
$ grep -n '[^a-z]oo' regx.txt
$ grep -n '0-9' regx.txt

# 考虑到语系对编码顺序的影响,还可以使用如下方式
$ grep -n '[^[:lower:]]oo' regx.txt
$ grep -n '[[:digit:]]' regx.txt
# [:lower:] 就是 a-z,[a-z] 自然就是 [[:lower:]]

例题三:行首与行尾字符 ^ $

# 只让 the 在行首列出
$ grep -n '^the' regx.txt

# 开头是小写字母的那一行就列出
$ grep -n '^[a-z]' regx.txt
$ grep -n '^[[:lower:]]' regx.txt

# 不想让开头是英文字母
$ grep -n '^[^a-zA-Z]' regx.txt
# ^ 在 [] 内表示反向选择,在 [] 外表示定位在行首

# 找出行尾结束为 . 的行
# 因为小数点具有其他意义,所以需要使用 \ 解除其特殊含义
# 需要注意 Linux 与 Windows 下换行符的不同
$ grep -n '\.$' regx.txt

# 找出那一行是空白行
$ grep -n '^$' regx.txt

# 将空白行与 # 省略
$ grep -v '^$' /etc/rsyslog.conf | grep -v '^#'

例题四:任意一个字符 . 与重复字符 *

  • . 代表『一定有一个任意字符』
  • * 代表重复前一个字符,0 到无穷多次
# 找出 g??d 的字符串,即共有四个字符,开头为 g 结尾为 d
$ grep -n 'g..d' regx.txt

因为 * 代表的是『重复 0 个或多个前面的正则表达式字符』的含义,则 o* 表示拥有 0 个或一个 o 以上的字符。oo* 表示第一个 o 必须存在,第二个 o 则是可有可无的多个 o。因此当需要『至少两个 o 以上的字符串』时,就需要 ooo*

$ grep -n 'ooo*' regx.txt

# 开头和结尾都是 g,但是两个 g 之间仅能存在至少一个 o
$ grep -n 'goo*g' regx.txt

# 找出 g 开头和 g 结尾的字符串,中间的字符串可有可无
# 是 g*g 吗?
# g*g 中的 g* 代表 0 或一个以上的 g,在加上后面的 g 所以整个正则的内容就是 g, gg, ggg, ...
# 只要改行中拥有一个以上的 g 就符合需求了
$ grep -n 'g*g' regx.txt

# 怎么得出 g...g 的需求呢,答案是 g.*g
# .* 的正则表示任意字符
$ grep -n 'g.*g' regx.txt

# 找出任意数字的行
$ grep -n '[0-9][0-9]*' regx.txt
$ grep -n '[0-9]' regx.txt

例题五:限定连续正则字符范围 {}

# 利用 .* 可以设置 0 个到无限多个重复字符,如何限制一个范围区间内的重复字符数呢
# 需要使用 {},因为 {} 在 shell 中有特殊含义,所以需要使用 \ 使其失去特殊含义
$ grep -n 'o\{2\}' regx.txt
$ grep -n 'go\{2,5\}g' regx.txt # 找出 g 后接 2 到 5 个 o,然后再接一个 g 字符串
$ grep -n 'go\{2,\}g' regx.txt

基础正则表达式字符汇总

RE 字符意义与范例
^word意义:待查找的字符串(word)在行首。`grep -n '^#' regx.txt
word$意义:待查找的字符串(word)在行尾。`grep -n '!$' regx.txt
.意义:代表『一定有一个任意字符』。`grep -n 'e.e' regx.txt
\意义:转义字符。grep -n \' regx.txt
*意义:重复零个到无穷多个前一个正则字符。grep -n 'ess*' regx.txt
[list]意义:字符集合,里面列出想要攫取的字符,在 [] 仅代表想要查找其中的一个字符。grep -n 'g[ld]' regx.txt
[n1-nw]意义:字符集合,里面列出相处攫取的字符范围。`grep -n '[A-Z]' regx.txt
[^list]意义:字符集和,里面列出不要的字符或范围。grep -n 'oo[^t]' regx.txt
\{n,m\}意义:连续 n 到 m 个『前一个正则字符』,若为 \{n\} 则是连续 n 个前一个正则字符,若是 \{n,\} 则是连续 n 个以上的前一个正则字符。grep -n 'go\{2,3\}g regx.txt`

正则表达式的特殊字符与一般在命令输入指令的通配符并不相同。如通配符中的 * 代表的 0 到无穷多个字符的含义,但在正则表达式中,* 表示的是 0 到无穷多个前一个正则字符的含义。

举例来说,不支持正则表达式的 lsls -l * 代表的是列出任意文件名的文件,ls -l a* 代表是以 a 开头的任意文件名的文件。但在正则表达式中,如果要找到含有以 a 开头的文件,则必须这样:ls | grep -n '^a.*'

sed 工具

sed 本身也是一个管道命令,可以用来分析标准输入。sed 还可以用来对文件进行替换、删除、增加、截取特定行等功能。

$ Usage: sed [OPTION]... {script-only-if-no-other-script} [input-file]...
  -n, --quiet, --silent
                 suppress automatic printing of pattern space
  -e script, --expression=script
                 add the script to the commands to be executed
  -f script-file, --file=script-file
                 add the contents of script-file to the commands to be executed

以行为单位的新增/删除功能:

# 将 /etc/passwd 的内容列出并打印行号,同时将 2-5 行删除
# d 就代表删除,sed 后接的动作必须以 '' 括起来
$ nl /etc/passwd | sed '2,5d'

$ nl /etc/passwd | sed '2d' # 删除第 2 行
$ nl /etc/passwd | sed '3,$d' # $ 代表最后一行

# 在第二行后加上 drink tea
$ nl /etc/passwd | sed '2a drink tea'

# 在第二行前增加 drink tea
$ nl /etc/passwd | sed '2i drink tea'

# 增加两行以上,没一行之间都必须以 \ 来进行新行的增加
$ nl /etc/passwd | sed '2a Drink tea or ...... \
> drink beer?'

以行为单位进行替换和显示:

# 刚才介绍的是新增和删除,那么如何进行整行替换呢
$ nl /etc/passwd | sed '2,5c No 2-5 number'

# 如果想要列出第 11-20 行,可以使用 head -n 20 | tail -n 10
# 使用 sed 可以直接取出想要的那几行,注意使用 -n
$ nl /etc/passwd | sed -n '5,7p'

部分数据的查找并替换功能:

# 除了整行的处理模式之外,sed 还可以以行为单位进行部分数据的查找并替换的功能
# sed 's/要被替换的字符串/新的字符串/g'

# 1. 查看原始信息,利用 /sbin/ifconfig 查询 IP
$ /sbin/ifconfig eth0

# 2. 利用关键字配合 grep 截取除关键的一行数据
$ /sbin/ifconfig eth0 | grep 'inet '

# 3. 将 IP 前面的部分删除
$ /sbin/ifconfig eth0 | grep 'inet ' | sed 's/^.*inet //g'

# 4. 将 IP 后面的部分也删除
$ /sbin/ifconfig eth0 | grep 'inet ' | sed 's/^.*inet //g' | sed 's/ *netmask.*$//g'

# sed 与正则表达式的配合练习,只要 MAN 存在的几行数据,但是含有 # 在内的注释不要,且空白行也不要
# 1. 先使用 grep 将关键字 MAN 所在行取出来
$ cat /etc/man_db.conf | grep 'MAN'

# 2. 删除注释
$ cat /etc/man_db.conf | grep 'MAN' | sed 's/#.*$//g'

# 3. 经过第二步原来注释的部分都变成了空白行,所以下面删除空白行
$ cat /etc/man_db.conf | grep 'MAN' | sed 's/#.*$//g' | sed '/^$/d'

直接修改文件内容:sed 可以直接修改文件内容,而不必使用管道命令或数据流重定向。但是由于这个动作会直接修改到原始的文件,所以不要随便使用系统配置文件来进行测试。

# 利用 sed 将 regular_expression.txt 内的每一行结尾若为 . 则替换为 !
# -i 选项可以让 sed 直接区修改后面接的文件内容而不是屏幕输出
$ sed -i 's/\.$/\!/g' regular_expression.txt

# 利用 sed 直接在 regular_expression.txt 最后一行加入 # This is a test
$ sed -i '$a # This is a test' regular_expression.txt

扩展的正则表达式

一般情况下只需要了解基础正则表达式即可,但在某些时刻为了要简化整个指令操作,使用范围更广的扩展正则表达式会更方便,如上街例题三的最后一个例子中,要去除空白行与行首为 # 的行,使用的是 grep -v '^$' regular_express.txt | grep -v '^#',需要使用管道命令来查找两次,如果使用扩展型正则表达式,可以简化为 egrep -v '^$|^#' regular_express.txt。在单引号内的 | 的含义为或(or)。如果要使用扩展的正则表达式,可以使用 grep -Eegrep

  • + 意义:重复一个或一个以上的前一个 RE 字符。egrep -n 'go+d' regular_express.txt
  • ? 意义:零个或一个前一个 RE 字符。egrep -n 'go?d' regular_express.txt
  • | 意义:用或(or)的方式找出多个字符串。egrep -n 'gd|good' regular_express.txt
  • () 意义:组。egrep -n 'g(la|oo)d' regular_express.txt
  • ()+ 意义:多个重复组。echo 'AxyzxyzxyzxyzC' | egrep 'A(xyz)+C'

文件的格式化与相关处理

格式化打印 printf
$ printf '打印格式' 实际内容
  关于格式方面的几个特殊样式:
    \a 警告声音输出
    \b 倒退键(backspace)
    \f 清除屏幕(form feed)
    \n 输出新的一行
    \r 亦即 Enter 按键
    \t 水平的 [tab] 按键
    \v 垂直的 [tab] 按键
    \xNN NN 为两位数的数字,可以转换数字成为字符。
  关于 C 程序语言内,常见的变量格式
    %ns   n 是数字, s 代表 string ,亦即多少个字符;
    %ni   n 是数字, i 代表 integer ,亦即多少整数码数;
    %N.nf n 与 N 都是数字,f 代表 floating(浮点),如果有小数码数,假设我共要十个位数,但小数点有两位,即为 %10.2f
# 范例一:将刚刚上头数据的文件(printf.txt)内容仅列出姓名与成绩:(用 [tab] 分隔)
$ printf '%s\t %s\t %s\t %s\t %s\t \n' $(cat printf.txt)

$ printf '%10s %5i %5i %8.2f \n' $(cat printf.txt | grep -v Name)

$ printf '\x45\n'
awk 数据处理工具

相较于 sed 常作用域一整个行的处理,awk 则比较倾向于一行当中分成数个字段来处理。

$ awk '条件类型1{动作1} 条件类型2{动作2} ...' filename

awk 后面接两个单引号并加上大括号 {} 类设置想要对数据进行的处理工作。awk 可以处理后续接的文件,也可以读取来自前个指令的标准输出。awk 主要处理每一行的字段内的数据,默认的字符分隔符为空白键或 [tab] 键。

# 使用 last 将登录着的数据取出,仅取出前五行
$ last -n 5

# 取出账号与 IP,并且账号与 IP 之间以 tab 隔开
$ last -n 5 | awk '{print $1 "\t" $3}'

awk 的括号内,每一行的每个字段都是有变量名称的,即 $1, $2,...$0 代表一整行数据,$n 代表第 n 栏。整个 awk 的处理流程为:

  1. 读入第一行,并将第一行的数据填入 $0, $1, $2... 等变量中。
  2. 根据条件类型的限制,判断是否需要进行后面的动作。
  3. 昨晚所有的动作与条件类型。
  4. 如果还有后续的行的数组,则重复上面 1-3 的步骤,直到所有的数据都读完为止。

awk 每次以行进行处理,以行内的字段为最小处理单位。awk 如何知道数据有多少行,多少列呢?这就需要 awk 的内置变量帮忙了。

变量名称代表意义
NF每一行($0)拥有的字段总数
NR目前 awk 所处理的是第几行的数据
FS目前的分隔字符,默认为空白键

以上述的 last -n 5 为例,如果想要:

  • 列出每一行的账号(就是 $1
  • 列出当前正在处理的行数(就是 awk 内的 NR 变量)
  • 并且说明,该行有多少字段(就是 awk 内的 NF 变量)

awk 后续的所有动作是以单引号 ' 括住的,由于单引号与双引号都必须是成对的,所以,awk 的格式内容如果想要以 print 打印时,非变量的文字部分,包含上一小节 printf 提到的格式中,都需要使用双引号来定义。单引号已经是 awk 的指令固定用法。

$ last -n 5 | awk '{print $1 "\t lines: " NR "\t colums: " NF}'
# 在 awk 中的 NR, NF 等变量需要大写,且不需要 $

awk 的逻辑运算符:

运算单元代表意义
大于
<小于
>=大于或等于
<=小于或等于
==等于
!=不等于
# 在 /etc/passwd 中是以冒号 : 来作为字段的分隔,该文件中第一字段为账号,第三字段为 UID
# 查找第三栏中小于 10 以下的数据
$ cat /etc/passwd | awk '{FS=":"} $3 < 10 {print $1 "\t " $3}'

$ cat /etc/passed | 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 的动作,即在 {} 内的动作,如果需要使用多个指令时,可利用分号 ; 分隔, 或者直接以 [Enter] 按键来隔开每个指令。
  • 逻辑运算中,如果是等于的情况,需使用 ==
  • 格式化输出时,在 printf 的格式设置中需要使用 \n 才能进行换行
  • 与 shell 的变量不同,在 awk 中,变量可以直接使用,不需要加上 $ 符号。
  • awk 的动作 {} 内也是支持 if 条件的
$ cat pay.txt | \
> awk '{if (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}'
文件对比工具

当同一套软件的不同版本之间,需要比较配置文件与原始文件之间的差异。文件对比的指令包括 diff, cmp

diff 工具:

diff 用于对比两个文件之间的差异,以行为单位进行对比。一般是用在 ASCII 纯文本文件的对比上。由于是以行为单位对比,因此 diff 通常是用在同一文件(或软件)的新旧版本差异上。

# 将 /etc/passwd 处理成一个新的版本,处理方式为:将第四行删除,第六行则替换为 no six line,新的文件放置在 /tmp/test 中
$ mkdir -p /tmp/testpw
$ cd /tmp/testpw
$ cp /etc/passwd passwd.old
$ cat /etc/passwd | sed -e '4d' -e '6c no six line' > passwd.new
# sed 后面如果接两个以上的动作时,每个动作前面需要加 -e

diff 的用法:

$ diff [-bBi] from-file to-file
选项与参数:from-file :一个文件名,作为原始比对文件的文件名 
          to-file :一个文件名,作为目的比对文件的文件名
          注意,from-file 或 to-file 可以 - 取代,那个 - 代表 Standard input 之意
          -b :忽略一行当中,仅有多个空白的差异(例如 "about me""about   me" 视为相同 
          -B :忽略空白行的差异 
          -i :忽略大小写的不同

# 对比 passwd.old 与 passwd.new 的差异
$ diff passwd.old passwd.new

cmp 指令主要用在对比两个文件,主要利用字节单位去对比,因此,也可以对比二进制文件。

$ cmp [-l] file1 file2
选项与参数: 
  -l :将所有的不同点的字节处都列出来。因为 cmp 默认仅会输出第一个发现的不同点

# 使用 cmp 比较 passwd.old 及 passwd.new
$ cmp passwd.old passwd.new

path 指令和 diff 指令密不可分,diff 可以用来分辨两个版本之间的差异。如果要将旧文件升级为新文件,就要先比较新旧版本之间的差异,然后将差异制作为补丁文件,再有补丁文件更新旧文件。

# 以 passwd.old 和 passwd.new 制作补丁文件
$ diff -Naur passwd.old passwd.new > passwd.patch

$ patch -PN < patch_file # 更新
$ patch -R -pN < patch_file # 还原
选项与参数:
  -p: 取消几层目录
  -R:代表还原,将新的文件还原成旧的版本

$ patch -p0 < passwd.patch
$ patch -R -p0 < passwd.patch

文件打印准备 pr。可以加入标题和页码。

重点回顾

  • 正则表达式就是处理字符串的方法,以行为单位来进行字符串的处理
  • 正则表达式通过一些特殊符号的辅助,可以让使用者轻易的达到查找/删除/替换某特定字符串的处理
  • 只要工具程序支持正则表达式,那么该工具程序就可以用来作为正则表达式的字串处理之用
  • 正则表达式与通配符是完全不一样的东西,通配符(wildcard)代表的是 bash 操作接口的一个功能,但正则表达式则是一种字符串处理的表示方式
  • 使用 grep 或其他工具进行正则表达式的字符串比对时,最好将 LANG 等变量设置为 C 或者是 en 等英文语系
  • grepegrep 在正则表达式里面是很常见的两个程序,egrep 支持更严谨的正则表达式的语法
  • 由于编码系统的不同,不同的语系(LANG)会造成正则表达式撷取数据的差异。因此可利用特殊符号如 [:upper:] 来替代编码范围比较好
  • 由于严谨度的不同,正则表达式之上还有更严谨的扩展正则表达式
  • 基础正则表达式的特殊字符有: *, ., [], [-], [^], ^, $
  • 常见的支持正则表达式的工具软件有:grep, sed, vim
  • printf 可以通过一些特殊符号来将数据进行格式化输出
  • awk 可以使用字段为依据,进行数据的重新整理与输出
  • 文件的比对中,可利用 diffcmp 进行比对,其中 diff 主要用在纯文本方面的新旧版本比对
  • patch 指令可以将旧版数据更新到新版(主要由 diff 创建 patch 的补丁来源文件)