2789 字
约 9 分钟
16
S15-X05 · 隐藏文件、软链接与 find 的组合用法

磁盘满、配置没备份、日志没清干净——这三个事故的共同点是:出问题的文件在 ls 里根本看不见。.bashrc、.env、.git 这类点文件默认被跳过,软链接被 du 重复计数,而 find 是唯一能按“时间/大小/权限/属主”这些元数据做批量判断的工具。

这篇把三件事串起来:点文件是什么、两种链接差在哪、find 怎么和 xargs 配合才不出事故。


1. 点文件:只是命名约定

$ ls            # 看不到 .env
$ ls -a
.  ..  .env  .git  app.py  logs
$ ls -A
.env  .git  app.py  logs

.env 的“隐藏”不来自权限,也不来自文件属性,只来自一个约定:名字以 . 开头,列目录的默认行为就跳过它。

命令 显示 . 与 .. 显示点文件 用途
ls 否 否 日常
ls -a 是 是 看到 . ..,适合排查“目录里到底有什么”
ls -A 否 是 脚本里优先用它,避免 . .. 干扰
ls -aR 是 是 递归,注意输出量大
find . -name '.*' — 是 按条件筛点文件

两个容易误解的点:

  1. shell 的通配符 * 不匹配点文件。rm * 删不掉 .env,这是历史上的保护设计;要删点文件得显式写 rm .* 或 rm -f .[!.]*。
  2. find 不受这个约定影响。find . -name '*.log' 会命中 .hidden.log,因为 find 自己拿 fnmatch 匹配,不经过 shell 展开。

所以“备份时漏了配置”的典型原因不是备份脚本写错,而是用了 cp -r app/* dest/——* 不展开点文件,.env 就丢了。改成 cp -a app/. dest/ 或 rsync -a app/ dest/ 才完整。


2. 软链接与硬链接的六维差异

$ ln -s /srv/app/releases/v2 current      # 软链接
$ ln /srv/app/data.db data.db.bak         # 硬链接
$ ls -l
lrwxrwxrwx 1 deploy deploy   20 Nov  9 05:02 current -> /srv/app/releases/v2
-rw-r--r-- 2 deploy deploy 8192 Nov  9 05:02 data.db.bak
维度 软链接(symbolic) 硬链接(hard)
inode 独立 inode,内容是目标路径字符串 同一个 inode,是目录项的另一个名字
跨文件系统 可以 不行,Invalid cross-device link
指向目录 可以 不行(普通用户与 root 都不允许,避免目录环)
目标删除后 变成悬空链接,ls 仍显示,访问报 No such file 数据仍在,直到链接数为 0
大小 等于目标路径长度(上面 20 字节) 与目标文件相同,不额外占空间
链接数 不影响目标的 ls -l 计数 每加一个硬链接,第二列 +1
权限 lrwxrwxrwx 是占位,实际按目标判定 与目标完全同一份权限与属主

ls -l 的第二列就是硬链接计数。一个文件的链接数降到 0 才真正释放空间——这就是“rm 了文件但 df 不降”的经典原因:还有进程持有 fd,或者还有另一个硬链接。


$ readlink current                 # 单层,输出目标路径
/srv/app/releases/v2
$ readlink -f current              # 递归解析到最终绝对路径
/srv/app/releases/v2.1.3/bin/app
$ realpath current                 # 同上,GNU coreutils 提供
$ readlink -e missing-link; echo $?   # 目标不存在时返回非 0
需求 命令 注意
看链接指向哪 readlink link 只解一层,链式链接只给第一跳
拿最终真实路径 readlink -f / realpath 目标不存在时 -f 仍输出拼接结果,-e 会失败
判断是否悬空 test -e link vs test -L link -e 跟随链接,悬空时假;-L 只看是不是链接
找出全部悬空链接 find /srv -xtype l -xtype l 表示“解引用后类型是链接”

发布目录用 current -> releases/vN 是标准做法,代价是任何脚本都必须先解析路径再比较,否则 cd current && pwd 拿到的是逻辑路径,pwd -P 才是物理路径。


4. find 的核心参数

$ find /var/log -maxdepth 1 -type f -name '*.log' -mtime +7
$ find /srv -type f -size +100M -printf '%s\t%p\n' | sort -rn | head
$ find /etc -newer /etc/hostname -type f
参数 语义 常见误用
-maxdepth N 限制递归深度(1 = 只看本层) 忘了它导致扫全盘
-mindepth N 跳过前 N 层 与 -maxdepth 组合可精确框定层数
-name / -iname 按文件名匹配,支持通配 忘了加引号,被 shell 先展开
-type f/d/l 普通文件/目录/软链接 -type l 与 -xtype l 语义相反
-mtime +7 修改时间早于 7×24 小时 误以为是“最近 7 天”
-mmin -30 30 分钟内修改过 与 -mtime 混用时互相收窄
-size +100M 大于 100M(向上取整到单位) 单位大小写:M 是 1048576,k 是 1024
-perm -644 至少包含这些位(- 是全部满足) 与 -perm 644(精确相等)混淆
-user / -group 按属主/属组 数字 UID 更可靠
-newer ref 比参照文件更新 适合“上次发布之后改过什么”
-print0 以 NUL 分隔输出 必须配 xargs -0

-mtime +7 的方向最容易错:+n 是大于 n 天前,也就是更旧;-n 才是最近 n 天内。find /var/log -mtime +7 -delete 删的是老日志,方向没错;但如果想“保留 7 天”,条件就写反了。


5. -exec {} \; 与 -exec {} +:差一个分号,差几十倍

# 每个文件 fork 一次
$ find /data -name '*.tmp' -exec rm -f {} \;

# 批量传给一个进程
$ find /data -name '*.tmp' -exec rm -f {} +

# 等价写法,但要注意文件名安全
$ find /data -name '*.tmp' -print0 | xargs -0 rm -f
写法 进程数 命令行长度限制 适用
-exec CMD {} \; 每个匹配一次 不受限 命令本身不支持多参数,或需要逐条输出
-exec CMD {} + 分批(ARGG_MAX 上限内) 受 getconf ARG_MAX 约束 大批量删除/改权限,默认选它
xargs 分批,可用 -P 并行 同上,-n/-s 可调 需要并行或需要管道分流

一万个文件时,\; 是一万次 fork+exec,+ 是几十次。这个差距在机械盘或容器里尤其明显,因为每次 exec 都要走一遍路径解析与 inode 查找。


6. 文件名含空格:-print0 + xargs -0

$ find /data -name '*.log' | xargs rm -f        # 遇到 "my file.log" 会拆成两个参数
$ find /data -name '*.log' -print0 | xargs -0 rm -f   # 正确

xargs 默认按空白(空格、Tab、换行)切分,文件名里只要有一个空格就会被拆错,轻则删不掉,重则误删。

场景 正确写法
文件名含空格 find ... -print0 | xargs -0 CMD
文件名含换行 只有 NUL 分隔可靠,-print0 是唯一正解
需要看到将要执行什么 find ... -print0 | xargs -0 -t CMD
需要限制并发 find ... -print0 | xargs -0 -P 4 CMD
用 -exec 系列 不用管,{} 天然按文件边界传参

同理,for f in $(find ...) 也是错的写法,IFS 会把带空格的名字切开;要用 while IFS= read -r -d '' f 配 -print0。


7. 四组高频组合

7.1 清理 7 天前的日志

$ find /var/log/app -type f -name '*.log' -mtime +7 -print   # 先看清单
$ find /var/log/app -type f -name '*.log' -mtime +7 -delete

-delete 隐含 -depth,所以它必须写在表达式末尾,且不能与 -prune 同用。永远先 -print 一遍,再换 -delete。

7.2 找出大于 100M 的文件

$ find / -xdev -type f -size +100M -printf '%10s %p\n' 2>/dev/null | sort -rn | head -20
$ du -ah /srv | sort -rh | head -20

-xdev 不跨文件系统,避免扫到 /proc、挂载的网络盘。du 更适合看目录总量,find 更适合单文件清单——两者都会重复计算硬链接,du 加 -l 才按链接计数。

7.3 按权限筛

$ find / -xdev -perm -4000 -type f 2>/dev/null        # 全部 SUID
$ find /srv -type f -perm -o+w                        # 其他人可写
$ find /srv -type d ! -perm -o+x                      # 目录缺 x,进不去

7.4 按时间做增量

$ find /srv/app -type f -newermt '2026-11-01' -printf '%TY-%Tm-%Td %p\n' | sort
$ find /srv/app -newer /srv/app/.last-deploy -type f

-newermt 接受人类可读时间,比 -newer 配参照文件更直观;-printf 的 %T 系列能直接排出时间线。


8. 排错:六种“find 结果不对”

现象 原因 处理
-name *.log 报错 通配符被 shell 先展开 加引号:-name '*.log'
-mtime +7 结果为空 文件其实不够旧,或时间戳被 touch 改过 用 -mmin 交叉验证;stat 看 atime/mtime
-size +100M 与 du 不一致 find 看 apparent size,du 看实际块 稀疏文件/压缩文件会差很多
找不到明明存在的文件 权限不足,find 静默跳过 2>/dev/null 去掉看 Permission denied
扫到软链接目录里 默认 -P 不跟随,但参数写成了 -L 明确 -P(默认)或 -H(只跟命令行给的)
-delete 报 Directory not empty 与 -depth 顺序冲突 把 -delete 放在最后,或改用 -exec rm -rf {} +

9. 边界:这些场景别硬用 find

需求 更合适的工具 理由
按内容搜文本 rg / grep -r find 只看元数据,不看内容
实时监控文件变化 inotifywait 轮询有延迟且耗 IO
全盘秒级定位 locate(updatedb) 走预建索引,代价是数据可能过期
按目录看占用 du / ncdu find 输出需要自己聚合
大规模并行处理 parallel / xargs -P 单线程遍历是瓶颈

一句话收口:点文件是命名约定,不是权限;硬链接是同一个 inode 的别名,软链接是一个路径字符串;find 负责“按元数据选出集合”,-print0 + xargs -0 负责“安全地把集合交给下一个命令”。


10. 自测

  1. ls -a 与 ls -A 的差别是什么?脚本里该用哪个?
  2. rm * 为什么删不掉 .env?find 会不会也漏掉它?
  3. 软链接能跨文件系统、硬链接不能,根本原因是什么?
  4. -mtime +7 筛出的是更旧的文件还是最近的文件?
  5. 一万个文件用 -exec {} \; 和 {} + 差在哪里?
  6. 文件名含换行时,为什么只有 -print0 + xargs -0 可靠?
S15-X05 · 隐藏文件、软链接与 find 的组合用法
https://www.clxhxhhr.top/posts/5707/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。