正则表达式是一种符号表示法,被用来识别文本模式。在某种程度上,它们与匹配文件和路径名的shell通配符比较相似,但其规模更大。许多命令行工具和大多数编程语言都支持正则表达式,以此来帮助解决操作文本的问题。不同工具和语言之间的正则表达式都略有差异,我们将会限定POSIX标准中描述的正则表达式(其包括了大多数的命令行工具),供我们讨论,与许多编程语言相反,它们使用了更多和更丰富的符号集。
grep
到目前为止,我们已经使用grep 程序查找了固定的字符串,就像这样:
[me@linuxbox ~]$ ls /usr/bin | grep zip 这个命令会列出,位于目录/usr/bin 中,文件名中包含子字符串“zip”的所有文件。
这个grep 程序以这样的方式来接受选项和参数:
grep [options] regex [file...] 这里的regx 是指一个正则表达式。
这是一个常用的grep 选项列表:
-i:忽略大小写
-v:不匹配
-c:打印匹配数量
-l:打印包含匹配项的文件名,而不是文本行本身
-L:相似于-l 选项,但是只是打印不包含匹配项的文件名。
-n:在每个匹配行之前打印出其位于文件中的相应行号。
-h:应用于多文件搜索,不输出文件名。
元字符和文本
它可能看起来不明显,但是我们的grep 程序一直使用了正则表达式,虽然是非常简单的例子。这个正则表达式“bzip”意味着,匹配项所在行至少包含4 个字符,并且按照字符“b”,
“z”, “i”, 和“p”的顺序出现在匹配行的某处,字符之间没有其它的字符。字符串“bzip”中的所有字符都是原义字符,因为它们匹配本身。除了原义字符之外,正则表达式也可能包含
元字符,其被用来指定更复杂的匹配项。正则表达式元字符由以下字符组成:
^ $ . [ ] { } - ? * + ( ) | \
然后其它所有字符都被认为是原义字符,虽然在个别情况下,反斜杠会被用来创建元序列,也允许元字符被转义为原义字符,而不是被解释为元字符。
注意:正如我们所见到的,当shell 执行展开的时候,许多正则表达式元字符,也是对shell有特殊含义的字符。当我们在命令行中传递包含元字符的正则表达式的时候,把元字符用引号引起来至关重要,这样可以阻止shell 试图展开它们。
任何字符
我们将要查看的第一个元字符是圆点字符,其被用来匹配任意字符。如果我们在正则表达式中包含它,它将会匹配在此位置的任意一个字符。这里有个例子:
[me@linuxbox ~]$ grep -h '.zip' dirlist*.txt
bunzip2
bzip2
bzip2recover
gunzip
gzip
funzip
gpg-zip
preunzip
prezip
prezip-bin
unzip
unzipsfx
我们在文件中查找包含正则表达式“.zip”的文本行。对于搜索结果,有几点需要注意一下。注意没有找到这个zip 程序。这是因为在我们的正则表达式中包含的圆点字符把所要求的匹配项的长度增加到四个字符,并且字符串“zip”只包含三个字符,所以这个zip 程序不匹配。另外,如果我们的文件列表中有一些文件的扩展名是.zip,则它们也会成为匹配项,因为文件扩展名中的圆点符号也会被看作是“任意字符”。
锚点
在正则表达式中,插入符号和美元符号被看作是锚点。这意味着正则表达式只有在文本行的开头或末尾被找到时,才算发生一次匹配。
例如:[me@linuxbox ~]$ grep -h '^zip' dirlist*.txt
zip
zipcloak
zipgrep
zipinfo
zipnote
zipsplit
[me@linuxbox ~]$ grep -h 'zip$' dirlist*.txt
gunzip
gzip
funzip
gpg-zip
preunzip
prezip
[me@linuxbox ~]$ grep -h '^zip$' dirlist*.txt
zip
这里我们分别在文件列表中搜索行首,行尾以及行首和行尾同时包含字符串“zip”(例如,zip 独占一行)的匹配行。注意正则表达式‘.$’(行首和行尾之间没有字符)会匹配空行。配。另外,如果我们的文件列表中有一些文件的扩展名是.zip,则它们也会成为匹配项,因为
文件扩展名中的圆点符号也会被看作是“任意字符”。
中括号表达式和字符类
除了能够在正则表达式中的给定位置匹配任意字符之外,通过使用中括号表达式,我们也能够从一个指定的字符集合中匹配一个单个的字符。通过中括号表达式,我们能够指定一个字符集合(包含在不加中括号的情况下会被解释为元字符的字符)来被匹配。在这个例子里,使用了一个两个字符的集合:
[me@linuxbox ~]$ grep -h '[bg]zip' dirlist*.txt
bzip2
bzip2recover
gzip
我们匹配包含字符串“bzip”或者“gzip”的任意行。
否定
一个字符集合可能包含任意多个字符,并且元字符被放置到中括号里面后会失去了它们的特殊含义。然而,在两种情况下,会在中括号表达式中使用元字符,并且有着不同的含义。第一个元字符是插入字符,其被用来表示否定;第二个是连字符字符,其被用来表示一个字符区域。
如果在正则表示式中的第一个字符是一个插入字符,则剩余的字符被看作是不会在给定的字符位置出现的字符集合。通过修改之前的例子,我们试验一下:
[me@linuxbox ~]$ grep -h '[^bg]zip' dirlist*.txt
bunzip2
gunzip
funzip
gpg-zip
preunzip
prezip
prezip-bin
unzip
unzipsfx
通过激活否定操作,我们得到一个文件列表,它们的文件名都包含字符串“zip”,并且“zip”的前一个字符是除了“b”和“g”之外的任意字符。注意文件zip 没有被发现。一个否定的字符集仍然在给定位置要求一个字符,但是这个字符必须不是否定字符集的成员。这个插入字符如果是中括号表达式中的第一个字符的时候,才会唤醒否定功能;否则,它会失去它的特殊含义,变成字符集中的一个普通字符。
传统的字符区域
如果我们想要构建一个正则表达式,它可以在我们的列表中找到每个以大写字母开头的文件,我们可以这样做:
[me@linuxbox ~]$ grep -h '^[ABCDEFGHIJKLMNOPQRSTUVWXZY]' dirlist*.txt
这只是一个在正则表达式中输入26 个大写字母的问题。但是输入所有字母非常令人烦恼,所以有另外一种方式:
[me@lMAKEDEV
ControlPanel
GET
HEAD
POST
X
X11
Xorg
MAKEFLOPPIESinuxbox ~]$ grep -h '^[A-Z]' dirlist*.txt
通过使用一个三字符区域,我们能够缩写26 个字母。任意字符的区域都能按照这种方式表达,包括多个区域,比如下面这个表达式就匹配了所有以字母和数字开头的文件名:
[me@linuxbox ~]$ grep -h '^[A-Za-z0-9]' dirlist*.txt
在字符区域中,我们看到这个连字符被特殊对待,所以我们怎样在一个正则表达式中包含一个连字符呢?方法就是使连字符成为表达式中的第一个字符。考虑一下这两个例子:
[me@linuxbox ~]$ grep -h '[A-Z]' dirlist*.txt
这会匹配包含一个大写字母的文件名。然而:
[me@linuxbox ~]$ grep -h '[-AZ]' dirlist*.txt
上面的表达式会匹配包含一个连字符,或一个大写字母“A”,或一个大写字母“Z”的文件名。
POSIX 字符集
这个传统的字符区域在处理快速地指定字符集合的问题方面,是一个易于理解的和有效的方式。不幸地是,它们不总是工作。到目前为止,虽然我们在使用grep 程序的时候没有遇到任何问题,但是我们可能在使用其它程序的时候会遭遇困难。回到第5 章,我们看看通配符怎样被用来完成路径名展开操作。在那次讨论中,我们说过在某种程度上,那个字符区域被使用的方式几乎与在正则表达式中的用法一样,但是有一个问题:
[me@linuxbox ~]$ ls /usr/sbin/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]*
/usr/sbin/MAKEFLOPPIES
/usr/sbin/NetworkManagerDispatcher
/usr/sbin/NetworkManager
(依赖于不同的Linux 发行版,我们将得到不同的文件列表,有可能是一个空列表。这个例子来自于Ubuntu)这个命令产生了期望的结果——只有以大写字母开头的文件名,但是:
[me@linuxbox ~]$ ls /usr/sbin/[A-Z]*
/usr/sbin/biosdecode
/usr/sbin/chat
/usr/sbin/chgpasswd
/usr/sbin/chpasswd
/usr/sbin/chroot
/usr/sbin/cleanup-info
/usr/sbin/complain
/usr/sbin/console-kit-daemon
通过这个命令我们得到整个不同的结果(只显示了一部分结果列表)。为什么会是那样?说来话长,但是这个版本比较简短:追溯到Unix 刚刚开发的时候,它只知道ASCII 字符,并且这个特性反映了事实。在ASCII中,前32 个字符(数字0 - 31)都是控制码(如tabs,backspaces,和回车)。随后的32 个字符(32 - 63)包含可打印的字符,包括大多数的标点符号和数字0 到9。再随后的32 个字符(64 - 95)包含大写字符和一些更多的标点符号。最后的31 个字符(96 - 127)包含小写母和更多的标点符号。基于这种安排方式,系统使用这种排序规则的ASCII:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
这个不同于正常的字典顺序,其像这样:
aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ
随着Unix 系统的知名度在美国之外的国家传播开来,就需要支持不在U.S. 英语范围内的字符。于是就扩展了这个ASCII 字符表,使用了整个8 位,添加了字符(数字128 - 255),这样就容纳了更多的语言。为了支持这种能力,POSIX 标准介绍了一种叫做locale 的概念,其可以被调整,来为某个特殊的区域,选择所需的字符集。通过使用下面这个命令,我们能够查看到我们系统的语言设置:
[me@linuxbox ~]$ echo $LANG
en_US.UTF-8
通过这个设置,POSIX 相容的应用程序将会使用字典排列顺序而不是ASCII 顺序。这就解释了上述命令的行为。当[A-Z] 字符区域按照字典顺序解释的时候,包含除了小写字母“a”之外的所有字母,因此得到这样的结果。为了部分地解决这个问题,POSIX 标准包含了大量的字符集,其提供了有用的字符区域。下表中描述了它们:
[:alnum:] 字母数字字符。在ASCII 中,等价于:[A-Za-z0-9]
[:word:] 与[:alnum:] 相同, 但增加了下划线字符。
[:alpha:] 字母字符。在ASCII 中,等价于:[A-Za-z]
[:blank:] 包含空格和tab 字符。
[:cntrl:] ASCII 的控制码。包含了0 到31,和127 的ASCII 字符。
[:digit:] 数字0 到9
[:graph:] 可视字符。在ASCII 中,它包含33 到126 的字符。
[:lower:] 小写字母。
[:punct:] 标点符号字符。在ASCII 中,等价于:
[:print:] 可打印的字符。在[:graph:] 中的所有字符,再加上空格字符。
[:space:] 空白字符,包括空格,tab,回车,换行,vertical tab, 和form feed. 在ASCII 中,等价于:[ \t\r\n\v\f]
[:upper:] 大写字母。
[:xdigit:] 用来表示十六进制数字的字符。在ASCII 中,等价于:[0-9A-Fa-f]
甚至通过字符集,仍然没有便捷的方法来表达部分区域,比如[A-M]。通过使用字符集,我们重做上述的例题,看到一个改进的结果:
[me@linuxbox ~]$ ls /usr/sbin/[[:upper:]]*
/usr/sbin/MAKEFLOPPIES
/usr/sbin/NetworkManagerDispatcher
/usr/sbin/NetworkManager
记住,然而,这不是一个正则表达式的例子,而是shell 正在执行路径名展开操作。我们在这里展示这个例子,是因为POSIX 规范的字符集适用于二者。
Alternation
我们将要讨论的扩展表达式的第一个特性叫做alternation(交替),其是一款允许从一系列表达式之间选择匹配项的实用程序。就像中括号表达式允许从一系列指定的字符之间匹配单个字符那样,alternation 允许从一系列字符串或者是其它的正则表达式中选择匹配项。为了说明问题,我们将会结合echo 程序来使用grep 命令。首先,让我们试一个普通的字符串匹配:
[me@linuxbox ~]$ echo "AAA" | grep AAA
AAA
[me@linuxbox ~]$ echo "BBB" | grep AAA
[me@linuxbox ~]$
一个相当直截了当的例子,我们把echo 的输出管道给grep,然后看到输出结果。当出现一个匹配项时,我们看到它会打印出来;当没有匹配项时,我们看到没有输出结果。现在我们将添加alternation,以竖杠线元字符为标记:
[me@linuxbox ~]$ echo "AAA" | grep -E 'AAA|BBB'
AAA
[me@linuxbox ~]$ echo "BBB" | grep -E 'AAA|BBB'
BBB
[me@linuxbox ~]$ echo "CCC" | grep -E 'AAA|BBB'
[me@linuxbox ~]$
这里我们看到正则表达式’AAA|BBB’,这意味着“匹配字符串AAA 或者是字符串BBB”。注意因为这是一个扩展的特性,我们给grep 命令(虽然我们能以egrep 程序来代替)添加了-E 选项,并且我们把这个正则表达式用单引号引起来,为的是阻止shell 把竖杠线元字符解释为一个pipe 操作符。Alternation 并不局限于两种选择:
[me@linuxbox ~]$ echo "AAA" | grep -E 'AAA|BBB|CCC'
AAA
为了把alternation 和其它正则表达式元素结合起来,我们可以使用() 来分离alternation。
[me@linuxbox ~]$ grep -Eh '^(bz|gz|zip)' dirlist*.txt
这个表达式将会在我们的列表中匹配以“bz”,或“gz”,或“zip”开头的文件名。如果我们删除了圆括号,这个表达式的意思:
[me@linuxbox ~]$ grep -Eh '^bz|gz|zip' dirlist*.txt
会变成匹配任意以“bz”开头,或包含“gz”,或包含“zip”的文件名。
限定符
扩展的正则表达式支持几种方法,来指定一个元素被匹配的次数。
? - 匹配零个或一个元素
这个限定符意味着,实际上,“使前面的元素可有可无。”比方说我们想要查看一个电话号码的真实性,如果它匹配下面两种格式的任意一种,我们就认为这个电话号码是真实的:
(nnn) nnn-nnnn
nnn nnn-nnnn
这里的“n”是一个数字。我们可以构建一个像这样的正则表达式:
^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]$
在这个表达式中,我们在圆括号之后加上一个问号,来表示它们将被匹配零次或一次。再一次,因为通常圆括号都是元字符(在ERE 中),所以我们在圆括号之前加上了反斜杠,使它们成为文本字符。
让我们试一下:
[me@linuxbox ~]$ echo "(555) 123-4567" | grep -E '^\(?[0-9][0-9][0-9]
\)? [0-9][0-9][0-9]$'
(555) 123-4567
[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]$'
[me@linuxbox ~]$
这里我们看到这个表达式匹配这个电话号码的两种形式,但是不匹配包含非数字字符的号码。
* - 匹配零个或多个元素
像? 元字符一样,这个* 被用来表示一个可选的字符;然而,又与? 不同,匹配的字符可以出现任意多次,不仅是一次。比方说我们想要知道是否一个字符串是一句话;也就是说,字符串开始于一个大写字母,然后包含任意多个大写和小写的字母和空格,最后以句号收尾。为了匹配这个(非常粗略的)语句的定义,我们能够使用一个像这样的正则表达式:
[[:upper:]][[:upper:][:lower:] ]*.
这个表达式由三个元素组成:一个包含[:upper:] 字符集的中括号表达式,一个包含[:upper:] 和[:lower:] 两个字符集以及一个空格的中括号表达式,和一个被反斜杠字符转义过的
圆点。第二个元素末尾带有一个* 元字符,所以在开头的大写字母之后,可能会跟随着任意数目的大写和小写字母和空格,并且匹配:
[me@linuxbox ~]$ echo "This works." | grep -E '[[:upper:]][[:upper:][[:lower:]]*.'
This works.
[me@linuxbox ~]$ echo "This Works." | grep -E '[[:upper:]][[:upper:][[:lower:]]*.'
This Works.
[me@linuxbox ~]$ echo "this does not" | grep -E '[[:upper:]][[:upper: ][[:lower:]]*.'
[me@linuxbox ~]$
这个表达式匹配前两个测试语句,但不匹配第三个,因为第三个句子缺少开头的大写字母
和末尾的句号。
+ - 匹配一个或多个元素
这个+ 元字符的作用与* 非常相似,除了它要求前面的元素至少出现一次匹配。这个正则表达式只匹配那些由一个或多个字母字符组构成的文本行,字母字符之间由单个空格分开:
^([[: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 ~]$ echo "abc d" | grep -E '^([[:alpha:]]+ ?)+$'
[me@linuxbox ~]$
我们看到这个正则表达式不匹配“a b 9”这一行,因为它包含了一个非字母的字符;它也不匹配“abc d”,因为在字符“c”和“d”之间不止一个空格。
{ } - 匹配特定个数的元素
这个{ 和} 元字符都被用来表达要求匹配的最小和最大数目。它们可以通过四种方法来指定:
n 匹配前面的元素,如果它确切地出现了n 次。
n,m 匹配前面的元素,如果它至少出现了n 次,但是不多于m次。
n, 匹配前面的元素,如果它出现了n 次或多于n 次。
,m 匹配前面的元素,如果它出现的次数不多于m 次。
回到之前处理电话号码的例子,我们能够使用这种指定重复次数的方法来简化我们最初的正则表达式:
^\(?[0-9][0-9][0-9]\)? [0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]
简化为:
^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$
让我们试一下:
[me@linuxbox ~]$ echo "(555) 123-4567" | grep -E '^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$'
(555) 123-4567
[me@linuxbox ~]$ echo "555 123-4567" | grep -E '^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$'
555 123-4567
[me@linuxbox ~]$ echo "5555 123-4567" | grep -E '^\(?[0-9]{3}\)? [0-9]{3}-[0-9]{4}$'
[me@linuxbox ~]$
我们可以看到,我们修订的表达式能成功地验证带有和不带有圆括号的数字,而拒绝那些格式不正确的数字。