查找文件及更多

作者:Eric Goebelbecker

新用户在获得他们的第一个 Linux 系统后不久,通常需要找到系统上的某个文件。因此,他们从朋友那里,或者可能从书或杂志上学习了以下命令

$ find / -name filename -print

现在,虽然这个命令确实可以完美地工作,但对于不熟悉 find 命令的人来说,语法似乎很笨拙。为什么我们必须指定 print?[注意:在 Linux 系统和其他使用 GNU find 的系统中,我们不需要。但是标准的 Unix find 坚持要这样做,所以如果您同时使用 Unix 和 Linux,最好还是习惯它。]

就此而言,为什么我们必须指定 name?为什么不直接 find filename?正是这种看似神秘的结构使 find 成为 Unix 工具箱中最未被充分利用的命令之一。

查看 find 手册页(在任何系统上,而不仅仅是 Linux)使情况更加令人困惑。对于不熟悉 Unix 的人来说,find 的“运算符”和“表达式”使其成为一个非常复杂的程序,仅仅是为了定位文件。

如果只想定位文件,有一种更好的方法可以做到这一点

locate filename

这将适用于正确设置的带有 GNU find 的 Linux 系统。当我们已经有一个像 locate 这样的简单命令时,为什么还要有一个像 find 这样复杂的命令呢?因为 find 的用途远不止查找文件。(良好的 Linux 发行版都正确设置了 update。如果您的发行版没有,您可以以 root 身份运行 updatedb 来更新它使用的数据库,或者只是像上面显示的那样使用 find)。

我在家使用的 Caldera/Redhat 系统在 crontab 中有几个条目运行此命令

find /tmp/* -atime +10 -exec rm -f {} \;

此命令删除 /tmp 中过去十天内未被访问过的任何文件。find 只删除过去十天内 访问 过的文件,而不是删除很久以前 创建 的文件,这是一个微妙但非常重要的点。Find 使我们能够访问存储在 Unix 文件系统中关于文件和目录的非常有价值的信息集。

与大多数 Unix 文件系统一样,大多数 Linux 系统上使用的第二个扩展文件系统(“ext2”)存储的文件数据比 DOS 等系统中的名称、大小和上次更改日期更广泛。它还存储所有者和组、访问模式、文件上次修改和访问的日期、文件上次更改状态的日期以及类型。(别担心,我们会在接下来的内容中解释这些)。

除了名称之外,所有这些信息都存储在称为 inode 的结构中,用于每个文件和目录。在 Unix 文件系统中,目录只是包含带有 inode 编号的文件名列表的 文件

Finding Files and More

表 1 列出了 inode 条目字段以及它们如何为 Linux 支持的不同文件系统类型进行“翻译”。虽然此表对您来说可能意义不大,但在您读完本文时,它应该是显而易见的。

命令行

让我们分析一下 find 命令行

find starting-point options criteria action
  • starting-point 一个或多个开始搜索的目录。默认值为当前目录。

  • options 以多种方式修改用于搜索的方法。

  • criteria 指定选择哪些文件,忽略哪些文件。默认情况下,选择所有找到的文件。

  • action 对选择的文件执行什么操作。GNU find 的默认操作是 -print,但标准 Unix find 没有默认操作,除非显式提供操作,否则将中止并报错。

起始点

起始点 参数对 find 的操作有两个影响。最明显的是它指定在哪个目录(或哪些目录;可以有多个起始点)开始查找文件。另一个影响是如何处理所选文件名,如下例所示

$ cd /usr/X11/man
$ find man5 -print
man5
man5/XF86Config.5x
man5/pbm.5
man5/pgm.5
man5/pnm.5
man5/ppm.5
$ find /usr/X11/man/man5 -print
/usr/X11/man/man5
/usr/X11/man/man5/XF86Config.5x
/usr/X11/man/man5/pbm.5
/usr/X11/man/man5/pgm.5
/usr/X11/man/man5/pnm.5
/usr/X11/man/man5/ppm.5

当用户只是查找文件时,行为上的这种差异并不重要。但是,当您想使用 find 的输出驱动另一个程序时,这可能非常重要,具体取决于所驱动的程序。

除了起始点之外,我们还可以控制 find 行为的其他一些方面,例如它应该如何处理软链接、如何评估文件时间戳以及要跟踪目录结构的深度。这些由 options 控制。

-follow 选项告诉 find 跟随软(或符号)链接到实际文件。软链接是“指向”另一个文件的文件。为了演示此选项,请(以普通用户身份,而不是以 root 身份)在您的主目录中使用 ln 创建一个指向属于 root 的文件的软链接。

$ cd
$ ln -s /vmlinuz ./kernel

现在使用 ls 生成文件的长列表。

$ ls -l kernel
lrwxrwxrwx ... kernel -> /vmlinuz

模式的第一列 l 告诉我们它是一个软链接。我们也被告知链接“指向”哪个文件。

现在让我们演示 find 的 -follow 选项的效果,通过搜索目录查找属于 root 的文件,并使用它。(uid 0 是 root;我们稍后将更详细地介绍 -uid 选项。)

$ find . -uid 0 -print
nothing is printed
$ find . -follow -uid 0 -print
./kernel

您创建了指向内核的链接,因此您拥有名为 ./kernel 的链接。但是文件 /vmlinuz 归 root 所有。

当涉及评估时间时,-daystart 选项会修改 find 的行为。当指定 -daystart 时,find 将从一天的开始而不是从 24 小时前开始计算天数。(我们稍后将介绍与时间相关的参数。)

用户经常需要查找一个他或她知道在本地硬盘上的文件,而不是在挂载的 cdrom 或网络卷上。防止 find 迷失到这些其他磁盘的简单方法是使用 -xdev 选项。

$ find / -name document -print

将导致 find 在 / 下的每个目录中搜索文件“document”,这对于挂载了 CDROM 或网络文件系统的情况可能非常慢。

$ find / -xdev -name document -print

相反,将使 find 将其搜索限制为 / 挂载到的设备。(-xdev 的别名是 -mount)当然,如果您有多个本地文件系统,则需要为其执行不同的搜索。也许

$ find / /usr -xdev -name document -print

如果您有两个分区,一个用于 /,一个用于 /usr。或者,您可以说

$ find / -fstype ext2 -name document -print

如果您的所有本地分区都是 ext2 文件系统。

节省搜索时间的另一种方法是使用与目录深度相关的选项。

$ find /usr -maxdepth 4 -name document -print

将限制 find 在 /usr 下“深度”为四级或更低的目录中搜索文档。

与目录深度相关的另一个选项是 -depth,它使目录在其中的任何文件之前被选择。稍后我们将看到为什么这很有用。

-noleaf 选项用于搜索非 Unix 类的文件系统。表 1 告诉您对于哪些文件系统,指定 -noleaf 可能会加快您的搜索速度。

我们已经有一个按名称查找文件的示例。其他用于匹配文件名的机制是 -path,它按目录名匹配;-iname,它类似于 -name 但不区分大小写;以及 -ipath,它也不区分大小写。

挑选和选择

条件 允许您选择文件。

每个文件都有访问时间、状态时间和修改时间,find 提供了三个基于时间的条件,每个值一个。它们可以以天或分钟为增量进行检查,并且可以根据这些时间比较文件。

每次文件内容更改时,都会设置修改时间。

$ find . -mtime +10 -print

将打印出过去十天内未修改的文件,类似于我们的第二个示例。

在前面的示例中,我们使用加号来表示“大于”。除此之外,find 还支持减号来表示小于。

$ find / -mtime -5 -print

将打印出 5 天前访问的文件。缺少这些运算符将导致 find 选择 精确 匹配。如前所述,-daystart 选项将修改搜索,使日期基于最近的午夜而不是现在的 24 小时前。

要使用分钟而不是天,请使用 -mmin 条件。

$ find . -mmin +10 -print

将输出十分钟前修改的文件。

-newer 条件

$ find . -newer document -print

将输出比文档更新修改的文件。

该命令设置文件的 访问 时间和 修改 时间。如果文件不存在,将创建该文件。我们可以用它来举例。

$ touch foo

如果当前目录中尚不存在名为“foo”的文件,则会创建一个名为“foo”的文件。现在,

$ find -mmin 1 -print

应该输出 foo,但

$ find -mmin 2 -print

不应该。

对于访问时间(指示上次打开文件的时间),find 具有类似的选项。对于天数,有 -atime;对于分钟,有 -amin;对于比较,有 -anewer

状态时间最初指示创建时间,然后跟踪对文件或其 inode 的任何修改。它可以与 -ctime-cmin-cnewer 一起使用。这些条件根据上次更改文件的所有权、访问模式或其他特征的时间来匹配文件。

Find 还有一个 -used 选项。它将匹配自上次更改状态以来已被访问的文件

find -used +2

将查找自上次更改状态以来已使用超过两天的文件。

在本文中,我已经多次提到文件模式。文件模式表示哪些用户可以对文件执行某些操作,文件的类型以及有关文件的其他一些信息。find 允许我们根据文件的模式匹配文件。

在介绍这些选项之前,我将解释文件模式以及它们如何显示和设置。

用户最常接触文件模式是在它们涉及文件所有权和访问时。文件属于所有者和组,因此访问控制是相对于三个实体进行的:所有者、组和世界。(“世界”由非所有者且不属于附属组的用户组成。)

访问控制是相对于三个操作进行的:读取、写入(包括删除)和执行。让我们看一下 ls 的长列表的输出。

$ ls -l foo
-rw-rw-r-- 1 eric staff  0 Sep  6 22:55 foo

(我删除了 ls 通常创建的一些空格,以便适应整个输出。)输出的最左列有十个字符,显示了使用 foo 的模式和文件类型。从左侧开始,第一个字符供 ls 用于向我们显示文件类型。例如,如果是链接或目录,我们会在那里看到 ld

其余九个字符向我们显示了访问模式。它们以三组为单位,按顺序向我们显示所有者、组和世界的权限。每个三元组都有一个用于读取 r、写入 w 和执行 x 的字段。

$ chmod 777 foo
$ ls -l foo
-rwxrwxrwx 1 eric staff  0 Sep  6 22:55 foo

我们已为文件“foo”上的所有用户打开了所有权限。

chmod 命令可以使用两种不同的表示法,符号表示法和八进制表示法。虽然符号表示法对大多数人来说更容易记住,但我使用了八进制表示法,因为它是 find 期望的模式格式。使用此表示法,每个数字代表每个用户类别的八进制权限。

权限通过添加以下内容来计算

  • 4 读取

  • 2 写入

  • 1 执行

因此,如果您想给文件的所有者完全权限,而组和世界只读和执行权限,则要“设置”所有者的所有位,以及其他人的读取和执行位

Owner = 4 + 2 + 1 = 7
Group = 4 + 1     = 5
World = 4 + 1     = 5

所以命令将是

$ chmod 755 program
$ ls -l program
-rwxr-xr-x 1 eric  staff 106410 Sep  6 22:55 program

列表显示了我们期望的模式。

回到 find:-perm 条件接受这种类型的表示法。

$ find . -perm 777 -print

将查找当前目录和当前目录下所有用户都设置了读取、写入和执行权限的所有文件。

-perm 选项还支持 +- 运算符。

$ find . -perm +600 -print

将输出所有者可读 可写的所有文件。

$ find . -perm -600 -print

将输出所有者可读 可写的所有文件。

因此,+ 充当布尔值“或”,而 - 充当布尔值“与”。

根据权限查找文件的能力是一个重要的安全工具。稍后,我将介绍一些重要的特殊文件模式,以及 find 如何帮助保护系统免受使用这些模式的攻击。

文件大小是 find 提供的另一个选项。文件大小可以以 512 字节块、2 字节字、千字节或字节为单位指定。由于 size 是一个数值选项,因此也支持 + 和 -。

$ find . -size +4096k -print

将打印出任何大于 4 兆字节的文件的名称。

$ find . -size -1c -print

将打印出任何小于 1 字节的文件的名称。-empty 选项也匹配空文件。

对于 512 字节块,数字后应跟一个“b”,对于 2 字节字,应跟一个“w”。

按大小搜索文件时,有一个需要注意的地方。某些文件(例如 /var/adm/lastlog)分配的空间多于实际使用的空间。这些文件被称为“稀疏”文件或“空洞”文件。与 ls 一样,find 将按这些文件已分配的空间而不是它们实际使用的空间报告这些文件。如果您对文件使用的空间量有任何疑问,请使用 du 命令。

$ ls -l /var/adm/lastlog

在我的系统上报告的大小为 16032 (15k);

$ du -k /var/adm/lastlog

仅报告 3k。

我们的第一个示例向我们展示了如何在知道确切名称时查找文件。Find 也接受 * 通配符,但文件名必须用引号引起来,以防止 shell 在将文件名传递给 find 之前扩展文件名。

$ find / -name "*gif" -print

将输出整个系统中所有以“gif”结尾的文件。

除了简单的通配符之外,find 还支持带有 -regex 选项的正则表达式。

$ find . -regex './[0-9].*' -print

将在当前目录中查找任何以数字开头的文件。请注意,正则表达式应用于整个路径,这使得表达式很难编写。有关正则表达式的更多信息,请参阅 grep 的手册页或 Linux Journal 十月刊中的文章。

另一个搜索条件是文件类型。

$ find / -type d -print

将列出所有目录。以下是文件类型列表以及用于搜索它们​​的相应字母。

  • b 块特殊文件,例如磁盘设备。

  • c 字符特殊文件,例如终端设备。

  • d 目录

  • p 命名管道

  • f 常规文件

  • l 符号(软)链接

  • s 套接字

如果您不熟悉任何这些文件类型,请不要担心。您可以在学习的过程中了解它们。

文件也可以通过用户或组 ID 进行匹配。如前所示,

$ find . -uid 0 -print

将输出所有属于 root 的文件。

$ find . -uid 120 -print

将输出所有属于 UID 为 120 的用户的文件。

为了更轻松,

$ find -user eric -print

将输出所有属于 eric 的文件。

Find 还具有用于组的类似选项:-gid-group

不仅仅是打印!

现在您已经知道如何定位几乎任何文件,除了打印它们的名称之外,您还能用它们做什么?

$ find . -fprint foo

将当前目录中的文件列表发送到文件“foo”。如果该文件不存在,则会创建该文件。如果存在,则会替换其内容。

Find 还提供了 -printf 操作。这允许格式化输出。

$ find . -printf 'Name: %f Owner: %u %s bytes\n'

生成一个包含文件名、所有者和大小(以字节为单位)的表。

-printf 操作有许多预定义的字段,涵盖了文件的所有可用信息。有关选项的不完整列表,请参见表 2。Find 还有一个 -fprintf 开关,它会将输出发送到文件,如 -fprint

表 2. printf 选项

转义序列\a - 报警铃\b - 退格\f - 换页\n - 换行符(不自动提供)\r - 回车符- 水平制表符\v - 垂直制表符\\ - 字面反斜杠\c - 停止打印并刷新输出

格式化序列%b - 文件大小(以 512 字节块为单位)%k - 文件大小(以 1k 块为单位)%s - 文件大小(以字节为单位)%a - 访问时间(标准格式)%A - 格式化的访问时间(有关选项,请参见手册页)%c - 状态时间(标准格式)%C - 格式化的状态时间(与 %A 相同)%F - 文件系统类型%p - 文件名%f - 文件名(已删除路径)%P - 文件名(已删除 find 参数,文件而不是 ./文件)%u - 用户名%g - 组名

(有关完整列表,请参见手册页)

输出的第三个选项是 -ls。此选项生成的文件列表等效于 ls -idls 的输出。-fls 选项会将此输出发送到文件。

当然,仅仅生成格式化的文件列表并不是 find 功能的极限。Find 还允许我们使用 -exec-ok 在它们上执行命令。-exec 为每个匹配的文件执行一个命令。

我们之前的示例演示了 -exec 选项的常见用途:删除旧的和未使用的文件。

$ find /tmp/* -atime +10 -exec rm -f {} \;

-exec 开关本身之后,我们指定命令、任何选项(例如 -f),然后指定 {},它代表匹配的文件。然后必须以 ; 终止命令行(\ 是为了防止 shell 扩展)。

$ find . -type f -exec grep -l linux {} \;

将在当前目录和当前目录下的所有常规文件上执行命令 grep -l linux

-ok 开关的操作方式相同,但在每个文件上执行命令之前,将提示用户确认。

$ find . -ok tar rvf backup {} \;

此命令将遍历当前目录及其下级目录,询问用户应将哪些文件添加到 tar 存档“backup”中。

这使我们进入了 find 的一些实际用途。

有时需要复制目录或目录结构。为此,许多用户使用带有 -r 选项的 cp 命令。但是,此命令并不总是创建精确的副本!

创建一个目录,其中包含一个文件和一个链接。

$ mkdir test
$ touch test/bar
$ ln -s /vmlinuz /test/foo
$ ls -l test
-rwx--x--x eric staff 0 Sep  9 bar
lrwxrwxrwx eric staff 8 Sep  9 foo -> /vmlinuz

现在使用 cp -r 复制它

$ cp -r test test1
$ ls -l test1
-rwx--x--x eric staff      0 Sep  9 11:18 bar
-rw-rw-r-- eric staff 318436 Sep  9 11:18 foo

cp 命令跟随软链接并将内核复制到新目录中!

让我们尝试另一种方法

$ rm -r test1
$ cd test
$ find -depth -print | cpio -pdmv ../test1
$ ls -l ../test1
-rwx--x--x eric staff 0 Sep  9 bar
lrwxrwxrwx eric staff 8 Sep  9 foo -> /vmlinuz

此方法使用 cpio 将文件复制到新目录。Find 通过遍历目录结构生成文件列表。即使我们的示例只有一个目录深度,我们也知道 find 可以遍历整个目录结构。我们还知道,我们还可以控制它遍历哪些目录以及输出哪些文件。

在上面的命令中,我添加了 -depth 选项。它确保在目录中的文件之前输出目录名。这允许 cpio 在尝试将文件复制到目录之前创建目录。

cpio 命令是 Unix 工具箱中的另一个多用途工具。它可以创建各种格式的存档并从中提取。它还可以完美地处理 find 的 -print 选项的输出。结合使用,这些工具可以形成一个简单的备份系统。(请注意:我仅将其作为示例进行介绍。支持多个用户或具有不可替代数据的系统应使用更广泛和更强大的备份系统。)

$ find . -depth -print \
  | cpio -ov --format=crc > /dev/fd0

find 读取当前目录的内容,文件名通过管道传递给 cpio,cpio 将文件复制到软盘,格式为带有 CRC 校验和的 System V R4 存档格式。(此格式优于默认格式,因为它与平台无关,支持更大的硬盘,并至少提供简单的错误检查。)

当 cpio 到达每个软盘的末尾时,它会提示我们

Found end of tape.  To continue, type device/file
name when ready.

为了继续,请输入

/dev/fd0 RETURN

当然,如果您足够幸运拥有磁带驱动器或其他存储系统,您可能不必这样做,尽管如果存档无法容纳在一个磁带上,cpio 也可以跨磁带。

此系统至少有一个缺点:如果要存储的数据无法容纳在一个单元上,则备份无法完全自动化。

我的主目录的首次备份跨越了十个软盘。我查看了内容,注意到两个子目录可能不值得备份,因此我更改了 find 的参数

$ find . \
  \( -path ./.netscape-cache -o -path ./lg \)\
  -prune -o -print | \
  cpio -ov --format=crc > /dev/fd0

这引入了更多 find 选项。\(\) 是带有 \ 的括号,以防止 shell 扩展。Find 允许使用括号对表达式进行逻辑分组。这是必要的,因为命令中有两个表达式

\( -path ./.netscape-cache -o -path ./lg \)

在括号内,我们有两个用 -o 分隔的 -path 语句。这是一个 find “或”语句。

\( -path ./.netscape-cache -o -path ./lg \) -prune

Find 的 -prune 选项使 find 不进入目录。因此,我们可以将以上内容翻译为“如果路径是 ./.netscape-cache 或 ./lg,则不要进入该目录。”

在此子句之后,我们看到另一个 -o 语句。如果文件不符合修剪条件,则改为打印该文件。

因此,我的整个主目录(除了我的 Netscape 缓存和 lg 目录)现在都已备份。

对于初始备份来说,这很好。但是,如果下周我想备份我的目录,但我实际上只接触了几个文件怎么办?

$ find . \
  \( -path ./.netscape-cache -o -path ./lg \) \
  -prune -o \( -mtime -7 \) -print | \
  cpio -ov --format=crc > /dev/fd0

这增加了一个子句:“如果文件不在 netscape 缓存或 lg 目录下,请检查它是否在过去 7 天内被修改过。如果是,则打印名称。” 然后将名称发送到 cpio 进行存档。

显然,这些命令行可能会变得非常复杂。通常最好通过管道将输出通过 more 进行测试,然后再使用 cpio。

除了 -o 之外,find 还有一个“与”运算符 -and 和一个否定运算符 -not。当指定多个匹配条件时,-and 是隐含的。

$ find -mtime -5 -type f -print

打印出过去五天内修改过的 是常规文件的文件。

$ find -mtime -5 -not -type f -print

打印出过去五天内修改过的 不是 常规文件的内容:目录、软链接等。

但是等等,灾难发生了!您的(姐妹、儿子、女儿、小弟弟、妈妈、配偶、任何人)删除了一个非常重要的文件!是时候使用备份了。

$ cpio -t < /dev/fd0

从存档中生成目录表。与备份操作期间一样,cpio 在读取目录表时会提示下一个磁盘。

$ cpio -i core < /dev/fd0

-i 开关告诉 cpio 提取指定的文件。缺少文件名会导致 cpio 恢复整个存档。

系统维护任务也可以通过 find 简化。我们的第二个示例演示了如何使用 find 清理旧文件。

$ find /home -name core -o -name foo \
  -exec rm -f {} \; 2> /dev/null

此命令从主目录中清除任何 core dump 或名为“foo”的文件。(尽管某些名为“foo”的文件可能非常重要!)

$ find /var/adm/messages -size +32k \
  -exec Mail -s "{}" root < /var/adm/messages \;
  -exec cp /dev/null {} \;

这是我的 Caldera/Red Hat 系统上 crontab 中的另一个示例。它使用隐式“与”功能将系统消息文件通过邮件发送给 root,然后将其清空。

Find 还具有重要的安全应用。我之前没有介绍的两个文件模式是 SUID 和 SGID。当程序执行时,这些模式为用户提供程序的所有者或组的权限。

passwd 程序就是一个例子。此程序允许用户更改其密码。为此,必须修改 /etc/passwd(或 /etc/shadow)文件,这是只有 root 才能执行的功能。由于 passwd 程序属于 root 并且设置了 SUID 模式,因此它可以修改必要的文件。当 passwd 完成时,用户的权限恢复正常。passwd 程序负责确保用户在充当 root 时不会做错任何事情。

$ ls -l /usr/bin/npasswd
-r-s--x--x 1 root /usr/bin/npasswd

(在我的系统上,/usr/bin/passwd 链接到 /usr/bin/npasswd。)所有者的执行字段中的 s 表示 SUID。SGID 程序在组的执行字段中将具有 s

这种机制具有明显的安全隐患。已入侵系统的用户(或入侵者)可以安装一个设置了此模式的程序(例如 shell),然后在他们想通过运行该程序来做任何事情时都可以这样做。

在八进制表示法中,SUID 表示为 4000,SGID 表示为 2000,因此

$ find / -perm 4000 -print

生成系统上 SUID 文件的列表。

$ find / -type f \( -perm 2000 -o -perm 4000 \) \
  -print

生成设置了 SGID 或 SUID 模式的常规文件的列表。

此列表可以保存到文件(使用 -fprint),并每天与前一天的输出进行比较。

本文未涵盖 find 的所有选项。这也只是对文件系统和访问模式的粗略解释。希望我能够为您提供足够的信息,使 Linux 的使用变得更轻松,更有价值。

资源

Essential System Administration,作者:Æleen Frisch,O'Reilly and Associates

Practical Unix Security,作者:Simson Garfinkel 和 Gene Spafford,O'Reilly and Associates

手册页。

Eric Goebelbecker 是 Reuters America, Inc. 的系统分析师。他为客户(主要是金融机构)提供支持,这些客户在交易室和后台操作中使用市场数据检索和操作 API。在他的业余时间(每周大约 15 分钟...),他阅读哲学并使用 Linux 进行黑客攻击。可以通过电子邮件 eric@cnct.com 与他联系。

加载 Disqus 评论