find 的动作:-exec、-delete、-print本节摘要:上一节我们把
find讲成了「表达式求值器」——对每个文件求一个布尔值。那么,求出来为真之后,find该做什么?这就是本节的主题:动作(actions)。默认动作是find . -name '*.log'才会看到一堆路径。但你真正要的不只是「看一眼」,而是「删掉」「改权限」「拷走」「逐个 grep」。本节讲透四个动作:-print0(打印,后者处理怪文件名)、-delete(危险动作,必须放在表达式之后,否则删库)、-exec(对匹配文件执行任意命令,有\;逐个和+批量两种模式)。最后补上find | xargs这个经典替代方案——它在某些场景比-exec更高效,但也有空格和特殊字符的坑。学完本节,你的find就从「找」升级到「干」。
内容来源:原项目「Linux 命令大全」
command/find.md,精选并套用体系化模板。
阅读完本节,你应当能够:
-print 是默认动作,以及为什么一旦写了其他动作(如 -delete、-exec)就可能需要显式补 -print。-delete:理解「为什么 -delete 必须放在表达式之后」这条铁律,以及它隐含 -depth 的副作用。-exec command {} \;(逐个执行)与 -exec command {} +(批量执行)的行为差异,知道何时用哪个。find ... -print0 | xargs -0 ... 安全地处理含空格、换行的文件名。-execdir 比 -exec 更安全的理由,以及 -ok(交互式确认)的用法。-delete 前养成「先 -print 预演」的习惯,避免误删。上一节的判定(tests)只是「求真假」,本身不产生任何输出或改变。find 真正「干活」靠的是动作(actions)——它们有副作用(打印、删除、执行命令),并且也返回一个真假值,可以继续参与逻辑表达式。
-print如果你写了一个 find 命令但没显式写任何动作,find 会在表达式末尾自动追加一个 -print。这就是为什么 find . -name '*.log' 会把匹配的路径一行行打印出来。
find . -name '*.log' # 等价于 find . -name '*.log' -print
关键概念:
-delete、-exec、-ls),这个默认的-exec之后路径不打印了?因为你显式写了动作,默认
find . -name '*.log' # 自动有 -print, 打印路径 find . -name '*.log' -delete # 删! 不打印(默认 -print 被取消) find . -name '*.log' -delete -print # 删且打印(显式补回 -print)
动作除了副作用,还返回真假值。比如 -exec cmd {} \;:如果 cmd 的退出码是 0,-exec 这个动作就为「真」;非 0 就为「假」。这意味着动作可以出现在逻辑表达式中间,参与 -a/-o 组合:
find . -name '*.tmp' -exec rm {} \; -print # └判定 ─────└ 动作(也为真) ┘ └ 动作 # 解释: 名字为 *.tmp 且 rm 成功的, 才 -print
这种「动作当判定用」的写法不常见,但理解了这点,你就能解释为什么 -delete 放在最前面会灾难——下一节详述。
-delete(危险!先预演再执行)-delete 直接删除匹配的文件。它最大的安全规则是:必须放在表达式之后。
find . -name '*.tmp' -delete # 对: 表达式在前, -delete 在后 find . -delete -name '*.tmp' # 灾难! -delete 在前, 会先把所有文件删光
为什么会这样?回到「表达式求值器」模型。find 命令行被从左到右求值。当你写 find . -delete -name '*.tmp' 时,-delete 先执行,此时还没判定 -name,于是 find 对遍历到的每一个文件都执行删除——你的目录瞬间被清空。这就是 -delete 必须放最后的根本原因:它要在所有判定都通过之后才触发。
⚠️ 注意:
-delete是不可逆的破坏性操作。永远先用-delete,跑一遍看输出是不是你期望删的那些;确认无误后,在命令末尾补-delete再跑。养成这个习惯,能救你无数次。
# 安全工作流: 先预演 find /var/log -name '*.log' -mtime +30 -print # 输出对了, 再加 -delete find /var/log -name '*.log' -mtime +30 -delete
-delete 还有两个隐含行为:它自动启用 -depth(先处理目录内容再处理目录本身,这样空目录才能被删),且不能删非空目录。要删非空目录用 rm -rf,但那是 rm 的能力,不是 find 的:
find . -type d -empty -delete # 删空目录(可以) find . -type d -name '__pycache__' -exec rm -rf {} + # 删非空目录: 用 -exec 调 rm
-exec 的两种模式——\; vs +-exec 让你对每个匹配的文件执行一条命令。{} 是占位符,会被替换为文件名。结尾有两种写法:
find . -name '*.log' -exec gzip {} \; # 模式 1: 逐个执行(分号) find . -name '*.log' -exec gzip {} + # 模式 2: 批量执行(加号)
两种模式的根本差异:
| 维度 | -exec ... \;(分号) |
-exec ... +(加号) |
|---|---|---|
| 执行次数 | 每个匹配文件执行一次命令 | 尽可能批量拼到一条命令里 |
| 等价于 | gzip a.log 然后 gzip b.log 然后... |
gzip a.log b.log c.log ...(一次) |
{} 位置 |
可在任意位置 | 必须是命令的最后一个参数 |
| 速度 | 慢(每文件 fork 一次) | 快(命令只 fork 几次) |
| 适用 | 命令一次只能处理一个文件(如 cp {} /dst) |
命令能吃多文件(如 gzip、rm、chmod) |
# 模式 1 (\;): 命令一次只处理一个文件, 或 {} 不在末尾 find . -name '*.log' -exec cp {} /backup/ \; # 每个文件单独 cp find . -name '*.txt' -exec mv {} {}.bak \; # {} 出现两次, 必须用 \; # 模式 2 (+): 命令能批量, 且 {} 在末尾 find . -name '*.log' -exec gzip {} + # 一次性 gzip 所有(快得多) find . -type f -exec chmod 644 {} + # 一次性改权限
💡 技巧:能用
+就别用\;。批量执行快得多(尤其是上千文件时,\;会 fork 上千次进程,+只 fork 几次)。但只要{}不在命令末尾(如mv {} {}.bak),就只能用\;。
为什么分号要转义成 \;? 因为 ; 在 shell 里是「命令分隔符」(一行写多条命令用的),不转义会被 shell 吃掉,find 收不到。+ 没这个特殊含义,不用转义。
find | xargs——更高效的替代方案-exec \; 对每个文件 fork 一次,文件多了很慢。除了 -exec +,另一个经典方案是管道配 xargs:
find . -name '*.log' | xargs gzip # 把找到的文件名传给 xargs 批量 gzip find . -name '*.c' | xargs grep TODO # 在所有 .c 文件里搜 TODO find . -name '*.log' -delete # 等价于 find . -name '*.log' | xargs rm
xargs 把 stdin 读到的内容(一行行文件名)收集起来,拼成一条命令的参数。它和 -exec + 类似,都是「批量」,但 xargs 更灵活(能跨命令、能配合任何吃 stdin 的工具)。
但 xargs 有个经典大坑:它默认按空白(空格、Tab、换行)分词,所以文件名含空格时会出问题:
# 假设有个文件叫 "my report.txt" find . -name '*.txt' | xargs rm # xargs 看到 "my report.txt" 会拆成两个参数: my 和 report.txt # 结果: rm 报错"找不到 my", 还可能误删叫 report.txt 的文件!
解法是用 -print0 和 xargs -0 这对组合,它们用 NUL 字符(空字符) 而不是空白分词,完美处理任何怪文件名:
find . -name '*.txt' -print0 | xargs -0 rm # 安全! 任何文件名都不怕 find . -name '*.c' -print0 | xargs -0 grep -l TODO # 安全批量 grep
关键概念:
-print0打印路径后跟 NUL 而不是换行,xargs -0按 NUL 分词。只要文件名可能含空格、换行、引号,就一律用-print0 | xargs -0,这是「防御性编程」在命令行的体现。现代find/xargs都支持,放心用。
-execdir(更安全)与 -ok(交互确认)-exec 有个安全隐患:它在 find 的起始目录执行命令,匹配的文件路径可能是绝对或相对路径,有「路径注入」风险。-execdir 改为在匹配文件所在的子目录里执行,且只用 basename,更安全:
find /tmp -type f -execdir rm {} \; # 进入每个文件所在目录再 rm(更安全)
⚠️ 注意:GNU find 的官方文档明确推荐用
-execdir替代-exec,因为前者避免了「文件路径被恶意构造」的竞争条件。生产脚本里能换就换。
-ok 是「会问你的 -exec」——每个匹配文件执行命令前,先打印命令并等你确认(y 才执行):
find . -name '*.tmp' -ok rm {} \; # 对每个文件输出 "<rm ... /path/a.tmp> ?", 你输 y 才删
-ok 适合「不确定要不要全删」的场合,是 -delete 的安全替代品。缺点是文件多了要点到手酸。
-delete 放在表达式前面(灾难级)find . -delete -name '*.tmp' # 错! 会删光当前目录所有文件 find . -name '*.tmp' -delete # 对! -delete 在最后
这条前面反复强调过,这里是排错清单里最严重的一条。任何 -delete 命令,先用 -print 替换跑一遍:
find . -name '*.tmp' -print # 预演 find . -name '*.tmp' -delete # 确认后改成 -delete
-exec 后路径不打印了find . -name '*.log' -exec gzip {} \; # 你期望: 打印路径 + gzip # 实际: 只 gzip, 不打印(默认 -print 被取消)
原因:写了显式动作(-exec)后,默认的 -print 不再自动加。要既执行又打印,显式补 -print:
find . -name '*.log' -exec gzip {} \; -print # gzip 且打印
-exec 的 {} 出现两次时不能用 +find . -name '*.txt' -exec mv {} {}.bak + # 错! + 模式要求 {} 在末尾 find . -name '*.txt' -exec mv {} {}.bak \; # 对! \; 模式允许 {} 多次出现
+ 模式要求 {} 是命令的最后一个参数(因为 find 要在 {} 处拼一堆文件名)。只要 {} 出现多次或不居末尾,就只能用 \;。
xargs 拆了含空格的文件名find . -name '*.txt' | xargs rm # 错! 文件名含空格会被拆 find . -name '*.txt' -print0 | xargs -0 rm # 对! 用 NUL 分词
铁律:把 find 的输出交给 xargs 时,一律 -print0 配 -0,不管文件名当前有没有空格。这是零成本的好习惯,能避免某天目录里突然出现「my file.txt」时的灾难。
-exec 命令找不到时静默失败find . -name '*.log' -exec gzzip {} \; # 拼错成 gzzip, 命令不存在 # find 会报错, 但如果你重定向了 stderr 就看不到
-exec 执行的命令如果不存在或退出码非 0,find 最终的退出码会是非 0。写脚本时检查 find 的退出码($?),别假设它一定成功。调试时不要重定向 stderr,让错误暴露。
-exec 里忘了 \; 或 +find . -name '*.log' -exec gzip {} # 错! find 会一直等, 把后续都当 cmd 参数 find . -name '*.log' -exec gzip {} \; # 对! 用 \; 结尾 find . -name '*.log' -exec gzip {} + # 对! 用 + 结尾
-exec 后面 find 把所有内容都当成命令参数,直到遇到 ; 或 +。忘了写结束符,find 就把命令行剩下的部分全吞进去,然后提示你表达式没写完。这是新手最常卡住的地方之一。
-print 是默认动作:没写任何动作时自动追加;一旦写了 -delete/-exec 等显式动作,默认 -print 就被取消,要打印得显式补。-delete 必须放在表达式之后:因为 find 从左到右求值,-delete 在前会对所有遍历到的文件先删为敬——灾难级错误。永远先 -print 预演。-delete 隐含 -depth,且不能删非空目录;删非空目录要用 -exec rm -rf {} +。-exec \; 逐个执行(慢,但 {} 可多次出现),-exec + 批量执行(快,但 {} 必须在末尾)——能 + 就别 \;。find -print0 | xargs -0 是处理含空格/换行文件名的安全组合,默认按 NUL 分词;裸 | xargs 会按空白拆词,危险。-execdir 比 -exec 安全(在文件所在子目录执行,用 basename),GNU find 官方推荐替代;-ok 是交互式确认版 -exec。-delete 顺序、加 -exec 后不打印、{} 多次出现误用 +、xargs 拆空格文件名、忘写 \;/+ 导致 find 卡住。下一节我们换个查找思路——locate。find 实时遍历目录树,准但慢;locate 查的是预建索引(updatedb 维护),快几个数量级但有索引延迟。理解它俩的取舍,你才知道什么时候该用哪个。