第17章:搜索文件

当我们徘徊在 Linux 系统时,有一件事变得非常清楚:一个典型的 Linux 系统有一大堆文件!这引出了一个问题,「我们怎么才能找到东西?」我们已经知道 Linux 文件系统是依初代类 Unix 系统传下来的惯例组织的,但是绝对数量的文件可能带来令人生畏的问题。

本章中,我们将会看到两个用来查找系统中文件的工具。

  • locate 用名称找文件

  • find 在目录等级中搜索文件

我们还将看到一个处理文件结果列表的文件搜索命令。

  • xargs 从标准输入建立和执行命令行

另外,还将介绍一对辅助我们浏览的命令。

  • touch 改变文件的时间

  • stat 显示文件或文件系统的状态

locate - 查找文件的简易方法

locate 程序执行快速路径名数据库的搜索,输出每一个匹配给定字符串的名称。例如,我们想找到所有文件名以 zip 开头的程序。由于我们要找的是程序,可以假定包含程序的目录会以 bin/ 结尾。因此,我们可以尝试这样用 locate 找到所需的文件:

[me@linuxbox ~]$ locate bin/zip

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

/usr/bin/zip
/usr/bin/zipcloak
/usr/bin/zipgrep
/usr/bin/zipinfo
/usr/bin/zipnote
/usr/bin/zipsplit

如果搜索的需求不是这么简单,我们可以将 locate 和其它工具如 grep 合并使用,来设计出更有趣的搜索。

[me@linuxbox ~]$ locate zip | grep bin
/bin/bunzip2
/bin/bzip2
/bin/bzip2recover
/bin/gunzip
/bin/gzip
/usr/bin/funzip
/usr/bin/gpg-zip
/usr/bin/preunzip
/usr/bin/prezip
/usr/bin/prezip-bin
/usr/bin/unzip
/usr/bin/unzipsfx
/usr/bin/zip
/usr/bin/zipcloak
/usr/bin/zipgrep
/usr/bin/zipinfo
/usr/bin/zipnote
/usr/bin/zipsplit

locate 程序已经有多年的历史了,并有几种常用的变体。Linux 发行版中最常用的两个是 slocatemlocate,不过它们通常是由名为 locate 的符号链接而得以访问。不同版本的 locate 有着重叠的选项集。有些版本包含正则表达式匹配(我们将在第 19 章学到)并支持通配符。请查阅 locate 的手册页来确定已安装的 locate 是哪个版本。

locate 的数据库从哪里来的?

你可能注意到,在有些发行版中,locate 在刚安装完系统后不能工作,不过第二天当你再尝试的时候,它就能正常工作了。是什么赋予了它工作的能力?locate 数据库有另一个名为 updatedb 的程序所创建。它通常由一个计划任务(cron job)周期性地运行,计划任务是指由 cron 守护程序定期执行的一个任务。大多数系统每天运行一次 updatedb 以支持 locate 程序。由于数据库不是持续更新的,你会注意到 locate 不会显示近期的文件。要克服这点,可以用超级用户权限在命令行中手动执行 updatedb 程序。

find - 查找文件的复杂方法

locate 程序可以单独基于文件名查找文件,find 程序基于各种属性搜索一个给定目录(及其子目录)中的文件。我们将会花费大量时间学习 find,因为它有大量有趣的特性,在后续章节中学习程序概念时会使得我们一再地查看。

最简单的用法是,给定一个或多个目录名,用 find 搜索。例如,要产生一份家目录的文件列表,我们会这么用它:

[me@linuxbox ~]$ find ~

在大多数活跃的用户帐户中,该命令会产生一份很长的列表。由于列表是被发送到标准输出的,我们可以将列表管道输出给其它程序。来用 wc 统计一下文件的数目。

[me@linuxbox ~]$ find ~ | wc -l
47068

哇,我们一直真忙!find 的优点在于它可用于识别符合特定条件的文件。它通过(有点奇怪)应用选项(options)、测试(tests)和行为(actions)来实现。首先来看测试。

测试

假设我们想要从搜索中得到一份目录的列表。要做到这点,可以加下列测试:

[me@linuxbox ~]$ find ~ -type d | wc -l
1695

添加 -type d 限制仅搜索目录。相反,我们可以限制仅搜索常规文件:

[me@linuxbox ~]$ find ~ -type f | wc -l
38737

表 17-1 列出了 find 测试所支持的常用的文件类型。

表 17-1:find 文件类型

文件类型

描述

b

块特殊设备文件

c

字符特殊设备文件

d

目录

f

常规文件

l

符号链接

我们还可以通过文件大小和文件名,添加一些附加的测试来搜索。来搜索全部常规文件,要求匹配通配符 *.JPG 并大于一兆字节。

[me@linuxbox ~]$ find ~ -type f -name "*.JPG" -size +1M | wc -l
840

在本例中,加了 -name 测试紧跟其后的通配符式样。注意是如何加引号以防止路径名扩展。随后,加了 -size 测试随后的字符串 +1M。为首的 + 指示我们正在查找的文件大于指定数字。一个为首的 - 则表明小于指定数字。如无指定符号,则「精确匹配数字」。末尾 M 指示度量单位为兆字节。表 17-2 列出了可用于指定单位的字符。

表 17-2: find 尺寸单位

字符

单位

b

512 字节块。缺省计量单位。

c

字节数。

w

两字节词。

k

千字节(1024 字节单位)。

M

兆字节(1048576 字节单位)。

G

吉字节(1073741824 字节单位)。

find 支持大量的测试。表 17-3 提供了常见的概述。请注意,在需要数字参数的情况下,可以应用上面讨论的相同的 +- 符号。

表 17-3:find 测试

测试

描述

-cmin n

匹配那些在 n 分钟内修改过内容或属性的文件或目录,-n 指定少于 n 分钟,而 +n 指定多于 n 分钟。

-cnewer file

匹配那些内容或属性修改时间比 file 文件的更近的文件或目录。

-ctime n

匹配那些内容或属性最后修改时间在 n * 24 小时内的文件或目录。

-empty

匹配空文件和空目录。

-group groupname

匹配那些属于 groupname 属组的文件或目录。groupname 可以是组名称或者是数字形式的组 ID。

-iname pattern

-name 测试一样,只是不区分字符大小写。

-inum n

匹配 inode 编号为 n 的文件。对于查找所有链接到特定 inode 的硬链接。

-mmin n

匹配那些最近 n 分钟内修改过内容的文件或目录。

-mtime n

匹配那些最近 n * 24 小时内修改过内容的文件或目录。

-name pattern

匹配通配符 pattern 的文件或目录。

-newer file

匹配那些内容修改时间比 file 文件的更近的文件或目录。在写脚本执行文件备份时很有用。每次你创建一个备份,更新一个文件(如日志),然后用 find 确定从上次更新之后哪些文件变动过。

-nouser

匹配那些不属于任一合法用户的文件或目录。这可以用来查找那些属于已被删除帐号的文件,或者检测攻击者的活动。

-nogroup

匹配那些不属于任一合法属组的文件或目录。

-perm mode

匹配那些权限设置等于指定 mode 的文件或目录。mode 可以是八进制或符号表示法。

-samefile name

-inum 测试类似。匹配共享同一 name 的文件。

-size n

匹配尺寸等于 n 的文件。

-type c

匹配类型等于 c 的文件。

-user name

匹配文件属主为 name 的文件或目录。用户可以是由用户名或数字用户 ID 表示。

以上不是一个完整的清单。find 手册页有完整的详述。

操作符

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

[me@linuxbox ~]$ find ~ \(-type f -not -perm 0600 \) or \( -type d -not -perm 0700\)

令人惊讶!看起来是这么古怪。都是些什么东西?实际上,一旦你对操作符有所了解,也就不觉得有多复杂了。表 17-4 描述了用在 find 上的逻辑操作符。

表 17-4:find 逻辑操作符

操作符

描述

-and

仅匹配操作符两边结果都为真的测试。可以缩写为 -a。注意当没有提供操作符时,默认隐含了 -and

-or

匹配操作符任一边结果为真的测试。可以缩写为 -o

-not

匹配操作符后结果为假的测试。可以缩略为一个感叹号(!)。

()

将测试和操作符组合在一起,组成更大的表达式。用来控制逻辑评估的先后顺序。默认地,find 按从左到右的顺序评估。而越过默认评估顺序以获得想要的结果,也是经常必要的。即便不需要,有时包含组合字符也能够增进命令的可读性。注意,由于括号在 shell 中有其特殊意义,所以当使用时必须被引用以允许它们作为 find 的参数。通常会用反斜杠符号来转义。

有了这份操作符列表,就可以试着解构 find 命令了。当从最上层开始看的时候,可以看到测试是由被 -or 操作符分隔成的两组组成的。

( expression 1 ) -or ( expression 2 )

这是有理的,因为我们正在检索具有某种权限的文件和不同属性的目录。我们正在找的是文件和目录,为什么要用 -or 而不是 -and?当 find 扫描文件和目录时,会评测每个文件和目录来看它是否匹配指定的测试。我们想要知道它是有着错误权限的文件还是一个有着错误权限的目录。它不可能同时都是。所以当我们扩展一下组合的表达式,就可以看到:

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

接下来的挑战是如何测试「错误的权限」。该怎么做呢?实际上,我们没有做。我们要测试的是「不正确的权限」是因为我们知道什么是「正确的权限」。对于文件,我们定义 0600 是正确的;对于目录,我们定义 0700 是正确的。下面这个表达式将测试具有「不正确」权限的文件:

-type f -and -not -perms 0600

而下面的是测试目录的:

-type d -and -not -perms 0700

如表 17-4 所述,-and 操作符由于是默认隐含的,所以可以被安全地移除。最后,我们把这些都组合在一起,得到最终的命令就是:

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

还有,因为括号在 shell 中有特殊含义,我们必须将其转义,以预防 shell 试图解释它们。就需要在每个括号前放一个反斜杠符号来完成转义。

关于逻辑操作符,还有一个很重要的特性需要理解。假设有两个表达式,被一个逻辑操作符分隔:

expr1 -operator expr2

在任何情况下,expr1 总是会被执行,然而,操作符将会决定 expr2 是否会被执行。表 17-5 描述了其如何工作。

表 17-5:find and/or 逻辑

expr1 的结果

操作符

expr2 是……

True

-and

总是被执行

False

-and

从不被执行

True

-or

从不被执行

False

-or

总是被执行

为何会这样?这样做是为了提高性能。拿 -and 为例,我们知道 expr1 -and expr2 的值在 expr1 的结果为假的时候,是不可能为真的,所以就没有必要去执行 expr2 了。同样的,如果我们有个 expr1 -or expr2 这个表达式,当 expr1 的结果为真时,就没有必要计算 expr2 了,因为已经知道这个表达式的结果是真了。

好了,这样有助于更快地运行。为何这很重要?因为我们可以依赖这个行为来控制操作的执行方式,我们很快就会看到。

预定义的行为

让我们来完成一些工作!从 find 命令获取一个结果列表是有用的,但我们真正想做的是操作列表上的项目。幸运的是,find 允许基于搜索结果来执行操作。这里有一组预定义的行为和几种途径来应用用户定义的行为。首先,我们看表 17-6 中所列的几个预定义行为。

表 17-6:预定义 find 行为

行为

描述

-delete

删除当前匹配的文件。

-ls

对匹配的文件执行等同于 ls -dils 的命令。将结果输出到标准输出。

-print

对匹配的文件输出其完整的路径名。如果没有指定别的行为,这就是其默认行为。

-quit

当匹配后退出。

伴随着测试,有着更多的操作。可以查看 find 的手册页获取完整细节内容。

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

find ~

这条命令会制造一份清单,包含我们家目录中的所有文件和子目录。之所以会产生一份清单,是因为若没有指定其它行为,则隐含了 -print 行为。所以,我们的命令也可以被表述为下面这个:

find ~ -print

我们可以用 find 来删除符合某一标准的文件。例如,要删除文件扩展名为 .bak 的文件(经常被用来指定备份文件),可以使用这条命令:

find ~ -type f -name '*.bak' -delete

此例中,会检索所有在用户家目录中的(包括其子目录中的)文件名以 .bak 结尾的文件。一旦找到,就删除。

警告:不言而喻,当使用 -delete 行为时,用户应当极其谨慎。总是应当先用 -print 行为替换 -delete 行为,以确认检索结果。

在继续展开之前,让我们再看一下逻辑操作符怎样影响行为。考虑下面的命令:

find ~ -type f -name '*.bak' -print

如我们所见,这条命令将寻找每个常规文件(-type f),其文件名以 .bak 结束(-name '*.bak'),并且会输出其每个匹配文件的相对路径到标准输出(-print)。然而,命令如此执行的原因是取决于每个测试和行为之间的逻辑关系的。记住,默认情况下,每个测试和行为之间都隐含了一个 -and 关系。我们可以将命令作如下表述,以便更容易地看清逻辑关系:

find ~ -type f -and -name '*.bak' -and -print

在完整表达了命令之后,来看下逻辑操作符如何影响其执行:

测试/行为

如果…则执行

-print

-type f-name '*.bak' 是真

-name '*.bak'

-type f 是真

-type f

总是会被执行,因为在 -and 关系中,这是第一个测试或行为。

由于测试和行为之间的逻辑关系决定了它们中的哪些能被执行,所以我们可以看到测试和行为的顺序是很重要的。举例而言,如果我们将测试和行为重新排序,使得 -print 行为在首位,则命令的行为将会有很大的不同。

find ~ -print -and -type f -and -name '*.bak'

这一版本的命令会打印每个文件(-print 行为总是评估为真),然后测试是否是文件类型和指定文件扩展名。

用户定义的行为

除了预定义行为,我们还可以调用任意命令。传统的做法是使用 -exec 行为。该行为工作方式:

-exec command {} ;

这里,command 是命令名,{} 是当前路径名的符号表示,分号(;)是指示命令结束的分隔符。这里有一个例子,用 -exec 来执行之前所讨论过的 -delete 行为:

-exec rm '{}' ';'

重申一下,由于括号和分号字符在 shell 中有其特殊意义,所以必须用引号或者转义。

还可以交互式地执行用户定义行为。用 -ok 行为替换 -exec,在执行每个指定命令之前,用户都会得到提示。

find ~ -type f -name 'foo*' -ok ls -l '{}' ';'
< ls ... /home/me/bin/foo > ? y
-rwxr-xr-x 1 me me 224 2007-10-29 18:44 /home/me/bin/foo
< ls ... /home/me/foo.txt > ? y
-rw-r--r-- 1 me me   0 2016-09-19 12:53 /home/me/foo.txt

在这个例子中,我们检索文件名以 foo 开头的文件,每当找到一个,就执行 ls -l 命令。使用 -ok 行为,会在执行 ls 命令之前提示用户。

增进效率

当使用 -exec 行为时,每找到一个匹配文件,就运行一次指定命令的新实例。或许我们可以合并所有的检索结果,运行一次命令的实例。例如,与其这样运行命令:

ls -l file1
ls -l file2

我们更喜欢这样运行:

ls -l file1 file2

这导致命令只执行一次而不是多次。有两种方法:传统的做法,使用外部命令 xargs,替代方法是,使用 find 自身的新特性。我们先讨论替代方法。

通过将句尾的分号改成加号,我们激活了 find 的能力,使其合并检索结果为一个命令的参数列表。回到我们的例子中,下例中每次找到匹配文件就会执行一次 ls

find ~ -type f -name 'foo*' -exec ls -l '{}' ';'
-rwxr-xr-x 1 me me 224 2007-10-29 18:44 /home/me/bin/foo
-rw-r--r-- 1 me me   0 2016-09-19 12:53 /home/me/foo.txt

变更后,如下:

find ~ -type f -name 'foo*' -exec ls -l '{}' +
-rwxr-xr-x 1 me me 224 2007-10-29 18:44 /home/me/bin/foo
-rw-r--r-- 1 me me   0 2016-09-19 12:53 /home/me/foo.txt

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

xargs

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

find ~ -type f -name 'foo*' -print | xargs ls -l
-rwxr-xr-x 1 me me 224 2007-10-29 18:44 /home/me/bin/foo
-rw-r--r-- 1 me me   0 2016-09-19 12:53 /home/me/foo.txt

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

注意:可以被置入命令行的参数数量是非常巨大的,没有任何限制。所以可能会制造一个太长的命令,以至于 shell 不能接受。当一条命令行超出了系统支持的最大长度时,xargs 以最大长度的参数执行指定命令,然后重复该进程,直到穷竭标准输出。要查看命令行的最大长度,以 --show-limits 为参数执行 xargs

处理有趣的文件名

类 Unix 系统允许在文件名中存在空格(甚至是换行符!)。这会使得像 xargs 这样的程序在给其它程序构建参数列表的时候出现问题。内嵌的空格会被当作一个分隔符,导致命令将每个被空格分隔的词语作为一个参数。要克服这点,findxargs 允许用 null 字符作为参数分隔。一个 null 字符在 ASCII 中被定义为数字 0(相反的,例如空格字符,则在 ASCII 中被定义为数字 32)。find 命令提供一个行为 -print0,以产生 null 分隔符的输出,而 xargs 命令则有 --null(或 -0)选项,以接受 null 分隔的输入。这个有个例子:

find ~ -iname '*.jpg' -print0 | xargs --null ls -l

使用该技术,我们可以保证所有文件,甚至那些文件名中内嵌有空格的文件都能得到正确地处理。

回到游戏场

是时候将 find 置于一些(几乎)实际用途中了。来建立一个游戏场,并试验一些我们已经学到的知识。

首先,建立一个游戏场,并建立许多子目录和文件。

[me@linuxbox ~]$ mkdir -p playground/dir-{001..100}
[me@linuxbox ~]$ touch playground/dir-{001..100}/file-{A..Z}

命令行的神奇威力!用这两条命令,我们创建了 playground 目录,内含 100 个子目录,每个子目录包含 26 个空文件。请用图形界面试试!

我们完成这个魔法所采用的方法,涉及一个熟悉的命令(mkdir)和一个奇特的 shell 扩展(大括号),还有一个新命令,touch。当 mkdir-p 选项(使得 mkdir 可以创建指定路径的上级目录)以及大括号扩展组合在一起的时候,我们就能创建 100 个子目录了。

touch 目录通常用来设置或更新访问、变更、修改文件的时间。然而,如果一个文件名参数是一个不存在的文件时,会创建一个空文件。

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

[me@linuxbox ~]$ find playground -type f -name 'file-A'

注意,不同于 lsfind 不会产生一个排序好的结果。其顺序取决于存储设备的布局。我们可以通过这个方法确认实际上有 100 个文件实例:

[me@linuxbox ~]$ find playground -type f -name 'file-A' | wc -l
100

接下来,基于修改时间来看一下找到的文件。这有助于创建备份文件或者按时间顺序组织文件。首先,将创建一个参考文件,以比较修改时间。

[me@linuxbox ~]$ touch playground/timestamp

上述命令创建了一个名为 timestamp 的空文件,并将其修改时间设置为当前时间。我们可以用另一个简便的命令 stat 来验证,它是一种不同版本的 lsstat 命令显示系统内容,理解文件及其属性。

[me@linuxbox ~]$ stat playground/timestamp
File: `playground/timestamp'
Size: 0          Blocks: 0         IO Block: 4096 regular empty file
Device: 803h/2051d Inode: 14265061 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1001/ me) Gid: ( 1001/ me)
Access: 2018-10-08 15:15:39.000000000 -0400
Modify: 2018-10-08 15:15:39.000000000 -0400
Change: 2018-10-08 15:15:39.000000000 -0400

若再次运行 touch 并用 stat 检查,会看到该文件的时间已经更新了。

[me@linuxbox ~]$ touch playground/timestamp
[me@linuxbox ~]$ stat playground/timestamp
File: `playground/timestamp'
Size: 0          Blocks: 0         IO Block: 4096 regular empty file
Device: 803h/2051d Inode: 14265061 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1001/ me) Gid: ( 1001/ me)
Access: 2018-10-08 15:23:33.000000000 -0400
Modify: 2018-10-08 15:23:33.000000000 -0400
Change: 2018-10-08 15:23:33.000000000 -0400

接下来,用 find 更新一些游戏场文件。

[me@linuxbox ~]$ find playground -type f -name 'file-B' -exec touch '{}' ';'

这会更新所有游戏场里名为 file-B 的文件。然后,我们通过与参考文件 timestamp 比较,用 find 识别处更新过的文件。

[me@linuxbox ~]$ find playground -type f -newer playground/timestamp

结果包含了 100 个 file-B 的实例。因为我们在更新了 timestamp 文件之后才执行 touch 更新了游戏场里所有名为 file-B 的文件,它们现在就比 timestamp 文件更新,因此能被 -newer 测试所识别。

最后,让我们回到之前执行过的错误权限的测试,将其应用到游戏场中。

[me@linuxbox ~]$ find playground \( -type f -not -perm 0600 \) -or \(-type d -not -perm 0700 \)

该命令列出游戏场中所有 100 个目录和 2,600 个文件(还包括 timestamp 文件和 playground 目录自身,共计 2,702 项),因为没有一个是符合我们「正确权限」的定义。用我们所学的操作符和行为的知识,可以加一些行为到命令行中,以便将新的权限应用到游戏场中的文件和目录。

[me@linuxbox ~]$ find playground \( -type f -not -perm 0600 -exec chmod 0600 '{}' ';' \) -or \( -type d -not -perm 0700 -exec chmod 0700 '{}' ';' \)

在日常操作中,我们或许会觉得写两条命令会比写这样一个大型组合命令更简单,一条给目录,另一条给文件,但是这个组合命令很好地让我们知道,我们可以通过这个方式实现。这里的重点是理解如何一起使用操作符和行为,以执行有用的任务。

选项

最后,来看选项。选项用来控制 find 的搜索范围。在构建 find 表达式时,它们可以被包含在其它测试和行为中。表 17-7 列出了最常用的 find 选项。

表 17-7:find 选项

选项

描述

-depth

指导 find 先处理目录中的文件,再处理目录自身。当指定 -delete 行为时,会自动应用该选项。

-maxdepth levels

当执行测试和行为时,设置 find 下探到目录树中的最大层次数。

-mindepth levels

当执行测试和行为时,设置 find 下探到目录树中的最小层次数。

-mount

指导 find 不要遍历加载在别的文件系统上的目录。

-noleaf

指导 find 不要基于类 Unix 文件系统的假设来优化其检索。当扫描 DOS/Windows 和 CD-ROM 文件系统时,需要该选项。

总结

易于发现,locate 较简单,而 find 较复杂。它们各有其用途。花点时间来探索 find 的诸多特性。经常使用,可以增进你对 Linux 文件系统操作的理解。

扩展阅读

  • locateupdatedbfind,和 xargs 程序,都是 GNU 项目的 findutils 包中的部分。GNU 项目提供了一个扩展在线文档的网站,质量非常好,如果你在高度安全环境中使用这些程序的话,应该读一下:http://www.gnu.org/software/findutils/

Last updated

Was this helpful?