vlambda博客
学习文章列表

Linux 中正则表达式介绍及运用

Linux 中正则表达式介绍及运用

内容均来自《Linux命令行大全》(第2版),仅做记录

1.什么是正则表达式

正则表达式是一种用于识别文本模式的符号表示法,在某种程度上类似于匹配文件和路径名的Shell通配符,但用途更广。

许多命令行工具和大多编程语言支持正则表达式,以便解决文本操作问题,但不同工具、不同编程语言,其正则表达式并非全部相同。

2.grep

「grep」源于 global regular expression print,译为「全局正则表达式输出」,其基本功能是在文本文件中搜索与指定的正则表达式匹配的文本,将包含匹配项的文本行输出到标准输出。

grep [opyions] regex [file...]

常见 grep 命令:

选项 描述
-i 忽略字母大小写
-v 反相匹配
-c 输出匹配数量
-l 输出包含匹配项的文件名,不输出文本行
-L 与 -l 类似,但只输出不包含匹配项的文件名
-n 添加行号
-h 禁止输出文件名

例如:

首先创建几个用于搜索的文本文件:

me@linuxbox:~$ ls /bin > dirlist-bin.txt
me@linuxbox:~$ ls /usr/bin > dirlist-usr-bin.txt
me@linuxbox:~$ ls /sbin > dirlist-sbin.txt
me@linuxbox:~$ ls /usr/sbin > dirlist-usr-sbin.txt
me@linuxbox:~$ ls dirlist*.txt
-rw-rw-r-- 1 me me  55 4月   1 16:04 dirlist-bin.txt
-rw-rw-r-- 1 me me  57 4月   1 16:04 dirlist-sbin.txt
-rw-rw-r-- 1 me me 88K 4月   1 16:04 dirlist-usr-bin.txt
-rw-rw-r-- 1 me me 27K 4月   1 16:05 dirlist-usr-sbin.txt

对多个文件执行简单的搜索:

me@linuxbox:~$ grep bzip dirlist*.txt
dirlist-usr-bin.txt:bzip2
dirlist-usr-bin.txt:bzip2recover

在这个例子中,grep 搜索所有文件,查找字符串bzip,结果找到两处匹配项,均位于 dislist-usr-bin.txt 文件中。如果我们只对包含匹配项的文件感兴趣,并不关心匹配项,可以指定 -l 选项:

me@linuxbox:~$ grep -l bzip dirlist*.txt
dirlist-usr-bin.txt

相反,如果只对不包含匹配项的文件感兴趣,可以加入 -L选项:

me@linuxbox:~$ grep -l bzip dirlist*.txt
dirlist-bin.txt
dirlist-sbin.txt
dirlist-usr-sbin.txt

3.元字符与文字字符

在通过grep搜索的时候其实已经在使用正则表达式了。正则表达式bzip的意思是仅当文件中的某行至少包含4个字符且字符顺序为 b、z、i、p 的时候(之间没有任何其他字符)才匹配。

字符串 bzip 中的字符全部都是「文字字符(literal character)」,只能匹配自身。除了普通字符,正则表达式还包括「元字符(metacharacter)」,用于指定更复杂的匹配。正则表达式元字符包括:

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

「其他所有字符均被视为普通字符」,不过在少数情况下,反斜线字符可用于创建「元序列(metasequence)」,还能转义元字符,使其成为普通字符。

「注意:很多正则表达式元字符对Shell扩展具有特殊含义。当包含元字符的正则表达式出现在命令行上时,一定要记得将其放入引号中,避免 Shell 去扩展这些字符,这一点非常重要。」

4.任意字符

元字符点号 「“.”」 可用于匹配任意字符。如果我们将其放入正则表达式,它能够匹配该字符位置上的任意字符。

me@linuxbox:~$ grep -h '.zip' dirlist*.txt
bunzip2
bzip2
bzip2recover
funzip
gpg-zip
gunzip
gzip
preunzip
prezip
prezip-bin
unzip
unzipsfx
zipdetails
zipgrep
zipinfo

我们在文件内搜索匹配正则表达式.zip的所有行。最终结果的有些地方值得注意:

  • 首先,在其中没有发现zip程序。这是因为正则表达式中的点号元字符将需要匹配的字符串长度增加到了4个字符,又因为zip只包含3个字符,所以不匹配;

  • 另外,如果文件列表中有扩展名为.zip的文件,也能够匹配,因为扩展名中的点号也属于“任意字符”的范畴。

5.锚点

在正则表达式中,「脱字符 ^」 和「美元符号 $」 被视为「锚点(anchor)」,分别表示仅当正则表达式出现在行首或行尾的时候才匹配:

# 行首搜索
me@linuxbox:~$ grep -h '^zip' dirlist*.txt
zipdetails
zipgrep
zipinfo

#
 行尾搜索
me@linuxbox:~$ grep -h 'zip$' dirlist*.txt
funzip
gpg-zip
gunzip
gzip
preunzip
prezip
unzip

#
 匹配唯一字符
me@linuxbox:~$ grep -h '^zip$' dirlist*.txt
zip

#
 匹配空行
me@linuxbox:~$ grep -h '^$' dirlist*.txt

上述正则表达式的用法可帮助我们进行填字游戏,例如有一个包含5个字母的单词,第三个字母为j,最后一个字母为r:

me@linuxbox:~$ grep -i '^..j.r$' /usr/share/dict/words
Major
major

6.方括号表达式与字符类

除了匹配正则表达式中指定位置上的任意字符,还可以使用方括号表达式来匹配指定字符集合中的「单个字符」

借助方括号表达式,可以指定一组待匹配的字符(包括会被解释为元字符的字符)

在下面的例子中,我们使用由两个字符组成的集合来匹配包含字符串bzip或gzip的行:

me@linuxbox:~$ grep -h '[bg]zip' dirlist*.txt
bzip2
bzip2recover
gzip

集合中可以包含任意数量的字符,其中出现的元字符会丢失其特殊含义。

但是,有两种特殊情况:「脱字符用于表示否定;连字符表示字符范围」

6.1排除

如果方括号表达式中的首个字符是脱字符,剩下的字符则被视为不该在指定字符位置上出现的字符集合:

me@linuxbox:~$ grep -h '[^bg]zip' dirlist*.txt
bunzip2
funzip
gpg-zip
gunzip
preunzip
prezip
prezip-bin
unzip
unzipsfx
zipdetails
zipgrep
zipinfo

#
 对比两条命令输出结果的区别
me@linuxbox:~$ grep -h '[bg]zip' dirlist*.txt
bzip2
bzip2recover
gzip

利用排除操作,我们得到了一份文件列表,其中的文件名均包含字符串 zip,而该字符串之前是除 b 或 g 之外的任意字符。

注意,zip 并不符合搜索条件。排除型字符集合仍需要指定位置上有一个字符存在,只不过这个字符不能是集合中的字符。

「注意:仅当脱字符是方括号表达式中的第一个字符的时候才表示排除含义;否则,它只代表一个普通的字符。」

6.2传统的字符范围

如果我们想构建一个正则表达式,查找文件列表中所有以大写字母开头的文件,可以这样做:

me@linuxbox:~$ grep -h '^[ABCDEFGHIJKLMNOPQRSTUVWXYZ]' dirlist*.txt
FileCheck-10
NF
VGAuthService
X
X11
Xephyr
Xorg
Xwayland
ModemManager
NetworkManager
FileCheck-10
NF
VGAuthService
X
X11
Xephyr
Xorg
Xwayland
ModemManager
NetworkManager

#
 该命令的输出结果相同
me@linuxbox:~$ grep -h '^[A-Z]' dirlist*.txt

通过使用3个字符表示的字符范围,直接实现了26个字母的缩写。「不管哪种字符范围,都可以用这种方式表达(包括多个字符范围)」,例如下面的正则表达式可以匹配以字母或数字开头的所有文件名:

me@linuxbox:~$ grep -h '^[A-Za-z0-9]' dirlist*.txt

字符范围表示中出现的连字符会被特殊对待,那我们该如何在方括号表达式中加入一个普通的连字符呢?答案是「将其作为第一个字符」。考虑下面两个例子:

me@linuxbox:~$ grep -h '[A-Z]' dirlist*.txt

这将匹配包含大写字母的文件名,以下文件将匹配每个包含破折号或大写A(Z)的文件名:

me@linuxbox:~$ grep -h '[-AZ]' dirlist*.txt

7.POSIX字符类

传统的字符范围易于理解,能够有效地解决快速指定字符集合的问题。但这种方法未必总是管用。

如果在 ubuntu 语言是 「en_US.UTF-8」 情况下,会出现如下问题:

me@linuxbox:~$ echo $LANG
en_US.UTF-8

me@linuxbox:~$ ls /usr/sbin/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]*
/usr/sbin/ModemManager
/usr/sbin/NetworkManager

#
 以下展示部分结果
me@linuxbox:~$ ls /usr/sbin/[A_Z]*
/usr/sbin/biosedecode
/usr/sbin/chat
/usr/sbin/chgpasswd
/usr/sbin/chpasswd
/usr/sbin/chroot
...

UNIX开发之初只识别ASCII字符,正是该特性导致了以上差异:

  • 在ASCII中,前32个字符(编码值为0-31)包含控制字符(如制表符、退格符、回车符),接下来的32个字符(编码值为32-63)包含可输出字符,大多数的标点符号和数字0-9在其中,随后的32个字符(编码值为64-95)包含大写字母和一些标点符号,最后的31个字符(编码值为96-127)包含小写字母和其他标点符号。基于这样的安排,使用ASCII的系统使用了下列排序规则(collation order):

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

这与正常的词典序(dictionary order)不同,后者如下:

aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ

随着UNIX的流行,支持美式英语字符以外的字符的需求也与日俱增。

为了支持这种功能,POSIX标准引入了语言环境(locale)的概念,能够通过调整来选择特定区域所需要的字符集。

有了 「en_US.UTF-8」 这项设置,兼容POSIX的应用就会使用词典序,而不再是ASCII的顺序。这就解释了上述命令结果的不同。在词典序中,字符范围[A-Z]包括除a之外的所有字母。

为了部分解决这个问题,POSIX标准包含了许多字符类(character class),提供了各种有用的字符范围:

字符类 描述
[:alnum:] 字母和数字字符,在ASCII中等价于[A-Za-z0-9]
[:word:] 和[:alnum:]一样,另外加入了下划线字符_
[:blank:] 包括空格符和制表符
[:cntrl:] ASCII控制字符,包括ASCII编码值为0-31和127的字符
[:digit:] 数字0-9
[:graph:] 可见字符,ASCII编码值为33-126的字符
[:lower:] 小写字母
[:punct:] 标点符号字符。ASCII中等价于[-!"#$%&'()*+,./:;<=>?@[\]_`{
[:print:] 可输出字符。包括 graph 中的所有字符加上空格符
[:space:] 空白字符,包括空格符、制表符、回车符、换行符、垂直制表符、换页符
[:upper:] 大写字母
[:xdigit:] 用于表示十六进制数值的字符,ASCII中等价于[0-9A-Fa-f]

及时有了POSIX字符类,还是没有便利的方法来表示部分范围,例如[A-M]

me@linuxbox:~$ ls /usr/sbin/[[:upper:]]*
/usr/sbin/ModemManager
/usr/sbin/NetworkManager

#
 比较,结果相同
me@linuxbox:~$ ls /usr/sbin/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]*
/usr/sbin/ModemManager
/usr/sbin/NetworkManager

只用把LANG变量设置为POSIX,就可以让系统采用传统的排序规则:

me@linuxbox:~$ export LANG=POSIX

这种方法会使系统使用ASCII作为字符集,可添加到 .bashrc文件中使改动永久生效,但不推荐

8.POSIX基本型正则表达式与扩展型正则表达式

POSIX把正则表达式的实现分为了两类:

  • 基本型正则表达式(Basic Regular Expression,BRE)
  • 扩展型正则表达式(Extended Regular Expression,ERE)

所有兼容POSIX并实现了BRE的应用程序都支持目前我们介绍过的这些特性。grep程序就是这样的程序之一。

BRE 与ERE的不同之处在于元字符不同。BRE 识别下列元字符:

^ $ . [] *

除此之外的所有字符均被视为文字字符。ERE 又加入了下列元字符:

() {} ? + |

如果使用反斜线将 ( )、{ } 转义的话,BRE将其视为元字符;而ERE会将转义后的这些字符视为文字字符。

9.多选结构

ERE 的一个特性叫做「多选结构(alternation)」,它允许匹配一组正则表达式中的某一个。就像方括号表达式允许匹配一组指定字符中的单个字符,多选结构可以从一组字符串或正则表达式中「寻找匹配」。

尝试匹配字符串:

me@linuxbox:~$ echo 'AAA'|grep AAA
AAA
me@linuxbox:~$ echo 'BBB'|grep AAA
me@linuxbox:~$ 

如果有匹配,就会出现输出结果;如果没有匹配,就看不到任何输出结果。

加入由 | 表示的多选结构:

me@linuxbox:~$ echo 'BBB'|grep -E 'AAA|BBB'
BBB
me@linuxbox:~$ echo 'CCC'|grep -E 'AAA|BBB'
me@linuxbox:~$ 

「这里的正则表达式'AAA|BBB'的意思是“要么匹配AAA,要么匹配BBB”」

注意,因为多选结构是ERE的特性之一,需要给grep添加 ==「-E」== 选项(尽管也可以使用egrep程序代替),同时将正则表达式放入引号中,避免Shell将其中的|解释为管道。

多选结构不仅能二选一:

me@linuxbox:~$ echo 'AAA'|grep -E 'AAA|BBB|CCC'
AAA

要想将多选结构与其他正则表达式元素组合起来,可以使用 () 来分隔:

me@linuxbox:~$ grep -Eh '^(bz|gz|zip)' dirlist*.txt
bzcat
bzcmp
bzdiff
bzegrep
bzexe
...

这里正则表达式匹配文件列表中以bz或gz或zip开头的文件名。

若去除括号,正则表达式的含义就变成了匹配以bz开头的文件,或者包含gz的文件,或者包含zip的文件名:

me@linuxbox:~$ grep -Eh '^bz|gz|zip' dirlist*.txt

10.量词

ERE支持用多种方式指定匹配次数

10.1 ? —— 匹配0次或1次

(nnn) nnn-nnnn nnn nnn-nnnn

构建正则表达式:

^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$

在括号后加上了问号,表示匹配括号内的内容0次或1次。因为括号是元字符(在ERE中),所以在其之前加上反斜线,使其成为文字字符:

me@linuxbox:~$ echo "(555) 123-4567" | grep -E '^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$'
(555) 123-4567

me@linuxbox:~$ echo "AAA 123-4567" | grep -E '^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$'
e@linuxbox:~$ 

10.2 * —— 匹配0次或多次

和 ?一样,* 也可用于表示可选项;但和 ?不同的是,* 之前的可选项可以出现任意多次,而不仅仅出现一次。

假设我们想判断某个字符串是否是一句话,即该字符串以一个大写字母开头,然后是任意多个大/小写字母和空格符,最后以点号结尾。要想匹配我们粗糙定义的这种句子,可以使用下列正则表达式:

[[:upper:]][[:upper:][:lower:] ]*\.

该正则表达式由3部分组成:

  • 包含字符类[:upper:]的方括号表达式
  • 包含字符类[:upper:]、[:lower:]以及 「空格符」 (可替换成[:blank:]或[:space:])的方括号表达式
  • 经过反斜线转义的点号

第二项结尾处是*,所以在句子开头的大写字母之后,不管有多少个大/小写字母和空格符,都能够匹配。

me@linuxbox:~$ echo "This works." | grep -E '[[:upper:]][[:upper:][:lower:] ]*\.'
This works.
me@linuxbox:~$ echo "This works." | grep -E '[[:upper:]][[:upper:][:lower:][:blank:]]*\.'
This works.
me@linuxbox:~$ echo "This works." | grep -E '[[:upper:]][[:upper:][:lower:][:space:]]*\.'
This works.
me@linuxbox:~$ echo "this works." | grep -E '[[:upper:]][[:upper:][:lower:] ]*\.'
me@linuxbox:~$

10.3 + —— 匹配1次或多次

+ 和 * 差不多,只不过要求之前的可选项至少匹配一次。下面的正则表达式所匹配的行只能包含由单个空格符分隔的一个或多个字母:

([[:alpha:]]+ ?)+$

me@linuxbox:~$ echo "This that" | grep -E '^([[:alpha:]]+ ?)+$'
This that
me@linuxbox:~$ echo "a b c" | grep -E '^([[:alpha:]]+ ?)+$'
a b c

me@linuxbox:~$ echo "a b 9" | grep -E '^([[:alpha:]]+ ?)+$'
me@linuxbox:~$ 

me@linuxbox:~$ echo "abc  d" | grep -E '^([[:alpha:]]+ ?)+$'
me@linuxbox:~$ 

10.4 {} —— 匹配指定次数

{和} 用于指定要求匹配的最小次数和最大次数,共有4种指定方式。

指定方式 含义
{n} 匹配之前的元素 n 次
{n,m} 匹配之前的元素至少 n 次,最多 m 次
n, 匹配之前的元素至少 n 次,最多不限
{,m} 匹配之前的元素不超过 m 次

^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$

me@linuxbox:~$ echo "(000) 111-2222" | grep -E '^\([0-9]{3})? [0-9]{3}-[0-9]{4}'
(000) 111-2222

11.练习

11.1使用grep验证电话号码列表

me@linuxbox:~$ for i in {1..10}; do echo "(${RANDOM:0:3}) ${RANDOM:0:3}-${RANDOM:0:4}" >> phonelist.txt; done
me@linuxbox:~$ head phonelist.txt 
(706) 285-1283
(193) 210-2673
(271) 211-643
(111) 271-2595
(265) 100-1641
(259) 124-3177
(196) 450-4434
(231) 124-8669
(807) 116-2538
(143) 605-4741

其中部分号码格式有误,正好用来检验:

# 扫描无效号码并显示
me@linuxbox:~$ grep -Ev '^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$' phonelist.txt
(271) 211-643

使用 -v 选项生成了反相匹配,只输出号码列表中不匹配指定正则表达式的号码。

正则表达式两端都加入了锚点字符,以确保号码两端没有多余的字符,除此之外还要求有效号码中必须包含括号。

11.2使用 find 查找路径名

find 命令支持正则表达式。在 find 和 grep 中使用正则表达式时,要记住一个重要的区别:

「只要行中含有与正则表达式匹配的字符串,grep就会将该行输出;而find则要求路径名必须严格匹配正则表达式。」

在下面的例子中,我们使用带有正则表达式的find来查找所有不包含下列字符集合成员的路径名:

[-_./0-9a-zA-Z]

通过使用正则表达式,找出包含内嵌空格符和其他潜在不规范字符的路径名:me@linuxbox:~$ find .-regex '.*[^-_./0-9a-zA-Z].*'因为要求严格匹配整个路径名,所以在正则表达式的两端使用了.*,以此匹配可能出现的0个或多个字符。在正则表达式中间,用到了排除型方括号表达式,其中包含若干能够接受的路径名字符.

11.3使用 locate 搜索文件

locate程序既支持BRE(--regexp选项),也支持ERE(--regex选项)。借助正则表达式,我们能够对dirlist文件执行很多先前演示过的操作:

me@linuxbox:~$ locate --regex 'bin/(bz|gz|zip)'
/bin/bzcat
/bin/bzcmp
/bin/gzip
/usr/bin/zip
...

利用多选结构,因此可以搜索包含bin/bz、bin/gz或/bin/zip的路径名。

10.4使用 less 和 vim 搜索文本

Less 和 Vim 都采用相同的文本搜索方法。按/键,接着输入正则表达式,就可进行搜索。

如果使用 Less 查看 phonelist.txt 文件,可以这样:

me@linuxbox:~$ less phonelist.txt

输入正则表达式:

      1 (706) 285-1283
      2 (193) 210-2673
      3 (271) 211-643
      4 (111) 271-2595
      5 (265) 100-1641
      6 (259) 124-3177
      7 (196) 450-4434
      8 (231) 124-8669
      9 (807) 116-2538
     10 (143) 605-4741
~
~
~
/^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$

less 会高亮标出匹配项,即可以分辨出无效号码。

vim 则只支持 BRE,因此用于搜索的正则表达式需要进行改写:/([0-9]\{3\}) [0-9]\{3\}-[0-9]\{4\}两种类型的正则表达式基本上一样;但是,很多在ERE中被视为元字符的字符,在BRE中却被视为文字字符。只有使用反斜线转义,这些文字字符才被作为元字符对待。