知道 fd 语法更直觉、速度更快,但 SSH 到一台没装第三方工具的服务器上,还是得翻出记忆里生锈的 find 语法。条件和动作到底谁写前面、-mtime-atime 哪个是修改时间、-exec 后面跟的是 \; 还是 +——每次都要重新查一遍。

find 是所有 Linux 发行版都自带的工具,这篇把它自己的用法讲清楚。

这是 Linux 常见命令系列第三篇详细教程,承接 Linux 常见命令与工具地图chmod/chowngrep。如果你的环境能装第三方工具,fd 语法比 find 直觉得多——但装不上的场合,还得靠这篇。

基本语法:起始路径、条件、动作三段式

find 命令的结构是固定的三段:

1
find 起始路径 条件 动作

比如 find . -name "*.log" -print. 是起始路径(从当前目录开始找),-name "*.log" 是条件(文件名匹配这个模式),-print 是动作(把匹配到的路径打印出来)。

新手最容易搞混的地方是把条件和动作当成一回事。-name-type-mtime 这些是条件,负责”筛选出哪些文件符合要求”;-print-delete-exec 这些是动作,负责”对筛出来的文件做什么”。不写动作时 find 默认执行 -print,所以 find . -name "*.log"find . -name "*.log" -print 效果一样——但显式写上 -print,尤其是命令里还有别的动作时,意图更清楚,不容易和条件搞混。

按名字和类型找:-name、-iname、-type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
find . -name "*.log"
# -name:按文件名匹配,区分大小写,*.log 和 *.LOG 是两回事

find . -iname "*.log"
# -iname:和 -name 一样,但不区分大小写,access.log、ACCESS.LOG 都能匹配到

find . -type f -name "*.go"
# -type f:只要普通文件,不要目录
# -type d:只要目录
# -type l:只要符号链接

find . -maxdepth 2 -name "*.md"
# -maxdepth:限制递归深度,2 表示最多进两层子目录,不会一路挖到底
# 忘加这个参数是 find 跑得慢、结果里混进一堆不相关文件的常见原因

find . -mindepth 1 -maxdepth 1 -type d
# -mindepth 1:不包括起始路径本身,只要子级
# 配合 -maxdepth 1,效果就是"只列出当前目录下的一级子目录",不递归

按时间找:-mtime、-atime、-ctime 的区别

三个时间条件容易搞混,先说清楚各自对应什么:

参数 对应的时间戳 什么情况下会变
-mtime(modify time) 内容修改时间 文件内容被写入/修改
-atime(access time) 访问时间 文件被读取(有些文件系统挂载时禁用了 atime 更新,出于性能考虑)
-ctime(change time) 状态变更时间 文件的元数据变了——权限、属主、链接数,不一定是内容变了

日常排查用得最多的是 -mtime

1
2
3
4
5
6
7
8
9
find . -mtime -7
# -7:前面带减号,表示"7 天以内"(比 7 天新)
# 符号方向:- 是"比这个数小",+ 是"比这个数大",不带符号是"精确等于这个数"

find /var/log -mtime +30 -name "*.log"
# +30:30 天以前修改过的日志文件,排查该清理哪些旧日志时常用

find . -newer reference.txt -name "*.go"
# -newer:找修改时间比 reference.txt 更新的文件,不用自己算具体天数

按大小找:-size 的符号前缀

-size 的符号方向和 -mtime 是同一套逻辑,但因为单位是大小不是时间,容易理解反:

1
2
3
4
5
6
7
8
find . -size +100M
# +100M:大于 100MB 的文件,排查磁盘占用时常用

find . -size -1k
# -1k:小于 1KB 的文件,找空文件或者几乎是空的配置文件

find . -size 0
# 不带符号:精确匹配大小为 0 的文件,找真正的空文件

单位后缀:c 是字节,k 是 KB,M 是 MB,G 是 GB,不写单位默认是 512 字节的块数(历史遗留,容易踩坑,写 size 条件时最好总是带上单位)。

逻辑组合:-and、-or、-not 与括号分组

多个条件之间默认是”且”的关系,-and 可以省略:

1
2
3
4
5
6
7
8
9
10
11
12
find . -type f -name "*.go"
# 等价于 find . -type f -and -name "*.go",-and 默认存在,通常不用写

find . -name "*.go" -or -name "*.md"
# -or:找 .go 或 .md 文件,这个必须显式写,不写就是 -and 的默认关系

find . -not -path "*/.git/*" -name "*.go"
# -not:排除条件,这里是"路径不包含 .git 目录"且"文件名匹配 .go"

find . \( -name "*.go" -or -name "*.md" \) -not -path "*/.git/*"
# 括号分组控制优先级,圆括号在 shell 里有特殊含义,必须转义成 \( \)
# 这条的意思是:(.go 或 .md 文件) 并且 不在 .git 目录下

-exec 的两种写法:\;+,性能差在哪

这是 find 里最容易被忽略、但影响最大的细节。两种写法看起来只差一个符号,行为和性能却完全不同:

1
2
3
4
5
6
7
find . -name "*.log" -exec gzip {} \;
# \; 结尾:每找到一个文件,就单独起一次 gzip 命令处理这一个文件
# {} 是占位符,代表当前这个匹配到的文件路径

find . -name "*.log" -exec gzip {} +
# + 结尾:尽量把多个文件路径打包,一次性作为参数传给 gzip
# 行为类似 xargs——攒够一批(受命令行长度上限约束)就调用一次

为什么性能差这么多\; 结尾时,找到 1000 个文件,就要 fork 1000 次子进程、启动 1000 次 gzip——每次进程创建都有实打实的系统开销(内存分配、内核调度),文件越多,这个开销累加得越明显。+ 结尾时,find 会把匹配到的文件路径尽量攒在一起,打包成尽可能少的调用次数,具体能攒多少受系统的 ARG_MAX(单次命令调用能传递的参数总长度上限)约束——Linux 上典型值是 2097152 字节(约 2MB,getconf ARG_MAX 能查到本机的实际值)。假设文件路径平均 50 字节,2MB 够装下四万多个路径,1000 个文件的路径总长度远小于这个上限,实际可能一次调用就打包完了,进程创建次数从”文件数”级别降到”个位数”,量级差出去很多。

日常写脚本的经验规则:只要命令支持一次接收多个文件路径(大多数命令都支持,比如 rmgzipchmod),优先用 +;只有命令一次只能处理一个文件、或者每个文件需要单独判断结果时,才用 \;

1
2
find . -name "*.tmp" -exec rm {} +
# 批量删除 .tmp 文件,用 + 一次调用处理完,比 \; 快得多

-delete:比 -exec rm 更安全高效

需要删除匹配到的文件时,find 自带的 -delete 动作比 -exec rm {} + 更直接——不用再额外 fork 一个 rm 进程:

1
2
find . -name "*.tmp" -delete
# -delete:直接删除匹配到的文件,不需要再调用外部 rm 命令

用之前一定要先确认范围:把 -delete 换成 -print,跑一遍看看到底会命中哪些文件,确认没问题了再换回 -delete——find 的条件写错很容易命中比预期更大的范围,删除操作又无法撤销:

1
2
3
4
find . -name "*.tmp" -print
# 先跑这条确认命中范围,看着输出结果没问题
find . -name "*.tmp" -delete
# 确认没问题后再换成 -delete 真正执行

两个常见坑:文件名带空格、通配符被 shell 提前展开

1
2
3
4
5
6
7
8
9
10
11
12
13
find . -name "*.txt" -print0 | xargs -0 rm
# -print0:文件名之间用 null 字符(\0)分隔,而不是换行或空格
# xargs -0:配合读取 null 分隔的输入
# 文件名里带空格(比如 "my file.txt")时,普通的换行分隔会被 xargs 误判成两个参数,
# null 分隔是唯一不会出现在合法文件名里的字符,能保证安全

find . -name *.txt
# 危险写法:*.txt 没加引号,如果当前目录下已经有匹配 *.txt 的文件,
# shell 会在 find 执行前就把通配符展开成具体文件名,find 实际收到的
# 是一堆文件名而不是模式字符串,只会在这些文件里精确匹配,行为完全不对

find . -name "*.txt"
# 正确写法:加引号,让 *.txt 原样传给 find,由 find 自己去做模式匹配

find 常用参数速查表

参数 作用
-name PATTERN 按文件名匹配,区分大小写
-iname PATTERN 按文件名匹配,不区分大小写
-type f / d / l 只要普通文件 / 目录 / 符号链接
-maxdepth N 最多递归 N 层
-mindepth N 至少从第 N 层开始算
-mtime ±N 内容修改时间在 N 天以内(-)/ 以前(+)/ 精确 N 天(不带符号)
-atime ±N 访问时间,符号规则同 -mtime
-ctime ±N 元数据(权限/属主)变更时间,符号规则同 -mtime
-newer FILE 修改时间比 FILE 更新
-size ±N[ckMG] 文件大小,符号规则同 -mtime,单位 c/k/M/G
-and 且,默认关系,通常省略
-or 或,必须显式写
-not 非,排除条件
\( ... \) 括号分组,圆括号需转义
-exec CMD {} \; 对每个匹配文件单独执行一次命令
-exec CMD {} + 尽量批量打包,减少命令调用次数
-delete 直接删除匹配到的文件
-print0 用 null 字符分隔输出,配合 xargs -0 处理带空格的文件名

find 之外

装得上第三方工具的话,fd 默认行为更符合直觉(自动跳过 .gitignore、语法更接近自然语言),日常开发优先用它,find 留给装不上工具的场合。Linux 命令系列下一篇计划写 rsynctar——都是比 find 参数量级还大的命令,增量同步、归档压缩各有一堆细节值得展开。