Linux技巧:详解正则表达式和通配符的用法和它们的区别

霜鱼片

描述正则表达式、通配符的用法,以及它们之间的区别

正则表达式的标准规范文档

正则表达式可用于匹配特定模式的字符串。

在 Linux 中,可以用 man 7 regex 命令查看正则表达式的说明,里面提到 POSIX.2 标准定义了正则表达式规范。

这里面的说明比较乱,内容也比较老,建议查看最新版的 POSIX 标准。

在网上找不到 POSIX 标准的在线版本,也可以查看 Single UNIX Specification 标准的在线版本。

Single UNIX Specification 标准和 POSIX 标准大部分内容是一样的。

2019年最新版 Single UNIX Specification 标准的正则表达式规范在线链接是:https://pubs.opengroup.org/on...

在下面的描述中,如无特别说明,关于正则表达式的引用内容都出自这个在线链接,并统称为 POSIX 标准。

注意:正则表达式本身是一套匹配字符串的规范,在 Linux 中没有一个独立的正则表达式命令来使用它,而是要在支持正则表达式的工具上使用

不同的工具(grep、awk、sed、find、vim,等)和不同的程序语言(Perl、Python、Java、JavaScript,等)都支持解析正则表达式,而且它们各自做了一些扩展,跟 POSIX 标准有所差异,在使用时要再查看各自工具的帮助文档。

正则表达式介绍

查看上面的正则表达式在线链接说明,对正则表达式介绍如下,后面会有相应的中文说明:

  • Regular Expressions (REs) provide a mechanism to select specific strings from a set of character strings.
  • The Basic Regular Expression (BRE) notation and construction rules in Basic Regular Expressions apply to most utilities supporting regular expressions. Some utilities, instead, support the Extended Regular Expressions (ERE) described in Extended Regular Expressions.
  • The concatenated set of one or more BREs or EREs that make up the pattern specified for string selection.
  • The use of regular expressions is generally associated with text processing. REs (BREs and EREs) operate on text strings; that is, zero or more characters followed by an end-of-string delimiter (typically NUL). Some utilities employing regular expressions limit the processing to lines; that is, zero or more characters followed by a newline character.
  • In the regular expression processing described in this specification, the newline character is regarded as an ordinary character and both a period and a non-matching list can match one.
  • Those utilities (like grep) that do not allow newline characters to match are responsible for eliminating any newline character from strings before matching against the RE.
  • The interfaces specified in this specification set do not permit the inclusion of a NUL character in an RE or in the string to be matched.
  • Matching is based on the bit pattern used for encoding the character, not on the graphic representation of the character. This means that if a character set contains two or more encodings for a graphic symbol, or if the strings searched contain text encoded in more than one codeset, no attempt is made to search for any other representation of the encoded symbol.
  • The search for a matching sequence starts at the beginning of a string and stops when the first sequence matching the expression is found, where first is defined to mean "begins earliest in the string". If the pattern permits a variable number of matching characters and thus there is more than one such sequence starting at that point, the longest such sequence will be matched. For example: the BRE bb* matches the second to fourth characters of abbbc.

即,正则表达式提供了一个从字符串中选取特定子字符串的机制,分为基本正则表达式(BRE)、扩展正则表达式(ERE)两种形式。

正则表达式通常用于文本处理,在文本字符串上进行操作,基于字符编码值进行逐字匹配,要求被处理的字符串以空字符 NUL 结尾。部分使用正则表达式的工具要求以换行符结尾,便于逐行处理,跟 POSIX 标准的定义有所不同。

一般来说,空字符 NUL 的 ASCII 编码值为 0,在 C语言 中写为 '\0'

在 POSIX 标准中,不允许匹配 NUL 这个空字符,换行符被当成普通字符处理,可以匹配到换行符。而部分工具(例如 grep)会去掉字符串末尾的换行符,匹配不到行末换行符。

即,正则表达式通过 BRE 表达式、或者 ERE 表达式指定要匹配的字符串模式。支持正则表达式的工具可以基于所给的字符串模式从文本中找到符合该模式的字符串。

注意:正则表达式是基于字符的编码值进行匹配,而不是基于字符的图形表示来匹配。对于具有多种字符编码集的字符来说,要特别注意这一点。

例如中文有 UTF-8 编码、Unicode 编码、GBK 编码等。如果一个包含中文汉字的文件是 GBK 编码格式,而解析正则表达式的工具使用 UTF-8 编码来匹配这个文件,就会匹配不到。

因为同一个汉字在不同的编码集中的编码值不一样,使用不同编码集的编码值来进行匹配,显然不相等。

正则表达式在匹配时,从所给字符串的开头开始查找匹配,默认遇到第一个匹配的内容就停止匹配。

如果所给正则表达式允许匹配变长字符串(例如通过 * 来匹配连续多个字符),那么会改成匹配到最长内容为止。

例如,对于 "AaaaaaA" 字符串来说,正则表达式 "aa" 在匹配到前面两个字符 a 时就停止匹配,匹配的内容是开头 aa 子字符串。

而正则表达式 aa* 会一直匹配到最后一个字符 a 为止,匹配的内容是 "aaaaa" 子字符串。

基本正则表达式

基本正则表达式(Basic Regular Expression,在很多文档中常缩写为 BRE)主要由传统字符(Ordinary Characters)、特殊字符(Special Characters)、方括号 [] 表达式、子表达式等组成:

  • 传统字符是可以匹配自身的单个字符。例如,在正则表达式中,字符 ‘a’ 就表示它自身。
  • 特殊字符具有特殊的含义,不表示自身的字符,如果要匹配特殊字符自身,需要用反斜线 \ 进行转义。
  • 方括号 [] 表达式用于匹配一串字符集合中的单个字符。注意,在实际匹配时,方括号表达式只匹配一个字符,只是这个字符可以是不同的字符。
  • 子表达式(subexpression)是用 \(\) 括起来的表达式,把多个字符集合起来,以便反向引用表达式进行引用。

基本正则表达式的特殊字符

基本正则表达式支持的特殊字符比较少。POSIX 标准定义的基本正则表达式只支持 . [ \ * ^ $ 这六个特殊字符,而且这些字符要在特定上下文环境中才具有特殊含义。具体说明如下。

. [ \ 这三个字符出现在方括号 [] 表达式之外时,才具有特殊含义,出现在方括号 [] 表达式之内没有特殊含义。

  • 点号 . 的特殊含义是可以匹配任意一个字符,但不包括空字符 NUL。对于那些要求以换行符结尾的工具来说,点号 . 匹配不到行末的换行符。
  • 反斜线 \ 的特殊含义是对特殊字符进行转义,表示特殊字符自身。例如,点号 . 具有特殊含义,如果要匹配点号 ‘.' 这个字符,需要写为 \.。当 \ 后面跟着左小括号(、右小括号)、左大括号{、右大括号}、或者数字 1 到 9 时,另有特殊含义,后面会具体说明。
  • 左大括号 [ 是预留的特殊字符,如果在方括号 [] 表达式之外单独提供一个左大括号,没有用 \ 对它转义,且没有出现匹配的右大括号,具体结果未定义(undefined)。未定义的结果一般会报错。即,不要使用一个单独的左大括号。要匹配左大括号自身,要在方括号表达式之外用 \[ 进行转义。

除了下面提到的三种情况之外,星号 * 具有特殊含义,其含义是匹配零个或连续多个前面的上一个 BRE 表达式。

一个单独的传统字符也是一个 BRE 表达式。例如,a* 匹配空字符串、"aa" 字符串、"aaaaa" 字符串等。

星号 * 没有特殊含义的情况说明如下:

  • 出现在方括号 [] 表达式内,没有特殊含义。
  • 出现在整个 BRE 表达式开头的第一个字符、或者跟在整个 BRE 表达式开头的 ^ 字符之后,没有特殊含义。因为此时在 * 前面没有字符,匹配为空。
  • 紧跟在子表达式开头的 \( 之后、或者紧跟在开头的 \(^ 之后,没有特殊含义。

^ 这个字符只在下面两种情况下出现时,才具有特殊含义:

  • 出现在方括号 [] 表达式之外,用于表示匹配字符串开头。在实现时,也可以用在子表达式开头的 \( 之后的第一个字符。例如,^ab 表达式可以匹配 abcdef 字符串开头的 ab,但不匹配 cdefab 字符符末尾的 ab
  • 出现在方括号 [] 表达式开头的第一个字符,紧跟在左大括号 [ 之后,表示不匹配方括号内的所有字符。例如,[^abc] 表达式不匹配字符 a、b、c,会匹配这三个字符之外的任意一个字符。

$ 字符只有出现在整个 BRE 表达式末尾的最后一个字符时才有特殊含义,用于表示匹配字符串末尾。

在实现时,也可以用在子表达式末尾 \) 前面的最后一个字符。例如,ab$ 表达式可以匹配 cdefab 字符串末尾的 ab,但不匹配 abcdef 字符串开头的 ab

当同时使用 ^$ 时,可以全词匹配一个字符串。例如,^abcdef$ 表达式只能匹配 abcdef 这个字符串。

POSIX 标准在描述 ^$ 的作用对象时,用的是 string 这个单词,也就是字符串,比较少用 line 这个单词。

这里可能会带来一个误解,认为可以用 ^$ 来匹配字符串中间子字符串的开头、或者结尾,这个是不能匹配的。

例如,有一个 "This is a test string." 字符串,它由几个单词组成,单词之间用空格隔开。

用正则表达式来匹配这个字符串时,字符串开头的单词是 "This" 子字符串,可以用 ^This 进行匹配,但是用 ^is 匹配不到中间的 "is" 子字符串。

^ 限定指向被匹配字符串的开头,不能指向被匹配字符串里面用空格隔开的某个子字符串的开头。

可以理解为,进行正则表达式匹配时,会传入匹配模式,和被匹配的整个字符串,该字符串里面可能带有空格,空格隔开了一些子字符串。

那么 ^ 只对应被匹配字符串的开头,不能对应被匹配字符串里面子字符串的开头。

注意:这里描述的是 POSIX 标准定义的基本正则表达式特殊字符。部分工具可能进行了一些扩展,支持更多的特殊字符,部分特殊字符的含义可能会发生改变,在实际使用时,要再参考工具自身的文档说明。

例如,GNU grep 帮助文档把特殊字符称为元字符(meta-character)。grep 命令使用 GNU BRE 的规则,添加了一些扩展,可以用 \< 来匹配字符串中间某个单词的开头,用 \> 来匹配字符串中间某个单词的末尾。同时,还扩展支持了 \+\| 等特殊字符。

具体可以参考 GNU grep 的在线链接说明: https://www.gnu.org/software/...

方括号表达式

方括号 [] 表达式(bracket expression)用于匹配 [] 内字符集合的任意一个字符,所给的字符集合不能为空。

例如,[ab] 可以匹配字符 a、或者字符 b,但只能匹配一个字符。如果要匹配 ab 这两个字符,要写为 [ab][ab]

由于所给的字符集合不能为空,右大括号 ] 紧跟在左大括号 [ 之后、或者紧跟在 [^ 之后时,并不是一个完整的方括号表达式,此时的右大括号 ] 表示匹配它自身,还需要再加上一个右大括号 ] 才是完整的表达式。

例如,只写为 [] 不是一个完整的方括号表达式,要写为 []] 才是一个完整的方括号表达式,第一个右大括号 ] 表示它自身,该表达式匹配 ‘]’ 这个字符。

grep 命令测试如下:

cat retestfile
This is a ] char.
$ grep '[]]' retestfile
This is a ] char.
$ grep '[]' retestfile
grep: Unmatched [ or [^
$ grep ']' retestfile
This is a ] char.

可以看到,grep '[]]' retestfile 命令能匹配到 ] 这个字符自身。

grep '[]' retestfile 命令执行报错,提示左大括号没有配对,也就是缺少闭合的右大括号。

grep ']' retestfile 命令也能匹配到 ] 这个字符自身,不需要用 \ 进行转义。

在上面列出的基本正则表达式特殊字符中并不包括 ] 这个字符。

基于这个行为,要在方括号表达式内匹配右大括号 ‘]’ 这个字符时,它必须写在字符集合的开头,不能写在字符集合中间,也不能用 \ 进行转义。

在方括号表达式内,反斜线 \ 没有转义的功能,[\] 就是一个完整的表达式,匹配 ‘’ 这个字符自身。

[\]] 是两个表达式,由 [\] 方括号表达式和 ] 字符所组成,会匹配 \] 字符串,不能匹配单个 ‘’ 字符,也不能匹配单个 ‘]’ 字符。

grep 命令测试如下:

$ cat retestfile
This is a ] char.
This is a \ char.
$ grep '[\]]' retestfile
$ grep '[\]' retestfile
This is a \ char.

可以看到,grep '[\]]' retestfile 命令什么都匹配不到,在 retestfile 文件中没有 \] 这个子字符串。

grep '[\]' retestfile 匹配到 \ 这个字符自身。

在方括号 [] 表达式内,如果 ^ 是开头的第一个字符,紧跟在左大括号 [ 之后,则整个表达式会匹配除了 [] 内字符集合之外的任意一个字符。

如果 ^ 没有紧跟在左大括号 [ 之后,则只表示 ‘^’ 字符自身。

例如,[^ab] 表达式匹配除了字符 a、字符 b 之外的任意一个字符。而 [a^b] 表达式匹配 a、^、b 这三个字符中的任意一个,只匹配一个字符。

字符类表达式

字符类表达式(character class expression)用于指定某一类字符集合。

前面提到,方括号表达式在 [] 内要提供非空的字符集合,字符类表达式就可以用于提供特定类型的字符集合。

字符类表达式的写法是 [:name:]。这里的 [::] 称之为 bracket-colon delimiters,是整个表达式的一部分。

name 指定具体的字符类别。POSIX 标准定义了 12 个字符类型,说明如下:

字符类别 含义
[:alnum:] 字母字符和数字字符 (可以匹配中文字符)
[:alpha:] 字母字符 (可以匹配中文字符)
[:blank:] 空白字符,特指空格和 tab 字符,不包含换行符
[:cntrl:] 控制字符
[:digit:] 数字字符
[:graph:] 图形字符,包括字母、数字、标点符号,不包括空格
[:lower:] 小写字符 (不能匹配中文字符)
[:print:] 可打印字符,包括字母、数字、标点符号、和空格
[:punct:] 标点符号,也包括运算符、各种括号等
[:space:] 所有空白字符 (空格,制表符,换行符,回车符)
[:upper:] 大写字符 (不能匹配中文字符)
[:xdigit:] 十六进制数字 (0-9,a-f,A-F)

部分工具扩展支持更多的字符类别,例如 [:word:] 等,可以再查看对应工具的说明文档。

字符类表达式只是一类字符集合,它自身不能用于匹配字符,必须用在方括号表达式里面,才能用来匹配字符集合中的任意一个字符,只匹配一个字符。

注意:字符类表达式的 [::] 是整个表达式的一部分,要区分于方括号表达式的 [] 这两个括号。要把整个字符类表达式再放到方括号 [] 里面,才是一个有效的方括号表达式。

例如,写成 [[:alpha:]] 的形式。用 grep 命令测试如下:

$ grep [:alpha:] retestfile
grep: character class syntax is [[:space:]], not [:space:]
$ grep [[:alpha:]] retestfile
This is a ] char.
This is a \ char.

可以看到,grep [:alpha:] retestfile 命令执行报错,提示正确的语法格式是把 [:space:] 再放到一个方括号 [] 里面。

这里打印的 [:space:] 是一个举例的提示,跟该命令提供的 [:alpha:] 无关。

grep [[:alpha:]] retestfile 命令没有执行报错,它会匹配到各个字母、或数字。

范围表达式

范围表达式(range expression)表示两个字符之间的所有字符集合,包括这两个字符本身。这两个字符通过连字符 - 隔开。

例如,A-Z 这个范围表达式表示所有大写英文字母的集合。

范围表达式只表示一类字符集合,它自身不能用于匹配字符,必须用在方括号表达式里面,才能用来匹配字符集合中的任意一个字符。

例如,[A-Z] 表示匹配任意一个大写英文字母,只匹配一个字符。

注意:范围表达式右边字符的字母顺序必须高于左边字符,也就是只能升序,不能降序。

例如,Z-A 不是一个有效的范围表达式。用 grep 命令测试如下:

$ grep [Z-A] retestfile
grep: Invalid range end

可以看到,grep [Z-A] retestfile 命令报错,提示无效的结束范围。

在方括号表达式内,如果要匹配连字符 ‘-’ 自身,它必须写在字符集合开头(或者跟在开头的 ^ 之后)、字符集合末尾、或者作为范围表达式的结束字符。

例如,[-ac][ac-] 都表示匹配 a、c、- 这三个字符中的任意一个。[^-ac][^ac-] 都表示匹配除了 a、c、- 这三个字符之外的任意一个字符。

[%--] 表示匹配在 % 和 - 之间的任意一个字符,包括这两个字符自身,这里的第二个 ‘-’ 是范围表达式的结束字符。

范围表达式可以使用中文汉字,但是顺序范围不那么直观,不是基于拼音字母顺序来选择范围,而是基于汉字编码值的顺序来选择范围。

grep 命令测试如下:

$ cat retestfile
一
二
三
四
五
六
七
八
九
十
$ grep [一-十] retestfile
一
二
三
五
六
七
八
九
十
$ grep [冰-雨] retestfile
四
十

可以看到,grep [一-十] retestfile 命令没有匹配到 ‘四’ 这个汉字。

‘一’ 对应的 Unicode 编码值是 \u4e00,‘十’ 对应的 Unicode 编码值是 \u5341,‘四’ 对应的 Unicode 编码值是 \u56db。即,‘四’ 的编码值不在 ‘一’ 到 ‘十’ 之间的范围内,所以没有匹配到。

子表达式

子表达式(subexpression)是使用 \(\) 括起来的表达式,被括起来的字符集合保持原有的含义。

例如在子表达式 \( \) 内,字符 a 还是匹配它自身,点号 . 表示匹配任意一个字符,可以用反斜线 \ 来转义,可以用方括号 [] 表达式来匹配字符集合中的任意一个字符,等等。

比较特别的是 ^ 字符。当 ^ 出现在子表达式开头、紧跟在 \( 之后时,是用于匹配字符串开头,还是匹配 ‘^’ 字符自身,是可选行为。POSIX 标准说是取决于具体的实现,也就是由工具自身决定。这可能会带来移植性问题。

使用 grep 命令测试子表达式如下:

$ cat retestfile
This is a ] char.
This is a \ char.
$ grep '\(]\)' retestfile
This is a ] char.
$ grep '\(\\\)' retestfile
This is a \ char.
$ grep '\(Th.s\)' retestfile
This is a ] char.
This is a \ char.
$ grep '\([]\]\)' retestfile
This is a ] char.
This is a \ char.

可以看到,\(]\) 表示匹配 ‘]’ 字符。\(\\\) 表示匹配 ‘’ 字符,子表达式内的特殊字符具有特殊含义,所以要用 \\ 来转义,才能匹配到 ‘’ 字符自身。

\(Th.s\) 可以匹配 "This" 字符串、"Thas" 字符串、"Thys" 字符串等等,在 Ths 之间可以是任意一个字符。

\([]\]\) 在子表达式内使用了方括号表达式 []\] 来匹配 ‘]’ 字符、或者 ‘’ 字符。

子表达式可以嵌套使用。例如写为 \(\(ab\).c\) 的形式。

由于子表达式内的字符集合保持原有的含义,把字符集合放到子表达式内看起来是多余的。

其实,子表达式可以作为一个整体,配合反向引用表达式、或者配合区间表达式来使用。如后面的说明所示。

反向引用表达式

反向引用表达式(back-reference expression)用于反向引用前面的子表达式,其写法是 \nn 必须是 1 到 9 之间的数字,包括 1 和 9。\n 会匹配从整个表达式开头数起的第 n 个子表达式对应的内容,最多只能匹配到第 9 个子表达式。但还是可以提供 10 个、以及更多的子表达式,只是不能通过反向引用表达式来引用到。

注意:反向引用表达式不是从 \n 表达式自身往前数起,而是从整个表达式开头数起。

例如,对 \(ab\)\(ef\)\(mn\)\1\3 这个表达式来说,\1 匹配从左边数起的第一个 \(ab\) 子表达式的内容,并不是匹配在它前面的第一个 \(mn\) 子表达式内容。

\3 匹配从左边数起第三个 \(mn\) 子表达式的内容。

grep 命令举例如下:

$ cat retestfile
ab cd ef ab cd ef
$ grep '\(ab\) \(cd\) \(ef\) \1 \2 \3' retestfile
ab cd ef ab cd ef

\(ab\) \(cd\) \(ef\) \1 \2 \3 这个正则表达式中,\1 匹配开头第一个 \(ab\) 子表达式的内容,也就是匹配 "ab" 字符串。

\2 匹配开头第二个 \(cd\) 子表达式的内容,也就是匹配 "cd" 字符串。

\3 匹配开头第三个 \(ef\) 子表达式的内容,也就是匹配 "ef" 字符串。

整个正则表达式对应的字符串是 "ab cd ef ab cd ef",而 retestfile 文件的第一个行,就是这个字符串,匹配成功,grep 命令打印出匹配行。

区间表达式

区间表达式(interval expression)指定在它前面的一个表达式要匹配多少次,其写法是 \{m\}\{m,\}、或者 \{m,n\}mn 的值要满足 0 <= m <= n 这个范围。

  • \{m\} 指定要精确匹配 m 个前一个表达式。例如,a\{3\} 匹配三个字符 a,也就是 "aaa" 字符串。
  • \{m,\} 指定至少匹配 m 个前一个表达式。例如,a\{3,\} 至少匹配三个字符 a,可以匹配更多的字符 a。
  • \{m,n\} 指定至少匹配 m 个前一个表达式,最多匹配 n 个前一个表达式,包括 n 本身。例如,a\{3,5\} 至少匹配三个字符 a,最多匹配五个字符 a,即可以匹配 "aaa"、"aaaa"、"aaaaa" 这三个字符串。

区间表达式可以结合子表达式使用,例如写为 \(ab\)\{2\} 会匹配 "abab" 字符串。

使用 grep 命令测试如下:

$ cat retestfile
abab
aaa
$ grep 'a\{3\}' retestfile
aaa
$ grep '\(ab\)\{2\}' retestfile
abab

扩展正则表达式

扩展正则表达式(Extended Regular Expressions,,在很多文档中常缩写为 ERE)在基本正则表达式上进行了扩展,支持更多的特殊字符,部分特殊字符的使用场景发生了变化,特殊字符是否需要使用 \ 进行转义的场景也发生了变化。具体说明如下。

扩展正则表达式的特殊字符

扩展正则表达式支持基本正则表达式定义的六个 . [ \ * ^ $ 特殊字符,并新增 ( ) { | + ? 六个特殊字符,总共支持十二个特殊字符。对这些特殊字符的含义和使用场景具体说明如下。

. [ \ ( 这四个字符出现在方括号 [] 表达式之外时,具有特殊含义,出现在方括号 [] 表达式之内没有特殊含义。当右小括号 ) 出现在方括号 [] 表达式之外,且前面有配对的左小括号 ((也是在方括号 [] 表达式之外)时,具有特殊含义。

  • 扩展正则表达式的点号 . 特殊含义跟基本正则表达式相同,可以参考上面的说明。
  • 扩展正则表达式的反斜线 \ 特殊含义跟基本正则表达式相同,可以参考上面的说明。要注意的是,扩展正则表达式支持更多的特殊字符。在扩展正则表达式中,\( 匹配 ‘(’ 字符自身。而基本正则表达式中,\( 是子表达式的开头,基本正则表达式就用 ( 来匹配字符 ‘(’ 自身。
  • 扩展正则表达式的左大括号 [ 特殊含义跟基本正则表达式相同,可以参考上面的说明。
  • 左小括号 ( 和右小括号 ) 把它们里面的字符优先组合到一起,小括号内部的字符保持原有的含义不变,跟基本正则表达式的子表达式用法类似。
  • 在左小括号 ( 之后紧跟着右小括号 ),即小括号 () 内部为空的情况,是未定义的行为。
  • 虽然右小括号 ) 需要匹配前面的左小括号 ( 才具有特殊含义,但是一个单独的 ) 并不能匹配 ‘)’ 字符自身,还是需要用 \) 进行转义来表示 ‘)’ 字符自身。

* + ? { 这四个字符在方括号表达式之外,具有特殊含义。

  • 扩展正则表达式的星号 * 特殊含义跟基本正则表达式相同,可以参考上面的说明。
  • 加号 + 的特殊含义是匹配一个或连续多个前面的上一个 BRE 表达式。一个单独的传统字符也是一个 BRE 表达式。例如,a+ 匹配一个字符 a、"aa" 字符串、"aaaaa" 字符串等。
  • 问号 ? 的特殊含义是匹配零个或一个前面的上一个 BRE 表达式。一个单独的传统字符也是一个 BRE 表达式。例如,a? 匹配空字符串,或者一个字符 a。
  • *+? 的匹配结果都依赖前一个字符,如果这三个特殊字符出现在表达式开头,没有前一个字符时,具体结果未定义。
  • 左大括号 { 是扩展正则表达式预留的特殊字符,使用一个单独的左大括号 {、且后面没有配对的右大括号 } 是未定义的行为。

竖线 | 字符在方括号表达式之外,具有特殊含义。其含义是表示在 | 前后的两个模式字符串是可选的。

例如,abc|efg 表示匹配 "abc" 字符串、或者匹配 "efg" 字符串。

注意:在正则表达式中,多个字符连续写在一起,默认用到了一个 concatenation 操作符。

例如,ab 这个写法是字符 a 和字符 b 的组合,默认用 concatenation 操作符进行组合。

concatenation 操作符的优先级高于 | 这个特殊字符,所以在 abc|efg 表达式中,在 | 左边的字符 a、b、c 先组合成字符串 "abc",在 | 右边的字符 e、f、g 也组合成 "efg" 字符串,再进行 | 的特殊处理,匹配 "abc" 字符串、或者匹配 "efg" 字符串。

如果 | 特殊字符的优先级高于 concatenation 操作符,就变成匹配 "abefg" 字符串、或者匹配 "abefg" 字符串。

基于这个优先级问题,| 字符一般会跟小括号 () 一起组合使用,小括号内的表达式会优先组合。

例如,a(b|c)d 表示匹配 "abd" 字符串、或者匹配 "acd" 字符串。如果不加小括号,写为 ab|cd,会变成匹配 "ab" 字符串、或者匹配 "cd" 字符串。

在扩展正则表达式中,^ 这个字符只在下面两种情况下出现时,才具有特殊含义:

  • 出现在方括号 [] 表达式之外,用于表示匹配字符串开头。例如,^ab 表达式、或者 (^ab) 表达式可以匹配 abcdef 字符串开头的 ab,但不匹配 cdefab 字符符末尾的 ab
  • 出现在方括号 [] 表达式开头的第一个字符,紧跟在左大括号 [ 之后,表示不匹配方括号内的所有字符。例如,[^abc] 表达式不匹配字符 a、b、c,会匹配这三个字符之外的任意一个字符。

注意:基本正则表达式要求 ^ 出现在表达式开头第一个字符才具有特殊含义,如果出现在表达式中间,没有特殊含义,可以匹配 ‘^’ 字符自身。但是在扩展正则表达式中,只要 ^ 出现在方括号表达式之外,就具有特殊含义,即使出现在表达式中间也有特殊含义,不能匹配 ‘^’ 字符自身,此时要匹配 ‘^’ 字符自身,需要用 \^ 进行转义。

例如,在 a^b 表达式中,^ 也是一个有效的特殊字符,但是这个表达式匹配不到任何字符串。因为没有字符串可以满足以字符 b 开头,且前面还有字符 a 的情况。

grep 命令测试如下:

$ grep 'a^b' retestfile
a^b
$ grep -E 'a^b' retestfile

可以看到,grep 'a^b' retestfile 命令匹配到了 "a^b" 字符串,该命令没有加 -E 选项,使用基本正则表达式进行匹配,在 a^b 中间的 ^ 匹配字符 ‘^’ 自身。

grep -E 'a^b' retestfile 命令匹配不到任何内容,加了 -E 选项指定用扩展正则表达式,在 a^b 中间的 ^ 不能匹配字符 ‘^’ 自身,而是表示要匹配字符 b 开头的字符串。但是前面多了一个字符 a,匹配不到这种字符串。

在扩展正则表达式中,$ 字符出现在方括号 [] 表达式之外就具有特殊含义,用于表示匹配字符串末尾。

例如,ab$ 表达式、或者 (ab$) 表达式可以匹配 cdefab 字符串末尾的 ab,但不匹配 abcdef 字符串开头的 ab

只要 $ 出现在方括号表达式之外,就具有特殊含义,即使出现表达式中间也有特殊含义,虽然 e$f 表达式会什么都匹配不到,但这是一个有效的表达式。

如果要匹配字符 ‘$’ 自身,需要用 \$ 进行转义。

注意:如前面说明,grep 命令使用 GNU BRE 规则,而 GNU BRE 对基本正则表达式进行了扩展,也可以支持 +?| 这三个特殊字符,只是要写为 \+\?\| 的形式。

例如,在 GRE BRE 中,\+ 就表示匹配一个或连续多个前面的上一个 BRE 表达式。具体可以查看 GNU grep 在线链接的说明。

特殊字符转义

当在方括号 [] 表达式之外使用特殊字符时,基本表达式的特殊字符要在特定上下文环境中才有特殊含义,当没有特殊含义时,可以直接用特殊字符来匹配自身。

例如,当 * 出现在表达式开头时,没有特殊含义,就可以匹配字符 ‘*’ 自身。

而扩展正则表达式在方括号 [] 表达式之外使用特殊字符时,如果要匹配特殊字符自身,所支持的十二个特殊字符都需要用 \ 进行转义。完整的特殊字符转义列表如下:

\^      \.      \[      \$      \(      \)
\*      \+      \?      \{       \\     \|

即使部分特殊字符在特定上下文环境中没有特殊含义,也需要用 \ 进行转义。这个规则跟上面说明的基本正则表达式特殊字符转义规则有所不同。

例如,在扩展正则表达式中,) 只有在前面有匹配的 ( 时才具有特殊含义。* 出现在表达式开头时,是未定义的行为,没有特殊含义。

grep -E 命令测试如下:

$ cat retestfile
*
)
$ grep -E ')' retestfile
grep: Unmatched ) or \)
$ grep -E '\)' retestfile
)
$ grep -E '*' retestfile
*
)
$ grep -E '\*' retestfile
*

grep -E 命令指定用扩展正则表达式,可以看到,匹配模式写为 ')' 会报错,提示没有匹配的左小括号。

匹配模式写为 '\)' 就能匹配到 ‘)’ 字符。

匹配模式写为 '*' 时,打印了 retestfile 文件的所有行。看起来'*' 被当成空字符串,grep 命令的匹配模式为空字符串时,会匹配所有行。

匹配模式写为 '\*' 时,就会匹配到 ‘*’ 字符。

扩展正则表达式和基本正则表达式的差异

基本正则表达式支持方括号 [] 表达式、子表达式 \( \)、反向引用表达式 \n、区间表达式 \{m,n\}

扩展正则表达式对这些表达式的支持情况、以及跟基本正则表达式之间的差异说明如下:

  • 扩展正则表达式的方括号 [] 表达式规则和基本正则表达式相同,包括字符类表达式、范围表达式的用法也相同。
  • 扩展正则表达式用两个小括号 ( ) 特殊字符来组合里面的字符,POSIX 标准文档称之为 grouping,不再称之为 subexpression。这个小括号 ( ) 组合跟基本正则表达式的 \( \) 子表达式作用相似,看起来只是写法不同。
  • 扩展正则表达式不支持反向引用表达式 \n
  • 扩展正则表达式的区间表达式写法是 {m}{m,}、或者 {m,n}。相比于基本正则表达式的写法,在大括号前面不需要添加 \ 字符,表达式的作用相同。

正则表达式特殊字符大总结

下面总结基本正则表达式和扩展正则表达式的特殊字符、各个表达式的基本用法。为了简化说明,这里只举例说明基本用法,完整说明可以查看前面的内容。

基本正则表达式的特殊字符列表

基本正则表达式支持的特殊字符

扩展正则表达式的特殊字符列表

扩展正则表达式支持的特殊字符

注意:部分工具支持使用 \d 匹配任意一位数字、用 \w 匹配任意一个字母或者数字、以及其他类似的形式。POSIX 标准对这些形式没有定义,这是其他工具自身的扩展。

正则表达式和通配符的区别

对于先接触 bash 通配符(wildcard)、再接触正则表达式的人来说,容易把通配符和正则表达式搞混,往往在 bash 中使用正则表达式出现不预期的结果。下面说明 bash 通配符的作用,并描述正则表达式和通配符之间的区别。

Bash 通配符的概念

Bash 本身不能解析正则表达式,目前也没有一个独立的正则表达式命令专门来解析、测试正则表达式,而是要在支持正则表达式的命令上使用,由这些命令自身来解析正则表达式,例如 grep、sed、awk、find 等命令。

Bash 对通配符的处理,发生在 bash 扩展阶段,先对命令行参数进行扩展,再进行处理。

部分扩展表达式的特殊字符跟正则表达式的特殊字符使用了同一个字符,含义也相近,导致容易出现混淆。

其实,在 man bash 的说明里面,没有出现 wildcard 这个单词。

在 bash 中,常用 * 来匹配任意字符串,但是 bash 并没有把星号 * 称为 wildcard

这是从其他文档挪用过来的称呼,例如 GNU make 在线链接 https://www.gnu.org/software/... 有如下说明:

A single file name can specify many files using wildcard characters. The wildcard characters in make are ‘*’, ‘?’ and ‘[…]’, the same as in the Bourne shell.

所以,大部分文档所说的 “bash 通配符” 其实是 bash 扩展表达式各个特殊字符的统称,这些特殊字符大部分来自路径名扩展(Pathname expansion)和大括号扩展(Brace Expansion),bash 自身没有提到 wildcard 这个概念。

路径名扩展

查看 man bash 的 Pathname Expansion 小节说明,对路径名扩展支持的特殊字符说明如下:

  • 星号 * 可以匹配任意字符串,包括空字符串,具体匹配内容跟它前面的字符无关。而正则表达式的 * 是匹配零个或连续多个前面的一个字符。这两者都使用了星号 * 作为特殊字符,但是含义不同。
  • 问号 ? 匹配任意一个字符,具体匹配内容跟它前面的字符无关。而扩展正则表达式的 ? 是匹配零个或一个前面的一个字符。这两者的含义不同。
  • 方括号 [...] 匹配方括号内的任意一个字符。如果在左大括号 [ 之后的第一个是 ^,也就是写为 [^...] 的形式,表示匹配除了方括号内字符之外的任意一个字符。这个含义跟正则表达式相同。在 bash 中,[^...] 也可以写为 [!...] 的形式。方括号内支持字符类表达式和范围表达式。例如,[[:alpha]] 匹配任意一个字母。[A-Z] 匹配任意一个大写字母。

注意:这里描述的路径名扩展特殊字符是用于扩展当前目录下的文件名,包括子目录名。所以,虽然 * 可以匹配任意字符串,但是实际扩展之后,它得到的字符串一定是当前目录下文件名的一部分,而且路径名扩展表达式不能用引号括起来。如果不理解这一点,就很容易出现误用。

举例说明如下:

$ cat retestfile
retestfile
renewtestfile
$ grep re*file retestfile
retestfile
$ grep "re*file" retestfile
$ grep 're*file' retestfile

可以看到,在 retestfile 文件中,有 "retestfile"、"renewtestfile" 这两行字符串。

按照 bash 的 * 可以匹配任意字符串的功能,可能认为 grep re*file retestfile 命令可以匹配这两行,也就是认为 re*file* 可以匹配到 "retestfile" 字符串中间的 "test",也能匹配到 "renewtestfile" 字符串中间的 "newtest"。

但是打印结果只匹配了 "retestfile" 这一行。原因在于,当前目录下有一个 retestfile 文件,但是没有 renewtestfile 文件。

即,虽然 re*file* 确实可以匹配到 "renewtestfile" 字符串中间的 "newtest",但是它匹配到的字符串来源是当前目录下文件名的一部分,bash 会对 re*file 进行扩展,得到匹配的当前目录下文件名,再把文件名作为参数传递给 grep 命令,grep 命令收到的参数里面并没有 * 这个字符,也不会基于这个字符的含义来匹配字符串。

事实上,如果当前目录下还有一个 renewtestfile 文件,grep re*file retestfile 命令也匹配不到 "retestfile"、"renewtestfile" 这两行字符串。

打开 bash 调试开关,测试如下:

$ set -x
$ grep re*file retestfile
+ grep --color=auto renewtestfile retestfile retestfile
retestfile:renewtestfile
retestfile:renewtestfile
$ set +x

可以看到,grep re*file retestfile 命令扩展之后的结果是 grep --color=auto renewtestfile retestfile retestfile

基于 grep 命令的格式,这个命令要查找的字符串是 "renewtestfile",后面的两个 retestfile 参数都是要查找的文件名,也就是查找了两次 retestfile 文件,所以打印结果只匹配到 renewtestfile 这一行,并打印了两次。

grep re*file retestfile 命令匹配不到 "retestfile"、"renewtestfile" 这两行字符串后,常见的误区是以为要用引号把 re*file 括起来,但是上面的 grep "re*file" retestfile 命令和 grep 're*file' retestfile 命令什么都没有都打印。

在 bash 中,用引号(无论是双引号,还是单引号)把 * 括起来后,不会再进行路径名扩展,此时,* 匹配的是字符 ‘*’ 自身,在 retestfile 文件中没有 "re*file" 这个字符串,所以什么都没有匹配到。

总的来说,在 bash 的路径名扩展中,*?[...] 这几个特殊字符的扩展结果来自于当前目录下的文件名,并把扩展结果作为参数传递给被执行的命令。

如果匹配到多个结果,会导致实际传递给命令的参数个数发生变化,可能会导致命令处理参数时遇到不预期的结果。

大括号扩展

查看 man bash 的 Brace Expansion 小节说明,对大括号扩展支持的特殊字符说明如下:

Brace expansion is a mechanism by which arbitrary strings may be generated. This mechanism is similar to pathname expansion, but the filenames generated need not exist. Patterns to be brace expanded take the form of an optional preamble, followed by either a series of comma-separated strings or a sequence expression between a pair of braces, followed by an optional postscript. The preamble is prefixed to each string contained within the braces, and the postscript is then appended to each resulting string, expanding left to right.
A sequence expression takes the form {x..y[..incr]}, where x and y are either integers or single characters, and incr, an optional increment, is an integer. When integers are supplied, the expression expands to each number between x and y, inclusive.

即,大括号扩展是一种用于生成任意字符串的机制,类似于路径名扩展,但是大括号扩展的结果不依赖于已有的文件名。其格式有两种:

  • preamble{string1,string2,string3,...}postscript:preamble 是一个可选的前缀字符串,postscript 是一个可选的后缀字符串,一旦提供,所有扩展结果都会带上相应的前缀、或后缀。大括号里面的字符串列表会逐个进行组合,得到多个字符串。例如,a{d,c,b}e 会扩展成 "ade"、"ace"、"abe" 三个字符串。
  • preamble{x..y[..incr]}postscript:preamble 是一个可选的前缀字符串,postscript 是一个可选的后缀字符串,一旦提供,所有扩展结果都会带上相应的前缀、或后缀。大括号里面的 xy 要么是整数,要么是单个字母。而 ..incr 是一个可选的整数增量值。当提供的是整数时,这个表达式扩展成在 x 和 y 之间的每一个数字,包含 x 和 y。当提供的是字母时,这个表达式根据词典顺序扩展成 x 和 y 之间的每一个字母,包含 x 和 y。例如,a{1..4}c 会扩展成 "a1c"、"a2c"、"a3c"、"a4c" 四个字符串。

具体执行结果举例如下:

$ echo a{d,c,b}e
ade ace abe
$ echo a{1..4}c
a1c a2c a3c a4c
$ echo a{1..6..2}c
a1c a3c a5c

跟路径名扩展不同,大括号扩展的结果跟当前目录下的文件名无关。

通配符和正则表达式的对比

基于前面描述的几个 bash 通配符特殊字符,和正则表达式的特殊字符对比如下。

特殊字符 通配符含义 正则表达式含义
* 用于路径名扩展,匹配任意字符串,匹配结果来自于当前目录下的文件名 匹配零个或连续多个前一个字符,跟通配符含义不同
? 用于路径名扩展,匹配任意一个字符,匹配结果来自于当前目录下的文件名 匹配零个或一个前一个字符,跟通配符含义不同
[..] 用于路径名扩展,匹配方括号内的任意一个字符,匹配结果来自于当前目录下的文件名 匹配字符方括号内的任意一个字符,跟通配符含义相近
[^..] 用于路径名扩展,不匹配方括号内的任意一个字符,从当前目录下的文件名中过滤掉匹配结果 不匹配方括号内的任意一个字符,跟通配符含义相近
[!..] 用于路径名扩展,跟 [^..] 含义相同 正则表达式不支持这个写法
{s1,s2,..} 用于大括号扩展,使用大括号内的字符串列表逐个进行组合 正则表达式不支持在大括号内提供三个或更多的字符串。扩展正则表达式支持类似的 {m,n} 写法,至少匹配 m 个前一个字符,最多匹配 n 个前一个字符
{n1..n2} 用于大括号扩展,依次得到 n1 到 n2 之间的数字 正则表达式不支持这个写法

注意:通配符的方括号表达式和正则表达式的方括号表达式类似,可以在方括号里面提供字符类表达式或范围表达式。

具体举例如下:

$ ls *.[c-h]
dfa.c  dfa.h
$ ls *.[[:alpha:]]
dfa.c  dfa.h

这里使用 ls 命令举例,ls 命令不支持解析正则表达式,可以确认是由 bash 解析所给的表达式。

这里的 [c-h] 属于路径名扩展表达式,会匹配字母 c 到 字母 h 之间的任意一个字母,包含 c 和 h 自身。类似的,[[:alpha:]] 也是路径名扩展表达式,匹配任意一个字母。

另外,通配符的字符类表达式支持用 [:word:] 来匹配字母、数字、或下划线 _ 之中的任意一个字符。POSIX 标准的正则表达式没有定义 [:word:] 这个字符类表达式。

由于 bash 通配符的部分特殊字符和正则表达式的特殊字符用了同一个字符,在 bash 中使用这些字符时,一定要确认是想当成通配符处理,还是当成正则表达式处理

如果要当成通配符处理,那么所给的特殊字符不能用引号括起来,由 bash 对这些特殊字符进行扩展。

如果要当成正则表达式处理,那么所给特殊字符要用引号括起来,避免 bash 优先当成通配符处理,扩展成了其他内容。

如果不方便用引号括起来,就要在 bash 中用 \ 进行转义,且不要加引号,\ 在引号内不能进行转义。

阅读 3k

南木阁
考据党一枚,力求讲述的每个知识点都有出处,有理有据。如能恰好地解答您的疑问,欢迎点赞,谢谢!

解读权威文档,编写易懂文章。如有恰好解答您的疑问,多谢赞赏支持~

358 声望
316 粉丝
0 条评论

解读权威文档,编写易懂文章。如有恰好解答您的疑问,多谢赞赏支持~

358 声望
316 粉丝
文章目录
宣传栏