Shell学习笔记(9)——正则表达式

正则表达式定义

定义

正则表达式是你所定义的模式模板(pattern template),Linux工具可以用它来过滤文本。Linux工具(比如sed编辑器或gawk程序)能够在处理数据时使用正则表达式对数据进行模式匹配。如果数据匹配模式,它就会被接受并进一步处理;如果数据不匹配模式,它就会被滤掉。下图描述了这个过程。

mark

正则表达式模式利用通配符来描述数据流中的一个或多个字符。Linux中有很多场景都可以使用通配符来描述不确定的数据。星号通配符允许你只列出满足特定条件的文件,如下所示:

1
2
3
4
biotest@ubuntu:~/sed$ ls data*
data10.cpp data1b.txt data3.txt
data11.txt data1.txt data4.txt
data12.txt data2.txt data9.txt

data* 参数会让 ls 命令只列出名字以da开头的文件。文件名中da之后可以有任意多个字符(包括什么也没有)。

正则表达式通配符模式的工作原理与之类似。正则表达式模式含有文本或特殊字符,为sed编辑器和gawk程序定义了一个匹配数据时采用的模板。可以在正则表达式中使用不同的特殊字符来定义特定的数据过滤模式。

类型

使用正则表达式最大的问题在于有不止一种类型的正则表达式。Linux中的不同应用程序可能会用不同类型的正则表达式。这其中包括编程语言(Java、Perl和Python)、Linux实用工具(比如sed编辑器、gawk程序和grep工具)以及主流应用(比如MySQL和PostgreSQL数据库服务器)。正则表达式是通过正则表达式引擎(regular expression engine)实现的。正则表达式引擎是一套底层软件,负责解释正则表达式模式并使用这些模式进行文本匹配。在Linux中,有两种流行的正则表达式引擎:

第一,POSIX基础正则表达式(basic regular expression,BRE)引擎;

第二,POSIX扩展正则表达式(extended regular expression,ERE)引擎

大多数Linux工具都至少符合POSIXBRE引擎规范,能够识别该规范定义的所有模式符号。遗憾的是,有些工具(比如sed编辑器)只符合了BRE引擎规范的子集。这是出于速度方面的考虑导致的,因为sed编辑器希望能尽可能快地处理数据流中的文本。POSIXBRE引擎通常出现在依赖正则表达式进行文本过滤的编程语言中。它为常见模式提供了高级模式符号和特殊符号,比如匹配数字、单词以及按字母排序的字符。gawk程序用ERE引擎来处理它的正则表达式模式。

定义BRE模式

最基本的BRE模式是匹配数据流中的文本字符。

纯文本

下面演示一下如何在sed编辑器和gawk程序中用标准文本字符串来过滤数据,如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/regx$ echo "This is a test"|sed -n '/test/p'
This is a test
biotest@ubuntu:~/regx$ echo "This is a test"|sed -n '/trial/p'
biotest@ubuntu:~/regx$ echo "This is a test"|gawk '/test/{print $0}'
This is a test
biotest@ubuntu:~/regx$ echo "This is a test"|gawk '/trial/{print $0}'

第一个模式定义了一个单词test。sed编辑器和gawk程序脚本用它们各自的print命令打印出匹配该正则表达式模式的所有行。由于echo语句在文本字符串中包含了单词test,数据流文本能够匹配所定义的正则表达式模式,因此sed编辑器显示了该行。

第二个模式也定义了一个单词,这次是trial。因为echo语句文本字符串没包含该单词,所以正则表达式模式没有匹配,因此sed编辑器和gawk程序都没打印该行。你可能注意到了,正则表达式并不关心模式在数据流中的位置。它也不关心模式出现了多少次。一旦正则表达式匹配了文本字符串中任意位置上的模式,它就会将该字符串传回Linux工具。关键在于将正则表达式模式匹配到数据流文本上。重要的是记住正则表达式对匹配的模式非常挑剔。第一条原则就是:正则表达式模式都区分大小写。这意味着它们只会匹配大小写也相符的模式。

正则表达式区分大小写

看个例子,如下所示:

1
2
3
biotest@ubuntu:~/regx$ echo "This is a test"|sed -n '/this/p'
biotest@ubuntu:~/regx$ echo "This is a test"|sed -n '/This/p'
This is a test

第一次尝试没能匹配成功,因为 this 在字符串中并不都是小写,而第二次尝试在模式中使用大写字母,所以能正常工作。
在正则表达式中,你不用写出整个单词。只要定义的文本出现在数据流中,正则表达式就能够匹配,如下所示:

1
2
biotest@ubuntu:~/regx$ echo "The books are expensive"|sed -n '/book/p'
The books are expensive

尽管数据流中的文本是books,但数据中含有正则表达式book,因此正则表达式模式跟数据匹配。当然,反之正则表达式就不成立了,如下所示:

1
2
biotest@ubuntu:~/regx$ echo "The book is expensive"|sed -n '/books/p'
# 完整的正则表达式文本并未在数据流中出现,因此匹配失败,sed编辑器不会显示任何文本。

可以在正则表达式中使用空格和数字,如下所示:

1
2
3
biotest@ubuntu:~/regx$ echo "This is line number 1"|sed -n '/ber 1/p'
This is line number 1
# 空格也能匹配

在正则表达式中,空格和其他的字符并没有什么区别,如下所示:

1
2
3
biotest@ubuntu:~/regx$ echo "This is line number1"|sed -n '/ber 1/p'
biotest@ubuntu:~/regx$
# number1之间没有空格,/ber 1/p就无法匹配,因此也不显示

如果你在正则表达式中定义了空格,那么它必须出现在数据流中。甚至可以创建匹配多个连续空格的正则表达式模式,如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/regx$ cat data1.txt
This is a normal line of text.
This is a line with too many spaces.
biotest@ubuntu:~/regx$ sed -n '/ /p' data1.txt
This is a line with too many spaces.
# This is这之间有两个空格

特殊字符

正则表达式识别的特殊字符包括: .*[]^${}\+?|(),在文本模式中不要单独使用这些字符,如果要用某个特殊字符作为文本字符,就必须转义,在转义特殊字符时,你需要在它前面加一个反斜线(\),如下所示:

1
2
3
4
5
biotest@ubuntu:~/regx$ cat data2.txt
The cost is $4.00
biotest@ubuntu:~/regx$ sed -n '/\$/p' data2.txt
The cost is $4.00
# 这里添加了反斜线对美元符号进行转义

如果要匹配反斜线,需要用反斜线本身对基进行转义,也就是两个反斜线,如下所示:

1
2
3
biotest@ubuntu:~/regx$ echo "\ is a special character" |sed -n '/\\/p'
\ is a special character
# 在这个案例中,匹配的是\,因此需要需要一个/来进行匹配,就成了//

需要注意的是,虽然/不是正则表达式的特殊字符,但是也也需要\进行匹配,如下所示:

1
2
3
biotest@ubuntu:~/regx$ echo "3/2"|sed -n '/\//p'
3/2
# 匹配模式就是/文本/命令;这里的文本就是/,添加上转义字符就是\/,整体来看就是/\//p。

锚字符

当指定一个正则表达式模式时,只要模式出现在数据流中的任何地方,它就能匹配。有两个特殊字符可以用来将模式锁定在数据流中的行首或行尾。

锁定在行首

脱字符(^)定义从数据流中文本行的行首开始的模式。如果模式出现在行首之外的位置,正则表达式模式则无法匹配,如果要使用脱字符,就必须将它放到正则表达式中指定的模式前面,如下所示:

1
2
3
4
5
biotest@ubuntu:~/regx$ echo "The book store"|sed -n '/^book/p'
# book不是首个单词,加了帽子的book无法匹配
biotest@ubuntu:~/regx$ echo "Books are great"|sed -n '/^Book/p'
Books are great
# Books是首个单词,加了帽子的Books能够匹配

脱字符会在每个由换行符决定的新数据行的行首检查模式,如下所示:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/regx$ cat data3.txt
This is a test line.
this is another test line.
A line that tests this feature.
Yet more testing of this
biotest@ubuntu:~/regx$ sed -n '/^this/p' data3.txt
this is another test line.
# 只要模式出现在新行的行首,脱字符就能够发现它。

如果你将脱字符放到模式开头之外的其他位置,那么它就跟普通字符一样,不再是特殊字符了,如下所示:

1
2
3
4
5
6
7
biotest@ubuntu:~/regx$ echo "This ^ is a test"|sed -n '/This ^/p'
This ^ is a test
# 这里匹配的是This ^,这里不用转义
biotest@ubuntu:~/regx$ echo "This ^is a test"|sed -n '/\^is/p'
This ^is a test
# 这里匹配的是^is,由于^后面有字符,因此需要转义,如果不转义,匹配时会认为是只匹配首个单词

需要注意的是,如果指定正则表达式模式时只用了脱字符,就不需要用反斜线来转义。但如果你在模式中先指定了脱字符,随后还有其他一些文本,那么你必须在脱字符前用转义字符。

锁定在行尾

特殊字符美元符($)定义了行尾锚点。将这个特殊字符放在文本模式之后来指明数据行必须以该文本模式结尾,如下所示:

1
2
3
4
5
biotest@ubuntu:~/regx$ echo "This is a good book"|sed -n '/book$/p'
This is a good book
biotest@ubuntu:~/regx$ echo "This book is good"|sed -n '/book$/p'
biotest@ubuntu:~/regx$ echo "There are a lot of good books"|sed -n '/book$/p'
# 无法完成匹配,因为原文本是books,而美元符号前面是book,可以理解为,原文本最后两个是ks,而美元符号是ok,肯定不能匹配了

组合锚点

在一些常见情况下,可以在同一行中将行首锚点和行尾锚点组合在一起使用。在第一种情况中,假定你要查找只含有特定文本模式的数据行,如下所示:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/regx$ cat data4.txt
this is a test of using both anchors
I said this is a test
this is a test
I'm sure this is a test.
biotest@ubuntu:~/regx$ sed -n '/^this is a test$/p' data4.txt
this is a test
# sed编辑器忽略了那些不单单包含指定的文本的行,例如I said this is a test

将两个锚点直接组合在一起,之间不加任何文本,这样过滤出数据流中的空白行,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/regx$ cat data5.txt
No.1 This is one test line.
No.2 This is another test line.
No.3 This third test line.
biotest@ubuntu:~/regx$ sed '/^$/d' data5.txt
No.1 This is one test line.
No.2 This is another test line.
No.3 This third test line.
# 定义的正则表达式模式会查找行首和行尾之间什么都没有的那些行

定义的正则表达式模式会查找行首和行尾之间什么都没有的那些行。由于空白行在两个换行符之间没有文本,刚好匹配了正则表达式模式。sed编辑器用删除命令d来删除匹配该正则表达式模式的行,因此删除了文本中的所有空白行。这是从文档中删除空白行的有效方法。

点号字符

特殊字符点号用来匹配除换行符之外的任意单个字符。它必须匹配一个字符,如果在点号字符的位置没有字符,那么模式就不成立。来看一些在正则表达式模式中使用点号字符的例子。

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/regx$ cat data6.txt
This is a test of a line.
The cat is sleeping.
That is a very nice hat.
This test is at line four.
at ten o'clock we'll go home.:wq
biotest@ubuntu:~/regx$ sed -n '/.at/p' data6.txt
The cat is sleeping.
That is a very nice hat.
This test is at line four.
# 在第四行中,at前面有一个空格,这也是一个字符,因此能够匹配。

字符组

如果要限定待匹配的具体字符的话,在正则表达式中,这称为字符组(characterclass)。可以定义用来匹配文本模式中某个位置的一组字符。如果字符组中的某个字符出现在了数据流中,那它就匹配了该模式。使用方括号来定义一个字符组。方括号中包含所有你希望出现在该字符组中的字符。然后你可以在模式中使用整个组,就跟使用其他通配符一样,看一个案例:

1
2
3
biotest@ubuntu:~/regx$ sed -n '/[ch]at/p' data6.txt
The cat is sleeping.
That is a very nice hat.

匹配这个模式的单词只有 cat 和 hat 。还要注意以 at 开头的行也没有匹配。字符组中必须有个字符来匹配相应的位置。

不区分大小写

有时候这种方法在不清楚大小写的时候很有用,如下所示:

1
2
3
4
5
6
7
8
9
10
biotest@ubuntu:~/regx$ echo "Yes" | sed -n '/[Yy]es/p'
Yes
biotest@ubuntu:~/regx$ echo "yes"|sed -n '/[Yy]es/p'
yes
biotest@ubuntu:~/regx$ echo "Yes"|sed -n '/[Yy][Ee][Ss]/p'
Yes
biotest@ubuntu:~/regx$ echo "yEs"|sed -n '/[Yy][Ee][Ss]/p'
yEs
biotest@ubuntu:~/regx$ echo "yeS"|sed -n '/[Yy][Ee][Ss]/p'
yeS

使用数字

字符组不必只含有字母,也可以在其中使用数字,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/regx$ cat data6.txt
This is a test of a line.
The cat is sleeping.
That is a very nice hat.
This test is at line four.
at ten o'clock we'll go home.:wq
biotest@ubuntu:~/regx$ sed -n '/[0123]/p' data7.txt
THis line has 1 number on it.
THis line a number 2 on it.
# 这个正则表达式模式匹配了任意含有数字0、1、2或3的行。含有其他数字以及不含有数字的行都会被忽略掉。

匹配某一位数的数字

有时候需要将字级组组合在一起,以检查数字是否具备正确的格式,比如电话号码和邮编。但当你尝试匹配某种特定格式时,必须小心。这里有个匹配邮编出错的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
biotest@ubuntu:~/regx$ cat data8.txt
46201
60633
223001
4353
22203
biotest@ubuntu:~/regx$ sed -n '
> /[0123456789][0123456789][0123456789][0123456789][0123456789]/p
> ' data8.txt
46201
60633
223001
22203

从结果可以看出,它滤过了4353,因为正则表达式中定义了5个字符组,4位的4353只有4个数字,最后一个数字无法匹配,就滤过了。但是223001这个6位数也通过了。因此正则表达式模式可见于数据流中文本的任何位置,因此我们设定过滤5个数字时,4个数字肯定能滤过去,但是大于5个数字的也能通过,因此如果要确保只匹配五位数,就必须将匹配的字符和其他字符分开,要么用空格,要么像这个例子中这样,指明它们就在行首和行尾,如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/regx$ sed -n '
/^[0123456789][0123456789][0123456789][0123456789][0123456789]$/p
' data8.txt
46201
60633
22203

检查拼写

字符组的一个极其常见的用法是解析拼错的单词,比如用户表单输入的数据。你可以创建正则表达式来接受数据中常见的拼写错误,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/regx$ cat data9.txt
I need to have some maintenence done on my car.
I'll pay that in a seperate invoice.
After I pay for the maintenance my car will be as good as new.
biotest@ubuntu:~/regx$ sed -n '
/maint[ea]n[ae]nce/p
/sep[ea]r[ea]te/p
' data9.txt
I need to have some maintenence done on my car.
I'll pay that in a seperate invoice.
After I pay for the maintenance my car will be as good as new.

本例中的两个sed打印命令利用正则表达式字符组来帮助找到文本中拼错的单词maintenance和separate。同样的正则表达式模式也能匹配正确拼写的maintenance。

排除型字符组

在正则表达式模式中,也可以反转字符组的作用。可以寻找组中没有的字符,而不是去寻找组中含有的字符。要这么做的话,只要在字符组的开头加个脱字符,如下所示:

1
2
3
4
5
6
7
8
9
biotest@ubuntu:~/regx$ cat data6.txt
This is a test of a line.
The cat is sleeping.
That is a very nice hat.
This test is at line four.
at ten o'clock we'll go home.:wq
biotest@ubuntu:~/regx$ sed -n '/[^ch]at/p' data6.txt
This test is at line four.

通过排除型字符组,正则表达式模式会匹配c或h之外的任何字符以及文本模式。由于空格字符属于这个范围,它通过了模式匹配。但即使是排除,字符组仍然必须匹配一个字符,所以以at开头的行仍然未能匹配模式。

区间

如果必须要在每个字符组中列出所有可能的数字,有点麻烦,单破折线符号在字符组中表示字符区间。只需要指定区间的第一个字符、单破折线以及区间的最后一个字符就行了,正则表达式会包括此区间内的任意字符,可以通过指定数字区间来简化邮编的例子:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/regx$ cat data8.txt
46201
60633
223001
4353
22203
biotest@ubuntu:~/regx$ sed -n '/^[0-9][0-9][0-9][0-9][0-9]$/p' data8.txt
46201
60633
22203

这样可是节省了不少的键盘输入,每个字符组都会匹配0~9的任意数字。

这种方法也适用于字母,如下所示:

1
2
3
4
5
6
7
8
9
10
biotest@ubuntu:~/regx$ cat data6.txt
This is a test of a line.
The cat is sleeping.
That is a very nice hat.
This test is at line four.
at ten o'clock we'll go home.:wq
biotest@ubuntu:~/regx$ sed -n '/[c-h]at/p' data6.txt
The cat is sleeping.
That is a very nice hat.

新的模式[c-h]at匹配了首字母在字母c和字母h之间的单词。这种情况下,只含有单词at的行将无法匹配该模式。还可以在单个字符组指定多个不连续的区间,如下所示:

1
2
biotest@ubuntu:~/regx$ sed -n '/[a-ch-m]at/p' data6.txtThe cat is sleeping.
That is a very nice hat.

该字符组允许区间a~c、h~m中的字母出现在at文本前,但不允许出现d~g的字母。

特殊的字符组

除了定义自己的字符组外,BRE还包含了一些特殊的字符组,可用来匹配特定类型的字符。下表是可用的BRE特殊的字符组。

描述
[[:alpha:]] 匹配任意字母字符,不管是大写还是小写
[[:alnum:]] 匹配任意字母数字字符0~9、A
[[:blank:]] 匹配空格或制表符
[[:digit:]] 匹配0~9之间的数字
[[:lower:]] 匹配小写字母字符a~z
[[:print:]] 匹配任意可打印字符
[[:punct:]] 匹配标点符号
[[:space:]] 匹配任意空白字符:空格、制表符、NL、FF、VT和CR
[[:upper:]] 匹配任意大写字母字符A~Z

可以在正则表达式模式中将特殊字符组像普通字符组一样使用,如下所示:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/regx$ echo "abc"|sed -n '/[[:digit:]]/p'
biotest@ubuntu:~/regx$ echo "abc"|sed -n '/[[:alpha:]]/p'
abc
biotest@ubuntu:~/regx$ echo "abc123"| sed -n '/[[:digit:]]/p'
abc123
biotest@ubuntu:~/regx$ echo "This is , a test"|sed -n '/[[:punct:]]/p'
This is , a test
biotest@ubuntu:~/regx$ echo "This is a test"|sed -n '/[[:punct:]]/p'

使用特殊字符组可以很方便地定义区间。可以用[[:digit:]]来代替区间[0-9]

星号

在字符后面放置星号表明该字符必须在匹配模式的文本中出现0次或多次,如下所示:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/regx$ echo "ik"|sed -n '/ie*k/p'
ik
biotest@ubuntu:~/regx$ echo "iek"|sed -n '/ie*k/p'
iek
biotest@ubuntu:~/regx$ echo "ieek"|sed -n '/ie*k/p'
ieek
biotest@ubuntu:~/regx$ echo "ieeek"|sed -n '/ie*k/p'
ieeek

这个模式符号广泛用于处理有常见拼写错误或在不同语言中有拼写变化的单词。举个例子,如果需要写个可能用在美式或英式英语中的脚本,可以这么写:

1
2
3
4
biotest@ubuntu:~/regx$ echo "I'm getting a color TV"|sed -n '/colou*r/p'
I'm getting a color TV
biotest@ubuntu:~/regx$ echo "I'm getting a colour TV"|sed -n '/colou*r/p'
I'm getting a colour TV

模式中的 u* 表明字母u可能出现或不出现在匹配模式的文本中。类似地,如果你知道一个单 词经常被拼错,你可以用星号来允许这种错误,如下所示:

1
2
3
4
biotest@ubuntu:~/regx$ echo "I ate a potatoe with my lunch."|sed -n '/potatoe*/p'
I ate a potatoe with my lunch.
biotest@ubuntu:~/regx$ echo "I ate a potato with my lunch."|sed -n '/potatoe*/p'
I ate a potato with my lunch.

在可能出现的额外字母后面放个星号将允许接受拼错的单词。另一个方便的特性是将点号特殊字符和星号特殊字符组合起来。这个组合能够匹配任意数量的任意字符。它通常用在数据流中两个可能相邻或不相邻的文本字符串之间。

1
2
3
biotest@ubuntu:~/regx$ echo "this is a regular pattern expression"|sed -n '
> /regular.*expression/p'
this is a regular pattern expression

可以使用这个模式轻松查找可能出现在数据流中文本行内任意位置的多个单词。星号还能用在字符组上。它允许指定可能在文本中出现多次的字符组或字符区间,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/regx$ echo "bt"|sed -n '/b[ae]*t/p'
bt
biotest@ubuntu:~/regx$ echo "bat"|sed -n '/b[ae]*t/p'
bat
biotest@ubuntu:~/regx$ echo "bet"|sed -n '/b[ae]*t/p'
bet
biotest@ubuntu:~/regx$ echo "btt"|sed -n '/b[ae]*t/p'
btt
biotest@ubuntu:~/regx$ echo "baat"|sed -n '/b[ae]*t/p'
baat
biotest@ubuntu:~/regx$ echo "baaeeet"|sed -n '/b[ae]*t/p'
baaeeet

只要a和e字符以任何组合形式出现在b和t字符之间(就算完全不出现也行),模式就能够匹配。如果出现了字符组之外的字符,该模式匹配就会不成立。

正则表达式扩展

POSIX ERE模式包括了一些可供Linux应用和工具使用的额外符号。gawk程序能够识别ERE模式,但sed编辑器不能。 记住,sed编辑器和gawk程序的正则表达式引擎之间是有区别的。gawk程序可以使用大多数扩展正则表达式模式符号,并且能提供一些额外过滤功能,而这些功能都是sed编辑器所不具备的。但正因为如此,gawk程序在处理数据流时通常才比较慢。

问号

问号类似于星号,不过有点细微的不同。问号表明前面的字符可以出现0次或1次,但只限于此。它不会匹配多次出现的字符。问号类似于星号,不过有点细微的不同。问号表明前面的字符可以出现0次或1次,但只限于此。它不会匹配多次出现的字符,如下所示:

1
2
3
4
5
6
7
biotest@ubuntu:~/regx$ echo "bt"|gawk '/be?t/{print $p}'
bt
biotest@ubuntu:~/regx$ echo "bet"|gawk '/be?t/{print $0}'
bet
biotest@ubuntu:~/regx$ echo "beet"|gawk '/be?t/{print $0}'
biotest@ubuntu:~/regx$ echo "beeet"|gawk '/be?t/{print $0}'
# 如果字符 e 并未在文本中出现,或者它只在文本中出现了1次,那么模式会匹配。

问号与星号一样,也能与字符组一起使用,如下所示:

1
2
3
4
5
6
7
8
9
10
biotest@ubuntu:~/regx$ echo "bt"|gawk '/b[ae]?t/{print $0}'
bt
biotest@ubuntu:~/regx$ echo "bat"|gawk '/b[ae]?t/{print $0}'
bat
biotest@ubuntu:~/regx$ echo "bot"|gawk '/b[ae]?t/{print $0}'
biotest@ubuntu:~/regx$ echo "bet"|gawk '/b[ae]?t/{print $0}'
bet
biotest@ubuntu:~/regx$ echo "baet"|gawk '/b[ae]?t/{print $0}'
biotest@ubuntu:~/regx$ echo "beat"|gawk '/b[ae]?t/{print $0}'
biotest@ubuntu:~/regx$ echo "beet"|gawk '/b[ae]?t/{print $0}'

如果字符组中的字符出现了0次或1次,模式匹配就成立。但如果两个字符都出现了,或者其中一个字符出现了2次,模式匹配就不成立。

加号

加号表明前面的字符可以出现1次或多次,但必须至少出现1次。如果该字符没有出现,那么模式就不会匹配,如下所示:

1
2
3
4
5
6
7
biotest@ubuntu:~/regx$ echo "beet"|gawk '/be+t/{print $0}'
beet
biotest@ubuntu:~/regx$ echo "beeet"|gawk '/be+t/{print $0}'
beeet
biotest@ubuntu:~/regx$ echo "bet"|gawk '/be+t/{print $0}'
bet
biotest@ubuntu:~/regx$ echo "bt"|gawk '/be+t/{print $0}'

如果字符e没有出现,模式匹配就不成立。加号同样适用于字符组,与星号和问号的使用方式相同,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/regx$ echo "bt"|gawk '/b[ae]+t/{print $0}'
biotest@ubuntu:~/regx$ echo "bat"|gawk '/b[ae]+t/{print $0}'
bat
biotest@ubuntu:~/regx$ echo "bet"|gawk '/b[ae]+t/{print $0}'
bet
biotest@ubuntu:~/regx$ echo "beat"|gawk '/b[ae]+t/{print $0}'
beat
biotest@ubuntu:~/regx$ echo "beet"|gawk '/b[ae]+t/{print $0}'
beet
biotest@ubuntu:~/regx$ echo "beeat"|gawk '/b[ae]+t/{print $0}'
beeat

这次如果字符组中定义的任一字符出现了,文本就会匹配指定的模式。

花括号

ERE中的花括号允许你为可重复的正则表达式指定一个上限。这通常称为间隔(interval)。可以用两种格式来指定区间,其中m是正则表达式出现m次;m,n表现表达式至少出现m次,最多出现n次。这个特性可以精确调整字符或字符集在模式中具体出现的次数。 ,gawk程序不会识别正则表达式间隔。必须指定gawk程序的--re- interval命令行选项才能识别正则表达式间隔,如下所示:

1
2
3
4
biotest@ubuntu:~/regx$ echo "bt"|gawk --re-interval '/be{1}t/{print $0}'
biotest@ubuntu:~/regx$ echo "bet"|gawk --re-interval '/be{1}t/{print $0}'
bet
biotest@ubuntu:~/regx$ echo "beet"|gawk --re-interval '/be{1}t/{print $0}'

通过指定间隔为1,限定了该字符在匹配模式的字符串中出现的次数。如果该字符出现多次,模式匹配就不成立。

可以同时指定下限和上限,如下所示:

1
2
3
4
5
6
7
biotest@ubuntu:~/regx$ echo "bt"|gawk --re-interval '/be{1,2}t/{print $0}'
biotest@ubuntu:~/regx$ echo "bet"|gawk --re-interval '/be{1,2}t/{print $0}'
bet
biotest@ubuntu:~/regx$ echo "beet"|gawk --re-interval '/be{1,2}t/{print $0}'
beet
biotest@ubuntu:~/regx$ echo "beeet"|gawk --re-interval '/be{1,2}t/{print $0}'
# 在这个例子中,字符 e 可以出现1次或2次,这样模式就能匹配;否则,模式无法匹配。

间隔模式匹配同样适用于字符组,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/regx$ echo "bt"|gawk --re-interval '/b[ae]{1,2}t/{print $0}'
biotest@ubuntu:~/regx$ echo "bat"|gawk --re-interval '/b[ae]{1,2}t/{print $0}'
bat
biotest@ubuntu:~/regx$ echo "bet"|gawk --re-interval '/b[ae]{1,2}t/{print $0}'
bet
biotest@ubuntu:~/regx$ echo "beat"|gawk --re-interval '/b[ae]{1,2}t/{print $0}'
beat
biotest@ubuntu:~/regx$ echo "beet"|gawk --re-interval '/b[ae]{1,2}t/{print $0}'
beet
biotest@ubuntu:~/regx$ echo "baeet"|gawk --re-interval '/b[ae]{1,2}t/{print $0}'
biotest@ubuntu:~/regx$ echo "baeaet"|gawk --re-interval '/b[ae]{1,2}t/{print $0}'
# 如果字母a或e在文本模式中只出现了1~2次,则正则表达式模式匹配;否则,模式匹配失败。

管道符号

管道符号允许你在检查数据流时,用逻辑OR方式指定正则表达式引擎要用的两个或多个模式。如果任何一个模式匹配了数据流文本,文本就通过测试。如果没有模式匹配,则数据流文本匹配失败。使用管道符号的格式为expr1|expr2,如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/regx$ echo "The cat is asleep"|gawk '/cat|dog/{print $0}'
The cat is asleep
biotest@ubuntu:~/regx$ echo "The dog is asleep"|gawk '/cat|dog/{print $0}'
The dog is asleep
biotest@ubuntu:~/regx$ echo "The sheep is asleep"|gawk '/cat|dog/{print $0}'
# 这个例子会在数据流中查找正则表达式cat或dog。正则表达式和管道符号之间不能有空格,否则它们也会被认为是正则表达式模式的一部分。

管道符号两侧的正则表达式可以采用任何正则表达式模式(包括字符组)来定义文本,如下所示:

1
2
3
biotest@ubuntu:~/regx$ echo "He has a hat."|gawk '/[ch]at|dog/{print $0}'
He has a hat.
# 这个例子会匹配数据流文本中的 cat 、 hat 或 dog 。

表达式分组

正则表达式模式也可以用圆括号进行分组。当你将正则表达式模式分组时,该组会被视为一个标准字符。可以像对普通字符一样给该组使用特殊字符,如下所示:

1
2
3
4
5
biotest@ubuntu:~/regx$ echo "Sat"|gawk '/Sat(urday)?/{print $0}'
Sat
biotest@ubuntu:~/regx$ echo "Saturday"|gawk '/Sat(urday)?/{print $0}'
Saturday
# 结尾的urday分组以及问号,使得模式能够匹配完整的 Saturday 或缩写 Sat 。

将分组和管道符号一起使用来创建可能的模式匹配组是很常见的做法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/regx$ echo "cat"|gawk '/(c|b)a(b|t)/{print $0}'
cat
biotest@ubuntu:~/regx$ echo "cab"|gawk '/(c|b)a(b|t)/{print $0}'
cab
biotest@ubuntu:~/regx$ echo "bat"|gawk '/(c|b)a(b|t)/{print $0}'
bat
biotest@ubuntu:~/regx$ echo "bab"|gawk '/(c|b)a(b|t)/{print $0}'
bab
biotest@ubuntu:~/regx$ echo "tab"|gawk '/(c|b)a(b|t)/{print $0}'
biotest@ubuntu:~/regx$ echo "tac"|gawk '/(c|b)a(b|t)/{print $0}'
# 模式 (c|b)a(b|t) 会匹配第一组中字母的任意组合以及第二组中字母的任意组合。

正则表达式实战

第一案例:目录文件计数

目标:对PATH环境变量中定义的目录的可执行文件进行计数。

第一步:查看PATH中的目录名,如下所示:

1
2
biotest@ubuntu:~/regx$ echo $PATH
/home/biotest/miniconda2/bin:/home/miniconda2/bin:/home/bio/miniconda2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/biotest/home/test:/home/biotest/home/biotest:/home/biotest:/home/biotest/

从结果可以看出,PATH中的每个路径都是由冒号分隔的,如果要获取目录列表,就需要用空格来替换冒号。

第二步:用空格替换冒号,如下所示:

1
2
3
# 用空格替换冒号
biotest@ubuntu:~/regx$ echo $PATH | sed 's/:/ /g'
/home/biotest/miniconda2/bin /home/miniconda2/bin /home/bio/miniconda2/bin /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin /usr/games /usr/local/games /snap/bin /home/biotest/home/test /home/biotest/home/biotest /home/biotest /home/biotest/

第三步:用for语句遍历每个目录,如下所示:

1
2
3
4
5
mypath=$(echo $PATH |sed 's/:/ /g')
for directory in $mypath
do
.# 代码先略过
done

第四步:一旦获得了单个目录,就可以用 ls 命令来列出每个目录中的文件,并用另一个 for 语句来遍历每个文件,为文件计数器增值。 这个脚本的最终版本如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
# count number of files in your PATH
mypath=$(echo $PATH | sed 's/:/ /g')
count=0
for directory in $mypath
do
check=$(ls $directory)
for item in $check
do
count=$[ $count+1 ]
done
echo "$directory - $count"
count=0
done

结果如下所示:

1
2
3
4
5
6
7
8
9
biotest@ubuntu:~/regx$ sudo bash countfiles.sh
/usr/local/sbin - 0
/usr/local/bin - 2
/usr/sbin - 200
/usr/bin - 1744
/sbin - 188
/bin - 167
ls: cannot access '/snap/bin': No such file or directory
/snap/bin - 0

第二案例:验证电话号码

在这个案例中,使用的验证的美国的电话号码,美国的电话号码有这几种形式,如下所示:

1
2
3
4
(123)456-7890
(123) 456-7890
123-456-7890
123.456.7890

这样用户在表单中输入的电话号码就有4种可能。

第一步:左括号。在构建正则表达式时,最好从左手边开始,然后构建用来匹配可能遇到的字符的模式。在这个例子中,电话号码中可能有也可能没有左圆括号。这可以用^/(?这个模式来匹配,脱字符用来表明数据的开始。由于左圆括号是个特殊字符,因此必须将它转义成普通字符。问号表明左圆括号可能出现,也可能不出现。

第二步:三个连续数字。紧接着就是3位区号。在美国,区号以数字2开始(没有以数字0或1开始的区号),最大可到9。要匹配区号,可以用[2-9][0-9]{2}进行匹配,这要求第一个字符是2~9的数字,后跟任意两位数字。

第三步:右括号。在区号后面,收尾的右圆括号可能存在,也可能不存在。,使用\)?进行匹配。

第四步:在区号后,存在如下可能:有一个空格,没有空格,有一条单破折线或一个点。你可以对它们使用管道符号,并用圆括号进行分组,匹配模式为(| |-|\.),这表示第一个管道符号紧跟在左圆括号后,用来匹配没有空格的情形。你必须将点字符转义,否则它会被解释成可匹配任意字符。

第五步:匹配3位电话交换机号码,模式为[0-9]{3}

第六步:在电话交换机号码之后,你必须匹配一个空格、一条单破折线或一个点(这次不用考虑匹配没有空格的情况,因为在电话交换机号码和其余号码间必须有至少一个空格)。,模式为( |-|\.)

第七步:最后,必须在字符串尾部匹配4位本地电话分机号,匹配模式为`[0-9]{4}$。

完整的模式为^\(?[2-9][0-9]{2}\)?(| |-|\.)[0-9]{3}( |-|\.)[0-9]{4}$

在gawk程序中用这个正则表达式模式来过滤掉不符合格式的电话号码。现在只需要在gawk程序中创建一个使用该正则表达式的简单脚本,然后用这个脚本来过滤电话薄。记住,在gawk程序中使用正则表达式间隔时,必须使用—re-interval命令行选项,否则就没法得到正确的结果,代码如下所示:

1
2
3
#!/bin/bash
# script to filter out bad phone numbers
gawk --re-interval '/^\(?[2-9][0-9]{2}\)?(| |-|\.)[0-9]{3}( |-|\.)[0-9]{4}/{print $0}'

运行结果如下所示:

1
2
3
biotest@ubuntu:~/regx$ chmod u+x isphone.sh
biotest@ubuntu:~/regx$ sudo echo "317-555-1234" |./isphone.sh
317-555-1234

第三案例: 解析邮件地址

电子邮件的基本格式为username$hostname,除此之外,还有可能含有点号,单破折线,加号,下划线等。而邮件的地址hostname部分是由一个或多个域名和一个服务器名组成。服务器名和域名也必须遵照严格的命名规则,只允许字母
数字字符以及点号或下划线组成。

第一步:顶级域名的数量在过去十分有限,正则表达式模式编写者会尝试将它们都加到验证模式中。然而遗憾的是,随着互联网的发展,可用的顶级域名也增多了。这种方法已经不再可行。从左侧开始构建这个正则表达式模式。我们知道,用户名中可以有多个有效字符。这个相当容易,匹配模式为^([a-zA-Z0-9_\-\.\+]+)@,这个匹配模式指定了用户名中允许的字符,加号表明必须有至少一个字符,最后一个字符是@

第二步:hostname 模式使用同样的方法来匹配服务器名和子域名,即([a-zA-Z0-9_\-\.]+),这个模式可以匹配文本。

第三步:顶级域名。对于顶级域名,有一些特殊的规则。顶级域名只能是字母字符,必须不少于二个字符(国家或地区代码中使用),并且长度上不得超过五个字符。级级域名用的正则表达式模式为\.([a-zA-Z]{2,5})$

整个匹配模式就是^([a-zA-Z0-9_\-\.\+]+)@([a-zA-Z0-9_\-\-\.]+)\.(a-zA-Z]{2,5})$

1
2
3
#!/bin/bash
# script to filter out bad mail
gawk '/^([a-zA-Z0-9_\-\.\+]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$/{print $0}'

运行结果如下所示:

1
2
3
biotest@ubuntu:~/regx$ sudo echo "rich@here.now" | ./ismail.sh
rich@here.now
biotest@ubuntu:~/regx$ sudo echo "rich#here.now" | ./ismail.sh