跳转至

Shell 高级文本处理与正则表达式

本文已基本完稿,正在审阅和修订中,不是正式版本。

导言

本章节将介绍一些常用的 shell 文本处理工具,它们可以帮助你更加得心应手地处理大量有规律的文本。

为保持简洁,各工具的介绍皆点到即止,进一步的用法请自行查找。

其他 shell 文本处理工具

sort

sort 用于文本的行排序。默认排序方式是升序,按每行的字典序排序。

一些基本用法:

  • -r 降序(从大到小)排序
  • -u 去除重复行
  • -o [file] 指定输出文件
  • -n 用于数值排序,否则“15”会排在“2”前
$ echo -e "snake\nfox\nfish\ncat\nfish\ndog" > animals
$ sort animals
cat
dog
fish
fish
fox
snake
$ sort -r animals
snake
fox
fish
fish
dog
cat
$ sort -u animals
cat
dog
fish
fox
snake
$ sort -u animals -o animals
$ cat animals
cat
dog
fish
fox
snake
$ echo -e "1\n2\n15\n3\n4" > numbers
$ sort numbers
1
15
2
3
4
$ sort -n numbers
1
2
3
4
15

小知识

为什么有必要存在 -o 参数?试试重定向输出到原文件会发生什么吧。

uniq

uniq 也可以用来排除重复的行,但是仅对连续的重复行生效。

通常会和 sort 一起使用:

$ sort animals | uniq

只是去重排序明明可以用 sort -u ,uniq 工具是否多余了呢?实际上 uniq 还有其他用途。

uniq -d 可以用于仅输出重复行:

$ sort animals | uniq -d

uniq -c 可以用于统计各行重复次数:

$ sort animals | uniq -c

正则表达式

正则表达式(regular expression)描述了一种字符串匹配的模式,可以用来检查一个串是否含有某种子串、将匹配的子串做替换或者从某个串中取出符合某个条件的子串等。

此处仅简单介绍正则表达式的一些用法,对正则表达式有更多兴趣,请移步拓展阅读

特殊字符

特殊字符 描述
[] 方括号表达式,表示匹配的字符集合,例如 [0-9][abcde]
() 标记子表达式起止位置
* 匹配前面的子表达式零或多次
+ 匹配前面的子表达式一或多次
? 匹配前面的子表达式零或一次
\ 转义字符,除了常用转义外,还有:\b 匹配单词边界;\B 匹配非单词边界等
. 匹配除 \n(换行)外的任意单个字符
{} 标记限定符表达式的起止。例如 {n} 表示匹配前一子表达式 n 次;{n,} 匹配至少 n 次;{n,m} 匹配 n 至 m 次
| 表明前后两项二选一
$ 匹配字符串的结尾
^ 匹配字符串的开头,在方括号表达式中表示不接受该方括号表达式中的字符集合

以上特殊字符,若是想要匹配特殊字符本身,需要在之前加上转义字符 \

简单示例

匹配正整数:

[1-9][0-9]*

匹配仅由 26 个英文字母组成的字符串:

^[A-Za-z]+$

匹配 Chapter 1-99 或 Section 1-99

^(Chapter|Section) [1-9][0-9]{0,1}$

匹配“ter”结尾的单词:

ter\b

基本/扩展正则表达式

基本正则表达式(Basic Regular Expressions, BRE)和扩展正则表达式(Extended Regular Expressions, ERE)是两种 POSIX 正则表达式风格。

BRE 可能是如今最老的正则风格了,对于部分特殊字符(如 +, ?, |, {)需要加上转义符 \ 才能表达其特殊含义。

ERE 与如今的现代正则风格较为一致,相比 BRE,上述特殊字符默认发挥特殊作用,加上 \ 之后表达普通含义。

具体的例子在下文介绍工具时可以看到。

帮助理解正则表达式的工具

Regex101 网站集成了常见编程语言正则表达式的解析工具,在编写正则时可以作为一个不错的参考。

常用 Shell 文本处理工具(正则)

grep

grep 全称 Global Regular Expression Print,是一个强大的文本搜索工具,可以在一个或多个文件中搜索指定 pattern 并显示相关行。

grep 默认使用 BRE,要使用 ERE 可以使用 grep -E 或 egrep。

命令格式:grep [option] pattern file

一些用法:

  • -n:显示匹配到内容的行号
  • -v:显示不被匹配到的行
  • -i:忽略字符大小写
$ ls /bin | grep -n "^man$"  # 搜索内容仅含 man 的行,并且显示行号
$ ls /bin | grep -v "[a-z]\|[0-9]"  # 搜索不含小写字母和数字的行
$ ls /bin | grep -iv "[A-Z]\|[0-9]"  # 搜索不含字母和数字的行

sed

sed 全称 Stream EDitor,即流编辑器,可以方便地对文件的内容进行逐行处理。

sed 默认使用 BRE,要使用 ERE 可以 sed -E。

命令格式:

$ sed [OPTIONS] 'command' file(s)
$ sed [OPTIONS] -f scriptfile file(s)

此处的 command 和 scriptfile 中的命令均指的是 sed 命令。

常见 sed 命令:

  • s 替换
  • d 删除
  • c 选定行改成新文本
  • a 当前行下插入文本
  • i 当前行上插入文本
$ echo -e "seD\nIS\ngOod" > sed_demo
$ cat sed_demo
seD
IS
gOod
$ sed "2d" sed_demo  # 删除第二行
seD
gOod
$ sed "s/[a-z]/~/g" sed_demo  # 替换所有小写字母为 ~
~~D
IS
~O~~
$ sed "3cpErfeCt" sed_demo  # 选定第三行,改成 pErfeCt
seD
IS
pErfeCt

awk

awk 是一种用于处理文本的编程语言工具,名字来源于三个作者的首字母。相比 sed,awk 可以在逐行处理的基础上,针对列进行处理。默认的列分隔符号是空格,其他分隔符可以自行指定。

awk 使用 ERE。

命令格式:awk [options] 'pattern {action}' [file]

awk 逐行处理文本,对符合的 patthern 执行 action。需要注意的是,awk 使用单引号时可以直接用 $,使用双引号则要用 \$

一些示例:

$ cat awk_demo
Beth    4.00    0
Dan     3.75    0
kathy   4.00    10
Mark    5.00    20
Mary    5.50    22
Susie   4.25    18
$ # 选择第三列值大于 0 的行,对每一行输出第一列的值和第二第三列的乘积
$ awk '$3 >0 { print $1, $2 * $3 }' awk_demo
kathy 40
Mark 100
Mary 121
Susie 76.5

示例中 $1$2$3 分别指代本行的第 1、2、3 列。特别地,$0 指代本行。

awk 语言是「图灵完全」的,这意味着理论上它可以做到和其他语言一样的事情。这里我们不仅可以对每行进行操作,还可以定义变量,将前面处理的状态保存下来,以下是一个求和的例子:

$ awk 'BEGIN { sum = 0 } { sum += $2 * $3 } END { print sum }' awk_demo
337.5

关于 awk,有一本知名的书籍《The AWK Programming Language》(中文翻译),感兴趣的读者可以考虑阅读。

思考题

正则表达式引擎

什么是 DFA/NFA 正则表达式引擎?如今常见编程语言里的正则表达式实现和此处的 BRE/ERE 有什么异同?

正则表达式练习 1:邮件标题匹配

最近你收到了很多垃圾邮件,而且垃圾邮件检测似乎没有生效。你发现这些垃圾邮件的标题似乎都满足一个正则表达式。这些垃圾邮件的标题类似如下:

  • 162935832----系统通知
  • 166819038----系统警告

请写出能够匹配类似标题的正则表达式。

此外,作为一个负责任的系统管理员,你订阅了 Debian Security 邮件列表(订阅后能够收到 Debian 安全更新的通知和说明邮件)。但是你发现,你的邮件系统真是成事不足败事有余,似乎喜欢把这些邮件放进垃圾邮件箱,要是漏掉了什么重要的安全更新就麻烦了!Debian Security 发送的邮件标题类似如下:

  • [SECURITY] [DSA 5075-1] minetest security update
  • [SECURITY] [DSA 5059-1] policykit-1 security update
  • [SECURITY] [DSA 5086-1] thunderbird security update

同样,请写出能够匹配类似标题的正则表达式。

正则表达式练习 2:弹幕过滤

某弹幕视频网站支持使用正则表达式过滤不想看到的弹幕。某日忍无可忍之下,你希望编写一条正则表达式过滤掉类似如下的弹幕(其中全角省略号为任意文本):

  • 当年我就是听的这首歌才……
  • 我就是听的这首歌帮……
  • 当年爷……时就是听的这首歌
  • 当年那天晚上要不是放这首歌我就被……

可以接受少许的误过滤(false positive)。

正则表达式练习 3:Vscode 中的文本批量替换

Vscode 支持使用正则表达式语法查找与替换文本内容。有一天,你的项目中使用的某个函数更新了:调用方式从 func1(a, b, c) 变成了 func1_new(c, b, a, null)。其中假设 abc 均为不含逗号的表达式。

尝试写出搜索的正则表达式与替换目标表达式。提示:正则表达式中使用 () 包裹的为一组,在 vscode 的替换目标表达式中可以使用 $1$2 的格式来引用第一组、第二组等内容。

Shell 文本处理工具练习 1:文件内容替换

某 shell 脚本会随着图形界面启动而启动,启动后会根据环境变量替换某程序配置文件的内容。该配置文件内容如下:

[settings]
homepage=http://example.com/
location-entry-search=http://cn.bing.com/search?q=%s

我们希望编写一条或多条 sed 命令,使得脚本运行后配置文件被修改为:

[settings]
homepage=http://example.com/index_new.html
location-entry-search=http://www.wolframalpha.com/input/?i=%s

假设配置文件路径存储在变量 $F 中。请注意:图形界面可能会重置,此时脚本会对已经修改的配置文件再次修改,如果编写不小心,可能会得到错误的结果。

Shell 文本处理工具练习 2:Nginx 日志分析

你的网站最近收到了一大批不正常的请求大量消耗服务器带宽,你希望通过 shell 文本处理工具确认攻击者的来源 IP。Nginx 访问日志的格式类似于如下:

123.45.67.89 - - [01/Mar/2022:00:58:17 +0800] "GET /downloads/nonexist HTTP/1.1" 404 190 "-" "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2226.0 Safari/537.36"

其中我们主要关注 IP(第一列)和下载大小(第 10 列,例子中为 190)。请给出使用 awksort 等工具输出下载量最大的前 50 个 IP 的命令。

Shell 文本处理工具练习 3:文件列表解析

Ports 是 BSD 系列操作系统管理编译软件的方式。下面我们将介绍 FreeBSD 操作系统中的 ports 目录结构。

Ports 目录的第一层为不同软件的分类(诸如音频程序、数据库程序会分别放置在 audio 和 databases 目录下),第二层则为各个软件的目录。在绝大多数软件的目录下都有 distinfo 文件,用于描述其依赖的源代码包文件的名称、大小和 SHA256 校验值信息。

例如,gcc10 软件包的 distinfo 位于 lang/gcc10/distinfo,内容类似如下:

TIMESTAMP = 1619249722
SHA256 (gcc-10.3.0.tar.xz) = 64f404c1a650f27fc33da242e1f2df54952e3963a49e06e73f6940f3223ac344
SIZE (gcc-10.3.0.tar.xz) = 76692288

你的任务是:搜索 ports 中的所有 distinfo,提取所有文件名和 SHA256,按照文件名以字典序排序并输出,每行格式要求如下:

64f404c1a650f27fc33da242e1f2df54952e3963a49e06e73f6940f3223ac344 gcc-10.3.0.tar.xz

现实中的 ports 文件可以从 https://mirrors.ustc.edu.cn/freebsd-ports/ports.tar.gz 下载解压得到。

注意:少量 distinfo 文件的 SHA256 对应行最后会有多余的空格或制表符,需要妥善处理。