解决问题的多种途径

作者: Dave Taylor

我参与的一个项目让我思考到,在 Linux 世界中,对于任何给定的问题,总是存在多种解决方案。对于另一个项目,我想要拼凑出一个 grep 版本,它允许我指定正确的正则表达式,而无需担心 -E 标志,并且还能获取匹配项的上下文。

当然,这些都是 grep 的常见扩展:前者通过 grep -Eegrep 快捷方式来演示,而后者则通过 grep -C 以及在某些 UNIX 和 Linux 系统上的 wgrep 来完成。

但是,有很多不同的方法可以创建该特定功能,而无需依赖于现代版本的 grep;较旧的版本可能具有 -E 标志,但不包括对上下文的支持。

因此,在本文中,我认为研究不同的方法来产生我称之为 wegrep 的工具会很有趣,wegrep 是一个 grep 版本,它既包含 -C 上下文窗口,又包含 -E 正则表达式模式支持。

封装器,也许我们只需要一个封装器

如果您拥有现代的 GNU grep,您可以通过简单地尝试使用 -C 标志来确定,这一切都变得容易了


$ grep -C
grep: option requires an argument -- C

在此之后有一个相当复杂的用法说明,但是如果您的版本可以理解 -C 或其冗长的兄弟 -context,那您就走运了。

引入“封装器”,这是一个简单的脚本,可以更改程序的默认行为。最简单的情况下,它实际上可以是一个系统别名,因此这个


alias ls="/bin/ls -F"

是一种封装器,确保每当我运行 ls 命令时,都会指定 -F 标志。

对于这个更智能的 grep 版本,我可以简单地告诉用户要使用哪些标志,或者使用环境变量 GREP_OPTIONS 设置特定的标志,但让我们按照讨论的那样构建 wegrep

对于用法,它将尽可能简单:命令、模式、源文件。像这样


wegrep '^Alice' wonderland.txt

这将在文件 wonderland.txt 中搜索正则表达式 "Alice",该表达式锚定到行的开头。

很容易做到


grep=/usr/bin/grep
if [ $# -ne 2 ] ; then
  echo "Usage: wegrep [pattern] filename" ; exit 1
fi
$grep -C2 -n -E "$1" "$2"

我甚至添加了一些错误检查,以确保用户指定了正确数量的参数,并使用简单的错误消息来隐藏实际 grep 命令的一些复杂性。

对于测试文件,我将使用刘易斯·卡罗尔不朽之作《爱丽丝梦游仙境》的前四个段落,该书从 古腾堡计划 下载。

这是我的第一次调用的结果


$ sh wegrep '^Alice' wonderland.txt
11-Down the Rabbit-Hole
12-
13:Alice was beginning to get very tired of sitting by her
14-sister on the bank, and of having nothing to do: once
15-or twice she had peeped into the book her sister was
--
--
26-
27-There was nothing so very remarkable in that; nor did
28:Alice think it so very much out of the way to hear the
29-Rabbit say to itself, 'Oh dear! Oh dear! I shall be
30-late!' (when she thought it over afterwards, it

您可以看到 grep 在此任务中做得很好,在每个匹配项的上方和下方显示了两行上下文,并通过使用冒号分隔行号和内容来表示哪一行包含匹配项本身。

但是,如果您的 grep 版本不支持 -C 标志怎么办?如果您实际上需要识别哪些行与模式匹配,然后自己实现上下文显示怎么办?

构建您自己的上下文

由于 grep 仍然可用,并且除了最古老的 grep 实现之外,所有实现都支持 -E 标志以允许用户指定正则表达式,因此,该任务可以分为两个部分:识别哪些行匹配,然后找出一种列出如上面输出中所示的行 (n-2)..n..(n+2) 的方法。

第一个任务可以非常容易地完成,因为 grep 有一个方便的 -n 标志,可以附加行号。这样,获取与指定模式匹配的行列表就很简单了。

但是,让我们先看看输出是什么


$ grep -n -E '^Alice' wonderland.txt
13:Alice was beginning to get very tired of sitting by her
28:Alice think it so very much out of the way to hear the

现在是超人的工作了!我的意思是,嗯,cut


grep -n -E "$pattern" "$file | \
  cut -d: -f1
13
28

让我们切换到另一个任务,即显示以指定行为中心的行范围。您可以使用 headtail 的繁琐组合来完成此操作,但这次 sed 是更好的工具。

实际上,sed 使其变得容易。想要获取第 12、13 和 14 行吗?这将奏效


sed '12,14p' wonderland.txt

嗯,不太对。问题是 sed 的默认行为是回显它看到的每一行,以及用户指定的任何内容,因此,您最终将得到 wonderland.txt 中的每一行,并且第 12-14 行还会再次出现,因为该语句已匹配并执行(p 后缀表示“打印”)。

这就是为什么如果您要使用 sed 做任何事情,了解其 -n 标志至关重要,-n 标志会抑制其输出读取的每一行的愿望。现在,这是一个可行的命令


$ sed -n '12,14p' wonderland.txt

Alice was beginning to get very tired of sitting by her
sister on the bank, and of having nothing to do: once

您能看到如何将这些链接在一起吗?这一切都可以在一个简单的 for 循环中完成(特别是如果您现在忽略错误检查)。但是,再次强调,还需要一个小步骤:需要计算匹配行 n 之前和之后的行数 n。这很容易计算


before=$(( $match - $context ))
after=$(( $match + $context ))

这里,context 指定您是否想要匹配行上方和下方的 1、2、3 行或更多行上下文。

让我们试一下


#!/bin/sh
# wegrep - grep with context and regular expressions
grep=/usr/bin/grep
sed=/usr/bin/sed
if [ $# -ne 2 ] ; then
  echo "Usage: wegrep [pattern] filename" ; exit 1
fi
for match in $($grep -n -E "$1" "$2" | cut -d: -f1)
do
  before=$(( $match - $context ))
  after=$(( $match + $context ))
  $sed -n '${before},${after}p' "$2"
done
exit 0

除非事实证明,上面的代码中有两个关键错误,当您运行第一个测试时,这会立即显现出来


$ sh wegrep '^Alice' wonderland.txt

wegrep: line 14: 13:Alice -  : syntax error in expression
 ↪(error token is ":Alice -  ")

您能看到第一个错误吗?第 14 行是变量 before 的计算。

那么哪里出错了?您需要使用一个值初始化 context,因此,数学表达式本质上是


15 +

这被正确地标记为错误。很容易修复。

然而,第二个错误更为微妙,但是,当您运行脚本并将 context 定义为 1 时,这里有一个线索


$ sh wegrep '^Alice' wonderland.txt
sed: 1: "${before},${after}p": unexpected EOF (pending }'s)
sed: 1: "${before},${after}p": unexpected EOF (pending }'s)

这绝对很奇怪。是 sed 在抱怨,但是调用 sed 的行有什么问题呢?

让我们再看一下那一行


$sed -n '${before},${after}p' "$2"

现在您能看到错误了吗?这是一个 shell 脚本中微妙且常见的问题:我使用了错误的引号。请记住,在 shell 脚本中,单引号会阻止变量的解释。将其切换为双引号,现在一切都运行良好了。


$ sh wegrep '^Alice' wonderland.txt

Alice was beginning to get very tired of sitting by her
sister on the bank, and of having nothing to do: once
There was nothing so very remarkable in that; nor did
Alice think it so very much out of the way to hear the
Rabbit say to itself, 'Oh dear! Oh dear! I shall be

现在又出现了一个问题:如何区分已匹配的块?简单,通过向 for 循环添加一些 echo 语句,在每个匹配项前后添加 ----


for match in $($grep -n -E "$1" "$2" | cut -d: -f1)
do
  before=$(( $match - $context ))
   after=$(( $match + $context ))
  echo "-----"
  sed -n "${before},${after}p" "$2"
  echo "-----"
done

这样做有效,但作为输出而言有点笨拙,尽管它与现代 grep 使用 -C 标志时的效果非常接近。


$ sh wegrep '^Alice' wonderland.txt
-----

Alice was beginning to get very tired of sitting by her
sister on the bank, and of having nothing to do: once
-----
-----
There was nothing so very remarkable in that; nor did
Alice think it so very much out of the way to hear the
Rabbit say to itself, 'Oh dear! Oh dear! I shall be
-----

作为一名纯粹主义者,我更希望在输出块之间有一条虚线,在第一个匹配项之前和最后一个匹配项之后各有一条,且不重复行。

这并不难做到,还有第二个任务是添加回行号,并理想地表示哪一行与正则表达式匹配。但我没有空间了,所以这些任务将不得不等到另一天。

Dave Taylor 长期以来一直在 UNIX 和 Linux 系统上编写 shell 脚本。他是《Learning Unix for Mac OS X》和《Wicked Cool Shell Scripts》的作者。您可以在 Twitter 上通过 @DaveTaylor 找到他,也可以通过他的技术问答网站:Ask Dave Taylor 联系他。

加载 Disqus 评论