find 是一个在 Linux 和类 Unix 系统中用于在文件系统中搜索文件和目录的强大命令行工具。它能够根据各种条件(如名称、大小、类型、修改时间、权限、所有者等)进行搜索,并对找到的文件执行指定的操作。

find 的核心作用是:

  1. 递归搜索:从指定目录开始,递归地遍历其所有子目录,查找符合条件的文件和目录。
  2. 多条件组合:支持使用逻辑运算符(AND, OR, NOT)组合多个搜索条件。
  3. 执行操作:对找到的文件执行命令(如删除、复制、移动、修改权限等)。

基本语法

1
find [路径] [表达式]
  • **[路径]**:指定从哪个目录开始搜索。可以是一个或多个目录。如果省略,则默认为当前目录 (.)。

  • **[表达式]**:由选项、测试、动作和操作符组成,用于定义搜索条件和对找到的文件执行的操作。

如果路径是相对径,输出就是相对路径;如果是绝对路径,输出就是绝对路径。

find 的常用选项和用法示例

1. 按名称或模式搜索

  • -name <pattern>:按文件名(或目录名)搜索,支持通配符(*, ?, [])。区分大小写

    1
    2
    find . -name "*.txt"         # 在当前目录及其子目录中查找所有以 .txt 结尾的文件
    find /home/user -name "report_???.pdf" # 查找 report_001.pdf, report_abc.pdf 等
  • -iname <pattern>:按文件名(或目录名)搜索,不区分大小写

    1
    find . -iname "document.pdf"  # 查找 document.pdf, Document.pdf, DOCUMENT.PDF 等

2. 按类型搜索

  • **-type <type>**:按文件类型搜索。

    • f:普通文件 (file)
    • d:目录 (directory)
    • l:符号链接 (symbolic link)
    • b:块设备 (block device)
    • c:字符设备 (character device)
    • p:命名管道 (named pipe / FIFO)
    • s:socket
    1
    2
    find /var/log -type f -name "*.log" # 查找 /var/log 下所有的日志文件
    find . -type d # 查找当前目录下的所有子目录

3. 按大小搜索

  • **-size <n[cwbkMG]>**:按文件大小搜索。n 是整数,后缀表示单位:

    • c:字节 (bytes)
    • w:双字 (2 bytes)
    • b:块 (512-byte blocks,默认单位)
    • k:千字节 (kilobytes)
    • M:兆字节 (megabytes)
    • G:千兆字节 (gigabytes)
    • +n:大于 n
    • -n:小于 n
    • n:等于 n (通常不精确,因为文件大小不总是精确匹配)
    1
    2
    find /tmp -size +10M          # 查找 /tmp 下所有大于 10MB 的文件
    find /var/log -size -1k # 查找 /var/log 下所有小于 1KB 的文件

4. 按时间搜索

  • **-atime <n>**:按文件最后访问时间(access time)搜索,n 天前。

  • **-mtime <n>**:按文件最后修改时间(modification time)搜索,n 天前。

  • **-ctime <n>**:按文件状态最后改变时间(change time,包括权限、所有者、修改时间等)搜索,n 天前。

    • +n:大于 n 天前
    • -n:小于 n 天前 (即 n 天以内)
    • n:恰好 n 天前
    1
    2
    find . -mtime +7             # 查找 7 天前修改的文件
    find /tmp -atime -1 # 查找 1 天内(今天)被访问过的文件
  • -amin, -mmin, -cmin:与上述类似,但以分钟为单位。

    1
    find . -mmin -60             # 查找 60 分钟内修改过的文件
  • **-newer <file>**:查找比指定文件更新(修改时间更晚)的文件。

    1
    find . -newer my_template.txt # 查找比 my_template.txt 更新的文件

5. 按权限或所有者搜索

find -perm 用于根据权限模式查找文件。它的语法是:

1
find <路径> -perm <模式>

<模式> 是一个三位或四位的八进制数(如 644755),或使用符号模式(如 u=rwx)。

这里的关键是理解 <模式> 前面的修饰符,它们决定了匹配的逻辑。

修饰符含义查找逻辑
无修饰符精确匹配权限必须完全等于给定的模式。例如,755 模式只会匹配权限恰好为 rwxr-xr-x 的文件。
-权限包含(“至少满足”)文件的权限必须包含给定的模式。例如,-755 会匹配 775755,因为它们的权限都包含 755
/任一用户权限匹配(“或”)文件的权限中,只要任一用户(所有者、组、其他人)的权限位与模式匹配,就符合条件。
1. 无修饰符:精确匹配模式

这是最严格的查找方式。文件权限必须与你指定的模式完全相同

  • 实例:查找当前目录下权限为 644rw-r--r--)的文件。
1
find . -type f -perm 644

如果某个文件的权限是 744,它就不会被找到,因为它比 644 多了执行权限。

2. - 修饰符:至少满足模式

这是最常用、最实用的方式。它查找权限至少是给定模式的文件。这就像一个“包含”的关系。

  • 实例 1:查找当前目录下所有至少拥有执行权限的文件(对于所有者、组、其他人任一)。
1
find . -perm -111

这里的 111 模式表示:所有者有执行权限(--x),所属组有执行权限(--x),其他人有执行权限(--x)。-111 意味着只要文件的权限中包含了这些权限,就会被匹配。例如,权限为 755 的文件会被匹配,因为它包含 111

  • 实例 2:查找当前目录下,所有者拥有读写执行权限,所属组和其他人至少拥有读执行权限的文件。
1
find . -perm -755

-755 意味着文件的权限必须包含 rwxr-xr-x。例如,权限为 775757 甚至 777 的文件都会被匹配,因为它们都“包含” 755 的所有权限位。

3. / 修饰符:任一用户权限匹配模式

这个修饰符的逻辑是:只要任一用户(所有者、组或其他)的权限位与给定的模式相匹配,就符合条件。这就像一个“或”的关系。

  • 实例 1:查找当前目录下,所有者、所属组或其他人中,只要有一个拥有写权限的文件。
1
find . -perm /222

222 模式表示所有者有写权限(-w-)、所属组有写权限(-w-)、其他人有写权限(-w-)。/222 表示只要满足其中任意一个条件(/ 意味着“或”),文件就会被找到。例如,644(所有者有写)、464(所属组有写)和 446(其他人有写)都会被匹配。

  • 实例 2:查找所有者拥有写权限,或所属组拥有执行权限的文件。
1
find . -perm /210

210 模式表示:所有者有写权限(2),所属组有执行权限(1),其他人无任何权限(0)。/210 的含义是:只要所有者的权限位中包含 w 所属组的权限位中包含 x,就匹配。

实例 1:查找并修复不安全的权限

假设你需要找到并修复那些权限过于宽松(例如,对所有人开放写权限)的文件。

1
2
# 查找对"其他人"开放写权限的文件
find . -type f -perm /002

找到后,你可以结合 xargs-exec 来批量修改权限。

1
2
# 找到后批量修改权限为 644
find . -type f -perm /002 -exec chmod 644 {} \;
实例 2:查找缺少执行权限的脚本

假设你的服务器上有很多脚本,但由于权限设置错误,一些脚本无法执行。你可以快速找出它们。

1
2
# 查找所有者、组或其他任何人都没有执行权限的脚本
find . -type f -name "*.sh" ! -perm /111

! -perm /111 的含义是,反向查找那些不包含任一执行权限的文件。

实例 3:查找并删除无用文件

你发现一些日志文件权限设置不正确,导致应用程序无法写入。你想找到这些文件并删除。

1
2
# 查找对所有者、组、其他人都无写权限的日志文件
find /var/log/my-app -type f ! -perm /222 -exec rm {} \;

find -perm 命令的精髓在于修饰符

  • 无修饰符:精确匹配。
  • -:权限包含,最常用。用于查找权限至少满足某些条件的文件。
  • /:任一用户权限匹配。用于查找任一用户满足某些条件的文件。

熟练掌握这三个修饰符,你就能像一个权限管理专家一样,轻松地在复杂的文件系统中定位和管理文件,这是每一位 Linux 运维工程师必备的技能。

  • **-perm <mode>**:按文件权限搜索。

    • mode:精确匹配权限(例如 644)。
    • -mode:至少拥有指定权限(例如 -755 意味着必须有 755 或更高,如 777)。
    • /mode:任意拥有指定权限位中的一个(例如 /222 意味着文件对所有者、组或其他用户中至少有一个拥有写权限)。
    1
    2
    find . -perm 644             # 查找权限为 644 的文件
    find . -perm -o+w # 查找其他用户有写权限的文件
  • **-user <name>**:按文件所有者搜索。

    1
    find /var/www -user apache   # 查找 apache 用户拥有的文件
  • **-group <name>**:按文件所属组搜索。

    1
    find /var/www -group developers # 查找 developers 组拥有的文件

6. 组合条件和逻辑操作符

find 默认的组合方式是 AND(逻辑与),也可以使用显式的操作符:

  • -a 或不写:AND(默认)
  • -o:OR(逻辑或)
  • !:NOT(逻辑非,通常需要加 \ 转义)
  • ():用于分组,通常需要加 \ 转义以避免 shell 解释。
1
2
3
4
5
6
7
8
9
10
11
12
13
# 查找名字以 .log 结尾,且大小大于 1M 的文件
find /var/log -name "*.log" -size +1M

# 查找名字以 .conf 结尾,或者名字以 .bak 结尾的文件
find . -name "*.conf" -o -name "*.bak"

# 查找不是普通文件 (f) 的所有内容
find . ! -type f

# 查找文件,但排除掉某个目录(例如 node_modules)
find . -path "./node_modules" -prune -o -name "*.js" -print
# -prune 会阻止 find 进入匹配的目录。-o 表示逻辑或,-print 表示打印结果
# 注意:-prune 总是和 -o 搭配使用,且 -print 必须在 -o 之后才能生效

7. 对找到的文件执行操作

  • **-print**:打印找到的文件名。这是默认动作,但显式写出有助于理解。

    1
    find . -name "*.bak" -print
  • **-exec <command> {} \;**:对每个找到的文件执行指定的命令。

    • {}:代表当前找到的文件名。
    • ;:命令结束符,需要用 \ 转义。
    • 注意:对于大量文件,这可能会很慢,因为它为每个文件启动一个新进程。
    1
    2
    find . -name "*.tmp" -exec rm {} \;   # 删除所有 .tmp 文件
    find . -name "*.log" -exec gzip {} \; # 压缩所有 .log 文件
  • **-exec <command> {} +**:类似于 -exec ... \;,但更高效。它会将多个找到的文件名作为参数一次性传递给命令,而不是每个文件启动一个进程。当文件数量很多时,推荐使用此方式。但要注意command是否支持多参数,否则报错。

    1
    find . -name "*.txt" -exec ls -l {} + # 列出所有 .txt 文件的详细信息
  • **-delete**:直接删除找到的文件。比 rm 更安全,因为 find 不会因为文件类型不对而删除目录,也不会因为权限问题而停止。

    1
    find /tmp -type f -name "*.old" -delete # 删除 /tmp 下所有名为 *.old 的普通文件
  • **-ls**:以 ls -dils 命令的格式打印找到的文件信息。这包括 inode 号、块数、权限、硬链接数、所有者、组、大小、修改时间以及文件名。提供比 -print 更详细的信息,但不如 -printf 灵活自定义。
1
find . -name "*.log" -ls # 列出所有 .log 文件的详细列表信息
  • -ok <command> {} \;:与 -exec ... \; 类似,但在执行每个命令之前会询问用户确认。提供一个交互式的安全网,在执行潜在危险操作(如删除)时非常有用。
1
find . -name "*.bak" -ok rm {} \; # 在删除每个 .bak 文件前会提示 rm ... ? y/n

只列出路径或文件名

find输出匹配文件的完整路径时,未指定任何其他动作,如果只列出路径,或者只列出文件名,find 可以通过结合不同的选项和动作来实现。

只列出路径:

当你执行 find . -name "*.txt" 时,它默认就是输出文件的相对路径(如果从当前目录开始搜)或绝对路径(如果你指定了绝对路径)。

1
2
3
4
5
# 在当前目录及其子目录中查找所有 .txt 文件,并显示它们的相对路径。
find . -name "*.txt"
# 比如会输出:
# ./my_documents/report.txt
# ./notes/daily_log.txt

如果你希望输出绝对路径,可以使用 find 结合 realpathpwd -P

1
2
3
4
5
6
7
# 使用 -exec 配合 realpath 获取绝对路径
find . -name "*.txt" -exec realpath {} +

# 或者,如果你当前就在要搜索的目录,先获取当前目录的绝对路径,再结合 find 的输出
# current_dir=$(pwd -P)
# find . -name "*.txt" -print0 | xargs -0 -I {} echo "${current_dir}/{}"
# 注意:这种方法在 find 结果中有特殊字符时可能需要更复杂的处理

只列出文件名(不含路径):

如果你只想要文件名,而不要前面的路径部分,可以使用 basename 命令来处理 find 的输出。

1
2
3
4
5
# 对每个找到的 .txt 文件,只打印其文件名部分
find . -name "*.txt" -exec basename {} \;
# 比如会输出:
# report.txt
# daily_log.txt
  • find . -name "*.txt":这部分是标准的 find 命令,用于查找所有 .txt 文件。
  • -exec basename {} \;
    • -exec:表示对每个找到的文件执行一个命令。
    • basename {}basename 命令会从给定的路径中提取出文件名(或目录名)部分。{}find 传递给 basename 命令的当前文件的占位符。
    • ;:是 -exec 命令的结束符,需要用反斜杠 \ 进行转义,以防止 shell 解释它。

要只列出文件名而不带路径,find 最强大的内置工具是 -printf 动作。它允许你以自定义的格式打印输出,类似于 C 语言的 printf 函数。

-printf 提供了许多格式化指令,其中:

  • %f: 打印找到的文件或目录的文件名(basename)
1
2
# 在当前目录及其子目录中查找所有以 .txt 结尾的文件,并只打印其文件名
find . -name "*.txt" -printf "%f\n"

-printf 的优势

  • 效率更高:与 -exec basename {} \; 相比,-printffind 的内置动作,它不需要为每个文件启动一个新的 basename 进程,因此在处理大量文件时会显著更快。
  • 格式化能力强:除了 %f-printf 还有其他有用的格式化指令,例如:
    • %p:完整路径名(与 -print 类似)
    • %h:目录名(dirname)
    • %s:文件大小(字节)
    • %t:修改时间
    • %u:所有者用户名
    • %m:权限(八进制)
    • 等等…

列出文件名和大小

1
2
3
4
5
# 查找 .txt 文件并列出文件名和大小
find . -name "*.txt" -printf "File: %f, Size: %s bytes\n"
# 示例输出:
# File: report.txt, Size: 1024 bytes
# File: meeting.txt, Size: 512 bytes

-print0

  • 作用:与 -print 类似,都是打印找到的文件名。但 -print0 不会在每个文件名后添加一个换行符,而是添加一个ASCII NULL 字符\0)。

    在 Linux 文件系统中,文件名可以包含几乎任何字符,除了 / 和 NULL 字符。这意味着文件名中可以有空格、换行符、制表符等。

    • 传统的 -print 会用换行符分隔文件名。如果文件名本身包含换行符,那么一个文件名就会被误识别为多个文件。
    • 如果文件名包含空格,当你把 find -print 的输出通过管道传递给 xargs 等命令时,xargs 默认会把空格作为分隔符,这会导致文件名被错误地分割,从而引发错误或意外操作。
    • NULL 字符保证了每个文件名都能被准确地识别,不会因内部的空格或换行符而被分割。
  • 使用-print0 几乎总是与能够处理 NULL 分隔输入的命令(如 xargs -0read -d $'\0')结合使用。

删除包含空格的文件名:

假设你有一个名为 my document.txt 的文件,你想删除它。

  1. 错误的方式(会失败或出错)

    1
    2
    find . -name "my document.txt" -print | xargs rm
    # 这里的 xargs 可能会尝试删除 'my' 和 'document.txt' 两个文件,导致错误。
  2. 正确的方式(推荐使用 -print0xargs -0

    1
    2
    find . -name "my document.txt" -print0 | xargs -0 rm
    # 这样 xargs 会正确地将 "my document.txt" 作为一个完整的参数传递给 rm。
  • find . -name "my document.txt" -print0find 找到文件后,输出 my document.txt\0 (一个字符串,后面跟着一个 NULL 字符)。

  • |:将 find 的输出通过管道传递。

  • xargs -0 rmxargs 接收到 NULL 分隔的输入,并知道如何将 my document.txt 作为一个单一的参数传递给 rm 命令。

    应用场景:

  • 处理带有空格或特殊字符的文件名:这是其主要用途,确保文件名在管道中传递时不会被错误解析。

  • xargs -0 结合:这是最常见的搭配,用于对 find 找到的文件执行各种命令,例如 rmmvcpgrep 等。

  • read -d $'\0' 结合:在 Bash 脚本中,如果你需要逐个处理 find -print0 的输出,可以使用 read -d $'\0' 命令。

    1
    2
    3
    4
    5
    6
    7
    #!/bin/bash

    while IFS= read -r -d $'\0' file; do
    echo "Processing file: '$file'"
    # 这里可以对文件进行其他操作,例如:
    # cp "$file" /backup/
    done < <(find . -type f -name "*.log" -print0)
    • IFS=read -r:防止 read 命令对文件名中的空格和反斜杠进行额外的解释。
    • -d $'\0':指定 NULL 字符作为分隔符。
    • < <(...):这是一个进程替换 (process substitution),允许 while 循环从 find 命令的输出中读取。

掌握 -print0xargs -0 的组合是 Linux 命令行操作中一个非常重要的最佳实践,能帮助你编写更健壮、更不容易出错的脚本。

结合sh -c / bash -c 使用:

比如重命名文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 有一些文件,文件名中带有空格
mugster@mug:~/Desktop/test$ ls
a 'a b' 'a b c' 'a c' b 'b c' c x.txt

mugster@mug:~/Desktop/test$ find -type f -exec bash -c 'mv "$0" "$0.txt"' {} \;
# 可以看到已经有.txt的文件再次被添加
mugster@mug:~/Desktop/test$ ls
'a b c.txt' 'a b.txt' 'a c.txt' a.txt 'b c.txt' b.txt c.txt x.txt.txt

# 可以添加判断
find . -type f ! -name "*.*" -exec sh -c 'mv "$0" "$0.txt"' {} \;
# 或
mugster@mug:~/Desktop/test$ find . -type f ! -name "*.*" -exec sh -c '[ ! -e "$0.txt" ] && mv "$0" "$0.txt"' {} \;

# 去除.txt
mugster@mug:~/Desktop/test$ find . -type f -name "*.txt" -exec bash -c 'f="$0"; mv "$f" "${f%.txt}"' {} \;
# 检测.txt是否存在,不存在才执行
find . -type f -name "*.txt" -exec bash -c 'f="$0"; target="${f%.txt}"; [ ! -e "$target" ] && mv "$f" "$target"' {} \;