解决问题的多种途径
我参与的一个项目让我思考到,在 Linux 世界中,对于任何给定的问题,总是存在多种解决方案。对于另一个项目,我想要拼凑出一个 grep
版本,它允许我指定正确的正则表达式,而无需担心 -E
标志,并且还能获取匹配项的上下文。
当然,这些都是 grep
的常见扩展:前者通过 grep -E
和 egrep
快捷方式来演示,而后者则通过 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
让我们切换到另一个任务,即显示以指定行为中心的行范围。您可以使用 head
和 tail
的繁琐组合来完成此操作,但这次 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
-----
作为一名纯粹主义者,我更希望在输出块之间有一条虚线,在第一个匹配项之前和最后一个匹配项之后各有一条,且不重复行。
这并不难做到,还有第二个任务是添加回行号,并理想地表示哪一行与正则表达式匹配。但我没有空间了,所以这些任务将不得不等到另一天。