fzf 做的事情,一句话能说完:从标准输入读一个列表,让你模糊地挑,把选中的写到标准输出。

就这一条管道模型,决定了它的适用范围——喂文件名进去就是文件选择器,喂历史命令就是搜索器,喂 Git 分支就是切换器,喂进程列表就是进程杀手,喂你自己的数据就是通用菜单。

它是 Unix 哲学做得最干净的一个现代工具:只干一件事,但能被组合进任何地方。2026 年它的最新稳定版是 0.74.x(2026-08 发布),每 4~8 周一个次版本,没有 LTS 线,跟着最新走就行。


一、为什么是它

先看它能替掉什么:

你现在的做法

换成 fzf 之后

find / fd 打印一堆路径再自己复制

CTRL-T 直接挑,路径插到光标处

一层层敲 cd ../../some/long/path

cd **<TAB> 模糊找到目录

内置的 Ctrl+R 反向搜索(只按顺序匹配)

CTRL-R 模糊匹配,跳着打字也能命中

git branch 然后复制粘贴分支名

git switch $(git branch \| fzf)

ps 看列表,手抄 PID,再 kill

ps -ef \| fzf \| awk '{print $2}' \| xargs kill -9

自己写 select 菜单做脚本交互

某个命令 \| fzf

选它的三个理由:

  1. 单个 Go 二进制,零依赖。丢进 PATH 就能用,服务器上 scp 一个文件过去也行。优化过的匹配算法在毫秒级处理百万行。

  2. 可被管道组合。它不假设自己在找文件——它假设自己在过滤文本。这个抽象层级刚好卡在"足够通用"和"足够好用"之间。

  3. 可编程。基于事件-动作绑定(--bind),你能把一个 shell 脚本、一个 rg 查询、甚至一个文件树变成带实时预览的 TUI,而不必去写 curses。

如果你只在终端里装两个效率工具,那就装 fzf 和 ripgrep——一个负责"挑",一个负责"搜",剩下的事情基本都能拼出来。


二、安装

包管理器一行搞定:

# macOS
brew install fzf

# Debian 9+ / Ubuntu 19.10+
sudo apt install fzf

# Fedora
sudo dnf install fzf

# Arch
sudo pacman -S fzf

# Windows
winget install fzf
scoop install fzf
choco install fzf

也可以用第 2 篇介绍过的 mise 统一管(跨平台一份配置):

mise use -g fzf@latest

或者用官方安装脚本,它会顺手帮你写好 shell 集成:

git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install

关键提醒:包管理器装完,你只是得到了 fzf 这个二进制。快捷键和模糊补全需要额外开启——这是"装完了但按 CTRL-R 没反应"的唯一原因。在 shell 配置里加一行:

# ~/.bashrc
eval "$(fzf --bash)"

# ~/.zshrc
source <(fzf --zsh)

# ~/.config/fish/config.fish
fzf --fish | source

Windows 上的两种情况

如果你的主力是 Git Bash 或 WSL,那跟 Linux 完全一样,eval "$(fzf --bash)" 加进 ~/.bashrc 就结束了。

如果主力是 PowerShell,注意官方的 shell 集成不覆盖 PowerShell,需要装社区模块 PSFzf:

winget install fzf
Install-Module PSFzf -Scope CurrentUser -Force

# 加进 $PROFILE
Import-Module PSFzf
Set-PsFzfOption -PSReadlineChordProvider 'Ctrl+t' -PSReadlineChordReverseHistory 'Ctrl+r'

两个容易翻车的点:PSReadline 必须先于 PSFzf 导入(PSFzf 是在模块导入时注册按键处理器的);如果报"禁止运行脚本",先执行 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser


三、三个默认快捷键

开启 shell 集成后,你得到这三个键。它们覆盖了日常 80% 的使用:

快捷键

做什么

列表来源

CTRL-T

把选中的文件/目录路径插入到光标位置

--walker file,dir,follow,hidden

CTRL-R

模糊搜索命令历史,选中后插到命令行

shell 历史(支持 TAB 多选)

ALT-C

cd 进选中的目录

--walker dir,follow,hidden

三个都可以单独关掉或换列表来源:

# 只要 CTRL-R,把另外两个禁用
FZF_CTRL_T_COMMAND= FZF_ALT_C_COMMAND= eval "$(fzf --bash)"

# 换 ALT-C 的目录来源
export FZF_ALT_C_COMMAND='fd --type d --hidden --exclude .git'

CTRL-R 是性价比最高的那个。它直接替换掉 shell 内置的反向搜索——内置版本只做按顺序的子串匹配,你得记住命令开头是什么;fzf 是模糊匹配,记住中间某个词就能翻出来。半年零散敲过的长命令,两秒钟能找到。


四、**<TAB> 模糊补全

除了三个快捷键,shell 集成还会把 ** 变成补全触发器。光标前的单词以 ** 结尾时按 TAB,就弹出 fzf:

vim **<TAB>       # 当前目录下的文件,可 TAB 多选
cd **<TAB>        # 当前目录下的目录
vim ~/**<TAB>     # 家目录下的文件
kill -9 **<TAB>   # 进程 ID
ssh **<TAB>       # 主机名(读 /etc/hosts 和 ~/.ssh/config)

不喜欢 ** 可以换:

export FZF_COMPLETION_TRIGGER='~~'

五、搜索语法速查(fzf 的隐藏考点)

fzf 默认开启扩展搜索模式:用空格分隔的多个条件是与(AND)关系。这一节是很多人用了几年都没打开的能力。

写法

含义

例子

foo

模糊匹配,字符按顺序出现即可

fz 能命中 fzf

'foo

精确匹配(取消模糊)

'main 只匹配含 main

'foo'

精确边界匹配,两端都在单词边界上

'test' 不会命中 testing

^foo

以 foo 开头(同时也是精确匹配)

^core

foo$

以 foo 结尾(同时也是精确匹配)

.go$

!foo

排除含 foo 的行

!test

\|

^core go$ \| rb$ \| py$

一行组合起来看:

# 精确含 wild、以 music 开头、以 .mp3 结尾、含 sbtrkt、排除 rmx
fzf --query "'wild ^music .mp3$ sbtrkt !rmx"

# 想要纯模糊、不要扩展语法
fzf --no-extended --exact

记忆点:' 是"别给我模糊",^ $ 是"卡头尾",! 是"排除",| 是"或"


六、命令行参数速查

参数

作用

--height 40%

在光标下方以指定高度启动,不全屏

--layout=reverse

从上往下排列(配合 --height 更像补全)

--border / --border=rounded

加边框;--header-border / --footer-border 可单独控制

--inline-info

匹配计数显示在提示行,省一行

--header '...' / --header-lines=1

显示标题行 / 把输入首行当表头

-m / --multi

开启多选,TAB 标记;--multi=3 限选 3 个

--preview 'bat --color=always {}'

预览窗口

--preview-window right:60%

预览位置与尺寸,也可 up,60%,border-bottom

--delimiter :

按分隔符切字段,配合 {1} {2}

--bind 'ctrl-r:reload(ps -ef)'

按键绑定动作

--bind 'enter:become(vim {})'

选中后直接执行,fzf 进程被替换掉

--popup bottom,40%

tmux 3.3+ / Zellij 0.44+ 里以弹窗运行

--listen

开 Unix socket 服务,供外部程序远程控制

占位符(用在 --preview--bind 的动作里):

占位符

含义

{}

当前聚焦的整行

{1} {2}

配合 --delimiter 的第 N 个字段

{q}

当前查询串

{+}

多选的全部选中项

{n}

序号

注意:--tmux 这个老选项在 0.71 之后已被 --popup 取代,老 tmux 用独立的 fzf-tmux 脚本。


七、界面内按键速查

fzf 跑起来之后,界面里的键基本是 readline 那一套:

按键

动作

ctrl-j / ctrl-k、上下箭头

下移 / 上移

ctrl-n / ctrl-p

跳到下 / 上一个匹配项(跳过不匹配行)

TAB / shift-TAB

多选模式下标记并下移 / 上移

Enter

确认并输出

ctrl-c / esc

退出,不输出

ctrl-a

跳到行首

ctrl-w

删除前一个单词

alt-b / alt-f

按词左右移动

ctrl-y

粘贴

ctrl-/

切换按单词换行


八、五个真实场景

场景 1:先把 CTRL-R 用起来

这是唯一一个"学会就再也回不去"的功能。压力测试一下:随便敲个关键词,比如 dockersshuv,看半年前的命令怎么被捞出来。

TAB 还能多选——一次捞多条历史命令出来,比如按顺序把三条 ssh 命令拼成一行。

场景 2:文件预览,只打不含糊

# 临时用一次
fzf --preview 'bat --color=always --style=numbers {}' --preview-window right:60%

# 让 CTRL-T 永远带预览
export FZF_CTRL_T_OPTS="--preview 'bat --color=always {}' --preview-window right:60%"

batcat 的彩色带行号版本,跟 fzf 是固定搭配。装上它,挑文件时右边直接显示内容,不用"打开看了发现不是"。

场景 3:Git 分支与提交

# 切分支
git switch $(git branch --all | grep -v HEAD | fzf)

# 看某个历史提交干了什么,右边直接预览 diff 摘要
git log --oneline | fzf --preview 'git show --stat --color=always {1}'

官方作者还做了一个 fzf-git.sh,把 CTRL-G CTRL-B(分支)、CTRL-G CTRL-F(文件)这类组合键做成脚本,加载后就能在命令行里直接找 Git 对象——和第 1 篇的 lazygit 是两种路子,一个全程 TUI,一个只借用 fzf 当选择器。

场景 4:在脚本里当选择器

这是 fzf 真正的抽象价值:任何脚本要"让人选一个",都可以不用写菜单。

# 找出并杀掉一个进程
ps -ef | fzf | awk '{print $2}' | xargs kill -9

# 多选后一起打开
fzf --multi --bind 'enter:become(vim {+})'

# 多选后全选确认
fzf --multi --bind 'ctrl-a:select-all+accept'

场景 5:rg + fzf,内容搜索的正确组合

这是高阶用法,也是唯一一个"用对了比用错了快十倍"的地方。

RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
fzf --ansi --delimiter : \
    --preview 'bat --color=always {1} --highlight-line {2}' \
    --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
    --bind "change:reload:$RG_PREFIX {q} || true" \
    --bind 'enter:become(vim {1} +{2})'

拆开看:rg 负责全项目内容检索;fzf 只做交互界面;--delimiter :文件:行号:内容 切成字段,于是 {1} 是文件、{2} 是行号;--previewbat 打开文件并高亮那一行;change:reload 让你边打字边用 rg 重新搜(所以大项目里不卡);enter:become 直接把 vim 起在准确的行号上。

性能上的取舍:fzf 本身很快,但它不适合直接搜大文件里的文本——那是 ripgrep 的活。上面这种写法把搜索完全交给 rg,fzf 只提供界面,代价是失去一部分模糊性,换来大项目下更好的性能和内存占用。另外 change:reload 最好加防抖,避免每敲一个字符就起一个子进程:

--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true"

九、配置

默认行为靠环境变量调:

# ~/.bashrc 或 ~/.zshrc
export FZF_DEFAULT_COMMAND='fd --type f --strip-cwd-prefix --hidden --follow --exclude .git'
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
export FZF_DEFAULT_OPTS="--layout=reverse --height 40% --border --inline-info"

选项多了之后,写成文件更清爽:

export FZF_DEFAULT_OPTS_FILE=~/.fzfrc
# ~/.fzfrc
--layout=reverse
--height=40%
--border
--inline-info

此外,三个快捷键各自还有独立的变量:FZF_CTRL_T_OPTSFZF_CTRL_R_OPTSFZF_ALT_C_OPTS,可以只给其中一个加预览。

一个明确的禁区:不要把 --preview 放进 FZF_DEFAULT_OPTS。fzf 是通用文本过滤器,ps -ef | fzf 这种输入下 {} 并不是文件名,预览命令会报错。预览只加在具体用途的 *_OPTS 里。


十、常见坑

现象

原因

解法

装完按 CTRL-T 没反应

只装了二进制,没开 shell 集成

eval "$(fzf --bash)" 或对应语句

PowerShell 里快捷键全无效

官方集成不含 PowerShell

装 PSFzf,且保证 PSReadline 先导入

ALT-C 不生效

很多 Windows 终端不传递 Alt 组合键

换 Windows Terminal 并检查 Alt 相关设置,或直接 cd $(fd -t d \| fzf)

列表里全是 node_modules.git

默认 walker 兜不住所有忽略规则

FZF_DEFAULT_COMMAND + fd--exclude

大项目里 CTRL-T 卡顿

列表是一次性全量生成的

fd + --exclude,或先用 rg 过滤再交给 fzf

文档里的选项你的 fzf 不认

发行版仓库的版本落后

用二进制 / brew / mise 装新版,以 fzf --version 为准

想在全屏 tmux 弹窗里跑,--tmux 报错

0.71 起已改名为 --popup

--popup(需 tmux 3.3+)或 fzf-tmux 脚本

预览窗口里的中文/图标乱码

终端字体或 --no-unicode 影响

装 Nerd Font,或明确用 --no-unicode


十一、上手路径

不用一次学完,按四天分步来:

阶段

任务

检验标准

第 1 天

只开 CTRL-R,其余禁用

一整天不用 翻历史

第 2 天

用起来 CTRL-T**<TAB>

不再手敲超过两层的路径

第 3 天

bat,给 CTRL-T 加预览;写 ~/.fzfrc

挑文件时有内容预览

第 4 天

写自己的 --bind,在脚本里用 fzf 当选择器

有至少一个自己的肌肉记忆快捷键

真正需要练的只有一件事:遇到"要在列表里选一个"的场景时,想到 fzf。想到了,剩下的都是查表。


十二、参考

  • 项目主页:https://github.com/junegunn/fzf

  • 完整手册:man fzf,或仓库里的 man/man1/fzf.1

  • 高级示例(rg 集成、日志跟踪、动态重载):仓库 ADVANCED.md

  • Git 集成脚本:https://github.com/junegunn/fzf-git.sh

  • PowerShell 集成模块 PSFzf:https://github.com/kelleyma49/PSFzf

  • 配色与主题调试:https://vitormv.github.io/fzf-themes/