使用 grep
当我刚开始从事系统集成工作时,我主要是一名 PC 支持人员。我花费大量时间在各种 PC LAN 配置中安装和支持 Windows 应用程序,运行各种版本(和供应商)的 TCP/IP 传输协议。从那时起,我成功地抛弃了 DOS 并继续前进。现在,在使用了多年各种版本的 Unix 之后,我正在将我们的一些网络和数据操作库移植到其他平台和环境,例如 AS/400 小型机和 Macintosh。这种持续的经验让我有机会体会到我们在 Linux 中理所当然使用的工具是多么强大。
在一组文件中搜索一个词(或任何其他值)是一项非常常见的任务。无论是搜索一组源代码模块中的函数,尝试在一组配置文件中查找参数,还是仅仅查找丢失的电子邮件消息,文本搜索和匹配操作在所有环境中都很常见。
不幸的是,这个常见的任务在所有平台上都没有简单的解决方案。在大多数平台上,可用的最佳解决方案是使用编辑器中的搜索功能。但是当涉及到 Linux(和其他 Unix 后代)时,您有很多解决方案。其中之一是 grep。
grep 是 “global regular expression print”(全局正则表达式打印)的缩写,它引用了旧的 ed 行编辑器中的命令,该命令打印文件中包含指定字符序列的所有行。grep 正是这样做的:它打印文件中包含与 正则表达式 匹配的行。我们将逐步深入研究正则表达式的含义。
首先,让我们看一个快速示例。我们将在 Linux 提供的 Configure 脚本中搜索一个单词,该脚本用于设置 Linux 内核源代码,通常安装在 /usr/src/linux 目录中。切换到该目录并键入($ 字符是提示符,不要键入它)
$ grep glob Configure
您应该看到
# Disable filename globbing once and for all.
glob 以 粗体 显示,以说明 grep 匹配的内容。grep 实际上不会以粗体打印匹配项。
grep 查找字符序列 glob 并打印 Configure 文件中包含该序列的行。它没有查找 单词 glob。它查找的是 g,后跟 l,后跟 o,后跟 b。这指出了正则表达式的一个重要方面:它们匹配字符序列,而不是单词。
在我们更深入地研究模式匹配的细节之前,让我们通过几个示例来了解 grep 的 “用户界面”。尝试以下两个命令
$ grep glob < Configure $ cat Configure | grep glob
这两个命令都应该打印
# Disable filename globbing once and for all.
这看起来可能很熟悉。
在所有这些命令中,我们都将正则表达式指定为 grep 的第一个参数。除了任何命令行开关之外,grep 始终期望正则表达式作为第一个参数。
但是,我们向 grep 提出了三种不同的情况,并收到了相同的响应。在第一个练习中,我们向 grep 提供了文件名,它打开该文件并搜索它。grep 还可以接受要搜索的文件名列表。
在其他两个练习中,我们演示了 grep 与许多其他实用程序共享的一个功能。如果在命令行上未指定任何文件,则 grep 读取 标准输入。为了进一步说明标准输入,让我们尝试再举一个例子
$ grep foo
当您运行该命令时,grep 似乎 “挂起” 等待某些内容。确实如此。它正在等待输入。键入
tttt
然后按 回车键。什么也没发生。现在键入
foobar
然后按回车键。这次,grep 在 foobar 中看到了字符串 foo,并将行 foobar 回显给您,这就是 foobar 出现两次的原因。现在键入 ctrl-d,即 “文件结束” 字符,告诉 grep 它已到达文件末尾,然后它退出。
您刚刚给 grep 提供了一个输入文件,该文件由 tttt、换行符、foobar、换行符和文件结束符组成。
从标准输入将输入通过管道传输到 grep 还有另一个常见的用途:过滤其他命令的输出。有时,使用 grep 剪切掉不必要的行比使用 more 或 less 逐页阅读输出更方便
$ ps ax | grep cron
有效地为您提供 crond 的进程信息。
许多 Unix 实用程序使用正则表达式来指定模式。在我们深入研究正则表达式的实际示例之前,让我们定义一些术语并解释一些我将在练习中使用的约定。
字符 任何可打印的符号,例如字母、数字或标点符号。
字符串 字符序列,例如 cat 或 segment(有时称为 字面量)。
表达式 也是字符序列。字符串和表达式之间的区别在于,字符串应按字面意思理解,而表达式必须先进行评估才能确定其真实值。(GNU grep 的手册页将正则表达式比作数学表达式。)一个表达式通常可以代表多个事物,例如正则表达式 th[ae]n 可以代表 then 或 than。此外,shell 也有其自身的表达式类型,称为 globbing,通常用于指定文件名。例如,*.c 匹配任何以字符 .c 结尾的文件。
元字符 其存在将字符串转换为表达式的字符。元字符可以被认为是确定如何评估表达式的运算符。当我们完成以下示例时,这将变得更加清楚。
您可能输入过类似这样的 shell 命令
$ ls -l *.c
在某个时候。shell “知道” 它应该用当前目录中名称以字符 .c 结尾的所有文件的列表替换 *.c。
如果我们想将字面量 *(或 ?、|、$ 等)字符传递给 grep,这会造成干扰。将正则表达式括在 `单引号' 中将阻止 shell 评估任何 shell 的元字符。如有疑问,请将您的正则表达式括在单引号中。
最基本的正则表达式只是一个字符串。因此,诸如 foo 之类的字符串是一个正则表达式,它只有一个匹配项:foo。
我们将继续使用同一目录中的另一个文件进行示例,因此请确保您仍在 /usr/src/linux 目录中
$ grep Linus CREDITS
Linus N: Linus Torvalds E: Linus.Torvalds@Helsinki.FI D: Personal information about Linus
这很自然地给出了以 Linus Torvalds 名字开头的四行。
正如我之前所说,Unix shell 具有不同的元字符,并使用不同类型的表达式。元字符 . 和 * 最容易让在学习正则表达式语法之前一直在使用 shell(以及 DOS,就此而言)的人感到困惑。
在正则表达式中,字符 . 的作用非常类似于 shell 提示符下的 ?:它匹配任何单个字符。相比之下,* 具有完全不同的含义:它匹配 零个 或多个 前一个 字符的实例。
如果我们键入
$ grep tha. CREDITS
我们会得到这个(仅部分列表)
S: Northampton E: Hein@Informatik.TU-Clausthal.de
如您所见,grep 打印了 tha 后跟任何字符的每个实例。现在尝试
$ grep 'tha*' CREDITS S: Northampton D: Author of serial driver D: Author of the new e2fsck D: Author of loopback device driver
我们收到了更大的响应,带有 “*”。由于 “*” 匹配 零个 或多个前一个字符(在本例中为字母 “a”)的实例,因此我们大大增加了匹配的可能性,因为我们使 th 成为合法的匹配项!
正则表达式语法中最强大的构造之一是 字符类。字符类指定要匹配的字符范围或集合。类中的字符由 [ 和 ] 符号分隔。类 [a-z] 匹配小写字母 a 到 z,类 [a-zA-Z] 匹配所有字母,包括大写或小写,而 [Lh] 将匹配大写 L 或小写 h。
$ grep 'sm[ai]' CREDITS E: csmith@convex.com D: Author of several small utilities
因为我们的表达式匹配 sma 或 smi。命令
$ grep '[a-z]' CREDITS
给了我们文件的大部分内容。如果您仔细查看该文件,您会发现只有少数几行没有小写字母;这些是 grep 未打印的唯一行。
既然我们可以匹配一组字符,为什么不排除它们呢?当插入符号 ^ 作为字符类的 第一个 成员包含时,它会匹配任何字符,除了 类中指定的字符。
$ grep Sm CREDITS
给了我们三行
D: Small patches for kernel, libc D: Smail binary packages for Slackware and Debian N: Chris Smith $ grep 'Sm[^i]' CREDITS
给了我们两行
D: Small patches for kernel, libc D: Smail binary packages for Slackware and Debian
因为我们排除了 i 作为 Sm 后面可能的字母。
要搜索包含字面量 ^ 字符的字符类,请不要将其放在类中的第一个位置。要搜索包含字面量 - 的类,请将其放在类中的最后一个字符。要搜索包含字面量字符 ] 的类,请将其放在类中的第一个字符。
通常,基于字符在行中的位置进行搜索很方便。^ 字符匹配行的开头(当然,在字符类之外),而 $ 匹配结尾。(vi 的用户可能会将这些元字符识别为命令。)早些时候,搜索 Linus 给出了四行。让我们将其更改为
grep 'Linus$' CREDITS
这给了我们
Linus D: Personal information about Linus
两行,因为我们指定 Linus 必须是该行的最后五个字符。类似地,
grep - CREDITS
产生 99 行,而
grep '^-' CREDITS
仅产生一行
----------
在某些情况下,您可能需要匹配元字符。在字符类集合中,所有字符都被视为字面量(如上所示,^、- 和 ] 除外)。但是,在类之外,我们需要一种将元字符转换为要匹配的字面量字符的方法。
为此,特殊的元字符 \ 用于 转义 元字符。转义的元字符按字面意思解释,而不是作为表达式的组成部分。因此 \[ 将匹配任何带有 [ 的序列
$ grep '[' CREDITS
产生错误消息
grep: Unmatched [ or [^
但是
$ grep '\[' CREDITS
产生两行
E: hennus@sky.ow.nl [My uucp-fed Linux box at home] D: The XFree86[tm] Project
如果您需要搜索 \ 字符,请像转义任何其他元字符一样转义它:\\
如您所见,仅凭其对正则表达式语法的支持,grep 就为我们提供了一些非常强大的功能。它的命令行选项增加了更多功能。
有时您正在查找一个字符串,但不知道它是大写、小写还是混合大小写。对于这种情况,grep 提供了 -i 开关。使用此选项,完全忽略大小写
$ grep -i lINuS CREDITS Linus N: Linus Torvalds E: Linus.Torvalds@Helsinki.FI D: Personal information about Linus
-v 选项使 grep 打印所有 不 包含指定正则表达式的行
$ grep -v '^#" /etc/syslog.conf | grep -v '^$'
打印 /etc/syslog.com 中既未注释(以 # 开头)也非空(^$)的所有行。这在我的系统上打印了六行,尽管我的 syslog.conf 文件实际上有 21 行。
如果您需要知道有多少行匹配,请将 -c 选项传递给 grep。这将输出匹配行的数量(而不是匹配项的数量;一行中的两个匹配项算作一个),而不打印匹配的行
$ grep -c Linux CREDITS 33
如果您正在搜索包含给定字符串的文件名,而不是包含它的实际行,请使用 grep 的 -l 开关
$ grep -l Linux * CREDITS README README.modules
grep 还会通知我们,对于每个子目录,它都无法搜索目录。这是正常的,并且每当您使用恰好包含目录名称以及文件名的通配符时都会发生这种情况。
-l 的反面是 -L。此选项将使 grep 返回 不 包含指定模式的文件名。
如果您正在搜索一个单词并希望抑制部分单词的匹配项,请使用 -w 选项。在没有 -w 选项的情况下,
$ grep -c a README
告诉我们它匹配了 146 行,但是
$ grep -wc a README
仅返回 35 行,因为我们只匹配了单词 a,而不是每行带有字符 a 的行。
另外两个有用的选项
$ grep -b Linus CREDITS 301: Linus 17446:N: Linus Torvalds 17464:E: Linus.Torvalds@Helsinki.FI 20561:D: Personal information about Linus $ grep -n Linus CREDITS 7: Linus 793:N: Linus Torvalds 794:E: Linus.Torvalds@Helsinki.FI 924:D: Personal information about Linus
-b 选项使 grep 在输出的相应行之前打印每个匹配项的字节偏移量(匹配项距文件开头的字节数)。-n 开关给出的是行号。
GNU 还提供了 egrep(增强型 grep)。GNU egrep 支持的正则表达式语法添加了一些其他元字符
? 类似于 *,但它匹配零个或一个实例,而不是零个或多个。
+ 前面的字符匹配一个或多个次。
| 用 OR 运算将正则表达式分隔开。
$ egrep -i 'linux|linus' CREDITS
输出任何包含 linus 或 linux 的行。
为了便于阅读,括号 “(” 和 “)” 可以与 “|” 结合使用,以分隔和分组表达式。
Eric Goedelbecker 是路透社美国公司的系统分析师。他为使用市场数据检索和操作 API 的交易室和后台运营的客户(主要是金融机构)提供支持。在他的业余时间(每周大约 15 分钟...),他阅读哲学并摆弄 Linux。可以通过电子邮件 eric@nymt.reuter.com 与他联系。