找东西的 5 件武器:find / fd / grep / rg / locate
这是 Linux 系列的第 6 篇。从这章开始我们离开"心智模型",进入日常 shell 工具——你打开终端就要用的那些。这一篇是最高频的:找东西。
0. 两类需求,两套工具
"找"分两种:
| 你想找 | 工具 |
|---|---|
| 文件名 / 路径("那个 config.yml 在哪?") | find / fd / locate |
文件内容("哪个文件含 TODO?") |
grep / rg(ripgrep) |
新手最常犯的错是混用——用 grep 找文件名(grep 不递归就漏,递归就慢),用 find 找内容(要套 -exec grep)。分清两类是入门第一步。
1. find:所有发行版都有,参数像古董
find 是 1970 年代的工具,所有 Linux/Mac 都自带。语法上像古董,但保证能用:
# 基本格式
$ find <起始路径> <表达式>
# 找所有 .log 文件
$ find /var/log -name '*.log'
# 当前目录递归找
$ find . -name '*.py'
# 不区分大小写
$ find . -iname 'readme*'
# 限定深度(只看 2 级以内)
$ find . -maxdepth 2 -name '*.md'
# 按类型筛
$ find . -type f -name '*.json' # f 普通文件
$ find . -type d -name 'node_modules' # d 目录
$ find . -type l # l 符号链接
# 按大小
$ find . -size +100M # 大于 100MB
$ find . -size -1k # 小于 1KB
# 按修改时间
$ find . -mtime -7 # 7 天内改过
$ find . -mtime +30 # 30 天前改过
$ find . -mmin -60 # 1 小时内改过
# 多条件 AND(直接连)
$ find . -type f -name '*.log' -size +100M -mtime -7
# OR 要小心括号
$ find . \( -name '*.py' -o -name '*.js' \)
find 的杀手锏:-exec
find 不止能找,还能对每个结果跑命令:
# 删掉所有 30 天前的 .tmp 文件
$ find /tmp -name '*.tmp' -mtime +30 -delete
# 找到所有大于 100MB 的文件,列出大小排序
$ find . -type f -size +100M -exec ls -lh {} \;
# 给所有 .sh 文件加可执行权限
$ find . -name '*.sh' -exec chmod +x {} \;
# 更现代的写法(用 + 而不是 \;,把所有结果合成一次调用,快 100x)
$ find . -name '*.log' -exec gzip {} +
{} 是占位符,被替换成当前找到的路径;\; 是结束符(要转义防 shell 吃掉)。
find 容易踩的坑
# ❌ 这样找不到('*.log' 没引号,shell 会先展开)
$ find . -name *.log
# ✅ 一定加引号
$ find . -name '*.log'
2. fd:find 的现代版,记忆负担只剩 1/10
fd 是 Rust 写的 find 替代品,默认行为聪明 10 倍:
- 默认就忽略
.gitignore里的内容(不会扫node_modules) - 默认不区分大小写
- 默认彩色 + 友好输出
- 默认 8 线程并行
# 装一下(Mac)
$ brew install fd
# Ubuntu/Debian
$ sudo apt install fd-find # 命令名是 fdfind,自己 alias 成 fd
# 用法直接
$ fd config # 当前目录递归找名字含 config 的
$ fd '\.py$' # 用正则找 .py
$ fd -e py # 按扩展名找
$ fd -t f config # -t f 只要文件
$ fd -t d node_modules # -t d 只要目录
$ fd --hidden config # 包含 .开头的隐藏文件
$ fd --no-ignore config # 不忽略 .gitignore
$ fd -x wc -l # 对每个结果跑命令(替代 find -exec)
经验:90% 的"找文件名"场景用 fd,剩下 10% 复杂条件(按大小 + 时间 + 多类型组合)才搬出 find。
3. locate:秒级响应的"老式索引"
find 和 fd 每次都是现场扫——大目录可能要几秒到几分钟。locate 走另一条路:
# locate 用一个预建的数据库(默认每天更新一次)
$ locate config.yml # 毫秒级返回所有匹配路径
# Mac 上叫 mdfind(macOS Spotlight 索引)
$ mdfind -name "config.yml"
数据库由 updatedb 维护(crontab 里跑):
# 强制刷一次索引
$ sudo updatedb
优点:闪电快。缺点:
- 新增文件要等下次 updatedb 才能找到(最坏 24h)
- 只索引文件名,不能按大小 / 时间筛
- Mac 上 mdfind 会被 Spotlight 索引覆盖范围限制
最佳用法:找你几个月前装到某处的库 / 文件,不知道在哪。find 全盘扫太慢,locate 一秒出。
4. grep:找内容的标配
grep 找文件内容:
# 基本
$ grep 'TODO' file.py
# 多文件
$ grep 'TODO' *.py
# 递归整个目录
$ grep -r 'TODO' . # 或 grep -R
# 只显示文件名
$ grep -rl 'TODO' .
# 忽略大小写
$ grep -i 'error' app.log
# 反向(不含某字符串的行)
$ grep -v 'DEBUG' app.log
# 显示行号
$ grep -n 'def main' *.py
# 显示上下 3 行(context)
$ grep -C 3 'error' app.log
$ grep -A 3 'error' app.log # 后 3 行
$ grep -B 3 'error' app.log # 前 3 行
# 正则(默认 BRE,加 -E 是 ERE,加 -P 是 Perl 正则)
$ grep -E '^(GET|POST)' access.log
$ grep -P '\d{4}-\d{2}-\d{2}' app.log
排除某些目录
# 排除 node_modules(不递归进去)
$ grep -r 'TODO' . --exclude-dir=node_modules
# 排除某些文件类型
$ grep -r 'TODO' . --exclude='*.log'
# 只在某些文件类型里找
$ grep -r 'TODO' . --include='*.py'
但你会发现写这堆 --exclude-dir 很烦——这就是 rg 出场的时机。
5. ripgrep(rg):grep 的下一代
ripgrep 也是 Rust 写的,速度和 UX 都甩 grep 几条街:
# 装
$ brew install ripgrep # Mac
$ sudo apt install ripgrep # Ubuntu 18.10+
# 默认就递归 + 默认尊重 .gitignore + 默认不进 .git
$ rg TODO # 当前目录找 TODO
# 默认就有颜色 + 行号 + 文件名分组
$ rg 'def main'
# 限定文件类型
$ rg --type py 'def main' # 只搜 .py
$ rg -t py 'def main' # 同上
$ rg -t-py 'def main' # 排除 .py
$ rg --type-list # 看支持哪些类型
# 文件名匹配 + 内容匹配组合
$ rg -g '*.py' 'TODO' # 类似 grep --include
# 显示上下文(同 grep)
$ rg -C 3 'error'
# 只显示文件名
$ rg -l 'TODO'
# 在压缩文件里搜(rg 直接搞)
$ rg -z 'error' app.log.gz
# 大小写:默认大小写敏感,全小写时自动 ignore-case
$ rg todo # 全小写 → 不分大小写
$ rg TODO # 含大写 → 严格匹配
ripgrep vs grep 性能:扫 Linux kernel 源码(~7 万文件)找一个关键词:
grep -r: ~6 秒
rg: ~0.3 秒
20 倍差距。原因:rg 多线程 + SIMD + 跳 .gitignore。
强烈建议把
rg设成日常——alias grep="rg"不推荐(rg 行为太不一样会让脚本翻车),但日常交互式直接rg替代grep -r。
6. 组合食谱
"找到所有大于 1MB 的 .log 文件,统计总大小"
$ find . -name '*.log' -size +1M -exec du -ch {} + | tail -1
"找到含 import requests 的所有 .py 文件,列出文件名"
$ rg -l 'import requests' --type py
"找到 node_modules 占了多大磁盘"
$ find . -type d -name node_modules -prune -exec du -sh {} +
-prune 让 find 找到 node_modules 后不再进入它内部递归——避免重复计算。
"找过去 1 小时被改过的所有文件"
$ find . -mmin -60 -type f
# 或 fd 更简洁:
$ fd --changed-within 1h
"在 git 历史里找某字符串第一次出现的 commit"
$ git log -S 'mysteryFunction' --oneline | tail -1
git log -S 找的是"哪个 commit 改变了这个字符串的出现次数"——比 grep 厉害得多。
7. 速查卡:什么场景用哪个
找文件名 + 当前项目(被 git 管的) → fd
找文件名 + 复杂条件(大小/时间组合) → find
找文件名 + 全盘 + 不在乎是不是当下 → locate / mdfind
找内容 + 项目里 → rg
找内容 + 系统文件(/etc /var 等) → grep -r 或 rg --no-ignore
git 历史里找代码改动 → git log -S
8. 现在做一件事
打开一个你熟悉的代码项目,跑这几条命令,感受工具差距:
# 1. 当前目录有多少 .py 文件
$ fd -e py | wc -l
# 2. 哪些 .py 含 TODO
$ rg -l 'TODO' -t py
# 3. 文件最大的前 5 个
$ find . -type f -size +1M -exec du -h {} + 2>/dev/null | sort -hr | head -5
# 4. 7 天内被改过的文件
$ fd --changed-within 7d -t f
跑完,决定一个:今天起把 fd + rg 加进你的 PATH。一年节省你的搜索时间能有几十小时。
下一篇:text-pipes——
awk / sed / cut / sort / uniq这些流水线积木,组合起来怎么 1 行解决很多看起来"得写脚本"的活。