zoxide 本机实测:一万条记查询中 5.7ms0.10 版 import --from

5 阅读8分钟

上一篇写完 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 条
zsh3 条全记了

原因是 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 已有目录
1009.6 KB0.98 ms0.99 ms1.06 ms0.77 ms
100096 KB1.29 ms1.61 ms2.61 ms1.11 ms
10000960 KB5.69 ms5.92 ms16.18 ms3.99 ms
200001.9 MB9.69 ms9.08 ms33.10 ms7.46 ms

参照:true 0.52 ms、zoxide --version 0.44 ms,所以 1000 条以内基本就是进程启动开销。耗时随条目数大致线性增长,-l 要逐条检查目录是否还存在,最慢。

和 cd 比(同一个 bash 进程里循环,3 轮中间值):

操作库 100 条库 10000 条
builtin cd 全路径 往返两次0.013 ms0.014 ms(另一轮最低 0.012)
z g00 proj050 + 一次 cd 回来2.29 ms5.41 ms
钩子:目录没变(每次出提示符)0.46 ms0.51 ms
钩子:目录变了(含一次 zoxide add)约 2.2 ms约 5.0 ms

钩子在目录没变时也要 0.5 ms,是因为 bash 版每次都用 $(pwd) 开一个子 shell 取当前目录;目录变了才真正调用 zoxide。启动开销:

命令中位数
bash --norc -c true1.13 ms
bash --norc -c 'eval "$(zoxide init bash)"'3.51 ms
zsh -f -c true1.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 集成踩不踩坑。有问题评论区见,看到都会回。