|
Harley Hahn 的
|
|
各章...
命令
教师 |
第 20 章... 正则表达式 (Regular Expressions)
正则表达式用于指定字符模式。正则表达式 最常见的用途是查找字符串。因此, 正则表达式常用于查找与替换操作。 作为一种工具,正则表达式既有用又强大, 以至于专门讨论这个主题的书籍和网站比比皆是。 可以肯定,精通正则表达式的运用之道,是你为 熟练掌握 Unix 所能做的最重要的事情之一。 正则表达式可以写得非常复杂。然而 在大多数情况下,你需要的正则表达式都简单 直接,所以你要做的只是学习几条简单的规则, 然后练习、练习、再练习。本章的目标就是 帮你入门。 注意:在开始之前,我想先提一件事。 本章中我会用 grep 命令(第 19 章) 给你展示大量示例。如果你发现某些正则表达式 功能在你的 grep 版本上不起作用,你可能 需要改用 egrep 或 grep -E。 在这种情况下,你可以设置一个别名,自动使用 这些变体之一。详细信息见第 19 章中关于 grep 和 egrep 的讨论。至于个中 原因,等我们稍后谈到扩展正则表达式与基本 正则表达式时就会明白。
本章中,我用 grep 命令来演示正则 表达式的示例,我们在第 19 章讨论过 grep。这些示例虽然方便,但你要知道, 正则表达式可以用在许多不同的 Unix 程序中, 比如 vi 和 Emacs 文本编辑器、less、 sed 等等。此外,正则表达式还能用于许多 编程语言,比如 Awk、C、C++、C#、Java、Perl、 PHP、Python、Ruby、Tcl 和 VB.NET。 我要教给你的概念适用于一般的正则表达式。 一旦掌握了基本规则,你以后只需要在需要时 学习一些变体即可。虽然正则表达式中一些较为 深奥的特性在不同程序之间可能有所不同 —— 例如 Perl 就有一整套高级特性 —— 但基本思想 始终相同。如果你遇到问题,只需查阅你所用 程序的文档即可。 正则表达式(REGULAR EXPRESSION,常缩写为 REGEX 或 RE)是一种指定字符模式的紧凑方式。 例如,考虑下面这个由三个字符串组成的集合: harley1 harley2 harley3 作为正则表达式,你可以把这个模式集合表示为 harley[123]。 再看一个例子。你想描述这样一组字符串:以 大写字母 "H" 开头,后面跟着任意多个小写字母, 最后以小写字母 "y" 结尾。对应的正则表达式是 H[[:lower:]]*y。 如你所见,正则表达式的威力来自于那些具有 特殊含义的元字符和缩写。我们将在接下来的 几节中讨论细节。作为参考,正则表达式的语法 汇总在图 20-1、20-2 和 20-3 中。现在花点时间 略读一下,在阅读本章其余内容时可以随时 回头查阅。 Figure 20-1: 正则表达式:基本匹配 正则表达式是一种指定字符模式的紧凑方式。在 正则表达式中,普通字符匹配自身,而某些 元字符具有特殊含义。本表列出了用于完成 基本模式匹配功能的元字符。
Figure 20-2: 正则表达式:重复操作符 在正则表达式中,可以使用下面这些称为重复 操作符的元字符来匹配特定字符的多个实例。 注意:有些程序不支持 {,m},因为它不是标准写法。
Figure 20-3: 正则表达式:预定义字符类 正则表达式中可以使用字符类来定义一组字符。 为了方便,有一些预定义的字符类可以当作 缩写来使用。本表列出了最重要的几个。注意, 方括号和冒号是名称的一部分。 最右边一列列出了一些与预定义字符类等价的 范围。如果你的系统使用 C 排序序列,你可以 用这些范围代替类名。为确保系统确实如此, 你可以把环境变量 LC_COLLATE 设置为 C。(详见正文。)
"正则表达式"这个术语来自计算机科学,指的 是一套指定模式的规则。这个名字源自美国杰出 数学家、计算机科学家 Stephen Kleene (1909–1994)的工作。(他的名字发音为 "Klej-nee"。) 20 世纪 40 年代初,两位神经科学家 Walter Pitts 和 Warren McCulloch 提出了一个 数学模型,描述他们认为的神经元(神经细胞) 的工作方式。作为模型的一部分,他们使用了 一种非常简单、虚构的机器,称为自动机 (automata)。20 世纪 50 年代中期,Kleene 发明了一种用数学方式描述自动机的方法,使用 他所谓的"正则集"(regular sets):即可以用 少数简单性质来描述的集合。Kleene 随后创造 了一种简单的符号体系,称为正则表达式, 用来描述这样的集合。 1966 年,Ken Thompson —— 后来开发出 Unix 的 那位程序员 —— 加入了贝尔实验室。他最早做的 事情之一,就是编写了一个 QED 文本编辑器的 版本,他在加州大学伯克利分校时用过这个编辑器。 Thompson 对 QED 做了大量扩展,增加了一项 模式匹配功能(除了其他特性之外),它使用的 是 Kleene 正则表达式的增强形式。在此之前, 文本编辑器只能查找精确的字符串。而现在, 借助正则表达式,QED 编辑器也能查找模式了。 1969 年,Thompson 创建了第一个原始的 Unix 版本(见第 1 章和第 2 章)。不久之后,他 编写了第一个 Unix 编辑器 ed(发音为 "ee-dee"),设计时使用了正则表达式的一个 简化形式,功能不如他在 QED 中用的那样强大。 ed 程序是所谓 UNIX Version 1 的一部分, 该系统于 1971 年发布。 ed 的流行带动了正则表达式在 grep 中的应用,后来又应用于许多其他 Unix 程序。如今,正则表达式的应用十分广泛, 不仅在 Unix 内部,而且遍及整个计算机世界。 (有趣的是,Thompson 当年编写 ed 时舍弃 的那些特性,现在又都被加回来了。) 提示 下次你参加 Unix 聚会、大家开始谈论正则表达式 时,你可以不经意地提一句:它们对应于乔姆斯基 层次(Chomsky hierarchy)中的 3 型文法。一旦 吸引了所有人的注意,你就可以接着解释:任何 正则表达式都可以与 NFA(非确定性有限自动机) 之间建立一个简单的映射,因为每个正则表达式 长度有限,因此操作符的数量也有限。 转眼间,你就会成为全场最受欢迎的人。(*) * 脚注 反正这招对我一直很灵。
本节是一份参考资料,第一遍阅读时 可能让人有点摸不着头脑。别担心。等你读完 本章剩下的内容并稍加练习之后,一切就都 说得通了。 正如我们在上一节所讨论的,当 Ken Thompson 创建 ed 文本编辑器时,正则表达式成了 Unix 的一部分;该编辑器随 UNIX Version 1 于 1971 年发布。最初的正则表达式功能很有用, 但也很有限,多年来已被大幅扩展。这就产生了 各种复杂程度不一的 regex 系统,容易让初学者 感到困惑。 就实际使用而言,你只需要记住几个基本观念。 我想现在花一点时间讲讲这些观念,这样等你 以后用本章示例做练习时就不会遇到麻烦。 Unix 支持两种主要的正则表达式变体:现代版本 和较旧的过时的版本。现代版本是扩展正则表达式 (EXTENDED REGULAR EXPRESSION,简称 ERE)。 它是当前的标准,是 IEEE 1003.2 总标准的 一部分(属于 POSIX;见第 11 章)。 较旧的版本是基本正则表达式(BASIC REGULAR EXPRESSION,简称 BRE)。它是一种较为原始的 正则表达式,使用多年,直到被 1003.2 标准化 所取代。基本正则表达式不如扩展正则表达式 强大,语法也稍微更令人困惑一些。因此,BRE 现在被认为是过时的,只是为了与旧程序保持 兼容才保留下来。 在本章中,我要教给你的是扩展正则表达式, 也就是现代 Unix 和 Linux 系统的默认类型。 不过,你有时会遇到只接受基本正则表达式的 旧程序。在这种情况下,我希望你知道自己在 做什么,所以这里先做几点说明。 最可能给你带来麻烦的两个命令是 grep 和 sed(两者都在第 19 章中介绍)。要 了解它们在你的系统上支持哪种正则表达式, 请查看 man 手册页:
man grep
如果你的系统使用 GNU 工具集 —— Linux 和 FreeBSD 就是如此 —— 你会发现一些命令已经 更新,提供了 -E 选项,允许你使用扩展 正则表达式。grep 就是一例。一般说来, 你可以通过查看命令的 man 手册页,或者使用 --help 选项显示其语法(见第 10 章), 来判断某个命令是否提供 -E 选项。 提示 在 Linux 和 FreeBSD 上,一些命令提供 -E 选项,允许你使用扩展正则表达式。 由于扩展 regex 总是更可取,你应当养成使用 -E 的习惯。 如果你经常使用某个这样的命令,可以创建一个 别名,让 -E 自动生效。有关如何做到的 示例,见第 19 章中关于 grep 和 egrep 的讨论。 尽管扩展正则表达式是现代标准,尽管一些程序 提供了 -E 选项,但有时你别无选择, 只能使用基本正则表达式。例如,你可能想使用 一个旧程序,而它在你的系统上只支持基本 正则表达式。(最常见的例子就是 sed。) 在这种情况下,懂得如何应对对你很有必要, 所以我要花一点时间解释基本正则表达式与 扩展正则表达式之间的区别。当然,这段讨论 有点为时过早,因为我们还没谈到 regex 的 技术细节。不过,正如我之前所说,第一遍 阅读时有什么不明白的地方,等你回头再看时 就会懂了。 正如我在本章前面提到的,正则表达式的威力 来自于使用具有特殊含义的元字符。本章将花 大量时间讨论如何使用这些元字符。(作为参考, 它们汇总在图 20-1、20-2 和 20-3 中。) 基本正则表达式与扩展正则表达式的最大区别 在于:使用基本 regex 时,某些元字符无法使用, 另一些则必须用反斜杠引用。(引用问题在第 13 章讨论。)无法使用的元字符是问号、加号和 竖线: ? + | 必须转义的元字符是花括号和圆括号: { } ( ) 作为参考,我在图 20-4 中汇总了这些限制。 第一次看这张表时你可能不明所以,但等你读完 本章,它就会变得一目了然。 Figure 20-4: 扩展正则表达式与基本正则表达式 正则表达式的现代标准是扩展正则表达式(ERE), 它作为 1003.2 POSIX 标准的一部分而定义。 扩展正则表达式取代了较旧的基本正则表达式 (BRE)。作为参考,本表列出了基本正则表达式 的限制,可以概括为以下几点:
1. 花括号必须用反斜杠引用。
正如我之前所解释的,正则表达式(简称 regex) 是一种指定字符模式的紧凑方式。要创建正则 表达式,你按照一定的规则把普通字符和 元字符组合在一起,然后用这个 regex 去查找 你想找到的字符串。 当一个正则表达式与某个特定的字符串相对应时, 我们就说它匹配(MATCHES)这个字符串。例如, regex harley[123] 匹配 harley1、 harley2 或 harley3 中的任何一个。 目前你不必纠结细节,只要认识到 harley 和 123 是普通字符,而 [ 和 ] 是元字符即可。换一种说法就是:在 正则表达式中,harley 和 123 匹配它们自身,而 [ 和 ](方括号) 字符具有特殊含义。最终,你会学完所有元字符 及其特殊含义。 在这一节中,我们将介绍一类称为锚点(ANCHORS) 的元字符,它们用于匹配字符串开头或结尾的 位置。例如,regex harley$ 匹配字符串 harley,但仅当它出现在行末时才是如此。 这是因为 $ 是一个元字符,它作为锚点 匹配行的结尾。(现在先不用担心细节。) 为了开始正则表达式的冒险之旅,我们先从基本 规则出发:所有普通字符,比如字母和数字, 都匹配自身。下面几个例子演示它是如何工作的。 假设你有一个名为 data 的文件,包含 以下四行:
Harley is smart
你想用 grep 找出所有在任意位置包含 "Harley" 的行。你可以这样用: grep Harley data 在这个例子中,Harley 实际上就是一个 正则表达式,它会让 grep 选出第 1、2、3 行,而不选第 4 行:
Harley is smart
这没什么新鲜的,但它说明了在正则表达式中, 一个 H 匹配 "H",一个 a 匹配 "a",一个 r 匹配 "r",依此类推。所有 regex 都源自这一基本思想。 要扩展正则表达式的能力,你可以使用锚点来 指定你要查找的模式的位置。^(抑扬符) 元字符是一个锚点,匹配行的开头。因此,要只 查找以 "Harley" 开头的行,你可以这样用: grep '^Harley' data 在我们的示例中,这条命令会选出第 1 行和第 2 行,而不选第 3、4 行(因为它们不是以 "Harley" 开头):
Harley is smart
你会注意到,在上一条命令中,我给正则表达式 加了引号。只要使用的 regex 包含元字符,你就 应当这样做,以确保 Shell 不去理会这些字符, 而是把它们原样传给程序(在本例中是 grep)。如果你不确定是否需要给正则 表达式加引号,那就加吧,这不会带来任何问题。 你会注意到,为了保险起见,我用了强引号 (单引号)而不是弱引号(双引号)。这能确保 所有元字符(而不只是其中一部分)都被正确 引用。(如果你想复习强引号与弱引号的区别, 见第 13 章。) 匹配行结尾的锚点是 $(美元符号) 元字符。例如,要只查找以 "Harley" 结尾的行, 你可以这样用: grep 'Harley$' data 在我们的示例中,这只会选出第 2 行和第 3 行:
Harley
只要做法合理,你可以在同一个正则表达式中 组合使用 ^ 和 $。例如,要查找 整行内容恰好是 "Harley" 的所有行,你会同时 使用两个锚点: grep '^Harley$' data 在我们的示例中,这会选出第 2 行,因为只有它 既以 "Harley" 开头又以 "Harley" 结尾: Harley 把两个锚点连用、中间什么都不放,是查找空行 的简单方法。例如,下面这条命令统计文件 data 中所有空行的行数: grep '^$' data | wc -l 同样,也有锚点可以用来匹配单词的开头或结尾, 或者两者同时匹配。要匹配单词的开头,使用两字符 组合 \<。要匹配单词的结尾,使用 \>。 例如,假设你要在一个名为 data 的文件中 查找所有包含字母 "kn" 的行,但仅当 "kn" 出现在 单词开头时才算数。你可以这样用: grep '\<kn' data 要查找字母 "ow",但仅限出现在单词结尾时,用: grep 'ow\>' data 要查找完整的单词,就把 \< 和 \> 一起用。例如,要查找 "know", 但仅作为完整单词查找,用: grep '\<know\>' data 这条命令会选出这一行: I know who you are, and I saw what you did. 但不会选出这一行: Who knows what evil lurks in the hearts of men? 作为方便之计,在使用 GNU 工具集的系统上 —— 比如 Linux 和 FreeBSD —— 你可以把 \b 用作替代锚点,同时代替 \< 和 \>。例如,下面两条命令是等价的:
grep '\<know\>' data
你可以把 \b 理解为"边界标记" (boundary marker)。 选择哪种单词边界锚点完全取决于你的喜好。我用 \< 和 \>,因为在我看来它们 更容易看清。不过,很多人更喜欢 \b,因为 它更便于输入,而且在单词的开头和结尾可以用同 一个锚点。 在使用正则表达式时,"单词"的定义比在英语中 更灵活。在 regex 中,单词(WORD)是自成一体的 连续字符序列,由字母、数字或 _(下划线) 字符组成。因此,在 regex 中,下面这些都被视为 单词: fussbudget Weedly 1952 error_code_5 许多 Unix 程序也采用同样的定义。例如,当你使用 -w 选项匹配完整单词时,grep 就用 这个定义。 提示 用 grep 查找完整单词时,使用 -w(word)选项往往比多次使用 \< 和 \> 或 \b 更省事。 例如,下面三条命令是等价的:
grep -w 'cat' data
在正则表达式中,元字符 .(点)匹配除 换行符以外的任意单个字符。(在 Unix 中,换行符 标记一行的结尾;见第 7 章。) 例如,假设你要在名为 data 的文件中查找 所有符合下面模式的行:字母 "Har",后面跟着任意 两个字符,再后面是字母 "y"。你可以用这条命令: grep 'Har..y' data 这条命令会找到包含如下内容的行,例如: harley harxxy harlly har12y 你会发现 . 元字符非常有用,而且会经常 用到它。尽管如此,有时你还是想更精确一些: . 会匹配任意字符,但你可能只想 匹配特定的字符。例如,你可能想查找大写的 "H", 后面跟着 "a" 或 "A"。在这种情况下,你可以通过把 字符放在方括号 [ ] 内来指定你想查找的 字符。这样的构造称为字符类(CHARACTER CLASS)。 例如,要在文件 data 中查找所有包含字母 "H"、后面跟着 "a" 或 "A" 的行,你可以这样用: grep 'H[aA]' data 在继续之前,我想强调一个重要观点。严格说来, 字符类并不包括方括号。例如,在上一条命令中, 字符类是 aA,而不是 [aA]。虽然 使用字符类时必须有方括号,但方括号并不算字符类 本身的一部分。当我们谈到称为"预定义字符类"的 特殊缩写时,这个区别就会很重要。 接下来是一个在同一个正则表达式中使用多个字符类 的例子。下面这条命令查找包含单词 "license" 的 行,即使有人把 "c" 和 "s" 弄混而拼错了它: grep 'li[cs]en[cs]e' data 为了让命令更有用,我们可以用 \< 和 \> 或 \b,只匹配完整的单词:
grep '\<li[cs]en[cs]e\>' data
这两条命令会匹配下面任何一种拼法: licence license lisence lisense
有些字符集合太常用了,以至于人们给它们起了名字, 以便使用。这些集合称为预定义字符类(PREDEFINED CHARACTER CLASSES),在本章前面的图 20-3 中就能 看到。(现在先停下来看一眼,再继续往下读,我要 你熟悉各个名称及其含义。) 使用预定义字符类很直截了当,只有一条奇怪的规则: 方括号实际上就是名称的一部分。因此,使用它们时, 你必须再加上第二对方括号才能保持语法正确。(你 还记得吧,前面我说过,使用字符类时,外面的方括号 不属于该字符类。) 例如,下面这条命令用 grep 找出名为 data 的文件中所有包含数字 21、后面紧跟 一个单个小写或大写字母的行: grep '21[[:alpha:]]' data 下一条命令找出所有包含连续两个大写字母、后面 跟一个数字、再跟一个小写字母的行: grep '[[:upper:]][[:upper:]][[:digit:]][[:lower:]]' data 除了预定义字符类,还有另一种指定一组字母或数字 的方法:使用字符范围(RANGE),即由连字符分隔的 首尾两个字符。例如,要查找文件 data 中 所有包含 3 到 7 之间某个数字的行,你可以用: grep '[3-7]' data 范围 0-9 与 [:digit:] 含义相同。 例如,要查找包含大写字母 "X"、后面跟着任意两个 数字的行,可以用下面两条命令中的任何一条:
grep 'X[0-9][0-9]' data
作为本节的结尾,我们再考虑一种情况:你想匹配 不在某个特定字符类之内的字符。你只需在 左方括号之后加上一个 ^(抑扬符)元字符 即可做到。在这种场合,^ 起的是取反操作符 的作用。 例如,下面这条命令在名为 data 的文件中 查找所有包含字母 "X" 的行,只要 "X" 后面跟的 不是 "a" 或 "o": grep 'X[^ao]' data 下面两条命令找出所有至少包含一个非字母字符的 行:
grep '[^A-Za-z]' data
提示 理解复杂正则表达式的诀窍,是记住每一条字符类 —— 不管它看上去多么复杂 —— 都只代表单个字符。
读到这里,你可能会想知道:能不能用其他范围来 代替预定义字符类?例如,能否用 a-z 代替 [:lower:]?类似地,能否用 A-Z 代替 [:upper:];用 A-Za-z 代替 [:alpha:];用 a-zA-Z0-9 代替 [:alnum:]? 答案是:看情况。在某些系统上可行,在另一些系统 上则不行。在我能向你解释个中缘由之前,我们需要 先谈谈"区域设置"(locale)这个概念。 当你写 0-9 时,它是 0123...9 的缩写,这当然说得通。 然而,当你写 a-z 时,它未必意味着 abcd...z。这是因为字母表在你所用 系统上的排列顺序,取决于所谓的"区域设置" (locale),而它在不同系统之间可能有所不同。 为什么会这样呢?在 20 世纪 90 年代之前,Unix (以及大多数计算机系统)使用的字符编码是 ASCII 码(ASCII CODE),常直接称为 ASCII。这个名字来自 "American Standard Code for Information Interchange"(美国信息交换标准代码)的首字母 缩略词。 ASCII 码创建于 1967 年。它为每个字符规定了一个 7 位模式,总共 128 个。这些位模式从 0000000 (十进制 0)到 1111111(十进制 127)。ASCII 码 包含我们在第 7 章讨论过的所有控制字符,以及 56 个可打印字符:字母、数字和标点。可打印字符如下。 (注意第一个字符是空格。)
!"#$%&'()*+,-./0123456789:;<=>?
可打印字符的顺序就是我上面列出的顺序。它们的编号 从 32 号字符(空格)到 126 号字符(波浪号)。 作为参考,完整的 ASCII 码图表见附录 D。(现在 花点时间看一眼。) 作为方便之计,大多数 Unix 系统都有一份包含 ASCII 码的参考页面。这很实用,让你随时都能快速 查阅。遗憾的是,ASCII 码参考页并没有统一标准, 显示它的方式取决于你使用的系统。详见图 20-5。 Figure 20-5: 显示 ASCII 码 你会在本书附录 D 中找到 ASCII 码的汇总。为了便于 联机查阅,大多数 Unix 系统都有一份方便实用的页面, 包含完整的 ASCII 码。传统上,这份页面存放在 /usr/pub/ 目录下一个名为 ascii 的文件里。近年来,一些系统重新组织了 Unix 文件系统,ASCII 参考文件被移到了 /usr/share/misc。在另一些系统上,这份 文件已被转换为联机手册中的一个页面。因此,显示 ASCII 参考页面的方式取决于你所使用的系统。
正如我们在第 19 章所讨论的,在一种字符编码方案 中,字符的排列顺序称为排序序列(collating sequence)。凡是需要给字符排序的场合都会用到 排序序列,例如对数据排序,或者在正则表达式中使用 范围。 当你使用 Unix 或 Linux 时,你的系统采用的排序 序列取决于你的区域设置。区域设置这一概念是 POSIX 1003.2 标准的一部分,我们在第 11 章和 第 19 章都讨论过。正如我在第 19 章所解释的, 你的区域设置 —— 由一个环境变量设定 —— 告诉 你的程序你想使用哪种语言约定。这使得世界上任何 人都可以选择与自己本地语言相匹配的区域设置。 在一些 Unix 系统上,区域设置被配置成 默认排序序列与 ASCII 码中字符的顺序一致。具体 而言,如上所示,所有大写字母归为一组,排在小写 字母之前。这种序列称为 C 排序序列(第 19 章), 因为 C 编程语言使用的就是它。 在另一些 Unix 系统上,包括许多 Linux 系统, 区域设置的配置方式使默认排序序列把大写和小写 字母成对归组:aAbBcCdD...zZ。 这种排序序列的优点在于,查找单词或字符时的顺序 与词典中的查找顺序一致。因此,它被称为词典排序 序列(dictionary collating sequence)(第 19 章)。 在使用正则表达式时,你可能会遇到麻烦,因为 Unix 会按照你系统所用的排序序列来展开 a-z 和 A-Z。 如果你使用 C 排序序列,所有大写字母自成一族, 所有小写字母也自成一族。这意味着,当你想指定 全部大写或小写字母时,可以用范围来代替预定义 字符类:用 A-Z 代替 [:upper:];用 a-z 代替 [:lower:];用 A-Za-z 代替 [:alpha:];用 A-Za-z0-9 代替 [:alnum:]。你在图 20-3 中就能看到这一点。 如果你使用词典排序序列,字母的顺序就不同了: AaBbCcDd...Zz。这意味着范围的 工作方式也会不同。例如,a-z 将表示 aBbCc...YyZz。注意,大写字母 "A" 不在其中。(你能明白为什么吗?)同样,A-Z 将表示 AaBbCc...YyZ。注意小写字母 "z" 不在其中。 举个例子,假设你要在文件 data 中查找 所有包含从 "A" 到 "E" 的某个大写或小写字母的 行。如果你的区域设置使用 C 排序序列,你会用: grep '[A-Ea-e]' data 在这种情况下,这个 regex 等价于 ABCDEabcde,这也是大多数经验丰富的 Unix 用户所期望的。然而,如果你的区域设置使用词典排序 序列,你会用: grep '[A-e]' data 传统上,Unix 使用 C 排序序列,许多资深 Unix 用户想当然地认为 a-z 总是指(仅指)小写 字母,A-Z 总是指(仅指)大写字母。然而, 这是一个不可靠的假设,因为某些类型的 Unix,包括 许多 Linux 发行版,默认使用的是词典排序序列, 而不是 C 排序序列。不过,无论你的区域设置的默认 排序序列是哪一个,都有办法确保它使用 C 排序序列, 而这正是大多数 Unix 用户所偏爱的。 首先,你需要判断你的系统默认使用哪种排序序列。 方法是用下面这条命令创建一个名为 data 的 简短文件: cat > data 键入下面两行,然后按 ^D 结束命令:
A
现在键入下面这条命令: grep '[a-z]' data 如果输出包含文件中的两行(A 和 a),说明你使用的是词典排序序列。如果输出 只包含 a 那一行,说明你使用的是 C 排序 序列。(这是为什么?) 如果你的系统使用 C 排序序列,你不需要做任何改动。 不过,请接着读完本节的其余部分,因为总有一天, 你会在别的系统上遇到这个问题。 如果你的系统使用词典排序序列,你多半会想改用 C 排序序列。为此,把名为 LC_COLLATE 的 环境变量设置为 C 或 POSIX。对于 Bourne Shell 家族,下面两条命令中的任何一条都 可以完成任务:
export LC_COLLATE=C
对于 C-Shell 家族,你会用下面两条之一:
setenv LC_COLLATE C
要让这一改动永久生效,你只需把其中一条命令放进 你的登录文件。(登录文件在第 14 章讨论;环境 变量在第 12 章讨论。) 一旦你的系统使用 C 排序序列,你就可以用相应的 范围来替代预定义字符类,如图 20-3 所示。在本章 余下的内容中,我将假定你确实在使用 C 排序序列 (所以,如果你还没有,现在就赶紧把相应的命令写进 登录文件)。 要显示你系统上关于区域设置的信息,可以使用 locale 命令。语法是: locale [-a] 你的区域设置通过设置一系列全局变量来维护,其中 包括 LC_COLLATE。要查看这些变量的当前 值,直接单独输入这条命令: locale 要列出你系统上所有可用的区域设置,使用 -a(all)选项: locale -a
一旦确认自己使用的是 C 排序序列(如上一节所述), 你就有了一定的自由度。当你想匹配所有大写或小写 字母时,既可以用预定义字符类,也可以用范围。 例如,下面两条命令都在名为 data 的文件中 查找所有包含字母 "H"、后面跟着从 "a" 到 "z" 任意一个小写字母的行,比如 "Ha"、"Hb"、"Hc" 等等:
grep 'H[[:lower:]]' data
下面两条命令查找所有包含单个大写或小写字母、 后跟一个数字、再跟一个小写字母的行:
grep '[A-Za-z][0-9][a-z]' data
下面是一个更复杂的例子,查找加拿大的邮政编码。 这些编码的格式是"字母 数字 字母 空格 数字 字母 数字",其中所有字母都是大写,例如 M5P 3G4。花点时间分析这两条命令,直到 你完全理解它们为止: |
|
grep '[A-Z][0-9][A-Z] [0-9][A-Z][0-9]' data
|
|
选择哪种字符类 —— 范围还是预定义名称 —— 完全 取决于你。许多老派 Unix 用户偏爱使用范围,因为 他们当初学的就是它。而且,输入范围比输入名称更省事, 名称还需要冒号和额外的方括号。(见上一个例子。) 不过,名称更易读,更适合用在 Shell 脚本中。 而且,名称的设计目标就是无论你的区域设置或语言 是什么都能正确工作,因此它们更可移植。例如,假设 你处理的文本包含非英文字符,比如 é (带尖音符的 "e")。使用 [:lower:],你 一定能匹配到 é。而如果你用 a-z,就不一定了。
在正则表达式中,单个字符(如 A)或字符类 (如 A-Za-z 或 [:alpha:])只匹配 一个字符。要一次匹配多个字符,你要使用重复操作符 (REPETITION OPERATOR)。 最有用的重复操作符是 *(星号)元字符。 * 匹配前一个字符出现零次或多次。(关于 "零次或多次"这一概念,见第 10 章。)例如,假设 你要在名为 data 的文件中查找所有包含 大写字母 "H"、后面跟着零个或多个小写字母的行。 你可以用下面两条命令中的任何一条:
grep 'H[a-z]*' data
这些命令会找到诸如以下模式: H Har Harley Harmonica Harpoon HarDeeHarHar 最常见的组合是一个 .(点)后面跟着一个 *。这匹配任意字符出现零次或多次。例如, 下面这条命令查找包含 "error"、后面跟着零或多个 任意字符、再跟着 "code" 的行: grep 'error.*code' data 举例来说,这条命令会选出下面这些行:
Be sure to document the error code.
下一个例子查找包含一个冒号、后面跟着零或多个任意 其他字符、再跟着另一个冒号的行: grep ':.*:' data 有时,你可能想匹配一个或多个字符,而不是零或多个。 为此,使用 +(加号)元字符代替 *。例如,下面两条命令在文件 data 中查找包含字母 "variable"、后面跟着一串数字的 行:
grep 'variable[0-9]+' data
这些命令会选出下面这些行中的任意几条:
You can use variable1 if you want.
而不会选出这些行:
Remember to use variable 1, 2 or 3.
如果你想让模式的开头既匹配大写 "V" 又匹配小写 "v",该怎么办?只需把第一个字母改成一个字符类: grep '[vV]ariable[0-9]+' data 下一个重复操作符是 ?(问号)元字符。它 允许你匹配某样东西出现零次或一次。换一种说法就是, ? 使某样东西变成可选的。例如,假设你想 在文件 data 中查找所有包含单词 "color" (美式拼法)或 "colour"(英式拼法)的行。你可以 用: grep 'colou?r' data 最后一组重复操作符允许你用花括号指定任意次数的 匹配,这叫作边界(BOUND)。边界有四种不同类型, 它们是:
注意:第三种写法 {,m} 不属于 POSIX 1003.2 标准,在某些程序中无法使用。 下面举几个例子。第一个例子精确匹配 3 个数字; 第二个匹配至少 3 个数字;第三个匹配 5 个或更少 的数字;最后一个例子匹配 3 到 5 个数字。
[0-9]{3}
为了演示边界与 grep 的配合用法,下面这条 命令在名为 data 的文件中查找所有包含 2 位 或 3 位数字的行。注意这里用 \< 和 \> 来匹配完整的数字: grep '\<[0-9]{2,3}\>' data 到目前为止,我们只把重复操作符用在单个字符上。 如果把多个字符放在圆括号里,就可以把它们用在 多个字符上。这样的模式称为分组(GROUP)。创建 分组之后,你就可以把一串字符当作一个整体来对待。 例如,要匹配连续 5 次的字母 "xyz",你可以用下面 两个正则表达式中的任何一个:
xyzxyzxyzxyzxyz
最后一个重复操作符是 |(竖线)字符,它 让我们可以使用交替。也就是说,我们可以匹配这个 或者那个。例如,假设我们要在文件中查找所有包含 下面任意一个单词的行: cat dog bird hamster 用交替就很简单了: grep 'cat|dog|bird|hamster' data 显然,这是一个强大的工具。不过在这个例子中,你能 看出问题吗?我们查找的是字符串,而不是完整的单词。 因此,上面的命令还会找到包含 "concatenate" 或 "dogmatic" 这类单词的行。要只查找完整的单词,我们 必须显式地匹配单词边界: grep '\<(cat|dog|bird|hamster)\>' data 注意这里用圆括号创建了一个分组。这让我们能够把 整个模式当作一个整体。花点时间思考一下,直到你 想通为止。 再看一个例子。假设我们要在文件 data 中 查找所有包含 "pathname"、"filename" 或 "basename" 这三个单词之一的行。(这些是我们将在 第 24 章遇到的三个术语。)下面两个正则表达式都 能匹配这些单词,尽管第二个 regex 更简洁:
pathname|filename|basename
再加上对单词边界的匹配,下面两条等价命令就能 完成任务:
grep '\<pathname|filename|basename\>' data
为了给本节收尾,我再解释最后一个元字符。你已知 道,元字符在正则表达式中具有特殊含义。那么问题来了: 如果你想匹配这些字符本身怎么办?例如,如果你想 匹配一个真正的 *(星号)、.(点) 或 |(竖线)字符,该怎么办? 答案是:你可以用 \(反斜杠)来引用这个 字符。这会让它从元字符变成普通字符,从而按字面 意义被解释。例如,要在文件 data 中查找 所有包含 "$" 字符的行,用: grep '\$' data 如果你想查找反斜杠字符本身,就连续使用两个 反斜杠。例如,要查找所有包含字符 "\*"、后面 跟着任意多个字符、再跟着一个或多个字母、最后是 "$" 的行,用: grep '\\\*.*[A-Za-z]+\$' data
一旦你理解了规则,大多数正则表达式都很容易写。 但它们读起来可能很难,尤其是篇幅很长的时候。 实际上,有经验的 Unix 用户常常连自己写过的正则 表达式都难以理解(*)。这里介绍一个我多年来总结 出来的简单技巧,可以帮助你理解那些看似晦涩的 regex。 * 脚注 因此就有了那个古老的谜语:"如果上帝无所不能, 他能否创造出一个连自己都看不懂的正则表达式?" 毫无疑问,托马斯·阿奎那在《神学大全》 (The Summa Theologica)中写的正是这个问题: "当我们说上帝能做所有事情时,'所有'一词的确切 含义可能令人存疑。" 当你遇到一个让你头疼的正则表达式时,把它写在纸上。 然后把 regex 拆分成若干部分,自上而下竖着写。 逐一处理每个部分,把它的含义写在同一行。例如, 考虑这个 regex: \\\*.*[A-Za-z]+\$ 我们可以把它分解如下:
作为本章讨论的收尾,我给你出三个有趣的谜题, 我们将用正则表达式来解答。解前两个谜题时,我们 要用到一个本身就很有趣的文件 —— 字典文件 (DICTIONARY FILE)。 字典文件从 Unix 诞生之初就包含在系统里,它是一 份极长的英语单词清单,其中包括简编词典里常见的 大部分单词。每个单词独占一行,各行按字母顺序 排列,这使得文件很容易查找。一旦你习惯了富有 创意地使用字典文件,你就能做出各种令人惊奇的 事情。一些 Unix 命令,比如 look(见第 19 章),就利用字典文件来完成它们的工作。 字典文件的名字是 words。在早期版本的 Unix 中,words 文件存放在名为 /usr/dict 的目录里。然而近年来,Unix 文件结构经过了重组,在大多数现代系统上 —— 包括 Linux 和 FreeBSD —— words 文件 存放在名为 /usr/share/dict 的目录中。 在少数系统上,比如 Solaris,该文件存放在 /usr/share/lib/dict。因此,字典文件的 路径名可能因系统而异。(我们将在第 23 章讨论 Unix 文件系统和路径名。) 作为参考,下面是你最可能找到字典文件的位置:
/usr/share/dict/words
在下面的示例中,我将使用第一个路径名,它最常见。 如果这个名字在你的系统上不管用,试试其他几个。 首先来一个简单的谜题。所有以 "qu" 开头、以 "y" 结尾的英语单词有哪些?要解这个谜题,我们只需要 用下面这个正则表达式去 grep(*)字典文件: grep '^qu[a-z]+y$' /usr/share/dict/words * 脚注 正如我在第 19 章提到的,"grep" 一词常被用作 动词。 为了理解这个正则表达式,我们使用前面提到的技巧。 把 regex 拆分成若干部分,自上而下竖着写。分解 结果如下:
记住,字典文件的每一行只包含一个单词。因此, 我们的查找从行首开始,到行尾结束。 下一个谜题是个老谜题。找出一个常见英语单词, 它按顺序包含全部五个元音字母 —— a、e、i、o、u。 这些字母不必相邻,但必须按字母表顺序出现。也就是 说,"a" 必须在 "e" 之前,"e" 必须在 "i" 之前, 依此类推。 要解这个谜题,我们可以 grep 字典文件,查找包含 字母 "a"、后跟零个或多个小写字母、再跟 "e"、 再跟零个或多个小写字母,依此类推的单词。这一次, 我们先把各个部分写下来,然后再拼装起来。当你构造 复杂的正则表达式时,这往往是一个有用的技巧:
于是,完整的命令是: grep 'a[a-z]*e[a-z]*i[a-z]*o[a-z]*u' /usr/share/dict/words 为了避免卖关子,我现在就告诉你:你应该能找到 若干个单词,其中大多生僻。不过,常见的只有三个, 它们是(*):
adventitious
* 脚注 严格说来,英语中有六个元音字母:a、e、i、o、u 和(有时是)y。如果你想要包含全部六个元音的单词, 把上面三个词变成副词即可:"adventitiously"、 "facetiously" 和 "sacrilegiously"。 最后一个谜题涉及在 Unix 文件系统中搜寻历史遗存。 许多最早的 Unix 命令只有两个字母:文本编辑器是 ed,复制程序是 cp,等等。我们要找出 所有这样的命令。 要解这个谜题,你需要知道:最古老的 Unix 程序都 存放在 /bin 目录中。要列出该目录下的所有 文件,我们使用 ls 命令(见第 24 章): ls /bin 要分析 ls 的输出,我们可以把它用管道 传给 grep。当我们这样做时,ls 会 自动把每个名字放在单独一行上,因为输出是送往 一个过滤器的。接着用 grep,我们就可以查找 仅由两个小写字母组成的行。完整的管道命令如下: ls /bin | grep '^[a-z]{2}$' 在某些系统上,grep 不会返回你想要的结果, 因为它不把花括号识别为元字符。如果你遇到这种 情况,有两个选择。你可以改用 egrep: ls /bin | egrep '^[a-z]{2}$' 或者,你可以干脆不用花括号。只要重复写字符类, 就不需要使用边界了: ls /bin | grep '^[a-z][a-z]$' 在你的系统上试试这些命令,看看能发现什么。当 你看到某个名字、想进一步了解对应的命令时,到 联机手册里查一查即可(第 9 章)。例如: man ed cp 除了你在 /bin 中找到的文件之外, /usr/bin 中还有其他一些古老的 Unix 命令。 要在该目录中查找两字符的命令名,只需把上一条 命令稍加改动。 ls /usr/bin | grep '^[a-z]{2}$' 要统计这样的命令有多少个,给 grep 加上 -c(count,计数)选项:
ls /bin | grep -c '^[a-z]{2}$'
注意:查看 /usr/bin 时,你可能会发现一些 两字符命令并不古老。要判断某个命令是否源自 Unix 早期,可以查看它的 man 手册页。
复习问题 #1: 什么是正则表达式? "regular expression"(正则表达式)的两个常见 缩写是什么? 复习问题 #2: 在正则表达式中,解释下列元字符各匹配什么: . ^ $ \< \> [list] [^list] 解释下列重复操作符各匹配什么: * + ? {n} 复习问题 #3: 对于下列每个预定义字符类,给出定义并指出 等价的范围: [:lower:] [:upper:] [:alpha:] [:digit:] [:alnum:] 例如,[:lower:] 代表所有小写字母; 等价的范围是 a-z。 复习问题 #4: 你的系统默认使用词典排序序列,而你想使用 C 排序序列。你该如何更改? 对于 Bourne Shell 家族,你会用什么命令? 对于 C-Shell 家族呢? 你会把这样的命令放进哪个初始化文件? 应用你的知识 #1: 创建正则表达式,匹配:
• "hello"
用 grep 检验你的答案。 应用你的知识 #2: 使用重复操作符,创建正则表达式,匹配:
• "start",后面跟 0 个或多个数字,再跟 "end"
用 grep 检验你的答案。 提示:确保 grep 使用的是扩展正则表达式 (而不是基本正则表达式)。 应用你的知识 #3: 正如我们在第 20 章讨论的,下面两条命令能找出 文件 data 中所有至少包含一个非字母字符 的行:
grep '[^A-Za-z]' data
要找出连一个字母字符都不包含的所有行,你会用 什么命令? 应用你的知识 #4: 在 Usenet 全球讨论组系统中,言论表达的自由 非常重要。然而,让人们能够在愿意时避开冒犯性的 帖子,也同样重要。解决办法是把可能冒犯人的文本 加以编码,让粗心的旁观者看起来只是一堆乱码。 但任何愿意解码的人,都可以轻松地把编码后的文本 还原。 用于编码的体系称为 Rot-13。它的规则如下: 字母表中的每个字母,都被替换为字母表中往后数 第 13 个字母,必要时绕回字母表开头:
B → O... O → B
创建一条单独的命令,从名为 input 的文件 读取内容,用 Rot-13 对文本编码,并把编码后的文本 写到标准输出。然后再创建一条命令,读取 Rot-13 编码后的数据,把它转换回普通文本。 检验你的解法:创建一个名为 input 的文本 文件,先编码,再解码。 进一步思考 #1: "regular expression"(正则表达式)这个术语来自 一个抽象的计算机科学概念。使用这样的名字是好 主意还是坏主意? 如果把 "regular expression" 换成一个更直白的 名字,比如 "pattern matching expression"(模式 匹配表达式)或 "pattern matcher"(模式匹配器), 会有什么大的区别吗?为什么? 进一步思考 #2: 随着为支持国际化而引入区域设置,一些多年来 一直有效的正则表达式模式在某些系统上不再工作 了。特别是,依赖传统 C 排序序列的正则表达式, 在词典排序序列下未必仍然有效。多数情况下, 解决办法是使用预定义字符类来代替范围。例如, 你应当用 [:lower:] 而不是 a-z。 (完整集合见图 20-3。)你如何看待这一安排? 给出三个旧体系更好的理由。 给出三个新体系更好的理由。
© 本书全部内容,2026 年版权所有,Harley Hahn
|