grep -r 在大型仓库里要等好几秒,find 的参数从来记不住,API 返回的 JSON 只能整块看。fzf、ripgrep、fd 和 jq 把这三个问题解决了,而且可以串联成一条搜索流水线。
这篇是系列第二篇,接在 kitty + Zim 之后。如果你在 Zim 里启用了 fzf-tab 插件,装完 fzf 之后它会自动生效。
fzf:模糊搜索框架
fzf 本身做一件事:接受文本输入,让你模糊过滤后选出一行。但它能接在任何命令后面,这让它成为整个工具链里”乘数效应”最强的一个。
安装与 Zsh 集成
1 2 3
| brew install fzf
$(brew --prefix)/opt/fzf/install
|
装完重启终端(或 exec zsh),三个快捷键立即可用。
三个核心快捷键
Ctrl+T:模糊查找文件,路径粘到命令行
Alt+C:模糊跳转目录
kitty 用户注意:需要 kitty.conf 里有 macos_option_as_alt yes,Alt+C 才能触发。第一篇里已经配过了。
Ctrl+R:交互式历史搜索
替换原生的 Ctrl+R,弹出可模糊搜索的历史列表,选中直接执行。如果你后续装了 atuin,atuin 会接管这个快捷键,功能更强(第五篇讲)。
fzf-tab 在 Zim 里自动生效
第一篇 Zim 配置里加了 fzf-tab 插件。只要 fzf 装好,重启终端后按 Tab 补全就会变成 fzf 交互界面——候选列表可以模糊过滤,用 ↑↓ 选择,回车确认。
常用参数
| 参数 |
说明 |
-m |
多选(Tab 标记,回车一次输出所有选中项) |
-e |
精确匹配,不模糊(适合搜确切文件名) |
-1 |
只有一个匹配时自动选择,不弹界面 |
-0 |
没有匹配时直接退出,不弹界面 |
--query "str" |
启动时预填搜索词 |
--header "text" |
在列表顶部显示说明文字 |
--prompt "str" |
自定义提示符(默认 > ) |
--preview 'cmd' |
预览命令,{} 代表当前高亮项 |
--preview-window pos:size |
预览窗口位置(right/left/up/down)和大小(如 right:50%) |
--bind "key:action" |
自定义按键绑定(如 ctrl-y:execute(echo {} | pbcopy)) |
--filter "str" |
非交互模式,直接过滤输出,不弹界面 |
--nth N |
只对第 N 列做模糊匹配(配合 --delimiter 用) |
--with-nth N |
只显示第 N 列,不影响实际输出内容 |
--no-sort |
保持输入顺序,不按匹配度排序 |
--tac |
反转输入顺序(常用于让最新历史显示在上方) |
几个实用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| git branch | fzf -1 | xargs git checkout
printf "yes\nno" | fzf --header "确认删除?" --prompt "选择: "
fzf --preview 'cat {}' --preview-window down:40%
ps aux | fzf --nth 11
cat list.txt | fzf --filter "keyword" --no-sort
fzf --bind 'ctrl-y:execute(echo {} | pbcopy)'
|
和其他命令组合
fzf 接受任意文本输入,输出用户选中的行:
1 2 3 4 5 6 7 8
| git branch | fzf | xargs git checkout
ps aux | fzf | awk '{print $2}' | xargs kill
fzf --preview 'bat --color=always {}'
|
多选:-m
加 -m 参数后,Tab 键可以多选,回车一次性输出所有选中项:
1 2 3 4 5
| fd -t f | fzf -m | xargs nvim
fd -e log | fzf -m | xargs rm
|
实时全文搜索:把 rg 接进 fzf
下面这个函数可以加到 ~/.zshrc 里,实现”边输边搜”的交互式全文搜索:
1 2 3 4 5 6 7 8 9 10 11
| rg-fzf() { local RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case" fzf --ansi --disabled --query "$*" \ --bind "start:reload:$RG_PREFIX {q}" \ --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ --bind "enter:become(nvim {1} +{2})" \ --delimiter : \ --preview 'bat --color=always {1} --highlight-line {2}' \ --preview-window 'right,60%,border-left,+{2}+3/3,~3' }
|
调用方式:rg-fzf "关键词" 或直接 rg-fzf,在 fzf 界面里继续输入过滤。输入关键词时 rg 实时重新查询,选中条目后按回车用 nvim 打开对应文件并跳到对应行。
FZF_DEFAULT_OPTS:统一配置预览窗口
在 ~/.zshrc 里设置 FZF_DEFAULT_OPTS,可以让所有 fzf 调用都带上默认选项:
1 2 3 4 5 6 7
| export FZF_DEFAULT_OPTS=" --height=60% --layout=reverse --border=rounded --preview-window=right:50% "
|
设置之后,单独运行 fzf 也会默认带预览窗口布局,不需要每次手动传参。
ripgrep(rg):比 grep 快一个数量级的全文搜索
安装
核心用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| rg "pattern"
rg -t go "http.Handle" rg -t py "import requests"
rg -i "TODO"
rg -w "error"
rg -l "pattern"
rg -C 2 "panic"
|
rg 和 grep -r 的主要区别:不需要加 -r 就默认递归;自动跳过 .git/、.gitignore 里的路径、隐藏文件;输出带文件名、行号和彩色高亮。在大型代码仓库里,速度差距非常明显。
常用参数
| 参数 |
说明 |
-i |
忽略大小写 |
-s |
强制区分大小写 |
-S / --smart-case |
智能大小写:有大写字母时区分,全小写时忽略 |
-w |
整词匹配 |
-F |
固定字符串(不当作正则,特殊字符不需要转义) |
-v |
反向匹配(输出不包含 pattern 的行) |
-l |
只输出文件名,不显示匹配行 |
-c |
只输出每个文件的匹配行数 |
-o |
只输出匹配的部分,不输出整行 |
-n / -N |
显示/隐藏行号(默认显示) |
-A N |
匹配行后面 N 行 |
-B N |
匹配行前面 N 行 |
-C N |
匹配行前后各 N 行 |
-t type |
只搜索指定类型(go/py/js/md 等) |
-T type |
排除指定类型 |
-g "glob" |
只搜索匹配 glob 的文件(如 -g "*.go") |
--hidden |
包含隐藏文件(默认跳过) |
-u |
不跳过 .gitignore 里的文件 |
-m N |
每个文件最多输出 N 条匹配 |
-e "pattern" |
指定多个匹配模式(可多次用) |
--json |
输出 JSON 格式(可接 jq 处理) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| rg -t go -g "*.proto" -i -C 3 "grpc"
rg -o 'GOPATH=\S+' ~/.zshrc
rg -l --files-without-match "TODO" -t go
rg -e "error" -e "panic" -e "fatal" main.go
rg --json "pattern" | jq 'select(.type=="match") | .data.path.text'
|
和 fzf 组合:交互式选择搜索结果
1 2 3 4 5
| rg -l "TODO" | fzf --preview 'bat --color=always {}'
rg -l "TODO" | fzf --preview 'bat --color=always {}' | xargs nvim
|
全局配置:~/.ripgreprc
每次都手动传参数很麻烦。把常用选项写进配置文件,rg 会自动加载:
1 2 3 4 5 6 7 8 9 10 11
|
--smart-case
--hidden --glob=!.git/
--color=always
|
然后在 ~/.zshrc 里指定配置文件路径:
1
| export RIPGREP_CONFIG_PATH="$HOME/.ripgreprc"
|
配置生效后,直接 rg "pattern" 就等于 rg --smart-case --hidden "pattern",不需要每次手动传这些参数。
fd:find 的现代替代品
安装
核心用法
fd 和 find 做同样的事,但语法直觉很多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| fd config
fd -e go fd -e md -e txt
fd -t f fd -t d src
fd -E node_modules -E .git -e js
fd -e go ~/code/github
fd -e go -x wc -l fd -e log -X rm
|
fd vs find 语法对比:
1 2 3 4 5 6 7
| find . -name "*.go" -type f fd -e go
find . -name "*test*" -type f fd test
|
常用参数
| 参数 |
说明 |
-e ext |
按扩展名过滤(不带点,可多次用) |
-t f/d/l/x |
按类型:文件/目录/符号链接/可执行文件 |
-E pattern |
排除匹配的路径(可多次用) |
-H |
包含隐藏文件(以 . 开头的) |
-I |
不跳过 .gitignore(忽略所有 ignore 规则) |
-L |
跟随符号链接 |
-d N |
最大搜索深度(-d 1 只搜当前目录) |
-g |
使用 glob 模式而不是正则(-g "*.go" 等) |
-s |
区分大小写(默认不区分) |
-l |
显示详细信息(类似 ls -l) |
-0 |
用 null 分隔输出(配合 xargs -0 处理含空格的路径) |
-x cmd |
对每个结果执行命令(逐个) |
-X cmd |
对所有结果合并执行一次命令 |
--changed-within N |
在 N 时间内修改过的(如 1h、2d、1week) |
--changed-before N |
在 N 时间之前修改过的 |
--max-results N |
最多输出 N 条结果 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| fd --changed-within 1h
fd -e log --changed-before 7d
fd -e txt -0 | xargs -0 wc -l
fd -d 1
fd -t x -E .git
fd -H -g ".*rc" ~
|
和 fzf 组合
1 2 3 4 5
| fd -t f | fzf | xargs nvim
cd $(fd -t d | fzf)
|
jq:JSON 命令行处理器
安装
基础语法
jq 的查询表达式作为第一个参数传入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| cat data.json | jq .
echo '{"name": "Alice", "age": 30}' | jq '.name'
jq '.data.user.email'
jq '.[]' jq '.[0]' jq '.[-1]'
jq '.[].name' jq '.items[] | .id'
jq '.[] | select(.status == "active")' jq '.[] | select(.age > 18)'
jq '.[] | {id, name}' jq '.[] | {user: .name, email: .contact.email}'
|
-r 参数去掉字符串的引号,适合把结果传给其他命令:
常用参数
| 参数 |
说明 |
-r |
原始输出:字符串不加引号(适合传给 shell) |
-c |
紧凑输出:不格式化,单行输出(适合写入文件或传给程序) |
-n |
不读取输入,从 null 开始(用于构造 JSON) |
-e |
输出为 false/null 时以非零退出码退出(脚本判断用) |
-s |
把所有输入合并成一个数组再处理 |
-R |
把每行输入当作原始字符串(不解析 JSON) |
-j |
等同 -r,输出后不加换行 |
-S |
对象的键按字母排序输出 |
--arg name val |
把 shell 变量作为字符串传入 jq(在表达式里用 $name) |
--argjson name val |
把 shell 变量作为 JSON 值传入(可以是数字、数组等) |
--slurpfile name file |
把 JSON 文件内容作为变量传入 |
--indent N |
缩进 N 个空格(默认 2) |
--tab |
用 Tab 缩进 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| jq -c '.[]' data.json
jq -n '{name: "Alice", scores: [90, 85, 92]}'
jq -s '.[0].users + .[1].users' a.json b.json
cat list.txt | jq -Rs 'split("\n") | map(select(. != ""))'
if jq -e '.error' response.json > /dev/null 2>&1; then echo "请求失败" fi
MIN=100 jq --argjson min "$MIN" '.[] | select(.count > $min)' data.json
|
实际场景:curl | jq
1 2 3 4 5 6 7 8 9 10 11 12 13
| curl -s https://api.github.com/users/octocat | jq '{name, public_repos, followers}'
curl -s https://api.github.com/users/octocat/repos \ | jq '.[] | {name, stargazers_count}'
curl -s https://api.github.com/users/octocat/repos \ | jq '[.[] | select(.stargazers_count > 100) | {name, stargazers_count}]'
jq '.database.host' config.json
|
进阶:排序、统计、变量传入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| curl -s https://api.github.com/users/octocat/repos \ | jq 'sort_by(-.stargazers_count) | .[] | {name, stargazers_count}'
echo '[1,2,3,4]' | jq 'length'
echo '{"name":"Alice","age":30}' | jq 'keys'
jq '.[] | select(has("email"))' users.json
STATUS="active" jq --arg s "$STATUS" '.[] | select(.status == $s)' data.json
|
输出格式转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| curl -s https://api.github.com/users/octocat/repos \ | jq -r '.[] | [.name, .stargazers_count, .language] | @csv'
jq -r '.[] | [.id, .name] | @tsv' data.json
curl -s https://api.github.com/users/octocat/repos \ | jq -r '.[].clone_url' | head -5
|
@csv 和 @tsv 会自动处理字段里的引号和特殊字符,比手动拼字符串安全。
管道把四个工具串成工作流
四个工具单独用都够用,组合起来才是真正的效率提升。fzf 在中间当连接器,把 rg/fd 找到的结果变成交互式选择,bat 做文件预览:
1 2 3 4 5 6 7 8
| rg -t go -l "TODO" | fzf --preview 'bat --color=always {}' | xargs nvim
fd -e json -e yaml | fzf --preview 'bat --color=always {}' | xargs nvim
rg -n "func HandleLogin" --type go
|
以上命令里的 bat --color=always 作为预览器,但 bat 还没装。下一篇讲 bat + eza + glow + mdcat,bat 装好后预览窗口就都能用了。