Contents

在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

解决问题的第一个想法可能是三步解决方案:

  1. 将压缩包中的所有文件提取到一个目录
  2. 对提取的文件进行grep搜索
  3. 删除提取的文件 这可能是实现目标的最直接方法。我们的示例只有四个小日志文件。然而,在现实世界中,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时使用单引号来防止扩展变量名。