继 Linux shell 脚本编程-高级篇 (三)
4. 正则表达式
在 shell 脚本中成功运用 sed 编辑器和 gawk 程序的关键在于熟练使用正则表达式。这可不是件简单的事,从大量数据中过滤出特定数据可能会(而且经常会)很复杂。
4.1 什么是正则表达式
4.1.1 定义
正则表达式是所定义的模式模板(pattern template),Linux 工具可以用它来过滤文本。Linux 工具(比如 sed 编辑器或 gawk 程序)能够在处理数据时使用正则表达式对数据进行模式匹配。如果数据匹配模式,它就会被接受并进一步处理;如果数据不匹配模式,它就会被滤掉。
正则表达式模式利用通配符来描述数据流中的一个或多个字符。Linux 中有很多场景都可以使用通配符来描述不确定的数据。例如 Linux 的 ls 命令中使用通配符列出文件和目录:
[devalone@devalone 19]$ ls -l da*
-rw-rw-r--. 1 devalone devalone 41 7月 11 12:09 data10.txt
-rw-rw-r--. 1 devalone devalone 83 7月 11 12:18 data11.txt
-rw-rw-r--. 1 devalone devalone 52 1月 7 2018 data12.txt
-rw-rw-r--. 1 devalone devalone 184 1月 7 2018 data1.txt
-rw-rw-r--. 1 devalone devalone 68 1月 7 2018 data2.txt
-rw-rw-r--. 1 devalone devalone 21 1月 7 2018 data3.txt
-rw-rw-r--. 1 devalone devalone 77 1月 7 2018 data4.txt
-rw-rw-r--. 1 devalone devalone 45 1月 7 2018 data5.txt
-rw-rw-r--. 1 devalone devalone 92 1月 7 2018 data6.txt
-rw-rw-r--. 1 devalone devalone 188 7月 11 10:56 data7.txt
星号通配符允许只列出满足特定条件的文件。
da* 参数会让 ls 命令只列出名字以 da 开头的文件。文件名中 da 之后可以有任意多个字符(包括什么也没有)。ls命令会读取目录中所有文件的信息,但只显示跟通配符匹配的文件的信息。
正则表达式通配符模式的工作原理与之类似。正则表达式模式含有文本或特殊字符,为 sed 编辑器和 gawk 程序定义了一个匹配数据时采用的模板。可以在正则表达式中使用不同的特殊字符来定义特定的数据过滤模式。
4.1.2 正则表达式的类型
-----------------------------------------------------------------------------------------------------------------------------------------
使用正则表达式最大的问题在于有不止一种类型的正则表达式。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 工具都至少符合 POSIX BRE 引擎规范,能够识别该规范定义的所有模式符号。遗憾的是,有些工具(比如 sed 编辑器)只符合了 BRE 引擎规范的子集。这是出于速度方面的考虑导致的,因为 sed 编辑器希望能尽可能快地处理数据流中的文本。
POSIX ERE 引擎通常出现在依赖正则表达式进行文本过滤的编程语言中。它为常见模式提供了高级模式符号和特殊符号,比如匹配数字、单词以及按字母排序
的字符。gawk 程序用 ERE 引擎来处理它的正则表达式模式。
4.2 定义 BRE 模式
-----------------------------------------------------------------------------------------------------------------------------------------
最基本的 BRE 模式是匹配数据流中的文本字符。
4.2.1 纯文本
-----------------------------------------------------------------------------------------------------------------------------------------
在 sed 编辑器和 gawk 程序中用标准文本字符串来过滤数据。
示例:
[devalone@devalone 20]$ echo "This is a test" | sed -n '/test/p'
This is a test
[devalone@devalone 20]$ echo "This is a test" | sed -n '/trial/p'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "This is a test" | gawk '/test/{print $0}'
This is a test
[devalone@devalone 20]$ echo "This is a test" | gawk '/trial/{print $0}'
[devalone@devalone 20]$
第一个模式定义了一个单词 test。sed 编辑器和 gawk 程序脚本用它们各自的 print 命令打印出匹配该正则表达式模式的所有行。由于 echo 语句在文本
字符串中包含了单词 test,数据流文本能够匹配所定义的正则表达式模式,因此 sed 编辑器显示了该行。
第二个模式也定义了一个单词,这次是 trial。因为 echo 语句文本字符串没包含该单词,所以正则表达式模式没有匹配,因此 sed 编辑器和 gawk 程序
都没打印该行。
正则表达式并不关心模式在数据流中的位置。它也不关心模式出现了多少次。一旦正则表达式匹配了文本字符串中任意位置上的模式,它就会将该字符串
传回 Linux 工具。
关键在于将正则表达式模式匹配到数据流文本上。重要的是记住正则表达式对匹配的模式非常挑剔。
第一条原则就是:正则表达式模式都区分大小写。这意味着它们只会匹配大小写也相符的模式。
示例:
[devalone@devalone 20]$ echo "This is a test" | sed -n '/this/p'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "This is a test" | sed -n '/This/p'
This is a test
第一次尝试没能匹配成功,因为 this 在字符串中并不都是小写,而第二次尝试在模式中使用大写字母,所以能正常工作。
在正则表达式中,不用写出整个单词。只要定义的文本出现在数据流中,正则表达式就能够匹配。
示例:
[devalone@devalone 20]$ echo "The books are expensive" | sed -n '/book/p'
The books are expensive
尽管数据流中的文本是 books,但数据中含有正则表达式 book,因此正则表达式模式跟数据匹配。当然,反之正则表达式就不成立了。
示例:
[devalone@devalone 20]$ echo "The book is expensive" | sed -n '/books/p'
[devalone@devalone 20]$
完整的正则表达式文本并未在数据流中出现,因此匹配失败,sed 编辑器不会显示任何文本。
也不用局限于在正则表达式中只用单个文本单词,可以在正则表达式中使用空格和数字。
示例:
[devalone@devalone 20]$ echo "This is line number 1" | sed -n '/ber 1/p'
This is line number 1
在正则表达式中,空格和其他的字符并没有什么区别。
示例:
[devalone@devalone 20]$ echo "This is line number1" | sed -n '/ber 1/p'
[devalone@devalone 20]$
如果在正则表达式中定义了空格,那么它必须出现在数据流中。甚至可以创建匹配多个连续空格的正则表达式模式。
示例:
[devalone@devalone 20]$ cat data1
This is a normal line of test.
This is a line with too many space
[devalone@devalone 20]$ sed -n '/ /p' data1
This is a line with too many space
单词间有两个空格的行匹配正则表达式模式。这是用来查看文本文件中空格问题的好办法。
4.2.2 特殊字符
-----------------------------------------------------------------------------------------------------------------------------------------
在正则表达式模式中使用文本字符时,有些事情值得注意。在正则表达式中定义文本字符时有一些特例。有些字符在正则表达式中有特别的含义。
正则表达式识别的特殊字符包括:
.*[]^${}\+?|()
要记住不能在文本模式中单独使用这些字符。
如果要用某个特殊字符作为文本字符,就必须转义。在转义特殊字符时,需要在它前面加一个特殊字符来告诉正则表达式引擎应该将接下来的字符当作普通的
文本字符。这个特殊字符就是反斜线(\)。
示例: 如果要查找文本中的美元符,要在它前面加个反斜线 \
[devalone@devalone 20]$ cat data2
The cost is $4.00
[devalone@devalone 20]$ sed -n '/\$/p' data2
The cost is $4.00
由于反斜线是特殊字符,如果要在正则表达式模式中使用它,必须对其转义,这样就产生了两个反斜线。
示例:
[devalone@devalone 20]$ echo "\ is a special character" | sed -n '/\\/p'
\ is a special character
尽管正斜线不是正则表达式的特殊字符,但如果它出现在 sed 编辑器或 gawk 程序的正则表达式中,会得到一个错误。
[devalone@devalone 20]$ echo "3 / 2" | sed -n '///p'
sed: -e expression #1, char 2: No previous regular expression
要使用正斜线,也需要进行转义。
示例:
[devalone@devalone 20]$ echo "3 / 2" | sed -n '/\//p'
3 / 2
现在 sed 编辑器能正确解释正则表达式模式了。
4.2.3 锚字符
-----------------------------------------------------------------------------------------------------------------------------------------
默认情况下,当指定一个正则表达式模式时,只要模式出现在数据流中的任何地方,它就能匹配。有两个特殊字符可以将模式锁定在数据流中的行首或行尾。
■ 锁定在行首
-----------------------------------------------------------------------------------------------------------------------------------------
脱字符(^)定义从数据流中文本行的行首开始的模式。如果模式出现在行首之外的位置,正则表达式模式则无法匹配。
要用脱字符,就必须将它放在正则表达式中指定的模式前面。
示例:
[devalone@devalone 20]$ echo "The book store" | sed -n '/^book/p'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "Books are great" | sed -n '/^Book/p'
Books are great
脱字符会在每个由换行符决定的新数据行的行首检查模式。
示例:
[devalone@devalone 20]$ cat data3
This is a test line
this is another test line.
A line that tests this feature.
Yet more testing of this
[devalone@devalone 20]$ sed -n '/^this/p' data3
this is another test line.
[devalone@devalone 20]$
只要模式出现在新行的行首,脱字符就能够发现它。
如果将脱字符放到模式开头之外的其他位置,那么它就跟普通字符一样,不再是特殊字符了:
[devalone@devalone 20]$ echo "This ^ is a test" | sed -n '/s ^/p'
This ^ is a test
由于脱字符出现在正则表达式模式的尾部,sed 编辑器会将它当作普通字符来匹配。
■ 锁定在行尾
-----------------------------------------------------------------------------------------------------------------------------------------
跟在行首查找模式相反的就是在行尾查找。特殊字符美元符($)定义了行尾锚点。将这个特殊字符放在文本模式之后来指明数据行必须以该文本模式结尾。
示例:
[devalone@devalone 20]$ echo "This is a good book" | sed -n '/book$/p'
This is a good book
[devalone@devalone 20]$ echo "This book is a good" | sed -n '/book$/p'
[devalone@devalone 20]$
使用结尾文本模式的问题在于必须要留意到底要查找什么。
示例:
[devalone@devalone 20]$ echo "There are a lot of good books" | sed -n '/book$/p'
[devalone@devalone 20]$
将行尾的单词 book 改成复数形式,就意味着它不再匹配正则表达式模式了,尽管 book 仍然在数据流中。要想匹配,文本模式必须是行的最后一部分。
■ 组合锚点
-----------------------------------------------------------------------------------------------------------------------------------------
在一些常见情况下,可以在同一行中将行首锚点和行尾锚点组合在一起使用。
在第一种情况中,假定要查找只含有特定文本模式的数据行。
示例:
[devalone@devalone 20]$ cat data4
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
[devalone@devalone 20]$ sed -n '/^this is a test$/p' data4
this is a test
sed 忽略了那些不单单包含指定的文本的行。
第二种情况乍一看可能有些怪异,但极其有用。将两个锚点直接组合在一起,之间不加任何文本,这样过滤出数据流中的空白行。
示例:
[devalone@devalone 20]$ cat data5
This is one test line
This is another test line
[devalone@devalone 20]$ sed '/^$/d' data5
This is one test line
This is another test line
定义的正则表达式模式会查找行首和行尾之间什么都没有的那些行。由于空白行在两个换行符之间没有文本,刚好匹配了正则表达式模式。sed 编辑器用
删除命令 d 来删除匹配该正则表达式模式的行,因此删除了文本中的所有空白行。这是从文档中删除空白行的有效方法。
4.2.4 句点字符 (.)
-----------------------------------------------------------------------------------------------------------------------------------------
特殊字符句点字符 (.) 用来匹配除换行符之外的任意单个字符。它必须匹配一个字符,如果在句点字符的位置没有字符,那么模式就不成立。
示例:
[devalone@devalone 20]$ cat data6
This is a test of line
THe cat is sleeping
Tha is a very nice hat
This test is at line four
at ten o'clock we'll go home
[devalone@devalone 20]$ sed -n '/.at/p' data6
THe cat is sleeping
Tha is a very nice hat
This test is at line four
第一行无法匹配,而第二行和第三行就可以。第四行有点复杂。注意,我们匹配了 at,但在 at 前面并没有任何字符来匹配点号字符。其实是有的!在正则
表达式中,空格也是字符,因此 at 前面的空格刚好匹配了该模式。第五行证明了这点,将 at 放在行首就不会匹配该模式了。
4.2.5 字符组 []
-----------------------------------------------------------------------------------------------------------------------------------------
句点特殊字符在匹配某个字符位置上的任意字符时很有用。但如果想要限定待匹配的具体字符呢?在正则表达式中,这称为字符组(character class)。
可以定义用来匹配文本模式中某个位置的一组字符。如果字符组中的某个字符出现在了数据流中,那它就匹配了该模式。
使用方括号 [] 来定义一个字符组。方括号中包含所有希望出现在该字符组中的字符。然后可以在模式中使用整个组,就跟使用其他通配符一样。
下面是个创建字符组的例子:
[devalone@devalone 20]$ cat data6
This is a test of line
THe cat is sleeping
Tha is a very nice hat
This test is at line four
at ten o'clock we'll go home
[devalone@devalone 20]$ sed -n '/[ch]at/p' data6
THe cat is sleeping
Tha is a very nice hat
这里用到的数据文件和句点特殊字符例子中的一样,但得到的结果却不一样。这次我们成功滤掉了只包含单词 at 的行。匹配这个模式的单词只有 cat 和
hat。还要注意以 at 开头的行也没有匹配。字符组中必须有个字符来匹配相应的位置。
在不太确定某个字符的大小写时,字符组会非常有用。
示例:
[devalone@devalone 20]$ echo "Yes" | sed -n '/[Yy]es/p'
Yes
[devalone@devalone 20]$ echo "yes" | sed -n '/[Yy]es/p'
yes
可以在单个表达式中用多个字符组。
示例:
[devalone@devalone 20]$ echo "Yes" | sed -n '/[Yy]es/p'
Yes
[devalone@devalone 20]$ echo "yes" | sed -n '/[Yy]es/p'
yes
[devalone@devalone 20]$ echo "yes" | sed -n '/[Yy][Ee][Ss]/p'
yes
[devalone@devalone 20]$ echo "Yes" | sed -n '/[Yy][Ee][Ss]/p'
Yes
[devalone@devalone 20]$ echo "yeS" | sed -n '/[Yy][Ee][Ss]/p'
yeS
正则表达式使用了 3 个字符组来涵盖了3个字符位置含有大小写的情况。
字符组不必只含有字母,也可以在其中使用数字。
示例:
[devalone@devalone 20]$ cat data7
This line doesn't contain a number.
This line has 1 number on it
This line a number 2 on it
This line has number 4 on it
[devalone@devalone 20]$ sed -n '/[0123]/p' data7
This line has 1 number on it
This line a number 2 on it
这个正则表达式模式匹配了任意含有数字0、1、2或3 的行。含有其他数字以及不含有数字的行都会被忽略掉。
可以将字符组组合在一起,以检查数字是否具备正确的格式,比如电话号码和邮编。但当尝试匹配某种特定格式时,必须小心。这里有个匹配邮编出错的例子
示例:
[devalone@devalone 20]$ cat data8
60633
46201
223001
4353
22203
[devalone@devalone 20]$ sed -n '
> /[0123456789][0123456789][0123456789][0123456789][0123456789]/p
> ' data8
60633
46201
223001
22203
它成功过滤掉了不可能是邮编的那些过短的数字,因为最后一个字符组没有字符可匹配。但它也通过了那个六位数,尽管只定义了5个字符组。
正则表达式模式可见于数据流中文本的任何位置。经常有匹配模式的字符之外的其他字符。如果要确保只匹配五位数,就必须将匹配的字符和其他字符分开,
要么用空格,要么像这个例子中这样,指明它们就在行首和行尾。
示例:
[devalone@devalone 20]$ sed -n '
/^[0123456789][0123456789][0123456789][0123456789][0123456789]$/p
' data8
60633
46201
22203
字符组的一个极其常见的用法是解析拼错的单词,比如用户表单输入的数据。可以创建正则表达式来接受数据中常见的拼写错误。
示例:
[devalone@devalone 20]$ cat data9
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.
[devalone@devalone 20]$ sed -n '
> /maint[ea]n[ae]nce/p
> /sep[ea]r[ea]te/p
> ' data9
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。
4.2.6 排除型字符组 [^]
-----------------------------------------------------------------------------------------------------------------------------------------
在正则表达式模式中,也可以反转字符组的作用。可以寻找组中没有的字符,而不是去寻找组中含有的字符。只要在字符组 [] 的开头加个脱字符 [^]。
示例:
[devalone@devalone 20]$ cat data6
This is a test of line
THe cat is sleeping
Tha is a very nice hat
This test is at line four
at ten o'clock we'll go home
[devalone@devalone 20]$ sed -n '/[^ch]at/p' data6
This test is at line four
通过排除型字符组,正则表达式模式会匹配 c或h 之外的任何字符以及文本模式。由于空格字符属于这个范围,它通过了模式匹配。但即使是排除,字符组
仍然必须匹配一个字符,所以以 at 开头的行仍然未能匹配模式。
4.2.7 区间 [-]
-----------------------------------------------------------------------------------------------------------------------------------------
可能注意到了,之前演示邮编的例子的时候,必须在每个字符组中列出所有可能的数字,这实在有点麻烦。好在有一种便捷的方法可以让人免受这番劳苦。
可以用单破折线符号在字符组中表示字符区间。只需要指定区间的第一个字符、连字符以及区间的最后一个字符就行了。根据 Linux 系统采用的字符集,
正则表达式会包括此区间内的任意字符。
现在可以通过指定数字区间来简化邮编的例子:
[devalone@devalone 20]$ sed -n '/^[0-9][0-9][0-9][0-9][0-9]$/p' data8
60633
46201
22203
这样可是节省了不少的键盘输入,每个字符组都会匹配 0~9 的任意数字。如果字母出现在数据中的任何位置,这个模式都将不成立。
示例:
[devalone@devalone 20]$ echo "a8392" | sed -n '/^[0-9][0-9][0-9][0-9][0-9]$/p'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "1839a" | sed -n '/^[0-9][0-9][0-9][0-9][0-9]$/p'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "18a32" | sed -n '/^[0-9][0-9][0-9][0-9][0-9]$/p'
[devalone@devalone 20]$
同样的方法也适用于字母。
示例:
[devalone@devalone 20]$ sed -n '/[c-h]at/p' data6
THe cat is sleeping
Tha is a very nice hat
新的模式 [c-h]at 匹配了首字母在字母 c 和字母 h 之间的单词。这种情况下,只含有单词 at 的行将无法匹配该模式。
还可以在单个字符组指定多个不连续的区间。
示例:
[devalone@devalone 20]$ sed -n '/[a-ch-m]at/p' data6
THe cat is sleeping
Tha is a very nice hat
该字符组允许区间 a~c、h~m 中的字母出现在 at 文本前,但不允许出现 d~g 的字母。
示例:
[devalone@devalone 20]$ echo "I'm getting too fat." | sed -n '/[a-ch-m]at/p'
[devalone@devalone 20]$
该模式不匹配 fat 文本,因为它没在指定的区间。
4.2.8 特殊的字符组
-----------------------------------------------------------------------------------------------------------------------------------------
除了定义自己的字符组外,BRE 还包含了一些特殊的字符组,可用来匹配特定类型的字符。下表列出 BRE 特殊的字符组。
BRE 特殊字符组
+---------------+---------------------------------------------------------------------------------
| 组 | 描 述
+---------------+---------------------------------------------------------------------------------
| [[:alpha:]] | 匹配任意字母字符,不管是大写还是小写
+---------------+---------------------------------------------------------------------------------
| [[:alnum:]] | 匹配任意字母数字字符0~9、A~Z或a~z
+---------------+---------------------------------------------------------------------------------
| [[:blank:]] | 匹配空格或制表符
+---------------+---------------------------------------------------------------------------------
| [[:digit:]] | 匹配0~9之间的数字
+---------------+---------------------------------------------------------------------------------
| [[:lower:]] | 匹配小写字母字符a~z
+---------------+---------------------------------------------------------------------------------
| [[:print:]] | 匹配任意可打印字符
+---------------+---------------------------------------------------------------------------------
| [[:punct:]] | 匹配标点符号
+---------------+---------------------------------------------------------------------------------
| [[:space:]] | 匹配任意空白字符:空格、制表符、NL、FF、VT和CR
+---------------+---------------------------------------------------------------------------------
| [[:upper:]] | 匹配任意大写字母字符A~Z
+---------------+---------------------------------------------------------------------------------
可以在正则表达式模式中将特殊字符组像普通字符组一样使用。
示例:
[devalone@devalone 20]$ echo "abc" | sed -n '/[[:digit:]]/p'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "abc" | sed -n '/[[:alpha:]]/p'
abc
[devalone@devalone 20]$ echo "abc123" | sed -n '/[[:alpha:]]/p'
abc123
[devalone@devalone 20]$ echo "this is, a test" | sed -n '/[[:punct:]]/p'
this is, a test
[devalone@devalone 20]$ echo "this is a test" | sed -n '/[[:punct:]]/p'
[devalone@devalone 20]$
使用特殊字符组可以很方便地定义区间。可以用[[:digit:]]来代替区间[0-9]。
4.2.9 星号 (*)
-----------------------------------------------------------------------------------------------------------------------------------------
在字符后面放置星号表明该字符必须在匹配模式的文本中出现 0 次或多次。
示例:
[devalone@devalone 20]$ echo "ik" | sed -n '/ie*k/p'
ik
[devalone@devalone 20]$ echo "iek" | sed -n '/ie*k/p'
iek
[devalone@devalone 20]$ echo "ieek" | sed -n '/ie*k/p'
ieek
[devalone@devalone 20]$ echo "ieeek" | sed -n '/ie*k/p'
ieeek
这个模式符号广泛用于处理有常见拼写错误或在不同语言中有拼写变化的单词。例如,如果需要写个可能用在美式或英式英语中的脚本,可以这么写:
示例:
[devalone@devalone 20]$ echo "I'm getting a color TV" | sed -n '/colou*r/p'
I'm getting a color TV
[devalone@devalone 20]$ echo "I'm getting a colour TV" | sed -n '/colou*r/p'
I'm getting a colour TV
模式中的 u* 表明字母u可能出现或不出现在匹配模式的文本中。
类似地,如果知道一个单词经常被拼错,可以用星号来允许这种错误。
示例:
[devalone@devalone 20]$ echo "I ate a potatoe with my lunch." | sed -n '/potatoe*/p'
I ate a potatoe with my lunch.
[devalone@devalone 20]$ echo "I ate a potato with my lunch." | sed -n '/potatoe*/p'
I ate a potato with my lunch.
在可能出现的额外字母后面放个星号将允许接受拼错的单词。
另一个方便的特性是将句点特殊字符和星号特殊字符组合起来。这个组合能够匹配任意数量的任意字符。它通常用在数据流中两个可能相邻或不相邻的文本
字符串之间。
示例:
[devalone@devalone 20]$ echo "this is a regular pattern expression" | sed -n '
> /regular.*expression/p'
this is a regular pattern expression
可以使用这个模式轻松查找可能出现在数据流中文本行内任意位置的多个单词。
星号还能用在字符组上。它允许指定可能在文本中出现多次的字符组或字符区间。
示例:
[devalone@devalone 20]$ echo "bt" | sed -n '/b[ae]*t/p'
bt
[devalone@devalone 20]$ echo "bat" | sed -n '/b[ae]*t/p'
bat
[devalone@devalone 20]$ echo "bet" | sed -n '/b[ae]*t/p'
bet
[devalone@devalone 20]$ echo "btt" | sed -n '/b[ae]*t/p'
btt
[devalone@devalone 20]$ echo "baat" | sed -n '/b[ae]*t/p'
baat
[devalone@devalone 20]$ echo "baaeeet" | sed -n '/b[ae]*t/p'
baaeeet
[devalone@devalone 20]$ echo "baaeeeat" | sed -n '/b[ae]*t/p'
baaeeeat
[devalone@devalone 20]$ echo "baakeeet" | sed -n '/b[ae]*t/p'
[devalone@devalone 20]$
只要 a和 e字符以任何组合形式出现在b和t字符之间(就算完全不出现也行),模式就能够匹配。如果出现了字符组之外的字符,该模式匹配就会不成立。
4.3 扩展正则表达式
-----------------------------------------------------------------------------------------------------------------------------------------
POSIX ERE 模式包括了一些可供 Linux 应用和工具使用的额外符号。gawk 程序能够识别 ERE 模式,但 sed 编辑器不能。
NOTE:
-------------------------------------------------------------------------------------------------------------------------------------
记住,sed 编辑器和 gawk 程序的正则表达式引擎之间是有区别的。gawk 程序可以使用大多数扩展正则表达式模式符号,并且能提供一些额外过滤功能,
而这些功能都是 sed 编辑器所不具备的。但正因为如此,gawk 程序在处理数据流时通常才比较慢。
4.3.1 问号 (?)
-----------------------------------------------------------------------------------------------------------------------------------------
问号类似于星号,不过有点细微的不同。问号表明前面的字符可以出现 0 次或 1 次,但只限于此。它不会匹配多次出现的字符。
示例:
[devalone@devalone 20]$ echo "bt" | gawk '/be?t/{print $0}'
bt
[devalone@devalone 20]$ echo "bet" | gawk '/be?t/{print $0}'
bet
[devalone@devalone 20]$ echo "beet" | gawk '/be?t/{print $0}'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "beeeet" | gawk '/be?t/{print $0}'
[devalone@devalone 20]$
如果字符 e 并未在文本中出现,或者它只在文本中出现了 1 次,那么模式会匹配。
与星号一样,可以将问号和字符组一起使用。
示例:
[devalone@devalone 20]$ echo "bt" | gawk '/b[ae]?t/{print $0}'
bt
[devalone@devalone 20]$ echo "bat" | gawk '/b[ae]?t/{print $0}'
bat
[devalone@devalone 20]$ echo "bot" | gawk '/b[ae]?t/{print $0}'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "bet" | gawk '/b[ae]?t/{print $0}'
bet
[devalone@devalone 20]$ echo "baet" | gawk '/b[ae]?t/{print $0}'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "beat" | gawk '/b[ae]?t/{print $0}'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "beet" | gawk '/b[ae]?t/{print $0}'
[devalone@devalone 20]$
如果字符组中的字符出现了 0 次或 1 次,模式匹配就成立。但如果两个字符都出现了,或者其中一个字符出现了2次,模式匹配就不成立。
4.3.2 加号 (+)
-----------------------------------------------------------------------------------------------------------------------------------------
加号是类似于星号的另一个模式符号,但跟问号也有不同。加号 + 表明前面的字符可以出现 1 次或多次,但必须至少出现 1 次。如果该字符没有出现,那么
模式就不会匹配。
示例:
[devalone@devalone 20]$ echo "beeet" | gawk '/be+t/{print $0}'
beeet
[devalone@devalone 20]$ echo "beet" | gawk '/be+t/{print $0}'
beet
[devalone@devalone 20]$ echo "bet" | gawk '/be+t/{print $0}'
bet
[devalone@devalone 20]$ echo "bt" | gawk '/be+t/{print $0}'
[devalone@devalone 20]$
如果字符e没有出现,模式匹配就不成立。
加号同样适用于字符组,与星号和问号的使用方式相同。
示例:
[devalone@devalone 20]$ echo "bt" | gawk '/b[ae]+t/{print $0}'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "bet" | gawk '/b[ae]+t/{print $0}'
bet
[devalone@devalone 20]$ echo "beat" | gawk '/b[ae]+t/{print $0}'
beat
[devalone@devalone 20]$ echo "beet" | gawk '/b[ae]+t/{print $0}'
beet
[devalone@devalone 20]$ echo "beeat" | gawk '/b[ae]+t/{print $0}'
beeat
如果字符组中定义的任一字符出现了,文本就会匹配指定的模式。
4.3.3 使用花括号 ({})
-----------------------------------------------------------------------------------------------------------------------------------------
ERE 中的花括号允许为可重复的正则表达式指定一个上限。这通常称为间隔(interval)。
可以用两种格式来指定区间:
□ m:正则表达式准确出现 m 次。
□ m, n:正则表达式至少出现 m 次,至多 n 次。
这个特性可以精确调整字符或字符集在模式中具体出现的次数。
NOTE:
-------------------------------------------------------------------------------------------------------------------------------------
默认情况下,gawk 程序不会识别正则表达式间隔。必须指定 gawk 程序的--re-interval 命令行选项才能识别正则表达式间隔。
示例:
[devalone@devalone 20]$ echo "bt" | gawk --re-interval '/be{1}t/{print $0}'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "bet" | gawk --re-interval '/be{1}t/{print $0}'
bet
[devalone@devalone 20]$ echo "beet" | gawk --re-interval '/be{1}t/{print $0}'
[devalone@devalone 20]$
通过指定间隔为 1,限定了该字符在匹配模式的字符串中出现的次数。如果该字符出现多次,模式匹配就不成立。
很多时候,同时指定下限和上限也很方便。
示例:
[devalone@devalone 20]$ echo "bt" | gawk --re-interval '/be{1,2}t/{print $0}'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "bet" | gawk --re-interval '/be{1,2}t/{print $0}'
bet
[devalone@devalone 20]$ echo "beet" | gawk --re-interval '/be{1,2}t/{print $0}'
beet
[devalone@devalone 20]$ echo "beeet" | gawk --re-interval '/be{1,2}t/{print $0}'
[devalone@devalone 20]$
在这个例子中,字符 e 可以出现 1次或 2次,这样模式就能匹配;否则,模式无法匹配。
间隔模式匹配同样适用于字符组。
示例:
[devalone@devalone 20]$ echo "bt" | gawk --re-interval '/b[ae]{1,2}t/{print $0}'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "bat" | gawk --re-interval '/b[ae]{1,2}t/{print $0}'
bat
[devalone@devalone 20]$ echo "bet" | gawk --re-interval '/b[ae]{1,2}t/{print $0}'
bet
[devalone@devalone 20]$ echo "beat" | gawk --re-interval '/b[ae]{1,2}t/{print $0}'
beat
[devalone@devalone 20]$ echo "beet" | gawk --re-interval '/b[ae]{1,2}t/{print $0}'
beet
[devalone@devalone 20]$ echo "beeat" | gawk --re-interval '/b[ae]{1,2}t/{print $0}'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "baeet" | gawk --re-interval '/b[ae]{1,2}t/{print $0}'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "baeaet" | gawk --re-interval '/b[ae]{1,2}t/{print $0}'
[devalone@devalone 20]$
如果字母a或e在文本模式中只出现了1~2次,则正则表达式模式匹配;否则,模式匹配失败。
4.3.4 管道符号 (|)
-----------------------------------------------------------------------------------------------------------------------------------------
管道符号允许在检查数据流时,用逻辑 OR 方式指定正则表达式引擎要用的两个或多个模式。如果任何一个模式匹配了数据流文本,文本就通过测试。如果
没有模式匹配,则数据流文本匹配失败。
使用管道符号的格式如下:
expr1|expr2|...
示例:
[devalone@devalone 20]$ echo "The cat is asleep" | gawk '/cat|dog/{print $0}'
The cat is asleep
[devalone@devalone 20]$ echo "The dog is asleep" | gawk '/cat|dog/{print $0}'
The dog is asleep
[devalone@devalone 20]$ echo "The sheep is asleep" | gawk '/cat|dog/{print $0}'
[devalone@devalone 20]$
这个例子会在数据流中查找正则表达式 cat 或 dog。正则表达式和管道符号之间不能有空格,否则它们也会被认为是正则表达式模式的一部分。
管道符号两侧的正则表达式可以采用任何正则表达式模式(包括字符组)来定义文本。
示例:
[devalone@devalone 20]$ echo "He has a hat." | gawk '/[ch]at|dog/{print $0}'
He has a hat.
这个例子会匹配数据流文本中的 cat、hat或dog。
4.3.5 表达式分组 ()
-----------------------------------------------------------------------------------------------------------------------------------------
正则表达式模式也可以用圆括号()进行分组。当将正则表达式模式分组时,该组会被视为一个标准字符。可以像对普通字符一样给该组使用特殊字符。
示例:
[devalone@devalone 20]$ echo "Sat" | gawk '/Sat(urday)?/{print $0}'
Sat
[devalone@devalone 20]$ echo "Saturday" | gawk '/Sat(urday)?/{print $0}'
Saturday
结尾的 urday分组以及问号,使得模式能够匹配完整的 Saturday或缩写 Sat。
将分组和管道符号一起使用来创建可能的模式匹配组是很常见的做法。
示例:
[devalone@devalone 20]$ echo "cat" | gawk '/(c|b)a(b|t)/{print $0}'
cat
[devalone@devalone 20]$ echo "cab" | gawk '/(c|b)a(b|t)/{print $0}'
cab
[devalone@devalone 20]$ echo "bat" | gawk '/(c|b)a(b|t)/{print $0}'
bat
[devalone@devalone 20]$ echo "bab" | gawk '/(c|b)a(b|t)/{print $0}'
bab
[devalone@devalone 20]$ echo "tab" | gawk '/(c|b)a(b|t)/{print $0}'
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "tac" | gawk '/(c|b)a(b|t)/{print $0}'
[devalone@devalone 20]$
模式(c|b)a(b|t)会匹配第一组中字母的任意组合以及第二组中字母的任意组合。
4.4 正则表达式实战
-----------------------------------------------------------------------------------------------------------------------------------------
4.4.1 目录文件计数
-----------------------------------------------------------------------------------------------------------------------------------------
写个 shell 脚本,对 PATH 环境变量中定义的目录里的可执行文件进行计数。
首先将 PATH 变量解析成单独的目录名:
[devalone@devalone 20]$ echo $PATH
/usr/local/protoc/bin:/home/devalone/programs/apache-tomcat-8.5.11/bin:/usr/local/apache/maven/bin:/usr/local/apache/ant/bin
:/usr/java/latest/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/opt/hadoop/hadoop/hadoop/bin:/opt/hadoop/hadoop/hadoop/sbin
:/home/devalone/.local/bin:/home/devalone/bin:/home/devalone/programs/jetty/bin
:/home/devalone/workspaces/dearall/dearall/runtime/local/bin:/home/devalone/bin/repo
要意识到 PATH 中的每个路径由冒号分隔。要获取可在脚本中使用的目录列表,就必须用空格来替换冒号。sed 编辑器用一条简单表达式就能完成替换工作。
[devalone@devalone 20]$ echo $PATH | sed 's/:/ /g'
分离出目录之后,就可以使用标准 for 语句中来遍历每个目录:
mypath=$(echo $PATH | sed 's/:/ /g')
for directory in $mypath
do
...
done
一旦获得了单个目录,就可以用 ls 命令来列出每个目录中的文件,并用另一个 for 语句来遍历每个文件,为文件计数器增值。
最终脚本如下:
[devalone@devalone 20]$ cat countfiles.sh
#!/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
运行:
[devalone@devalone 20]$ ./countfiles.sh
/usr/local/protoc/bin - 1
/home/devalone/programs/apache-tomcat-8.5.11/bin - 23
/usr/local/apache/maven/bin - 6
/usr/local/apache/ant/bin - 13
/usr/java/latest/bin - 49
/usr/local/bin - 4
/usr/bin - 1909
/usr/local/sbin - 0
/usr/sbin - 687
/opt/hadoop/hadoop/hadoop/bin - 11
/opt/hadoop/hadoop/hadoop/sbin - 29
/home/devalone/.local/bin - 0
/home/devalone/bin - 1
/home/devalone/programs/jetty/bin - 0
/home/devalone/workspaces/dearall/dearall/runtime/local/bin - 2
/home/devalone/bin/repo - 1
4.4.2 验证电话号码
-----------------------------------------------------------------------------------------------------------------------------------------
电话号码有几种常见的形式:
(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 程序来使用该正则表达式,然后用这个脚本来过滤的电话薄。
脚本如下:
[devalone@devalone 20]$ cat isphone.sh
#!/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}'
运行:
[devalone@devalone 20]$ echo "317-555-1234" | ./isphone.sh
317-555-1234
[devalone@devalone 20]$ echo "000-555-1234" | ./isphone.sh
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "312-555-1234" | ./isphone.sh
312-555-1234
或者将含有电话号码的整个文件重定向到脚本来过滤掉无效的号码:
[devalone@devalone 20]$ cat phonelist
000-000-0000
123-456-7890
212-555-1234
(317)555-1234
(202) 555-1234
33523
1234567890
234.123.4567
[devalone@devalone 20]$ cat phonelist | ./isphone.sh
212-555-1234
(317)555-1234
(202) 555-1234
234.123.4567
只有匹配该正则表达式模式的有效电话号码才会出现。
4.4.3 解析邮件地址
-----------------------------------------------------------------------------------------------------------------------------------------
邮件地址的基本格式为:
username@hostname
username 值可用字母数字字符以及以下特殊字符:
□ 点号 (.)
□ 连字符 (-)
□ 加号 (+)
□ 下划线 (_)
在有效的邮件用户名中,这些字符可能以任意组合形式出现。
邮件地址的 hostname 部分由一个或多个域名和一个服务器名组成。服务器名和域名也必须遵照严格的命名规则,只允许字母数字字符以及以下特殊字符:
□ 点号 (.)
□ 连字符 (-)
□ 下划线 (_)
服务器名和域名都用点分隔,先指定服务器名,紧接着指定子域名,最后是后面不带点号的顶级域名。
从左侧开始构建这个正则表达式模式。用户名中可以有多个有效字符:
^([a-zA-Z0-9_\-\.\+]+)@
这个分组指定了用户名中允许的字符,加号表明必须有至少一个字符。下一个字符很明显是 @,没什么意外的。
hostname 模式使用同样的方法来匹配服务器名和子域名:
([a-zA-Z0-9_\-\.]+)
这个模式可以匹配如下文本:
server
server.subdomain
server.subdomain.subdomain
对于顶级域名,有一些特殊的规则。顶级域名只能是字母字符,必须不少于二个字符(国家或地区代码中使用),并且长度上不得超过五个字符。下面就是
顶级域名用的正则表达式模式:
\.([a-zA-Z]{2,5})$
将整个模式放在一起会生成如下模式:
^([a-zA-Z0-9_\-\.\+]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$
这个模式会从数据列表中过滤掉那些格式不正确的邮件地址。现在可以创建脚本来实现这个正则表达式:
[devalone@devalone 20]$ cat isemail.sh
#!/bin/bash
# script to filter out bad emails
gawk --re-interval '/^([a-zA-Z0-9_\-\.\+]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$/{print $0}'
运行:
[devalone@devalone 20]$ echo "[email protected]" | ./isemail.sh
[email protected]
[devalone@devalone 20]$ echo "[email protected]." | ./isemail.sh
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "[email protected]" | ./isemail.sh
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "rich@here-now" | ./isemail.sh
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "[email protected]" | ./isemail.sh
[email protected]
[devalone@devalone 20]$ echo "[email protected]" | ./isemail.sh
[email protected]
[devalone@devalone 20]$ echo "deva/[email protected]" | ./isemail.sh
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "deva#[email protected]" | ./isemail.sh
[devalone@devalone 20]$
[devalone@devalone 20]$ echo "deva*[email protected]" | ./isemail.sh
[devalone@devalone 20]$
系列目录:
Linux shell 脚本编程-高级篇 (一)
Linux shell 脚本编程-高级篇 (二)
Linux shell 脚本编程-高级篇 (三)
Linux shell 脚本编程-高级篇 (四)
Linux shell 脚本编程-高级篇 (五)
Linux shell 脚本编程-高级篇 (六)
Linux shell 脚本编程-高级篇 (七)
-----------------------------------------------------------------------------------------------------------------------------------------
参考:
《Linux 命令行与 shell 脚本编程大全》 第 3 版 —— 2016.8(美)Richard Blum Cristine Bresnahan