Skip to content

Linux与正则表达式

约 1759 个字 29 行代码 预计阅读时间 6 分钟

正则表达式是一种符号表示法,它可以识别文本的特定模式,但是它没有一个严格的语法定义,在各种语言中的语法规则都略有不同,这里我们特指POSIX标准下的正则表达式(能兼容绝大多数命令行工具)。

Linux中万物皆文件,所以快速匹配并操作特定文本的技能是必须的,grep命令——global regular expression print,就是一个用正则表达式匹配文本并将结果输出的强大工具。

grep

grep程序以如下方式接受参数与选项。

grep [options] regex [file...]

其中字符串regex代表了一段特定的正则表达式。

下表列出了grep常用的选项。

选项 功能描述
-i 忽略大小写。不区分大写和小写字符,也可以用--ignore-case指定
-v 不匹配。正常情况下,grep会输出匹配行,而该选项可使grep输出不包含匹配项的所有行。也可以用--invert-match指定
-c 输出匹配项数目(如果有-v选项,那就输出不匹配项的数目)而不是直接输出匹配行自身。也可以用--count指定
-l 输出匹配项文件名而不是直接输出匹配行自身。也可以用--files-with-matches指定
-L 与-l选项类似,但输出的是不包含匹配项的文件名。也可以用--files-without-match指定
-n 在每个匹配行前面加上该行在文件内的行号。也可以用--line-number指定
-h 进行多文件搜索时,抑制文件名输出。也可以用--no-filename指定

我们接下来用简单的示例展示grep的使用。

 ls /usr/ > file1.txt
 ls /usr/bin > file2.txt
 ls ~/ > file3.txt
# 查找含有bzip的文件与匹配行
 grep bzip file*.txt
file2.txt:bzip2
file2.txt:bzip2recover
# 只输出匹配文件
 grep -l bzip file*.txt
file2.txt
# 只输出匹配行
 grep -L bzip file*.txt
file1.txt
file3.txt

元字符与文字

就像编程语言有它们自己的关键字一样,正则表达式的这些关键字被称为元字符,它们在正则表达式里被赋予了不同的含义,具有更强大的匹配功能。

^ $ . [ ] { } - ? * + ( ) | \

而非元字符的字符被称为文字字符,也就是最普通的字符,它们只能和自身进行匹配。

元字符\与其他语言类似,都是一种转义字符,可以将特定的文字字符转义为特定的控制符(比如\n),也可以将元字符转义为文字字符。

Tip

由于元字符与Shell命令里的很多特殊含义的字符(比如通配符*)重合,在使用grep之类的命令时建议把正则表达式用引号包起来,避免不必要的歧异发生。

'.': 来者不拒

.,句点,或者说点字符的含义是“任意”,它可以匹配任何一种字符,同样包括它自己。

'^' & '$': 哼哈二将

^代表以...开头,$代表以...结尾,它们可以像锚一样固定一串正则表达式的匹配对象必须以什么模式开头,又或者以什么模式结尾,又或者两者皆有。

'[]': 集体主义

[]支持你在特定的位置上匹配多种可能,但不是全部。简单来说,如果你使用[abc]ip,你会匹配到aipbipcip三种结果。

[]中,^表示非,比如[^a]ip会匹配到除了aip以外.ip能匹配到的内容。但需要注意的是,仅有^[]内最前面的时候表示非,在其他地方都会被认为是一个文字字符,并且该表达仍要求对应的位置上要有一个文字字符。

-表示字符范围,比如[a-c]ip[abc]ip是等价的,它也可以有更多的扩展形式,比如[a-zA-Z0-9]。当然,如果你不把-放在两个字符之间,它就只是一个普通的文字字符,[-AZ]

POSIX字符类

实际上之前我们对正则表达式的介绍都基于其遵循ASCII码表的前提下,实际上,在很多程序中,特别是POSIX兼容的,其遵循的字母表可能长这样:

aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ

这会导致[A-Z]在ASCII码表下的正则表达式与POSIX兼容应用程序的正则表达式解释不同,前者会被解释为A到Z的全部大写字母,而后者会被解释为除了a以外的所有大小写字母。

为了解决这个问题,POSI提供了一些标准的字符类以共所有遵循POSIX标准的程序通用。

字符类 描述
[[:alnum:]] 字母字符和数字字符;在ASCII码中,与[A-Za-z0-9]等效
[[:word:]] 基本与[[:alnum:]]一样,只是多了一个下划线字符(_)
[[:alpha:]] 字母字符;在ASCII中,等效于[A-Za-z]
[[:blank:]] 包括空格和制表符
[[:cntrl:]] ASCII控制码;包括ASCII字符0–31以及127
[[:digit:]] 数字0–9
[[:graph:]] 可见字符;在ASCII中,包括字符33–126
[[:lower:]] 小写字母
[[:punct:]] 标点符号字符;在ASCII中,与[-!"#$%&'()*+,.:;<=>?@[\]^_`{|}~]等效
[[:print:]] 可打印字符;包括[[:graph:]]中的所有字符再加上空格字符
[[:space:]] 空白字符如空格符、制表符、回车符、换行符、垂直制表符以及换页符。在ASCII中,等效为[ \t\r\n\v\f]
[[:upper:]] 大写字母
[[:xdigit:]] 用于表示十六进制的字符;在ASCII中,与[0-9A-Fa-f]等效

扩展正则表达式

扩展正则表达式ERE是基本正则表达式BRE的进一步扩展,其支持更多的元字符。

^ $ . [ ] { } - ? * + ( ) | \ ( ) { }

一般来说,使用到ERE的需用通过egrep来进行,但是GUN下的grep可以通过启用-E选项来启用对ERE的支持。

POSIX

早期的Unix系统是很混乱的,各个生产商都在取得Unix系统源码后研发出互不完全兼容的系统,这极大增加了Unix系统程序的开发难度,毕竟你也不想写一个简单的小工具就要为其适配五花八门的系统吧(

于是20世纪80年代中期,IEEE指定了一套规范UNIX和类UNIX系统工作方式的标准,即IEEE 1003,后被提议命名为POSIX (是Portable Operating System Interface)。

'|': 中庸之道

|,也就是或,说世界不是非黑即白的,它允许目标字符串存在任意一种我们所期待的匹配模式。

 echo "BBB" | grep -E "AAA|BBB"
BBB
 echo "AAA" | grep -E "AAA|BBB"
AAA
 echo "CCC" | grep -E "AAA|BBB"

'?' & '*' & '+': 取之有道与狼吞虎咽

?可以匹配某个元素0次或1次,*可以匹配某个元素0次或无数次,+可以匹配某个元素1次或无数次。

它们具体要匹配什么模式取决于它们前一个字符是啥。

 echo "11122233333" | grep -E "^(\+86 )?[[:digit:]]+$"
11122233333
 echo "+86 11122233333" | grep -E "^(\+86 )?[[:digit:]]+$"
+86 11122233333
 echo "+86 11122233333A" | grep -E "^(\+86 )?[[:digit:]]+$"
 echo "11122233333" | grep -E "^(\+86 )?[[:digit:]]+$"
11122233333

'{ }': 计划匹配

{ }提供了控制匹配次数的能力,具体见下表:

指定项 含义
{n} 前面的元素恰好出现 n 次则匹配
{n,m} 前面的元素出现的次数在 n~m 次之间时则匹配
{n,} 前面的元素出现次数超过 n 次则匹配
{,m} 前面的元素出现次数不超过 m 次则匹配

有了{ },我们就可以让前面的正则表达式更加精确一点。

 echo "+86 11122233333" | grep -E "^(\+86 )?[[:digit:]]{11}$"
+86 11122233333