第十七章:搜索文件

当我们在Linux系统中漫步时,有一件事变得非常清楚:一个典型的Linux系统有很多文件!这就引出了一个问题,“我们如何找到东西?”我们已经知道,Linux文件系统是按照从一代类Unix系统传给下一代的惯例组织良好的,但文件的数量可能会带来一个令人望而生畏的问题。

在本章中,我们将介绍用于在系统上查找文件的两个工具。

我们还将研究一个经常与文件搜索命令一起使用的命令,以处理生成的文件列表。

此外,我们将介绍几个命令来帮助我们进行探索。

第十七章:搜索文件locate —— 轻松查找文件locate 数据库从哪里来?find —— 艰难地查找文件测试操作员预定义操作用户定义操作提高效率xargs 处理有趣的文件名重返游乐场选项总结

locate —— 轻松查找文件

locate 程序执行路径名的快速数据库搜索,然后输出与给定子字符串匹配的每个名称。例如,我们想找到所有名称以 zip 开头的程序。由于我们正在寻找程序,我们可以假设包含程序的目录名称将以 bin/ 结尾。因此,我们可以尝试使用 locate 来查找我们的文件:

locate 将搜索其路径名数据库,并输出任何包含字符串 bin/zip 的路径名。

如果搜索要求不是那么简单,我们可以将 locategrep 等其他工具相结合,设计出更有趣的搜索。

locate 程序已经存在多年,有几种常用的变体。现代Linux发行版中最常见的两种是 plocatemlocate ,尽管它们通常是通过名为 locate 的符号链接访问的。不同版本的 locate 具有重叠的选项集。一些版本包括正则表达式匹配(我们将在【第19章.正则表达式】中介绍)和通配符支持。查看手册页中的 locate 以确定安装了哪个版本的 locate

locate 数据库从哪里来?

您可能会注意到,在某些发行版上,系统安装后立即运行 locate 会失败,但如果您第二天再试一次,它会正常工作。什么情况? locate 数据库是由另一个名为 updatedb 的程序创建的。通常,它作为 cron job 定期运行,即由 cron 守护进程定期执行的任务。大多数配备 locate 的系统每天运行一次 updatedb 。由于数据库不是连续更新的,您会注意到使用 locate 时不会显示最新的文件。为了克服这个问题,可以通过成为超级用户并在提示符下运行 updatedb 来手动运行 updatedb 程序。

find —— 艰难地查找文件

虽然 locate 程序可以仅根据文件名称查找文件,但 find 程序会根据各种属性在给定的目录(及其子目录)中搜索文件。我们将花很多时间在 find 上,因为它有很多有趣的功能,当我们在后面的章节中开始介绍编程概念时,我们会一次又一次地看到这些功能。

在最简单的用法中, find 被赋予一个或多个要搜索的目录名称。例如,要生成我们的主目录列表,我们可以使用以下命令:

在最活跃的用户帐户上,这将产生一个大列表。由于列表被发送到标准输出,我们可以将列表通过管道传输到其他程序中。让我们使用 wc 来计算文件的数量:

哇,我们一直很忙! find 的美妙之处在于,它可以用来识别符合特定标准的文件。它通过(有点奇怪)应用选项(options)、测试(tests)和操作(actions)来实现这一点。我们先看看测试。

测试

假设我们希望从搜索中获得一个目录列表。为此,我们可以添加以下测试:

添加测试 -type d 将搜索限制在目录中。相反,我们可以通过此测试将搜索限制在常规文件上:

下表列出了 find 支持的常见文件类型测试:

文件类型说明
b块特殊设备文件
c字符专用设备文件
d目录
f普通文件
l符号链接

我们还可以通过添加一些额外的测试来按文件大小和文件名进行搜索。让我们查找所有与通配符模式 *.JPG 匹配且大于1MB的常规文件:

在这个例子中,我们添加了 -name 测试,后面是通配符范式(pattern)。请注意我们如何将其括在引号中,以防止shell扩展路径名。接下来,我们添加 -size 测试,后跟字符串“+1M”。前导加号表示我们正在查找大于指定数量的文件。前导减号会将字符串的含义更改为小于指定的数字。不使用符号意味着“与值完全匹配”。后面的字母M表示度量单位为兆字节。下表列出了可用于指定单位的字符:

测试说明
-cmin n匹配其内容或属性上一次修改是在 n 分钟前的文件或目录。
若要指定小于 n 分钟前的时间,请使用 -n ,若要指定大于 n 分钟前,请使用 +n
-cnewer file匹配其内容或属性上次修改时间比 file 最近的文件或目录。
-ctime n匹配其内容或属性(如所有权或权限)上次修改为 n *24小时前的文件或目录。
-empty匹配空文件和目录
-group name匹配属于 group 的文件或目录。group 可以表示为组名或数字组ID。
-iname pattern类似于 -name 测试,但不区分大小写。
-inum n将文件与索引节点编号 n 进行匹配。这有助于查找指向特定索引节点的所有硬链接。
-mmin n匹配其内容上次修改于 n 分钟前的文件或目录。
-mtime n匹配内容上次修改时间为 n *24小时前的文件或目录。
-name pattern使用指定的通配符 pattern 匹配文件和目录。
-newer file匹配其内容比指定 file 最近修改的文件和目录。
这在编写执行文件备份的shell脚本时非常有用。每次进行备份时,更新一个文件(如日志),然后使用 find 确定自上次更新以来哪些文件已更改。
-nouser匹配不属于有效用户的文件和目录。这可用于查找属于已删除帐户的文件或检测攻击者的活动。
-nogroup匹配不属于有效组的文件和目录。
-perm mode匹配权限设置为指定 mode 的文件或目录。 mode 可以用八进制或符号表示法表示。
-samefile name类似于 -inum 测试。匹配与文件名共享相同索引节点号的文件。
-size n匹配大小为 n 的文件。
-type c匹配类型为 c 的文件。
-user name匹配属于用户名的文件或目录。用户可以通过用户名或数字用户ID来表示。

这不是一个完整的列表。 find 手册页包含所有详细信息。

操作员

即使有 find 提供的所有测试,我们可能仍然需要一种更好的方法来描述测试之间的逻辑关系(logical relationships)。例如,如果我们需要确定目录中的所有文件和子目录是否都具有安全权限,该怎么办?我们将查找所有权限不是0600的文件和权限不是0700的目录。幸运的是, find 提供了一种使用逻辑运算符组合测试以创建更复杂的逻辑关系的方法。为了表达上述测试,我们可以这样做:

诶呀这看起来确实很奇怪。这些东西是什么?事实上,一旦你了解了操作员,他们就没那么复杂了。下表描述了与 find 一起使用的逻辑运算符:

操作员说明
-and如果操作员两侧的测试均为真,则匹配。这可以缩写为 -a 。请注意,当没有运算符存在时,-and 默认是隐含的。
-or如果操作员任一侧的测试为真,则匹配。这可以缩短为 -o
-not如果跟随操作员的测试为假,则匹配。这可以用感叹号(!)缩写。
( )将测试和运算符组合在一起,形成更大的表达式。
这用于控制逻辑计算的优先级。默认情况下, find 从左到右计算。通常需要覆盖默认的评估顺序以获得所需的结果。即使不需要,有时也可以包含分组字符来提高命令的可读性。
请注意,由于括号对shell具有特殊意义,因此在命令行上使用它们时必须引用它们,以便将它们作为参数传递给 find 。通常使用反斜杠字符来转义它们。同样重要的是,() 字符周围要用空格隔开,以便与命令中的其他单词分开。例如,find ~ \( -type f \)

有了这个操作符列表,让我们解构一下 find 命令。从最上层来看,我们看到我们的测试被安排为由 -or 运算符分隔的两组。

( expression 1 ) -or ( expression 2 )

这是有道理的,因为我们正在搜索具有特定权限集的文件和具有不同权限集的目录。如果我们同时在查找文件和目录,为什么要使用 -or 代替 -and ?当 find 扫描文件和目录时,会评估每个文件和目录是否与指定的测试匹配。我们想知道它是权限错误的文件还是(either ... or)权限错误的目录。两者不可能同时存在。因此,如果我们展开分组表达式,我们可以这样看:

( file with bad perms ) -or ( directory with bad perms )

我们的下一个挑战是如何测试【bad permissions】。我们该怎么做?事实上,我们没有。我们将测试的是【not good permissions】,因为我们知道什么是【good permissions】。对于文件,我们将【good】定义为0600,对于目录,我们将其定义为0700。将测试文件【not good】权限的表达式如下:

-type f -and -not -perm 0600

对于目录,则是这样的:

-tpye d -and -not -perm 0700

如上表所示,可以安全地删除 -and 运算符,因为默认情况下它是隐含的。所以,如果我们把这一切放在一起,我们就能得到最终的命令:

find ~ ( -type f -not -perm 0600 ) -or ( -type d -not -perm 0700 )

然而,由于括号对shell具有特殊意义,我们必须转义它们以防止shell试图解释它们。在每个字符前面加一个反斜杠字符就可以了。

理解逻辑运算符的另一个重要特征。假设我们有两个由逻辑运算符分隔的表达式。

expr1 -operator expr2

在所有情况下, expr1 将始终执行;然而,操作员将确定是否执行 expr2 。下表概述了它的工作原理:

expr1 的结果操作员expr2 是...
True-and始终执行
False-and从不执行
True-or从不执行
False-or始终执行

为什么会发生这种情况?这样做是为了提高性能。以 -and 为例。我们知道,如果 expr1 的结果为 false ,则表达式 expr1 -and expr2 不能为 true ,因此执行 expr2 没有意义。同样,如果我们有表达式 expr1 -or expr2 ,并且 expr1 的结果为 true ,那么执行 expr2 就没有意义了,因为我们已经知道表达式 expr1 -or expr2true

好的,这有助于它走得更快。为什么这很重要?这很重要,因为我们可以依靠这种行为来控制如何执行操作,我们很快就会看到。

预定义操作

让我们完成一些工作!从 find 命令中获取结果列表是有用的,但我们真正想做的是对列表中的项目进行操作。幸运的是, find 允许根据搜索结果执行操作。有一组预定义的操作和几种应用用户定义操作的方法。首先,让我们看看下表中列出的一些预定义操作:

操作说明
-delete删除当前匹配的文件。
-ls对匹配的文件执行 ls -dils 的等效操作。输出被发送到标准输出。
-print将匹配文件的完整路径名输出到标准输出。如果没有指定其他操作,则这是默认操作。
-quit匹配结束后退出。

与测试一样,还有更多的操作。有关完整详细信息,请参阅 find 手册页。

在第一个例子中,我们这样做:

这生成了我们主目录中包含的每个文件和子目录的列表。它生成了一个列表,因为如果没有指定其他操作,则暗示了 -print 操作。因此,我们的命令也可以表示如下:

我们可以使用 find 删除符合特定条件的文件。例如,要删除文件扩展名为 .bak 的文件(通常用于指定备份文件),我们可以使用以下命令:

在这个例子中,搜索用户主目录(及其子目录)中的每个文件,查找以 .bak 结尾的文件名。当它们被找到时,它们会被删除。

警告:不用说,在使用 -delete 操作时应该格外小心。始终先测试命令,用 -print 操作替换 -delete 以确认搜索结果。

在我们继续之前,让我们再看看逻辑运算符是如何影响操作的。考虑以下命令:

正如我们所看到的,此命令将查找名称以 .bak-name '*.bak')结尾的每个常规文件(-type f),并将每个匹配文件的相对路径名输出到标准输出(-print)。但是,命令执行方式的原因取决于每个测试和操作之间的逻辑关系。记住,默认情况下,每个测试和操作之间都有一种隐含的关系。我们也可以这样表达命令,使逻辑关系更容易看到:

在命令完全表达后,让我们看看逻辑运算符如何影响其执行:

测试/动作只有在以下情况下才能执行。。。
-print-type f-name '*.back' 为真
-name '*.bak'-type f 为真
-type f始终执行,因为这是一段关系中的第一个测试/操作。

由于测试和操作之间的逻辑关系决定了执行哪些测试和操作,因此我们可以看到测试和操作的顺序很重要。例如,如果我们对测试和操作进行重新排序,使 -print 操作成为第一个,那么命令的行为将大不相同。

此版本的命令将打印每个文件( -print 操作的计算结果始终为true),然后测试文件类型和指定的文件扩展名。

用户定义操作

除了预定义的操作外,我们还可以调用任意命令。传统的方法是使用 -exec 操作。此操作的工作原理如下:

-exec command {} ;

这里 command 是命令的名称,{}是当前路径名的符号表示,分号是指示命令结束的必需分隔符。下面是一个使用 -exec 执行前面讨论的 -delete 操作的示例:

同样,由于大括号和分号字符对shell具有特殊含义,因此必须引用或转义它们。

也可以交互式地执行用户定义的操作。通过使用 -ok 操作代替 -exec ,在执行每个指定命令之前都会提示用户。

在这个例子中,我们搜索名称以字符串 foo 开头的文件,并在每次找到一个文件时执行命令 ls -l 。在执行 ls 命令之前,使用 -ok 操作会提示用户。

提高效率

当使用 -exec 操作时,每次找到匹配的文件时,它都会启动指定命令的新实例。有时,我们可能更喜欢组合所有搜索结果并启动该命令的单个实例。例如,与其执行这样的命令:

ls -l file1 ls -l file2

我们可能更喜欢这样执行它们:

ls -l file1 file2

这会导致命令只执行一次,而不是多次。我们有两种方法可以做到这一点:传统方法,使用外部命令 xargs ;另一种方法,使用 find 本身中的新功能。我们先谈谈另一种方式。

通过将尾随的分号字符更改为加号,我们激活了 find 功能,将搜索结果组合到一个参数列表中,以执行所需的命令。回到我们的例子,每次找到匹配的文件时,这将执行 ls

通过将命令更改为以下内容:

我们得到了相同的结果,但系统只需执行 ls 命令一次。

xargs

xargs 命令执行一个有趣的功能。它接受来自标准输入的输入,并将其转换为指定命令的参数列表。在我们的例子中,我们会这样使用它:

在这里,我们看到 find 命令的输出被管道传输到 xargs 中, xargs 反过来为 ls 命令构造一个参数列表,然后执行它。

注意:虽然可以放入命令行的参数数量相当大,但并不是无限的。创建的命令可能太长,shell无法接受。当命令行超过系统支持的最大长度时, xargs 会使用尽可能多的参数执行指定的命令,然后重复此过程,直到标准输入用尽。要查看命令行的最大大小,请使用 --show-limits 选项执行 xargs

树莓派的Debian12结果如下:

处理有趣的文件名

类Unix系统允许在文件名中嵌入空格(甚至换行!)。这会给像 xargs 这样为其他程序构造参数列表的程序带来问题。嵌入的空格将被视为分隔符,由此产生的命令将把每个空格分隔的单词解释为单独的参数。为了克服这个问题, findxargs 允许可选地使用空字符作为参数分隔符。在ASCII中,空字符被定义为由数字零表示的字符(与例如空格字符相反,空格字符在ASCII中定义为由数字32表示的字符)。 find 命令提供了动作 -print0 ,它产生以null分隔(null-separated)的输出,xargs命令有 --null (或 -0 )选项,它接受以null分隔输入。这里有一个例子:

使用此技术,我们可以确保正确处理所有文件,即使是名称中包含嵌入式空格的文件。

重返游乐场

是时候把 find 付诸实践了。我们将创建一个游乐场,并尝试我们学到的一些东西。

首先,让我们创建一个包含大量子目录和文件的游乐场。

惊叹于命令行的力量!通过这两行,我们创建了一个包含100个子目录的游乐场目录,每个子目录包含26个空文件。用GUI试试吧!

我们用来完成这个魔术的方法包括一个熟悉的命令(mkdir)、一个奇特的shell扩展(braces,大括号)和一个新的命令 touch。通过将 mkdir-p 选项(这会导致 mkdir 创建指定路径的父目录)结合使用大括号展开,我们能够创建100个子目录。

touch 命令通常用于设置或更新文件的访问、更改和修改时间。但是,如果文件名参数是不存在的文件的参数,则会创建一个空文件。

在我们的游乐场中,我们创建了100个名为 file-A 的文件实例。让我们找到它们。

请注意,与 ls 不同,find 不会按排序顺序产生结果。其顺序由存储设备的布局决定。我们可以通过这种方式确认我们实际上有100个文件实例。

接下来,让我们看看如何根据文件的修改时间来查找文件。这将有助于创建备份或按时间顺序组织文件。为此,我们将首先创建一个参考文件,并将其与修改时间进行比较:

这将创建一个名为timestamp的空文件,并将其修改时间设置为当前时间。我们可以通过使用另一个方便的命令 stat 来验证这一点, statls 的一种增强版本。 stat 命令揭示了系统对文件及其属性的所有理解。

如果我们再次使用 touch ,然后使用 stat 检查文件,我们将看到文件的时间已更新。

接下来,让我们使用 find 更新一些游乐场文件。

这将更新游乐场中名为 file-B 的所有文件。接下来,我们将使用 find 通过将所有文件与参考文件时 timestamp 进行比较来识别更新的文件。

结果包含 file-B 的所有100个实例。由于我们在更新 timestamp 后对名为 file-B 的游乐场中的所有文件进行了 touch ,因此它们现在比 timestamp “更新”,因此可以用 -newer 测试进行识别。

最后,让我们回到之前执行的错误权限测试,并将其应用于 playground

此命令列出 playground 中的所有100个目录和2600个文件(以及 timestampplayground 本身,总共2702个),因为它们都不符合我们对“良好权限”的定义。根据我们对运算符和操作的了解,我们可以向此命令添加操作,以将新权限应用于游乐场中的文件和目录。

在日常工作中(on a day-to-day basis),我们可能会发现发出两个命令更容易,一个用于目录,另一个用于文件,而不是这个大的复合命令,但很高兴知道我们可以这样做。这里的重点是了解如何将运算符和动作一起用于执行有用的任务。

选项

最后,我们有选项。这些选项用于控制查找搜索的范围。在构造 find 表达式时,它们可能会包含在其他测试和操作中。下表列出了最常用的查找选项。

选项描述
-depth指示 find 在目录本身之前处理目录的文件。
当指定-delete操作时,此选项会自动应用。
-maxdepth levels设置在执行测试和操作find 将下降到目录树中的最大级别数。
-mindepth levels设置在应用测试和操作之前find 将下降到目录树中的最小级别数。
-mount指示 find 不要遍历挂载在其他文件系统上的目录。
-noleaf指示 find 不要基于搜索类Unix文件系统的假设来优化其搜索。
扫描DOS/Windows文件系统和CD-ROM时需要这样做。

总结

很容易看出, locatefind 一样简单。两者都有其用途。花点时间探索 find 的许多功能。经常使用它可以提高您对Linux文件系统操作的理解。

locateupdatedbfindxargs 程序都是GNU项目的 findutils 包的一部分。GNU项目提供了一个包含大量在线文档的网站,如果您在高安全环境中使用这些程序,应该阅读这些文档:http://www.gnu.org/software/findutils/