正则表达式 (Regular Expression, regex, regexp) 使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。
A regular expression is a pattern that describes a set of strings.
常用的工具 grep 中的「re」就是指正则表达式,Ken Thompson 在 44 年前 (Nov. 1974) 将此符号系统引入 grep。正则表达式的 POSIX 规范,分为基本型正则表达式 (Basic Regular Expression, BRE) 和扩展型正则表达式 (Extended Regular Express, ERE)。顾名思义,后者会更强大。在 GNU 实现的 grep 中,基本语法和扩展语法的可用功能没有区别。Perl 兼容正则表达式 (Perl Compatible Regular Expressions, PCRE) 支持更多的功能,它是由 Philip Hazel 在上个世纪末 (1997) 开发的一个正则表达式引擎,实现与 Perl 5 一致的语法和语义,被用于许多现代的著名项目,如 Apache、PHP、KDE、Postfix、Nmap 以及 Apple Safari。
基本结构
我们暂且不管形式语言、自动机之类的理论,先来看一个简单的例子:hello*
。这个正则表达式是含有 6 个字符的字符串,它匹配 hello
、helloo
以及 hellooo
等等无穷多个以「hello」开头、后接 0 个或多个「o」的字符串。
正则表达式的基本结构是匹配一个字符的模式 (pattern)。大部分普通的字符,包括所有的字母和数字,都匹配它们自身,如 h
匹配 h
、e
匹配 e
。
一些有特殊功能的字符,如上例中表示匹配前面的子表达式 (o
) 任意次的 *
,叫作元字符 (meta-character)。**若要匹配元字符自身,则需要一个前缀的反斜杠 (\
, backslash) 进行转义 (escape)**,如 \*
匹配一个星号 (*
, asterisk)。
元字符有:^
$
(
)
*
+
?
.
[
\
{
|
。
句点 .
是元字符,匹配除回车符 (carriage return, CR) 和换行符 (newline / line feed, LF) 之外的任意的单字符。例如,w.ll
能匹配 will
和 well
。
一个正则表达式可后接像 *
这样的表达式,表示**重复操作 (repetition)**。详见下表:
表达式 | 描述 |
---|---|
* |
匹配前面的子表达式 0 次或更多次。 |
? |
匹配前面的子表达式 0 次或 1 次。 |
+ |
匹配前面的子表达式 1 次或更多次。 |
{n} |
匹配前面的子表达式恰好 n 次。 |
{n,} |
匹配前面的子表达式至少 n 次。 |
{n,m} |
匹配前面的子表达式至少 n 次、至多 m 次。 |
从上表可见重复操作不一定由单个字符完成,更复杂的操作可能需要成对出现的花括号 ({
& }
, curly braces) 以及数字。*
等价于 {0,}
,?
等价于 {0,1}
,+
等价于 {1,}
。此外 GNU grep 中还支持 {,m}
,表示匹配至多 m 次,这是 GNU 的扩展 (extension)。
两个正则表达式可以直接连接,表示**串接操作 (concatenation)**,其结果匹配任何由分别匹配原表达式的字符串串接而得的字符串。为了表达得严谨些,看起来有点绕,举个例子来看就会很简单:正则表达式 a
和 b
串接的结果 ab
,匹配由字符串 a
和 b
串接而得的字符串 ab
。显然,这种操作是基本而广泛的。
两个正则表达式可以用竖线符 |
连接起来,表示**选择操作 (alternation)**,其结果匹配两个表达式中的任意一个。举个例子:a|b
匹配 a
或 b
。(没错,这和或运算 (or) 的中缀表示法一致。)
重复操作优先于串接操作,串接操作优先于选择操作。例如,hi*
能匹配 hii
而不能匹配 hihi
,fuc|nk
能匹配 fuc
而不能匹配 funk
。
整个表达式可以使用圆括号 ((
& )
, parentheses) 括起来,形成一个新的子表达式 (subexpression),其优先级比前述三种操作都更高。例如,(hi)*
能匹配 hihi
,fu(c|n)k
能匹配 funk
。
空的正则表达式匹配空字符串 ``。
方括号表达式与字符类
除了前文介绍的花括号和圆括号,还有一种常见的括号——方括号 ([
& ]
, (square) brackets)。 由方括号括起来的一系列字符叫作方括号表达式 (bracket expression),匹配方括号内字符序列中的任意一个字符。例如,[fox]
匹配 f
、o
或者 x
,等价于 f|o|x
。
若序列中第一个字符为脱字符 (^
, caret),则匹配排除序列中字符的任何字符。例如,[^cat]
则无法匹配 c
、a
或 t
但能匹配 d
、0
或 g
等等。
方括号内仍有特殊功能的元字符仅余用于转义的反斜杠 \
以及在第一位的脱字符 ^
。例如,[(fu)*k]
能匹配 (
、f
、u
、)
、*
或者 k
。
在方括号表达式中,由两个字符及其之间的连字符 (-
, hyphen) 组成的字符串叫作字符范围表达式 (range expression),匹配在字符集中处于这两个字符之间范围的任意一个字符。还是举例子比较简单,对于一般的字符编码,[a-z]
匹配任意一个小写字母,[0-9]
匹配一位阿拉伯数字。所以,[A-D]
等价于 [ABCD]
即 A|B|C|D
。
此外,方括号表达式中预定义了一些命名字符类 (named character class),表示一系列特定的字符。详见下表:
类名 | 描述 |
---|---|
[:lower:] |
小写字母字符,同 [a-z] |
[:upper:] |
大写字母字符,同 [A-Z] |
[:alpha:] |
字母字符,即 [:lower:] 和 [:upper:] ,同 [A-Za-z] |
[:digit:] |
数字字符,同 [0-9] |
[:xdigit:] |
十六进制数字字符,同 [A-Fa-f0-9] |
[:alnum:] |
字母和数字字符,即 [:alpha:] 和 [:digit:] ,同 [0-9A-Za-z] |
[:blank:] |
空白符,指空格 (space, SP) 和制表符 (horizontal tab, HT),同 [ \t] |
[:space:] |
空格符,指制表符 (HT)、换行符 (LF)、垂直制表符 (vertical tab, VT)、换页符 (form feed, FF)、回车符 (CR) 和空格 (SP) |
[:punct:] |
标点字符,有:! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~ |
[:graph:] |
有形字符,即 [:alnum:] 和 [:punct:] |
[:print:] |
可打印字符,即 [:alnum:] 、[:punct:] 和空格 (SP) |
[:cntrl:] |
控制字符,在 ASCII 中指从十进制的 0 到 37,以及 177 (DEL) |
注意上表首列中的中括号是类名的一部分,含有命名符号类的正则表达式应该形如 [[:name:]]
。例如,正则表达式 [[:lower:]01]
匹配所有小写字母,以及两个数字 0
、1
。
这里提及一个有趣的问题:我们知道 [[:digit:]]
等价于 [0-9]
,那么如何写一个等价的一般的方括号表达式以在 shell 中使用 GNU grep 匹配 [:punct:]
这样一个复杂的符号类?这个问题主要是将有特殊功能的字符表示为普通字符,涉及 shell 中的引号使用以及中括号内的字符顺序。答案是:
1 | all shells, single quote is outside the quotes |
我们在 shell 中通常用单引号来引用正则表达式,因为单引号最大限度地避免扩展 (expansion),保留特殊字符的字面值 (literal value),如 $
仅仅是美元符 (dollar sign) 而非变量标志。前文说方括号内的元字符并未提到方括号 ([
& ]
),也就是说 [[]
匹配左方括号,[]]
匹配右方括号。这是没错的,虽然看起来有点不合常理的机智,因为一般而言 ]
不是标志着这个子表达式的终结吗?——类似于「在第一位的 ^
代表排除式内字符」的规定,特别地,方括号内在第一位的 ]
匹配自身,而非结束方括号表达式。所以若要匹配 ]
,必须将它放在第一位;正如若要匹配 ^
,不得将它放在第一位。除此二之外,更多规则详见下表:
模式 | 描述 |
---|---|
[. |
表示 collating symbol 的开始 |
.] |
表示 collating symbol 的结束 |
[= |
表示 equivalence class 的开始 |
=] |
表示 equivalence class 的结束 |
[: |
表示命名字符类的开始,后跟有效类名 |
:] |
表示命名字符类的结束 |
- |
表示字符范围,当在非首位且非末位时 |
匹配空串的特殊表达式
反斜杠符 \
作为转义字符(港台称「跳脫字元」),后跟一些特定的字符,表示一些特殊的模式匹配。
模式 | 描述 |
---|---|
\b |
匹配单词边缘的空字符串 |
\B |
匹配非单词边缘的空字符串 |
\< |
匹配单词开头位置的空字符串 |
\> |
匹配单词末尾位置的空字符串 |
\w |
匹配单词成分,等价于 [_[:alnum:]] |
\W |
匹配非单词成分,等价于 [^_[:alnum:]] |
\s |
匹配空白符,等价于 [[:space:]] |
\S |
匹配非空白符,等价于 [^[:space:]] |
其中「单词边缘」?「空字符串」?不太好理解。
举个例子,字符串 self-censorship
的每对相邻字符 (eg. s
e
)之间以及第一个 s
的左边、末尾 p
的右边的位置上,可以看作存在一个(或任意个)「空字符串」``。
那么位于第一个 s
和 c
的左边、f
和 p
的右边,也就是「单词边缘」的位置上的那个空串,能够由 \b
匹配。于是 \bs.
能匹配该串中的 se
而不匹配 so
或 sh
。所谓单词边缘就是指一个其左右两个字符一个属于单词成分 (\w
)、一个不属于单词成分的位置。于是不难理解 \B
、\<
和 \>
应匹配什么位置的空串。
匹配特定空串实际上就是匹配位置。
脱字符 ^
匹配行首的空串,美元符 $
匹配行末的空串。这两个字符被称为**锚 (anchor)**,因为它们分别被锚定到行首和行末。例如,一文本文件有如下字符:
1 | Hip-Hop |
则 ^H.p
匹配该行内的 Hip
,H.p$
匹配该行内的 Hop
。
反向引用与子表达式
反向引用 (back-reference) 的语法为 \n
,其中 n
是一位数字。\n
引用了正则表达式中第 n 个由圆括号形成的子表达式 (parenthesized subexpression)。例如,(f)\1
相当于 (f)(f)
,匹配 ff
。
当被引用的子表达式被用于选择操作 (|
) 中,假如该子表达式所在的部分不参与匹配,则整个匹配失败。例如,a(.)|b\1
无法匹配 ba
。GNU grep 给出的错误是「Invalid back reference」。
当有多个正则表达式进行匹配(比如 grep 使用 -e <PATTERN>
或 -f <FILE>
参数)时,向后引用的编号 (n) 是对于每一个正则表达式而言的。
以上是「正则表达式」的基本内容。
📖 [Ref] GNU grep docs