打磨 wegrep 包装脚本
上次我讨论 shell 脚本时,我展示了一个 shell 脚本,它提供了 GNU grep
中 -C 上下文标志的替代方案。虽然大多数现代 Linux 系统都具有功能更强大的 grep
命令,但较旧的系统可能没有此特定功能,并且这也是深入研究包装脚本的好机会。
“等等。什么是包装脚本?” 我可以听到你问,你们中的一些人现在也在试图想出一个著名的说唱歌手的名字,你可以用它来做双关语回应。我已经抢先一步了:“无法触碰!”
包装器是一个脚本,它替换了 Linux 系统上的命令,但秘密地调用了该命令,只是提供了更多更好的功能和特性。当您设置了一个别名,使得每次调用 ls
实际上都是 ls -F
时,这就是相同的基本思想。
Linux 及其饱经风霜的父亲 UNIX 非常强大,因为它们提供了这些类型的能力;相比之下,在 Windows 10 系统上为 Microsoft Excel 编写包装器是很困难的。
在野外有多个版本的命令也是包装器非常有益的一个完美例子。想象一下,您正在部署数百台服务器,并希望在它们上面运行一个精简的 Linux,以最大限度地利用可用周期。问题是,您的管理脚本依赖于最新版本的 sed
、grep
和 find
。解决方案?将脚本指向这些命令的包装器版本,并确保您需要的所有标志都在基本命令(在较新的系统上是这种情况)或通过包装器代码本身实现。
所以,回到 wegrep。上次我留下这个脚本时,它提供了基本的 -C 功能,即在每次匹配之前和之后提供一行或多行上下文到 grep
搜索。待办事项清单上是要使其更智能地决定何时添加 “- - - - - -” 分隔线,添加行号并突出显示实际匹配项。
让我们从使脚本更智能地处理分隔线开始,因为这是迄今为止最简单的。像任何试图整齐地分隔多个输出块的脚本一样,关键实际上是计算输出已发送的次数。这是解决方案
if [ $matches -eq 0 ] ; then
echo "-----"
fi
matches=$(( $matches + 1 ))
这出现在每个输出块之前。第一次它生成顶部分隔线,否则它将被跳过。然而,在匹配行或行之后,还有另一条分隔线,每次都包含在内。
添加行号可以通过多种方式完成,但我将利用 sed
命令本身的一个有趣的功能,即“=”表达式。让我用 wonderland.txt 数据文件来演示,该文件包含爱丽丝梦游仙境的前几段
$ head -5 wonderland.txt | sed =
1
------------------------------------------------------
2
3
ALICE'S ADVENTURES IN WONDERLAND
4
5
Lewis Carroll
你能看到它做了什么,我希望?它添加了行号,但通过让数字实际显示在实际匹配行之前的行上。这有点奇怪,但第二个 sed
调用解决了问题,并给出了更有意义的输出
$ head -5 wonderland.txt | sed = | sed 'N;s/\n/: /'
1: ------------------------------------------------
2:
3: ALICE'S ADVENTURES IN WONDERLAND
4:
5: Lewis Carroll
在上面,替换序列是一个冒号,后跟 Tab 字符本身,可以通过键入 Ctrl-V,然后键入 Tab 本身来输入 - 在脚本中很容易完成。
所以,这是两个完成的:更智能的分隔线和为输出行编号的能力。让我们看看它是如何工作的
$ sh wegrep.sh '^Alice' wonderland.txt
-----
12:
13: ^Alice was beginning to get very tired of sitting by
14: her sister on the bank, and of having nothing to do:
-----
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
-----
分隔符完美地工作,显示了清晰地表示每个匹配行块所需的最少量,并且行号整洁且有帮助。
更棘手的部分仍然需要解决。你如何实际突出显示每个部分中的匹配项?
ANSI 颜色序列您可能没有意识到,但您的终端或 xterm 窗口(无论您是直接在 Linux 系统中还是通过 Windows 或 Mac 计算机连接)极有可能模拟所谓的 ANSI 终端。
ANSI 是美国国家标准协会,但不要被误导了;这是一个全球标准,尤其是在颜色、粗体和终端的其他视觉方面。
问题是,打开和关闭粗体或特定颜色的序列必须相当晦涩难懂,以确保用户不会意外地调用它。因此,“color:” 将会失败, “<color>” 也会失败。相反,它是通过转义序列完成的:Escape + [ + 3 + 2 + m 导致所有后续文本都呈现为绿色,例如。
Escape + [ 序列前缀本身有一个名称。它是控制序列引导符,尽管您可能不需要知道!您可以在网上找到 ANSI 颜色序列的完整表格。
完成突出显示的文本后,您需要将显示改回普通文本,这可以通过序列 Escape + [ + 0 + m 完成。
将它们加起来,这就是您用来突出显示存储为字符串中 $1 的任何值的内容
\033[32m$1\033[0m
\033
是 Escape 的简写。与其将其作为 echo 语句,不如很好地使用 printf
,所以这是序列
sed ''/$1/s//`printf "\033[32m$1\033[0m"`/'' "$2"
这基本上用自身替换了 $1 的每次出现,前缀是 ANSI 绿色序列,后缀是将后续文本恢复到其正常显示特性的序列。
我在这里有点偷懒,也利用了脚本的工作方式。如果它可以显示文件中的匹配行,它也可以显示已插入 ANSI 序列的匹配行。所以这是新的流程,它比我最初尝试这个脚本时要复杂一些
sed ''/$1/s//`printf "\033[32m$1\033[0m"`/'' "$2" | \
sed = | sed 'N;s/\n/: /' | \
sed -n "${before},${after}p"
连续四次调用 sed——啊,我喜欢 Linux!
在上面,第一个 sed 调用添加了 ANSI 序列,第二个和第三个协同工作以添加行号前缀,第四个显示范围从 $before
到 $after
的流中的行。
要查看这些是如何计算的,这是完整的脚本
#!/bin/sh
# wegrep - grep with context and regular expressions
grep=/usr/bin/grep
sed=/usr/bin/sed
context=1
matches=0
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 ))
if [ $matches -eq 0 ] ; then
echo "-----"
fi
sed ''/$1/s//`printf "\033[32m$1\033[0m"`/'' "$2" | \
sed = | sed 'N;s/\n/: /' | \
sed -n "${before},${after}p"
echo "-----"
matches=$(( $matches + 1 ))
done
exit 0
考虑到这个包装脚本的实用性以及为一个较旧、粗糙的 grep
程序添加了多少新功能,它出奇地简短。
而且,这是它的使用情况
$ sh wegrep.sh 'Alice' wonderland.txt
-----
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
-----
16: reading, but it had no pictures or conversations in it,
17: 'and what is the use of a book,' thought Alice 'without
18: pictures or conversation?'
-----
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
-----
然而,脚本中仍然存在一个小问题。由于 ANSI 序列 sed 调用,正则表达式的正确功能丢失了(试试看,您就会明白我的意思)。这是一个大问题吗?可能不是,但我将把解决它作为留给您,读者的练习。
与往常一样,如果您有任何建议,请通过电子邮件告诉我:dave@linuxjournal.com。