在tar.gz存档中的grep
1. 概述
在 Linux 中,我们通常使用tar 命令将多个文件打包到一个存档中。此外, 带有*-z选项的tar允许我们使用gzip *压缩存档以节省磁盘空间。
在本教程中,我们将学习如何对* tar.gz 存档执行grep*以查找哪些文件包含我们感兴趣的模式。
2. 问题介绍
像往常一样,让我们通过一个例子来理解这个问题。
2.1. 这个例子
首先,让我们看看 logs目录下的一些日志文件:
$ head logs/**/*.log
==> logs/app1/app.log <==
2022-01-20 15:21:10 application started
2022-01-20 17:07:14 [Warn] Security alert: 10 Permission Denied Requests from the same IP ...
2022-01-20 17:08:14 [Warn] High RAM usage: 90%
2022-01-20 17:14:10 RAM usage is back to normal
==> logs/app1/user.log <==
2022-01-20 19:22:10 user Kevin login
2022-01-20 20:21:10 user Kevin logout
2022-01-20 22:18:10 security alert: 10 times failed login from the same IP
==> logs/app2/app.log <==
2021-11-20 15:21:10 application started
2021-11-20 17:08:14 [Warn] High CPU usage: 80%
2021-11-20 17:14:10 CPU usage is back to normal
==> logs/app2/user.log <==
2021-11-20 19:21:10 user Eric login
2021-11-20 22:08:14 security alert: 10 times failed login from the same IP
2021-11-20 23:44:10 user Eric logout
如上面的输出所示,我们有两个应用程序的四个日志文件——app1和*app2 *。
接下来,让我们使用tar命令将它们打包到存档中,并验证创建的 tarball 是否包含我们需要的所有文件:
$ tar czf app_logs.tar.gz logs
$ tar tzf app_logs.tar.gz
logs/
logs/app2/
logs/app2/app.log
logs/app2/user.log
logs/app1/
logs/app1/app.log
logs/app1/user.log
2.2. 问题
现在,假设我们要在app_logs.tar.gz tarball中进行不区分大小写的搜索,以找出哪些日志文件包含“安全警报”消息。
我们希望在结果中看到三个具有匹配日志条目的文件:
logs/app2/user.log:2021-11-20 22:08:14 security alert: 10 times failed login from the same IP
logs/app1/app.log:2022-01-20 17:07:14 [Warn] Security alert: 10 Permission Denied Requests from the same IP ...
logs/app1/user.log:2022-01-20 22:18:10 security alert: 10 times failed login from the same IP
解决问题的第一个想法可能是三步解决方案:
- 将压缩包中的所有文件提取到一个目录
- 对提取的文件进行grep搜索
- 删除提取的文件 这可能是实现目标的最直接方法。我们的示例只有四个小日志文件。然而,在现实世界中,tarball 可能包含大量文件。此外,存档中的文件可能比我们的示例大得多。因此,这三个步骤可能会急剧增加磁盘IO负载。
如果我们只想知道哪些文件包含给定的模式,则将所有文件提取到磁盘是不必要且低效的。
在本教程中,我们将看到一些更有效的一次性解决问题的方法。
3. 关于zgrep命令
当我们看到“在 gzipped tarball 中搜索”的要求时,我们中的许多人可能会想到一个名为zgrep 的便捷实用程序。实际上,顾名思义,zgrep命令可以对 gzip 文件执行grep而无需将所有文件提取到磁盘。
此外,zgrep很好地支持grep的大部分选项。
首先,让我们尝试使用zgrep解决我们的问题:
$ zgrep -Hai 'security alert' app_logs.tar.gz
app_logs.tar.gz:2021-11-20 22:08:14 security alert: 10 times failed login from the same IP
app_logs.tar.gz:2022-01-20 17:07:14 [Warn] Security alert: 10 Permission Denied Requests from the same IP ...
app_logs.tar.gz:2022-01-20 22:18:10 security alert: 10 times failed login from the same IP
我们在上面的命令中使用了grep的三个选项:
- -H:输出每个匹配项的文件名
- -a:将二进制文件视为文本文件
- -i:匹配模式时忽略大小写区别
如上面的输出所示, zgrep已成功找到三个“安全警报”事件。
但是,如果我们仔细查看输出中的文件名,我们只会看到tar.gz文件的名称,而不是存档中日志文件的名称。
接下来,要弄清楚为什么会这样,我们需要了解zgrep的工作原理。
首先, zgrep只是一个 shell 脚本:
$ file $(which zgrep)
/usr/bin/zgrep: POSIX shell script, ASCII text executable
这意味着我们可以阅读源代码以了解其工作原理。简而言之,zgrep使用gzip将文件解压缩到 Stdout 并将其通过管道传输到grep以执行搜索。
基本上,它与命令非常相似:
tar xzfO app_logs.tar.gz | grep -Hai 'security alert'
在这里,我们使用*-O*选项要求 tar命令将文件提取到 Stdout 而不是磁盘。
因此,zgrep可以在压缩存档中搜索文件的内容,但无法判断存档中的哪个文件匹配。
也就是说,zgrep不适合解决我们的问题。
4. 将tar与*–to-command*选项一起使用
4.1. 解决方案
尽管zgrep无法解决我们的问题,但我们了解到,如果我们将文件提取到 Stdout,我们可以将其直接通过管道传输到grep,从而避免额外的磁盘 IO 负载。
因此,如果有一种方法可以将 tarball 中的文件解压缩到 Stdout 并保留其原始文件名,那么我们的问题就可以轻松解决。
幸运的是,tar提供了*–to-command=COMMAND*选项。
此选项告诉tar将文件提取到 Stdout 并通过管道传输到COMMAND。
而且,COMMAND可以通过一组 TAR_* 环境变量获取tarball里面文件的信息,例如:
- TAR_FILENAME – 文件名
- TAR_SIZE – 文件的大小
- TAR_FILETYPE – 文件的类型,例如是普通文件、目录还是符号链接等
- …
显然,变量TAR_FILENAME正是我们要找的。
另一方面,grep命令有–label=LABEL选项来显示LABEL*作为文件名*。当我们在 Stdin 上进行* grep*时,它非常有用。
因此,为了解决这个问题,我们可以组装一个命令,如tar … –to-command=‘grep …’并将tar的 TAR_FILENAME变量传递给 grep的*–label*选项。
试一试吧:
$ tar xzf app_logs.tar.gz --to-command='grep --label=$TAR_FILENAME -Hi "security alert";true'
logs/app2/user.log:2021-11-20 22:08:14 security alert: 10 times failed login from the same IP
logs/app1/app.log:2022-01-20 17:07:14 [Warn] Security alert: 10 Permission Denied Requests from the same IP ...
logs/app1/user.log:2022-01-20 22:18:10 security alert: 10 times failed login from the same IP
有用!我们得到了预期的结果。
4.2. 一些注意事项
我们已经解决了这个问题。但是,仍有一些小问题值得一提。
第一个是在grep命令之后添加true命令。
使用*–to-command=COMMAND选项时,如果COMMAND*返回错误(非零)代码,tar命令将输出一条错误消息。
另一方面,当grep在输入中找到匹配项时,grep命令返回 0。否则,它返回 1。
因此,这意味着tar会将grep的所有“未找到匹配”情况视为错误并输出错误消息:
$ tar xzf app_logs.tar.gz --to-command='grep --label=$TAR_FILENAME -Hi "security alert"'
tar: 207869: Child returned status 1
logs/app2/user.log:2021-11-20 22:08:14 security alert: 10 times failed login from the same IP
logs/app1/app.log:2022-01-20 17:07:14 [Warn] Security alert: 10 Permission Denied Requests from the same IP ...
logs/app1/user.log:2022-01-20 22:18:10 security alert: 10 times failed login from the same IP
tar: Exiting with failure status due to previous errors
这弄乱了输出,这绝对不是我们想要的。因此,我们在末尾添加true命令,使COMMAND始终返回 0并抑制这些错误消息。
我们应该注意的另一点是我们用单引号将COMMAND括起来。
这是因为 TAR_* 变量是在tar的执行期间分配 并传递给COMMAND的。因此,例如,如果我们双引号 COMMAND,$TAR_FILENAME变量将在调用tar命令时由 shell 扩展:
$ tar xzf app_logs.tar.gz --to-command="grep --label=$TAR_FILENAME -Hi 'security alert';true"
:2021-11-20 22:08:14 security alert: 10 times failed login from the same IP
:2022-01-20 17:07:14 [Warn] Security alert: 10 Permission Denied Requests from the same IP ...
:2022-01-20 22:18:10 security alert: 10 times failed login from the same IP
如上面的测试所示,我们在输出中有三个空文件名,因为当我们启动tar命令时shell 变量*$TAR_FILENAME*不存在。
因此,我们需要在启动tar时使用单引号来防止扩展变量名。