D16:常见任务和基本工具之六
正则表达式
目录:
什么是正则表达式
简单来说,正则表达式是用于识别文本中模式的象征性符号。
在某些方面,类似于shell匹配文件名和路径名的通配符方法,但规则要多很多。
不是所有的正则表达式都是相同的,在不同的工具和编程语言之间略有不同。
本节只讨论POSIX标准中描述的正则表达式。许多编程语言(例如Perl)使用的符号集更大更丰富。
grep
grep是处理正则表达式的主要程序。“grep”这个名字实际上来源于“global regular expression print”(全局正则表达式打印)。
本质上,grep在文本文件中搜索与指定正则表达式匹配的引用文本,并将包含匹配项的任何行输出到标准输出。
grep格式:
grep [options] regex [file ...]
下表列出grep的常用选项:
选项 |
长选项 |
说明 |
-i |
--ignore-case |
忽略。不区分大小写。 |
-v |
--invert-match |
反转匹配。
通常,grep列出包含匹配项的行。此选项列出不包含匹配项的行。 |
-c |
--count |
显示匹配数量,而不是显示匹配的行。
如果还指定了-v选项,则显示不匹配的数。 |
-l |
--files-with-matches |
显示包含匹配项的每个文件的名称,而不是行本身。 |
-L |
--files-without-match |
显示不包含匹配项的每个文件的名称。 |
-n |
--line-number |
在每个匹配行前面加上文件中的行号。 |
-h |
--no-filename |
对于多文件搜索,不输出文件名。 |
以下是一个关于grep的实验:
首先创建一些文本文件进行搜索
[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
dirlist-bin.txt dirlist-sbin.txt dirlist-usr-sbin.txt dirlist-usr-bin.txt
对文件列表执行以下简单搜索:
[me@linuxbox ~]$ grep bzip dirlist*.txt
dirlist-bin.txt:bzip2
dirlist-bin.txt:bzip2recover
以上例子中,grep在所有列出的文件中搜索字符串bzip,并在文件dirlist-bin.txt中找到两个匹配项。
如果仅对包含匹配项的文件列表感兴趣,而不是对匹配项本身感兴趣,那么可以指定-l选项:
[me@linuxbox ~]$ grep -l bzip dirlist*.txt
dirlist-bin.txt
相反,如果只想查看不包含匹配项的文件列表,可以使用以下命令:
[me@linuxbox ~]$ grep -L bzip dirlist*.txt
dirlist-sbin.txt
dirlist-usr-bin.txt
dirlist-usr-sbin.txt
元字符和文字
以上例子中grep命令后面跟的就是个简单的正则表达式bzip,它的意思是:
只有当文件中的某行至少包含四个字符,并且在该行的某个地方字符b、z、i、p按顺序排列且中间没有其他字符时,才会发生匹配。
字符串bzip中的字符都是“文字”字符,可用来直接匹配。
正则表达式还可以包含用于指定更复杂匹配的元字符:
^ $ . [ ] { } - ? * + ( ) | \
所有其他字符都被视为文字。
反斜杠在少数情况下用于创建元序列,也可以转义元字符并将其视为文字而不是解释为元字符。
许多正则表达式元字符也是在执行扩展时对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,它们也会被匹配,因为文件扩展名中的句点字符也会与“any character”匹配。
锚
在正则表达式中,插入符号(caret, ^)和美元符号($)被视为锚定符。这意味着只有在正则表达式位于行的开头(^)或结尾($)时,它们才会导致匹配。
[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
unzip
zip
[me@linuxbox ~]$ grep -h '^zip$' dirlist*.txt
zip
注意,'^zip'、'zip$'、'^zip$'都是匹配三个字符。如果正则表达式是'^$'则返回空行。
一个用于填字游戏的例子:
Linux系统中有一个字典,位于/usr/share/dict中,通常名字叫words(也可能是个符号链接)。这个“字典”中每行一个单词,按字母顺序排列。
以下命令可以找出“第三个字母是j,最后一个字母是r的,五个字母的单词”:
[me@linuxbox ~]$ grep -i '^..j.rs$' /usr/share/dict/words
括号表达式和字符类
使用括号表达式可以匹配指定字符集中的单个字符。例如:
[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,前面有除了b和g以外的任何字符。
注意,取反字符集仍然需要在给定的位置上有一个字符,但该字符不能是取反字符集的成员。
插入符号字符仅当它在括号表达式中位于第一个字符时才表示取反,否则就会失去特殊意义变成普通字符。
传统字符范围
以下命令用来查找文件中以大写字母开头的行:
grep -h '^[ABCDEFGHIJKLMNOPQRSTUVWXYZ]' dirlist*.txt
但基于少打字的原则可以换成以下方法:
grep -h '^[A-Z]' dirlist*.txt
通过使用三个字符的范围表示26个字母。
以下方法匹配所有以字母和数字开头的文件名:
grep -h '^[A-Za-z0-9]' dirlist*.txt
在字符范围中,破折号是经过特殊处理的,要想在括号表达式中包含破折号,需要使其称为表达式中的第一个字符:
[me@linuxbox ~]$ grep -h '[-AZ]' dirlist*.txt
以上例子将匹配包含破折号或大写字母A或大写字母Z的文件名。
以下例子匹配第一个字符不是大小写字母和数字的文件名:
grep -h '^[^A-Za-z0-9]' dirlist*.txt
POSIX字符类
传统字符范围是处理快速指定字符集问题的一种容易理解且有效的方法。但并不总是有效。
使用grep时没有问题,但是在使用其他程序时可能会遇到问题。
在某些系统中(主要还是跟操作系统以及shell有关)我们预期以下命令生成的结果是:仅列出名称以大写字母开头的文件:
[me@linuxbox ~]$ ls /usr/sbin/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]*
/usr/sbin/MAKEFLOPPIES
/usr/sbin/NetworkManagerDispatcher
/usr/sbin/NetworkManager
但如果使用传统字符范围的方法则获取到的结果却完全不同:
[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)是控制代码(例如制表符、退格和回车符)
- 接下来的32个(32-63)包含可打印字符,包括大多数标点符号和数字0-9
- 接下来的32个(64-95)包含大写字母和几个标点符号
- 接下来的32个(96-126)包含小写字母和更多标点符号
- 最后一个(127)是删除(DELL)
基于这种安排,使用ASCII的系统使用的排序如下所示:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
这与正确的字典顺序不同:
aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ
随着Unix在美国之外普及,人们越来越需要支持在美式英语中找不到的字符。ASCII(美国信息交换标准代码)被扩展为使用完整的8位,添加了128-255,可容纳更多的语言。
为了支持这种能力,POSIX标准引入了一个称为locale的概念,可以对其进行调整,以选择特定位置所需的字符集。
可以使用以下命令查看系统的语言设置:
[me@linuxbox ~]$ echo $LANG
通过此设置,符合POSIX的应用程序将使用字典排序顺序,而不是ASCII顺序。
这解释了前面命令的行为。当按字典顺序解释时,[A-Z]的字符范围包括除小写字母a之外的所有字母字符,所以会得出以上结果。
为了解决这个问题,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(32为空格,127为删除) |
[:lower:] |
小写字母 |
[:punct:] |
标点符号。 在ASCII中,相当于[-!"#$%&'()*+,./:;<=>?@[\\\]_`{|}~] |
[:print:] |
可显示字符。 包括[:graph:]加上空格 |
[:space:] |
空白字符。 包括空格(space)、制表符(tab)、回车符(carriage return,CR)、换行符(newline)、垂直制表符(vertical tab,VT)和换页符(form feed,FF)。在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字符类可以同时用于这两方面。
回归传统的排序规则
通过更改LANG环境变量的值,可以选择让系统使用传统的(ASCII)排序顺序。比如:
[me@linuxbox ~]$ export LANG=POSIX
POSIX基础与扩展正则表达式
POSIX将正则表达式实现分为两类:
- 基本正则表达式——basic regular expressions(BRE)
- 扩展正则表达式——extended regular expressions(ERE)
区别在于元字符。使用BRE,可以识别以下元字符(其他的都被视为文字):
^ $ . [ ] *
在ERE中,添加了以下元字符(及其相关函数):
( ) { } ? + |
然而,在BRE中,如果(、)、{、}用反斜杠转义,则被视为元字符。而在ERE中,在任何元字符前面加反斜杠都会导致它们被视为文字。
接下来要讨论的功能是ERE的一部分,传统上由egrep程序执行,但GNU版本的grep -E也支持扩展正则表达式。
交替
alternation(交替)是扩展正则表达式的一个特性,它允许在一组表达式中进行匹配。
示例:
[me@linuxbox ~]$ echo "AAA" | grep AAA
AAA
[me@linuxbox ~]$ echo "BBB" | grep AAA
[me@linuxbox ~]$
[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。
注意,这是一个扩展功能,所以要加-E选项。如果使用egrep命令的话就不用这个选项了。
交替不限于两种选择。
使用()来分隔交替,可以将交替和其他正则表达式组合使用:
[me@linuxbox ~]$ grep -Eh '^(bz|gz|zip)' dirlist*.txt
这个表达式将匹配列表中所有以bz、gz、zip开头的文件名。如果去掉括号,则匹配规则变成:以bz开头、包含gz或zip的文件名。
量词
扩展正则表达式支持多种方法指定元素的匹配次数。
? —— 将一个元素匹配零次或一次
这个量词实际上意味着“使前面的元素可选”。
比如在下面正则表达式中,(转义过的)括号字符后面加上问号,表示它们将被匹配零次或一次:
[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 "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 ~]$
第三句不匹配,是因为它第一个字母不是大写,且没有最后的句号。
但是一下却匹配,因为*认为匹配0此也算匹配:
[me@linuxbox ~]$ echo "T." | grep -E '[[:upper:]][[:upper:][:lower:] ]*\.'
[me@linuxbox ~]$ T.
+ —— 将一个元素匹配一次或多次
与*类似,但是+要求至少匹配一次,上面例子中如果将*改成+,则不匹配:
[me@linuxbox ~]$ echo "T." | grep -E '[[:upper:]][[:upper:][:lower:] ]*\.'
[me@linuxbox ~]$ T.
[me@linuxbox ~]$ echo "T." | grep -E '[[:upper:]][[:upper:][:lower:] ]+\.'
[me@linuxbox ~]$
[me@linuxbox ~]$ echo "Ta." | grep -E '[[:upper:]][[:upper:][:lower:] ]+\.'
[me@linuxbox ~]$ Ta.
一下正则表达式只匹配一个或多个字母组成的行,这些字符由单个空格分隔:
^([[: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 ~]$ echo "a b c " | grep -E '^([[:alpha:]]+ ?)+$' c后面一个空格
a b c
[me@linuxbox ~]$ echo "a b c " | grep -E '^([[:alpha:]]+ ?)+$' c后面两个空格
[me@linuxbox ~]$
[me@linuxbox ~]$ echo "a b c " | grep -E '^([[:alpha:]]+ *)+$' b和c后面两个空格
a b c
{} —— 将一个元素匹配特定的次数
{}元字符用于表示所需匹配的最小和最大数量。可通过四种方式对其进行规定:
指示符 |
含义 |
{n} |
如果前面的元素正好出现n次,则匹配它 |
{n,m} |
如果出现至少n次,但不大于m次,则匹配它 |
{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 ~]$
实操中,似乎{,m}不能用,会提示:grep: invalid repetition count(s))错误
使用正则表达式
使用grep验证电话列表
示例:
[me@linuxbox ~]$ for i in {1..10}; do echo "(${RANDOM:0:3}) ${RANDOM:0:3}-${RANDOM:0:4}" >> phonelist.txt; done
以上语句在当前目录下生成一个文件phonelist.txt,该文件中包含十行,每行一个形如“(xxx) xxx-xxxx”的电话号码一样:
[me@linuxbox ~]$ cat phonelist.txt
(232) 298-2265
(624) 381-1078
(540) 126-1980
(874) 163-2885
(286) 254-2860
(292) 108-518
(129) 44-1379
(458) 273-1642
(686) 299-8268
(198) 307-2440
以上生成的数字有不正确的,使用grep验证它们:
[me@linuxbox ~]$ grep -Ev '^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$' phonelist.txt
(292) 108-518
(129) 44-1379
[me@linuxbox ~]$
-v选项生成反向匹配。
使用find查找丑陋的文件名
find命令支持基于正则表达式的测试。
在find和grep中使用正则表达式时需要记住一个重要的事项:
当某一行包含与正则表达式匹配的字符串时,grep将打印此行,而find要求路径名与正则表达式完全匹配。
示例:
[me@linuxbox ~]$ find . -regex '.*[^-_./0-9a-zA-Z].*'
表达式的两端使用.*来匹配任何字符的零个或多个实例;表达式的中间包含一组可接受路径名字符的否定括号表达式。
此例中的命令会找出当前路径下包含横杠、下划线、点、数字、(大小写)字母以外字符的文件名,比如含有空格或#或$等“非常规”字符的文件名。
使用locate搜索文件
locate程序支持基本(--regexp)和扩展(--regex)正则表达式。
示例(实操不能正确显示结果):
[me@linuxbox ~]$ locate --regex 'bin/(bz|gz|zip)'
/bin/bzcat
/bin/bzcmp
/bin/bzdiff
/bin/bzegrep
/bin/bzexe
/bin/bzfgrep
/bin/bzgrep
/bin/bzip2
/bin/bzip2recover
/bin/bzless
/bin/bzmore
/bin/gzexe
/bin/gzip
/usr/bin/zip
/usr/bin/zipcloak
/usr/bin/zipgrep
/usr/bin/zipinfo
/usr/bin/zipnote
/usr/bin/zipsplit
使用less和vim搜索文本
less和vim都使用相同的文本搜索方法。按/键进入搜索模式,然后输入正则表达式执行搜索。
vim支持基本正则表达式,所以正则表达式像以下形式:
/([0-9]\{3\} [0-9]\{3\}-[0-9]\{4\}