简介
正则表达式是用来处理文字信息的,这在类Unix系统中是很重要的。
Shell中要输入的命令是文本,脚本里写的也是文本,文件里读出来的每一行也是文本。
所以学了这个不仅仅是磨刀不误砍柴工,而是直挂云帆济沧海。
首先说一下正则表达式的适用范围,一般很多编程语言中有关于支持正则表达式的功能,比如C#中:
using System;
using System.Text.RegularExpressions;
public class Example
{
public static void Main()
{
string input = "1851 1999 1950 1905 2003";
string pattern = @"(?<=19)\d{2}\b";
foreach (Match match in Regex.Matches(input, pattern))
Console.WriteLine(match.Value);
}
}
上面代码就是C#中正则表达式引擎的使用例子。
还有就是很多命令行工具也支持正则表达式,比如我们熟知的Linux里Shell的grep命令。
那正则表达式究竟是什么?
直白的讲,正则表达式是一种符号标记,来标识文本模式。就好比用星号通配符在shell中来匹配文件名或路径名,这就是一种文本模式。而正则表达式则是一种在这简单功能上的极大扩展。
上面讲了很多不同的工具和环境下都可以使用正则表达式,但实际上不同场景下会有些微不同。
比如我们这里讨论的主要是POSIX标准的,也就是我们一般在命令行工具中使用的,类Unix系统中的。
而和Perl对比,就有很大不同,Perl里面会有更大和风丰富的正则表达式的标记。
元字符和字面字符
好,那让我们从grep命令开始。global regular expression print。
grep命令的功能是从许多文本文件中查找匹配某种文本模式的内容,然后把这行内容输出到标准输出。
通常我们直接使用固定字符串:
ls /usr/bin | grep zip
上面会输出所有包含zip这个子字符串的/usr/bin下面的文件名。
grep命令可以接受正则表达式作为参数:
grep [options] regex [file...]
关于参数详情,options的内容,请参照我的另一篇博文。
其实固定字符串bzip也是正则表达式的一种。表示文本内容匹配成功的条件是,这一行文本的内容里出现了4个连在一起的字符,依次是b,z,i 和p。
bzip这个字符串,包含的就是字面字符,是正则表达式中的一种。
字面字符就是要直接匹配的内容。
除了字面字符,正则表达式还包含一些元字符来识别更复杂的匹配条件。
包含的元字符如下: ^ $ . [ ] { } - ? * + ( ) | \
那所有其他剩下的字符都是字面字符。
比较特别的是反斜线字符,一般用来创建元字符的转义字符, 比如 \( 表示的就是字面字符 ( ,否则单独出现( 就是元字符。
需要注意的是,上面的元字符也是shell扩展时会使用的,所以为了避免shell的扩展,我们在命令行上传递包含元字符的正则表达式时,要使用单引号包含起来。
单引号里面的字符串,在shell解析命令行时,不会被扩展。
匹配模式之任意字符
第一个是点字符,或者句点字符。英文dot or period character。
grep -h '.zip' dirlist*.txt
bunzip2
bzip2
bzip2recover
结果里zip的文件名是不符合的,因为点表示任意字符,要四个字符。
而扩展名是.zip的文件,也符合要求。
匹配模式之锚
美元符号$,和脱字符/插入符 ^,作为正则表达式中的锚功能,表示匹配条件满足时要发生在行尾($)或行首(^)。
grep -h '^zip' dirlist*.txt
zip
zipcloak
....
grep -h 'zip$' dirlist*.txt
gunzip
gzip
...
grep -h '^zip&' dirlist*.txt
zip
特别的是,直接使用 ^$查找,得到的结果是空行。
填字游戏上的应用
在linux中,有个字典应用: /usr/share/dict
比如在十字填字游戏中,找某个单词,第三个字符是j,第五个字符是r的,长度为5个字符的单词:
grep -i '^..j.r$' /usr/share/dict/words
括号表达式和字符类
上面的功能,是在指定位置匹配某个字符,同样可以在某个位置匹配一组字符中的某一个。
使用括号来指定一组字符,匹配时这一组字符中的一个被包含即满足条件。
grep -h '[bg]zip' dirlist*.txt
bzip2
gzip
表示查找匹配包含bzip和gzip的内容。
在中括号内是没有元字符存在的,比较特殊的是,^(caret 脱字符)表示不包含,- (dash 横线)表示范围。
grep -h '[^bg]zip' dirlist*.txt
在dirlist开头的文本文件中,查找不包含bzip和gzip内容的行;显示行内容,不包括文件名。
注意,^只有在括号表达式的开头出现才是不包含的语义,其他情况下是普通字符。
传统的字符范围表示,在我们想指定可以匹配某个字符集,如果字符比较多,可以使用范围。
grep -h '^[ABCDEFGHIJKLMNOPQRSTUVWXYZ]' dirlist*.txt
查询所有大写字母开头的行。
可以简化为:
grep -h '^[A-Z]' dirlist*.txt
可以一次使用多个范围:
grep -h '^[A-za-z0-9]' dirlist*.txt
查询字母和数字开头的行内容。
如果需要匹配字符'-'本身,可以下面这样写:
grep -h '[-AZ]' dirlist*.txt
表示匹配 -, A , Z这三个字符中的某一个。
POSIX Character Classes
随着Unix/Linux的普及,OS不仅要支持U.S. English还要支持其他语言,所以对ASCII语言进行了扩展,扩展后增加到8位。
随之而来,POSIX标准也进入了一个概念:locale。针对不同地区可以选择不同的字符集。
echo $LANG
en_US.UTF-8
所以使用字符范围时,就会受到影响。
为了临时绕过这个问题,POSIX标准定义了一系列字符类,来提供更方便的字符范围选择。
Character Class & Description
[:alnum:] The alphanumeric characters. In ASCII, equivalent to: [A-Za-z0-9]
[:word:] The same as [:alnum:], with the addition of the underscore (_) character.
[:alpha:] The alphabetic characters. In ASCII, equivalent to: [A-Za-z]
[:blank:] Includes the space and tab characters.
[:cntrl:] The ASCII control codes. Includes the ASCII characters 0 through 31 and 127.
[:digit:] The numerals 0 through 9.
[:graph:] The visible characters. In ASCII, it includes characters 33 through 126.
[:lower:] The lowercase letters.
[:punct:] The punctuation characters. In ASCII, equivalent to: [-!"#$%&'()*+,./:;<=>?@[\\\]_`{|}~]
[:print:] The printable characters. All the characters in [:graph:] plus the space character.
[:space:] The whitespace characters including space, tab, carriage return, newline, vertical tab, and form feed. In ASCII, equivalent to: [ \t\r\n\v\f]
[:upper:] The uppercase characters.
[:xdigit:] Characters used to express hexadecimal numbers. In ASCII, equivalent to: [0-9A-Fa-f]
ls /usr/sbin/[[:upper:]]*
/usr/sbin/MAKEFLOPPIES
。。。。
注意,上面不是正则表达式的使用示例,而是一个shell的路径名的扩展。都可以使用。
更改排序规则
使用local命令,查看地区设置:
$locale
LANG=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=
这个设置初始是系统安装时指定的,如果更改,可以影响到排序规则。
比如,通过修改LANG变量:
$export LANG=POSIX
如果要永久的让这个设置生效, 可以可以把这行加到 .bashrc文件里面。
export LANG=POSIX
POSIX Basic vs. Extended Regular Expressions
上面介绍的都是POSIX兼容的正则表达式。
扩展正则表达式,支持更多的metacharacters和与其相关的功能:
( ) { } ? + |
但在BRE中, ( ) { } ,这4个字符不是元字符,但前面加上 \ 反斜线 backslash后,仍被解释为常量字符。
如果要使用扩展正则表达,需要grep -E 。
POSIX的历史
上世界80年代,Unix是一个非常流行的商业操作系统,但到1988年时,整个Unix的世界变得混乱不堪。
因为很多计算机生产商都从Unix的所有者AT&T实验室那里获得了源码授权,然后就把不同版本的Unix集成到自家的系统中。
在制作差异化的产品时,每家都对Unix进行了相应的修改和扩展。这样就限制了软件的兼容性。
每家厂商都想通过手段来锁定自己的客户。这段时间成为Unix的巴尔干化。
进入到电子电器工程师机构IEEE(Institute of Electrical and Electronics Engineers),在80年代中期,就开始开发一系列关于Unix如何操作的标准。
这些标准最开始叫做IEEE 1003,定义了API,shell和一些功能,作为类Unix系统需要具备的。
命令为POSIX,叫做可扩展操作系统接口Portable Operating System Interface,最后的X表示留有一定的扩展余地。
IEEE所采纳的正是Richard Stallman的提议。
Alternation 多选
第一个扩展正则功能,功能是匹配条件为多个表达式。
类似上面的中括号内多个字符只需匹配一个即可。
而多选功能就是匹配多个正则表达式中的某一个即可。
为了演示,我们使用grep命令和echo命令的组合,先用一个简单的字符串匹配:
$echo "AAA" | grep AAA
AAA
$echo "BBB" | grep AAA
上面例子将echo的输出用管道传给grep命令。
$echo "AAA" | grep -E 'AAA|BBB'
AAA
$echo "BBB" | grep -E 'AAA|BBB'
BBB
$echo "CCC" | grep -E 'AAA|BBB'
这里我们看到了正则表达式,表示AAA或BBB的匹配条件。
注意使用单引号,避免被shell把竖线解析成管道操作符。
可以使用多次竖线表示多个选项。
可以和其他正则表达式元素组合使用:
$grep -Eh '^(bz|gz|zip)' dirlist*.txt
$grep -Eh '^bz|gz|zip' dirlist*.txt
量词修饰符
? 某个元素匹配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次或多次
[[:upper:]][[:upper:][:lower:] ]*\.
这个例子表示以句号结尾,首字母是大写字母,后面跟着任意数量的大小写字母或空格。
+ 一个或多个元素匹配
+前面的元素,要有一个或多个。
举例: ^([[: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 ~]$
那这个正则表达式的意思,开头和结尾的锚定符用上了,表示这一行要都符合中间的模式。
中间内容表示,是有一个或多个字符,后面可以跟0个或1个空格。
所以“a b 9”不满足,因为里面有数字。
{} 匹配某个元素指定的次数
有以下几种表示方式:
{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 ~]$
改进后的表达式,功能一样。
使用举例:
我们先生成一个随机的电话号码文件:
[me@linuxbox ~]$ for i in {1..10}; do echo "(${RANDOM:0:3}) ${RANDOM:0:3}-${RANDOM:0:4}" >> phonelist.txt; done
这里RANDOM命令产生一个0 - 32767的随机数。
${parameter:offset:length},比如${RANDOM:0:3},这个格式表示从0偏移位置开始,取3个字符长度的数字。
查看产生的文件,里面是电话号码,不过会有些格式不符合,所以我们使用上面的正则表达式把不符合的找出来:
[me@linuxbox ~]$ grep -Ev '^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$'
phonelist.txt
(292) 108-518
(129) 44-1379
[me@linuxbox ~]$
使用选项-v是反向显示,找出不匹配的内容行。
找出不规范的路径名:
[me@linuxbox ~]$ find . -regex '.*[^-_./0-9a-zA-Z].*'
这个就是找出包含空格的路径名。
使用locate查找文件:
locate程序支持基本正则表达式和扩展正则表达式:--regexp和--regex
[me@linuxbox ~]$ locate --regex 'bin/(bz|gz|zip)'
/bin/bzcat
/bin/bzcmp
使用less和vim搜索文本:
在less和vim支持相同的方法来搜索文本,按下 / 字符然后输入一个正则表达式就能够搜索,定位搜索项后按下p或n,继续搜索前一个或后一个。
[me@linuxbox ~]$ less 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
~
~
~
/^\([0-9]{3}\) [0-9]{3}-[0-9]{4}$
回车以后,内容匹配的行会高亮,就很容易发现不匹配格式的内容行。
使用vim要注意的是,不支持扩展正则只支持基本正则表达式。
所以要在command模式下输入:/([0-9]\{3\}) [0-9]\{3\}-[0-9]\{4\}
注意一些元字符需要前面加上反斜线进行转义。
需要高亮显示的话,要进行设置,根据vim版本不同,命令不同:
:hlsearch 或者 :set hlsearch
查询压缩文件:
[me@linuxbox ~]$ cd /usr/share/man/man1
[me@linuxbox man1]$ zgrep -El 'regex|regular expression' *.gz
使用zgrep命令可以读取压缩文件。