跳转至

第 5 章:补全系统 —— fpathcompinit 与缓存一致性

5.1 补全不是普通函数调用

在 zsh 中,用户按 Tab 时,shell 会根据当前命令查找 _command 补全函数。Oh My Zsh 不会为每个命令手动绑定函数,而是准备 fpath,再让 compinit 建立补全系统。

flowchart TB
  A[插件目录 completions/] --> B[fpath]
  C[内置 completions/] --> B
  D[custom/functions/] --> B
  B --> E[compinit]
  E --> F[zcompdump]
  F --> G[Tab 时 autoload _command]
  H[cache/completions/] --> B

5.2 fpath 的顺序就是优先级

oh-my-zsh.sh 会把 cache completions、内置目录、custom 目录和启用插件目录加入 fpath。zsh 从前往后查找同名函数,因此一个自定义 _docker 可以遮蔽内置 _docker

这是一种强大的覆盖机制,也意味着问题可能来自“同名文件被更早找到”。排查时使用:

whence -v _docker
print -l $fpath

不要只检查文件是否存在,要检查 zsh 实际选择了哪一个文件。

5.3 compaudit 是安全边界

补全代码会在当前 shell 进程中执行。如果 fpath 中的目录对其他用户可写,攻击者可能放入恶意 _command 文件,在用户按 Tab 时执行。因此 compinit 默认会检查目录权限。

Oh My Zsh 的默认路径是:

compinit -i -d "$ZSH_COMPDUMP"
handle_completion_insecurities &|

-i 忽略不安全目录并继续;handle_completion_insecurities 负责把问题告知用户。设置 ZSH_DISABLE_COMPFIX=true 后改用 compinit -u,相当于明确选择“允许不安全路径”。

5.4 zcompdump 为什么需要 metadata

如果每次启动都完整扫描所有补全文件,启动成本会很高,所以 compinit 使用 dump 文件缓存函数索引。Oh My Zsh 额外写入:

#omz revision: <git HEAD>
#omz fpath: <当前 fpath>

只要 HEAD 或 fpath 变化,就删除 dump 重新生成。这样解决了两个典型问题:更新 Oh My Zsh 后新补全没有出现;启用/禁用插件后 dump 仍指向旧目录。

ZSH_COMPDUMP 还包含短主机名和 zsh 版本,避免不同机器或不同 zsh 版本共用一份不兼容缓存。

5.5 补全插件的两个模式

静态补全

仓库直接提供 plugins/foo/completions/_foo。优点是可复现、无网络、启动逻辑简单;缺点是外部 CLI 新增子命令后可能落后。

动态生成

插件在加载时运行外部 CLI 的 completion 命令,把结果写入 ZSH_CACHE_DIR/completions。优点是贴合本机版本;缺点是首次启动可能变慢,外部命令输出变化也会导致失败。

Docker 插件同时支持二者,并用 zstyle :omz:plugins:docker legacy-completion 控制选择。

5.6 lib/completion.zsh 的默认策略

该模块设置了多项交互策略:

  • WORDCHARS='':让路径中的特殊字符更容易按词移动;
  • auto_menucomplete_in_wordalways_to_end:改变菜单和光标行为;
  • matcher-list:默认大小写不敏感,支持 partial/substring completion;
  • HYPHEN_INSENSITIVE=true:把 _- 纳入同一匹配关系;
  • special-dirs:允许补全 ...
  • use-cache yes:为 apt/dpkg 等昂贵补全启用 zsh cache;
  • list-colors:在 theme 加载后再与 LS_COLORS 对齐。

注意 CASE_SENSITIVEHYPHEN_INSENSITIVE 会在读取后被 unset。它们是启动期输入,不是运行时公共变量。

5.7 补全问题的最小实验

# 看某个命令是否注册补全
whence -v _git

# 看 zsh 正在使用的 dump
echo $ZSH_COMPDUMP

# 强制重建一次补全缓存
rm -f -- "$ZSH_COMPDUMP" "$ZSH_COMPDUMP.zwc"
autoload -Uz compinit && compinit

# 检查 fpath 中的可疑目录
compaudit

删除 dump 是可恢复的缓存清理,不是卸载;但不要随意删除整个 cache 目录,因为插件可能在里面存放动态生成的补全和更新状态。

5.8 性能分析的关键点

补全启动耗时通常来自三处:

  1. compinit 扫描大量 fpath
  2. 插件在 source 阶段生成补全;
  3. zcompdump 因 metadata 变化频繁失效。

先测量再优化:

time zsh -ilc exit
zmodload zsh/zprof
source "$ZSH/oh-my-zsh.sh"
zprof

如果只有某个插件首次加载很慢,考虑让它把生成物写入 cache;如果每次都慢,检查它是否无条件改写 dump、调用网络或执行外部命令。