[笔记]快乐的Linux命令行《二十》正则表达式

120 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第21天,点击查看活动详情

前言

正如我们所见到的,在类似于 Unix 的操作系统中,比如 Linux 中,文本数据起着举足轻重的作用。

但是在我们能完全理解这些工具提供的所有功能之前,我们不得不先看看,经常与这些工具的高级使用相关联的一门技术——正则表达式.

一、正则表达式

什么是正则表达式?

正则表达式 是一种符号表示法,被用来识别文本模式。

在某种程度上,它们与匹配文件和路径名的 shell 通配符比较相似,但其规模更庞大。

许多命令行工具和大多数的编程语言都支持正则表达式,以此来帮助解决文本操作问题。

然而,并不是所有的正则表达式都是一样的,这就进一步混淆了事情;

不同工具以及不同语言之间的正则表达式都略有差异。

我们将会限定 POSIX 标准中描述的正则表达式(其包括了大多数的命令行工具),供我们讨论,与许多编程语言(最著名的 Perl 语言)相反,它们使用了更多和更丰富的符号集。

1.0 grep命令

我们将使用的主要程序是我们的老朋友,grep 程序,它会用到正则表达式。

实际上,grep这个名字来自于短语“global regular expression print”,所以我们能看出 grep 程序和正则表达式有关联。

本质上,grep 程序会在文本文件中查找一个指定的正则表达式,并把匹配行输出到标准输出。

到目前为止,我们已经使用 grep 程序查找了固定的字符串,就像这样:

[me@linuxbox ~]$ ls /usr/bin | grep zip

这个命令会列出,位于目录/usr/bin 中,文件名中包含子字符串 zip 的所有文件。

这个 grep 程序以这样的方式来接受选项和参数:

grep [options] regex [file...]

image.png

简单使用

我们能够对我们的文件列表执行简单的搜索,像这样:

[me@linuxbox ~]$ grep bzip dirlist*.txt
``dirlist-bin.txt:bzip2
dirlist-bin.txt:bzip2recover

grep 程序在所有列出的文件中搜索字符串 bzip,然后找到两个匹配项,其都在文件 dirlist-bin.txt 中。

grep -l 查看包含的文件列表

如果我们只是对包含匹配项的文件列表,而不是对匹配项本身感兴趣的话,我们可以指定 -l 选项:

[me@linuxbox ~]$ grep -l bzip dirlist*.txt
dirlist-bin.txt

grep -L 查看不包含匹配项的文件列表

如果我们只想查看不包含匹配项的文件列表,我们可以这样操作:

[me@linuxbox ~]$ grep -L bzip dirlist*.txt
dirlist-sbin.txt
dirlist-usr-bin.txt
dirlist-usr-sbin.txt

除了原义字符之外,正则表达式也可能包含元字符,其被用来指定更复杂的匹配项。

正则表达式元字符由以下字符组成:

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

其它所有字符都被认为是原义字符.

  1. 虽然在个别情况下,反斜杠会被用来创建元序列,也允许元字符被转义为原义字符,而不是被解释为元字符。

注意:

正如我们所见到的,当 shell 执行展开的时候,许多正则表达式元字符,也是对 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 程序不匹配。

另外,如果我们的文件列表中有一些文件的扩展名是.zip,则它们也会成为匹配项,因为文件扩展名中的圆点符号也会被看作是“任意字符”。

1.1 中括号表达式和字符类

除了能够在正则表达式中的给定位置匹配任意字符之外,通过使用中括号表达式,我们也能够从一个指定的字符集合中匹配一个单个的字符。通过中括号表达式,我们能够指定一个字符集合(包含在不加中括号的情况下会被解释为元字符的字符)来被匹配。

在这个例子里,使用了一个两个字符的集合:

[me@linuxbox ~]$ grep -h '[bg]zip' dirlist*.txt
bzip2
bzip2recover
gzip

我们匹配包含字符串“bzip”或者“gzip”的任意行。

一个字符集合可能包含任意多个字符,并且元字符被放置到中括号里面后会失去了它们的特殊含义。

然而,在两种情况下,会在中括号表达式中使用元字符,并且有着不同的含义。第一个元字符是插入字符,其被用来表示否定;第二个是连字符字符,其被用来表示一个字符区域。

如果在正则表示式中的第一个字符是一个插入字符,则剩余的字符被看作是不会在给定的字符位置出现的字符集合。

1.2 传统的字符区域

如果我们想要构建一个正则表达式,它可以在我们的列表中找到每个以大写字母开头的文件,

我们可以这样做:

[me@linuxbox ~]$ grep -h '^[ABCDEFGHIJKLMNOPQRSTUVWXZY]' dirlist*.txt

这只是一个在正则表达式中输入 26 个大写字母的问题。但是输入所有字母非常令人烦恼,所以有另外一种方式:

[me@linuxbox ~]$ grep -h '^[A-Z]' dirlist*.txt
MAKEDEV
ControlPanel
GET
HEAD
POST
X
X11
Xorg
MAKEFLOPPIES
NetworkManager
NetworkManagerDispatcher

通过使用一个三字符区域,我们能够缩写 26 个字母。任意字符的区域都能按照这种方式表达,包括多个区域,比如下面这个表达式就匹配了所有以字母和数字开头的文件名:

[me@linuxbox ~]$ grep -h '^[A-Za-z0-9]' dirlist*.txt

在字符区域中,我们看到这个连字符被特殊对待,所以我们怎样在一个正则表达式中包含一个连字符呢?方法就是使连字符成为表达式中的第一个字符。

考虑一下这两个例子:

[me@linuxbox ~]$ grep -h '[A-Z]' dirlist*.txt

这会匹配包含一个大写字母的文件名。然而:

[me@linuxbox ~]$ grep -h '[-AZ]' dirlist*.txt

上面的表达式会匹配包含一个连字符,或一个大写字母“A”,或一个大写字母“Z”的文件名。

1.3 POSIX 字符集

这个传统的字符区域在处理快速地指定字符集合的问题方面,是一个易于理解的和有效的方式。

不幸地是,它们不总是工作。

到目前为止,虽然我们在使用 grep 程序的时候没有遇到任何问题,但是我们可能在使用其它程序的时候会遭遇困难。

回到第 5 章,我们看看通配符怎样被用来完成路径名展开操作。

在那次讨论中,我们说过在某种程度上,那个字符区域被使用的方式几乎与在正则表达式中的用法一样,但是有一个问题:

[me@linuxbox ~]$ ls /usr/sbin/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]*
/usr/sbin/MAKEFLOPPIES
/usr/sbin/NetworkManagerDispatcher
/usr/sbin/NetworkManager

(依赖于不同的 Linux 发行版,我们将得到不同的文件列表,有可能是一个空列表。这个例子来自于 Ubuntu)这个命令产生了期望的结果——只有以大写字母开头的文件名,但是:

[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)都是控制码(如 tabs,backspaces,和回车)。随后的 32 个字符(32 - 63)包含可打印的字符,包括大多数的标点符号和数字 0 到 9。再随后的 32 个字符(64 - 95)包含大写字符和一些更多的标点符号。最后的 31 个字符(96 - 127)包含小写字母和更多的标点符号。

基于这种安排方式,系统使用这种排序规则的 ASCII:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

这个不同于正常的字典顺序,其像这样:

aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ

随着 Unix 系统的知名度在美国之外的国家传播开来,就需要支持不在 U.S. 英语范围内的字符。于是就扩展了这个 ASCII 字符表,使用了整个 8 位,添加了字符(数字 128 - 255),这样就容纳了更多的语言。

为了支持这种能力,POSIX 标准介绍了一种叫做 locale 的概念,其可以被调整,来为某个特殊的区域,选择所需的字符集。通过使用下面这个命令,我们能够查看到我们系统的语言设置

[me@linuxbox ~]$ echo $LANG
en_US.UTF-8

通过这个设置,POSIX 相容的应用程序将会使用字典排列顺序而不是 ASCII 顺序。这就解释了上述命令的行为。当 [A-Z] 字符区域按照字典顺序解释的时候,包含除了小写字母 a 之外的所有字母,因此得到这样的结果。

为了部分地解决这个问题,POSIX 标准包含了大量的字符集,其提供了有用的字符区域。

下表中描述了它们:

image.png

甚至通过字符集,仍然没有便捷的方法来表达部分区域,比如 [A-M]。 通过使用字符集,我们重做上述的例题,看到一个改进的结果:

[me@linuxbox ~]$ ls /usr/sbin/[[:upper:]]*

/usr/sbin/MAKEFLOPPIES

/usr/sbin/NetworkManagerDispatcher

/usr/sbin/NetworkManager

记住,然而,这不是一个正则表达式的例子,而是 shell 正在执行路径名展开操作。我们在 这里展示这个例子,是因为 POSIX 规范的字符集适用于二者。

恢复到传统的排列顺序

通过改变环境变量 LANG 的值,你可以选择让你的系统使用传统的(ASCII)排列规则。

如上所示,这个 LANG 变量包含了语种和字符集。这个值最初由你安装 Linux 系统时所选择的安装语言决定。使用 locale 命令,来查看 locale 的设置。

把这个 LANG 变量设置为 POSIX,来更改 locale,使其使用传统的 Unix 行为。

注意这个改动使系统为它的字符集使用 U.S. 英语(更准确地说,ASCII),所以要确认一下这是否是你真正想要的效果。

通过把这条语句添加到你的.bashrc 文件中,你可以使这个更改永久有效。

1.4 POSIX 基本的 Vs.扩展的正则表达式

就在我们认为这已经非常令人困惑了,我们却发现 POSIX 把正则表达式的实现分成了两类: 本正则表达式(BRE)扩展的正则表达式(ERE)

既服从 POSIX 规范又实现了BRE 的任意应用程序,都支持我们目前研究的所有正则表达式特性。

我们的 grep 程序就是其中一个。

1.5 限定符

扩展的正则表达式支持几种方法,来指定一个元素被匹配的次数

? - 匹配一个元素零次或一次

+ - 匹配一个元素一次或多次

{ } - 匹配一个元素特定的次数

1.6 让正则表达式工作起来

通过 grep 命令来验证一个电话簿

用 find 查找丑陋的文件名

用 locate 查找文件

在 less 和 vim 中查找文本

总结