Globbing 和 Regex:如此相似,又如此不同

作者:Shawn Powers

Grepping 很棒,只要你不用 glob 搞砸它!本文介绍了一些 grep 和 regex 的基础知识。

咖啡饮用者通常分为两种类型。第一种类型购买预磨咖啡豆,并在早上使用随附的勺子制作自动滴滤咖啡。第二种类型从世界各地挑选单一产地咖啡豆,只接受过去一周内烘焙的咖啡豆,并在冲泡前用锥形磨豆机研磨这些咖啡豆,采用各种复杂的冲泡方法。文本搜索也有点像这样。

对于命令行上的大多数事情,人们会想到 *.* 或 *.txt,并乐于使用文件 globbing 来选择他们想要的文件。然而,当涉及到 grepping 日志文件时,你需要稍微复杂一点。令人困惑的部分是 globbing 和 regex 的语法重叠时。幸运的是,弄清楚何时使用哪种结构并不难。

Globbing

命令 shell 使用 globbing 进行文件名补全。如果你输入类似 ls *.txt 的命令,你将获得当前目录中所有以 .txt 结尾的文件列表。如果你执行 ls R*.txt,你将获得所有以大写 R 开头并具有 .txt 扩展名的文件。星号是一个通配符,可让你快速过滤你想要的文件。

如果你想指定单个字符,也可以在 globbing 中使用问号。因此,输入 ls read??.txt 将列出 readme.txt,但不会列出 read.txt。这与 ls read*.txt 不同,后者将匹配 readme.txt 和 read.txt,因为在文件 glob 中,星号表示“零个或多个字符”。

这里有一个简单的方法来记住你是否正在使用 globbing(非常简单)与正则表达式:globbing 由 shell 对文件名执行,而 regex 用于搜索文本。唯一令人沮丧的例外是,有时 shell 太聪明了,在你不需要时方便地执行 globbing——例如


grep file* README.TXT

在大多数情况下,这将搜索文件 README.TXT,查找正则表达式 file*,这通常是你想要的。但是,如果当前文件夹中恰好有一个文件与 file* glob 匹配(比如 filename.txt),shell 将假定你想要将其传递给 grep,因此 grep 实际上将看到


grep filename.txt README.TXT

哇,非常感谢你,Shell 先生,但这并不是我想做的。因此,我建议在使用 grep 时 *始终* 使用引号。99% 的情况下你不会得到意外的 glob 匹配,但那 1% 可能会令人恼火。所以当使用 grep 时,这样做更安全


grep "file*" README.TXT

因为即使存在 filename.txt,shell 也不会自动替换它。

所以,globs 用于文件名,而 regex 用于搜索文本。这是首先要理解的事情。接下来要意识到的是,类似的语法意味着不同的含义。

Glob 和 Regex 的冲突

我不想让这篇文章变成一篇关于 regex 的超级深入的文章;相反,我希望你理解简单的 regex,尤其是在它与 blobbing 冲突时。表 1 显示了一些最容易混淆的符号以及它们在每种情况下的含义。

表 1. 常用符号
特殊字符 在 Globs 中的含义 在 Regex 中的含义
* * 零个或多个字符
? ? 零个或多个字符
. . 任何字符的单次出现

要让情况更糟的是,当您使用 grep 时,您可能正在考虑 globs,但仅仅因为您获得了预期的结果并不意味着您获得结果的原因是正确的。让我试着解释一下。这是一个名为 filename.doc 的文本文件


The fast dog is fast.
The faster dogs are faster.
A sick dog should see a dogdoc.
This file is filename.doc

如果你输入


grep "fast*" filename.doc

前两行将匹配。无论你考虑的是 globs 还是 regex,这都说得通。但是如果你输入


grep "dogs*" filename.doc

前三行将匹配,但如果你用 globs 的思维来考虑,那就没有道理了。由于 grep 在搜索文件时使用正则表达式 (regex),星号表示“前一个字符的零个或多个出现次数”,因此在第二个示例中,它匹配 dog 和 dogs,因为拥有零个 's' 字符符合 regex。

假设你输入了这个


grep "*.doc" filename.doc

这将匹配最后两行。星号实际上在这个命令中没有任何作用,因为它没有跟随任何字符。regex 中的点表示“任何字符”,因此它将匹配“.doc”,但它也将匹配“dogdoc”中的“gdoc”,因此两行都匹配。

这个故事的寓意是 grep 永远不会使用 globbing。唯一的例外是 shell 在将命令传递给 grep 之前执行 globbing,这就是为什么在你要 grep 的正则表达式周围使用引号始终是一个好主意。

使用 fgrep 避免 Regex

如果你不想要 regex 的强大功能,它可能会非常令人沮丧。如果你实际上是在一堆文本中查找一些特殊字符,尤其如此。你可以使用 fgrep 命令(或 grep -F,它们是同一回事)来跳过任何 regex 替换。使用 fgrep,你将搜索你键入的内容,即使它们是特殊字符。这是一个名为 file.txt 的文本文件


I really hate regex.
All those stupid $, {}, and \ stuff ticks me off.
Why can't text be text?

如果你尝试像这样使用常规的 grep


grep "$," file.txt

你将得不到任何结果。那是因为 '$' 是一个特殊字符(稍后会详细介绍)。如果你想 grep 特殊字符而无需转义它们,或者知道 regex 代码来获得你想要的结果,这将很好用


grep -F "$," file.txt

而且,grep 将返回文本文件的第二行,因为它匹配字面字符。可以构建一个 regex 查询来搜索特殊字符,但这可能会很快变得复杂。另外,fgrep 在大型文本文件上要快得多。

一些简单、有用的 Regex

好的,现在你知道何时使用 globbing 以及何时使用正则表达式了,让我们看看一些 regex,它可以使 grepping 更有用。我发现自己经常在 regex 中使用脱字符号和美元符号。脱字符号表示“行首”,美元符号表示“行尾”。我过去常常把它们搞混,所以我记住它们的愚蠢方法是,农民必须在季节开始时种植胡萝卜,才能在季节结束时卖出美元。这很傻,但对我来说很有效!

这是一个名为 file.txt 的示例文本文件


chickens eat corn
corn rarely eats chickens
people eat chickens and corn
chickens rarely eat people

如果你要输入


grep "chickens" file.txt

你将得到返回的所有四行,因为每行都包含“chickens”。但是如果你在其中添加一些 regex


grep "^chickens" file.txt

你将获得返回的第一行和第四行,因为单词“chickens”位于这些行的开头。如果你输入


grep "corn$" file.txt

你将看到第一行和第三行,因为它们都以“corn”结尾。但是,如果你输入


grep "^chickens.*corn$" file.txt

你将只得到第一行,因为它是唯一一行以 chickens 开头并以 corn 结尾的行。这个例子可能看起来令人困惑,但有三个正则表达式构建了搜索。让我们看看它们中的每一个。

首先,^chickens 表示该行必须以 chickens 开头。

其次,.* 表示零个或多个任何字符,因为请记住,点表示任何字符,而星号表示前一个字符的零个或多个。

第三,corn$ 表示该行必须以 corn 结尾。

当你构建正则表达式时,你只需像那样将它们全部混合在一个长字符串中。这可能会变得令人困惑,但如果你分解每个部分,它就变得有意义了。为了使整个正则表达式匹配,*所有* 部分都必须匹配。这就是为什么只有第一行匹配示例 regex 语句。

在 grepping 文本文件时,还有一些其他常见的 regex 字符很有用。记住只需将它们混合在一起以形成整个正则表达式

  • \ — 反斜杠否定了特殊字符的“特殊性”,这意味着你实际上可以使用 regex 搜索它们。例如,\$ 将搜索 $ 字符,而不是查找行尾。
  • \s — 这个结构表示“空白字符”,它可以是空格或多个空格、制表符或换行符。要查找被空白字符包围的单词 pickle,你可以搜索 \spickle\s,这将找到“pickle”而不是“pickles”。
  • .* — 这实际上只是星号的一种特定用法,但它是一种非常常见的组合,所以我在这里提到它。它基本上表示“零个或多个任何字符”,这就是上面玉米/鸡肉示例中使用的内容。
  • | — 这在 regex 中表示“或”。因此 hi|hello 将匹配“hi”或“hello”。它通常在括号中使用,以将其与正则表达式的其他部分分开。例如,(F|f)rankfurter 将搜索单词 frankfurter,无论它是否大写。
  • [] — 方括号是指定“或”选项的另一种方式,但它们支持范围。因此 regex [Ff]rankfurter 与上面的示例相同。方括号也支持范围,因此 ^[A-Z] 将匹配任何以大写字母开头的行。它也支持数字,因此 [0-9]$ 将匹配任何以数字结尾的行。
你的任务

你可以使用正则表达式做更复杂的事情。这些基本构建块通常足以从日志文件中获取你需要的文本类型。如果你想了解更多信息,请务必在谷歌上搜索 regex,或者找一本解释所有细节的书。如果你想让我写更多关于它的内容,请发送消息至 ljeditor@linuxjournal.com 并告诉我。

我真的,*真的* 鼓励你练习使用 regex。最好的学习方法是 *实践*,所以制作一些文本文件,看看你创建的 regex 语句是否给你了你期望的结果。幸运的是,grep 会突出显示它在返回的行中找到的“匹配项”。这意味着如果你获得的结果超出你的预期,你将看到为什么 regex 匹配的结果超出你的预期,因为 grep 会告诉你。

最重要的是要记住,grep 不执行 globbing——通配符的东西只适用于 shell 上的文件名。即使使用 grep 进行 globbing 似乎有效,那也可能只是巧合(如果你不知道我在说什么,请回顾一下这里的 dog/dogs 示例)。祝你 grepping 愉快!

Shawn 是 *Linux Journal* 的副编辑,并且从一开始就接触 Linux。他对开源充满热情,并且热爱教学。他还喝太多咖啡,这经常在他的写作中体现出来。

加载 Disqus 评论