上一篇写完 eza 之后预告过,这次实测 ajeetdsouza/zoxide(星数约 4.05 万,GitHub API 读于 2026-11-11)。
zoxide 自称「更聪明的 cd」:它记住你去过的目录,按「频率 + 最近」打分,之后 z foo 就能直接跳到最常用的那个 foo 目录,不用再敲一长串路径。它是 z / autojump 这一类工具的 Rust 版。这次我没在自己真实的 shell 历史上测,而是造了一棵假目录树、用 _ZO_DATA_DIR 把数据库隔离出来,在 bash 和 zsh 里各跑了一遍,顺手踩到了几个教程里很少提的坑。
本机实测 + AI 辅助整理。测量时间:2026-11-11(Asia/Shanghai)。单个命令的墙钟用 hyperfine(
-N -w 3 -r 30),文内写 中位数;shell 函数(z、钩子)没法单独交给 hyperfine,改在同一个 bash 进程里循环 200 次取平均、重复 3 轮取中间值。数据库文件在页缓存里,属于「热缓存」成绩。
本机环境
OS: Debian 13 (trixie) x86_64,8 核
zoxide: 0.10.0(GitHub release,musl 静态二进制)
bash: 5.2.37
zsh: 5.9
fzf: 0.60(zi 交互选择要用)
安装:apt 这次没装上,release 一个文件搞定
$ sudo apt-get install -y zoxide
E: Unable to locate package zoxide
本机 apt 源当天 apt-get update 一直返回 500,所以装不上。我用 curl 直接读了 Debian trixie 的软件包索引:里面其实有 zoxide 0.9.7-1+b1,比 GitHub 最新的 0.10.0 落后一个小版本——这个差别后面坑 1 会用到。直接下官方 release:
curl -sL -o zoxide.tar.gz \
https://github.com/ajeetdsouza/zoxide/releases/download/v0.10.0/zoxide-0.10.0-x86_64-unknown-linux-musl.tar.gz
tar xzf zoxide.tar.gz # 包里有 zoxide、man 手册、各 shell 补全
mkdir -p ~/.local/bin && cp zoxide ~/.local/bin/
zoxide --version # zoxide 0.10.0
二进制 1233512 字节(约 1.2 MB),musl 静态链接,老系统也能直接跑。release 里还有现成的 .deb。macOS 一般 brew install zoxide。装完要在 shell 配置最后加一行:
eval "$(zoxide init bash)" # zsh 换成 zoxide init zsh
zi 交互选择依赖 fzf,zsh 是我为了对比单独装的(这两个我是从 Debian 镜像 curl 下 .deb 再 dpkg -i 装的)。
测试素材
| 路径 / 变量 | 说明 |
|---|---|
tree/ | 假目录树:projects/{foo-api,foo-web,blog/src}、work/foo/bar、work/bar/foo、work/client-a/docs、notes/中文笔记、dir with space、tmp/gone |
big/ | 100 组 × 200 个,共 20000 个空目录,用来造大数据库 |
_ZO_DATA_DIR | 每组测试一个独立数据库目录,不碰真实 ~/.local/share/zoxide/db.zo |
| 伪造的 z / autojump 数据 | 手写带时间戳的 .z 格式文件和 autojump.txt,测导入与时间衰减 |
先搞懂它怎么记、怎么打分
eval "$(zoxide init bash)" 会定义 z、zi 两个函数,并往 PROMPT_COMMAND 里挂一个 __zoxide_hook:每次出提示符时,如果当前目录变了,就执行一次 zoxide add 当前目录。数据库默认在 ~/.local/share/zoxide/db.zo,_ZO_DATA_DIR 可以改,但必须是绝对路径:
$ export _ZO_DATA_DIR=../db; zoxide query foo
zoxide: _ZO_DATA_DIR must be an absolute path
每次 add 让该目录的 rank +1,查询时按「rank × 最近访问系数」排序。新加一次就显示 4.0、加三次 12.0。时间系数我用伪造的 z 数据导入测出来(每条 rank 都是 10):
| 最近一次访问 | query -ls 显示 | 系数 |
|---|---|---|
| 刚刚 | 40.0 | ×4 |
| 2 小时前 | 20.0 | ×2 |
| 2 天前 | 5.0 | ×0.5 |
| 10 天前 | 2.5 | ×0.25 |
| 40 天前 | 2.5 | ×0.25 |
也就是 1 小时内 ×4、1 天内 ×2、1 周内 ×0.5、更久 ×0.25。一周没去的目录,分数直接打到 1/16,所以它对「最近在忙什么」非常敏感。
坑 1:0.10 版 import --from 直接报错,也不收文件路径
网上大多数迁移教程(包括 README 的旧版本)写的是 zoxide import --from=z 路径。在 0.10.0 上:
$ zoxide import --from=z fixtures/import/z.txt
error: unexpected argument '--from' found
$ zoxide import z fixtures/import/z.txt
error: unexpected argument '.../z.txt' found
CHANGELOG 写得很清楚:0.10.0 把 import 改成子命令(atuin / autojump / fasd / z / z.lua / zsh-z),并且自动去默认位置找数据库,不再接受路径参数。要导入非默认位置的文件,得用原工具自己的环境变量。我实测能用的写法:
_Z_DATA=/path/to/.z zoxide import z # z 的数据默认在 ~/.z
HOME=/某个假 home zoxide import autojump # 读 ~/.local/share/autojump/autojump.txt
zoxide import autojump --merge # 数据库非空时必须加 --merge
数据库不为空时不加 --merge 会拒绝:current database is not empty, specify --merge to continue anyway。
版本差异是重点:Debian trixie 仓库里是 0.9.7,按 CHANGELOG 还是旧的 --from 写法(这个我没装 0.9.7 验证)。apt 装的和 release 装的,同一条导入命令可能一个能用一个报错,先 zoxide import --help 看一眼。
导入的另外两个细节:
- z 导入会带进不存在的目录:我故意放了一条
/does/not/exist|50,导入后-a能看到它 200 分排第一;正常查询时它被隐藏,不影响跳转,但数据库里一直留着。 - autojump 的权重被压平了:我写的权重 30 / 1.0 / -3,导入后分别显示 0.2 / 0.2 / 0.0。权重被压进 0~1 再按「很久以前」处理,原来的先后顺序基本丢了,你在 zoxide 里正常
cd进去一次(4.0)就能盖过它们。
坑 2:最后一个关键词必须命中「最后一段」目录名
数据库里有 projects/foo-api、projects/foo-web、work/foo/bar、work/bar/foo、projects/blog/src 等,下面全是本机输出(<T> 是测试树的根):
$ zoxide query foo → <T>/projects/foo-api (分数最高)
$ zoxide query foo bar → <T>/work/foo/bar
$ zoxide query bar foo → <T>/work/bar/foo (关键词顺序有意义)
$ zoxide query foo api → <T>/projects/foo-api
$ zoxide query api foo → no match found
$ zoxide query proj → no match found
$ zoxide query blog → no match found
$ zoxide query blog src → <T>/projects/blog/src
$ zoxide query FOO → <T>/projects/foo-api (不分大小写)
$ zoxide query 中文 → <T>/notes/中文笔记
规则是:关键词按顺序出现在路径里,最后一个关键词必须出现在路径最后一段。所以 z proj 跳不到 projects/foo-api,z blog 也去不了 blog/src——除非 projects、blog 这一层本身也进过数据库。
更隐蔽的是前面的关键词匹配的是整个绝对路径。我的测试树放在 /workspace/... 下面:
$ zoxide query work foo → <T>/projects/foo-api
我本意是「work 下的 foo」,结果它匹配了路径开头 /workspace 里的 work,跳到了分数更高的 projects/foo-api。上层路径里的词越常见(home、work、src),多关键词就越容易「歪」。想要精确,最后一个关键词写得具体一点,或者用 zi 选。
还有一条优先级:参数本身是个能 cd 进去的路径时,z 就是普通 cd。在测试树根目录 z work 进的是 ./work,不查数据库;换到 /tmp 再 z work,才会按数据库跳。
坑 3:bash 只在出提示符时记录,脚本里根本不记
同样一行连 cd 三次:
cd projects/foo-web; cd projects/blog/src; cd work/bar/foo
| shell | 数据库里新增 |
|---|---|
bash(默认 --hook pwd) | 只有 work/bar/foo 1 条 |
| zsh | 3 条全记了 |
原因是 bash 版钩子挂在 PROMPT_COMMAND 上,只看出提示符那一刻的目录;zsh 版挂在 chpwd_functions,每次切目录都触发。另外 bash 钩子里有个 [[ -o history ]] 判断,set +o history 时进的目录不记(CHANGELOG 里 0.10.0 新增的行为;本机关掉 history 后分两行 cd 进 notes/中文笔记、projects/blog,都没进库,重新打开后进的 work/client-a 正常记录)。
非交互脚本更彻底:
$ bash script.sh
script.sh: line 3: z: command not found # 脚本不读 ~/.bashrc
(脚本里 eval init 之后连 cd 两次)→ 数据库仍为空
(手动调用 __zoxide_hook) → 仍为空,非交互 bash 默认关着 history
结论:zoxide 只学习你在交互终端里的足迹,cron、CI、Makefile 里的 cd 不会污染排名;反过来脚本里也别指望 z,写全路径。
--hook prompt 则是另一个极端:每次出提示符都 +1。我 cd 进 foo-web 后敲了 4 次 true,它就变成 20.0 分(5 次 × 4),在一个目录里待得久的就会被抬得很高。
坑 4:_ZO_MAXAGE 不是天数
名字像「最长保留天数」,实际是所有条目 rank 总和的上限(默认 10000)。超过后所有 rank 按比例压到约 0.9 × 上限,压完小于 1 的条目直接删除。_ZO_MAXAGE=100 实测:
(rank 60 / 30 / 5 / 1 四条,总和 96)
240.0 projects/foo-api
120.0 projects/foo-web
20.0 work/foo/bar
4.0 notes
$ zoxide add -s 10 work/bar/foo # 总和 106 > 100,触发老化
203.8 projects/foo-api
101.9 projects/foo-web
34.0 work/bar/foo
17.0 work/foo/bar
# notes 被删了
压缩比例 203.8/240 ≈ 0.85 = 90/106,对得上。
极端情况我也测了:默认上限下,用脚本把 10000 个目录各 zoxide add 一次(每条 rank 1,总和正好 10000),再 add 第 10001 个——数据库从 10000 条变成 0 条,全部被压到 0.9 以下删光。正常人手动 cd 很难走到这一步,但如果你写脚本批量 zoxide add(比如把项目目录全灌进去),要么调大 _ZO_MAXAGE,要么用 -s 给高分。
坑 5:别的工具改写 PROMPT_COMMAND,zoxide 就「失忆」
init 之后如果别的配置把 PROMPT_COMMAND 整个覆盖(我用 PROMPT_COMMAND='history -a' 模拟),钩子就没了,之后 cd 不再进库。好在 0.10.0 有自检,第一次跑 z 会提示:
zoxide: detected a possible configuration issue.
Please ensure that zoxide is initialized right at the end of your shell configuration file (usually ~/.bashrc).
这就是 README 反复强调「init 放在配置文件最后」的原因。看到这条提示,先检查后面有没有提示符美化工具在改 PROMPT_COMMAND。
卖点:用顺手之后确实回不去
- 多关键词 + 打分:
z client docs直接到work/client-a/docs,z -回上一个目录,z不带参数回 home。 - 不重复跳当前目录:在
foo-api里再z foo,会跳到第二名;只有一个匹配时提示you are already in the only match。 --cmd cd直接接管 cd:zoxide init bash --cmd cd后type cd显示cd is a function,cd foo按数据库跳,cd ..、cd 全路径照旧,交互选择变成cdi,z不再定义。区别在于找不到时报zoxide: no match found(exit 1),而不是 bash 原来的No such file or directory;cd -也不像原版那样打印目录。原版 cd 用\builtin cd。- zi + fzf:没装 fzf 时
zoxide query -i报could not find fzf, is it installed?;stdin 不是终端时报fzf returned an error,所以 zi 只适合交互用。 - 自动排除 / 自愈:
$HOME默认不进库(zoxide add ~无报错但查不到);不存在的目录add会报not a directory。目录删掉后查询自动跳过它,但query -lsa看得到它还在库里,重新建回来又能跳;想彻底清用zoxide remove 路径。_ZO_EXCLUDE_DIRS支持 glob,'.../work/*'能排除子目录但不排除work本身。 - 中文、空格:
z 中文、z with都能正确跳到notes/中文笔记、dir with space。
速度:比 cd 慢几毫秒,人感觉不到
单条 zoxide 命令(hyperfine 中位数;数据库是 big/ 里的真实目录,hit 查 g00 proj050):
| 数据库条目 | db.zo 大小 | query 命中 | query 无匹配 | query -l 全列 | add 已有目录 |
|---|---|---|---|---|---|
| 100 | 9.6 KB | 0.98 ms | 0.99 ms | 1.06 ms | 0.77 ms |
| 1000 | 96 KB | 1.29 ms | 1.61 ms | 2.61 ms | 1.11 ms |
| 10000 | 960 KB | 5.69 ms | 5.92 ms | 16.18 ms | 3.99 ms |
| 20000 | 1.9 MB | 9.69 ms | 9.08 ms | 33.10 ms | 7.46 ms |
参照:true 0.52 ms、zoxide --version 0.44 ms,所以 1000 条以内基本就是进程启动开销。耗时随条目数大致线性增长,-l 要逐条检查目录是否还存在,最慢。
和 cd 比(同一个 bash 进程里循环,3 轮中间值):
| 操作 | 库 100 条 | 库 10000 条 |
|---|---|---|
builtin cd 全路径 往返两次 | 0.013 ms | 0.014 ms(另一轮最低 0.012) |
z g00 proj050 + 一次 cd 回来 | 2.29 ms | 5.41 ms |
| 钩子:目录没变(每次出提示符) | 0.46 ms | 0.51 ms |
钩子:目录变了(含一次 zoxide add) | 约 2.2 ms | 约 5.0 ms |
钩子在目录没变时也要 0.5 ms,是因为 bash 版每次都用 $(pwd) 开一个子 shell 取当前目录;目录变了才真正调用 zoxide。启动开销:
| 命令 | 中位数 |
|---|---|
bash --norc -c true | 1.13 ms |
bash --norc -c 'eval "$(zoxide init bash)"' | 3.51 ms |
zsh -f -c true | 1.32 ms |
zsh -f -c 'eval "$(zoxide init zsh)"' | 2.52 ms |
zoxide init bash 本身只要 0.47 ms,多出来的主要是 bash 解析那 181 行脚本。结论:每开一个终端多 1~2.4 ms,每次 cd 多几毫秒,都在人感知不到的范围里;条目真涨到上万,z 也只是 5~10 ms。
我会怎么配
# ~/.bashrc 最后一行(放在提示符美化工具之后)
export _ZO_EXCLUDE_DIRS="$HOME:/tmp/*"
eval "$(zoxide init bash)" # 保留 z/zi,不接管 cd,脚本行为不变
# 从 z 迁移(0.10 写法,旧版是 --from)
# _Z_DATA=~/.z zoxide import z --merge
我不用 --cmd cd:报错信息和 cd - 行为都变了,别人在我机器上敲 cd 会困惑;z 两个字母本来就够短。
小结
| 项 | 本机结果 |
|---|---|
| 版本 / 星数 | zoxide 0.10.0,约 4.05 万 星(2026-11-11) |
| 安装 | apt 未装上(Debian 索引里是 0.9.7)→ GitHub release → ~/.local/bin,1.2 MB 单文件 |
| 打分 | rank × 时间系数:1 小时内 ×4、1 天内 ×2、1 周内 ×0.5、更久 ×0.25 |
| 最大的坑 | 0.10 版 import 改成子命令、自动找文件,--from 和路径参数都报错 |
| 匹配规则 | 最后一个关键词必须命中最后一段;前面的词会匹配到上层 /workspace |
| 记录时机 | bash 只记提示符那一刻(一行 3 次 cd 记 1 条),zsh 记 3 条;脚本不记 |
_ZO_MAXAGE | 是总分上限不是天数;默认上限下 10000 条 rank 1 再 add 一次 → 清空 |
| 速度 | 一万条 query 中位 5.69 ms;钩子每次提示符 0.5 ms;init 让 bash 启动 +2.4 ms |
一句话:zoxide 是那种装上就忘了它存在、但每天都在省时间的工具。真正要注意的不是速度,而是它「什么时候记」和「怎么匹配」:最后一个词要写目录名本身,脚本里别用 z,从旧工具迁移先看你装的是哪个版本。
完整测量日志在本机 zoxide/measurements.txt、bench-*.json 与 feat-*.txt;完整安装脚本包和进阶踩坑表以后会整理上架 Gumroad,先关注专栏「GitHub实测笔记」。
上一篇:eza 本机实测(一万文件 -l 中位 48ms / ls 21.5ms)。
下一篇预告:junegunn/fzf——这篇里 zi 靠的就是它,实测它在大文件列表里的过滤速度、和 shell 的 Ctrl-R / Ctrl-T 集成踩不踩坑。有问题评论区见,看到都会回。