跳转至

第 2 章:启动链 —— 从 .zshrc 到第一个 prompt

oh-my-zsh.sh 是整个项目最重要的文件。它不是“公共配置集合”,而是一个有明确阶段的启动编排器。

2.1 起点:模板只负责声明

默认模板做的事情很少:设置 ZSH、选择 ZSH_THEME、配置少量开关、声明 plugins,最后执行:

source $ZSH/oh-my-zsh.sh

这条 source 之前的配置非常关键,因为后面的启动脚本会读取它们。典型例子:

export ZSH="$HOME/.oh-my-zsh"
ZSH_THEME="robbyrussell"
plugins=(git docker)

# 必须放在 source 前,才能影响初始化
zstyle ':omz:update' mode reminder
zstyle ':omz:*' aliases no

source "$ZSH/oh-my-zsh.sh"

如果把 zstyle 放到 source 之后,某些模块已经执行完,设置就只能影响后续行为,不能回溯已发生的加载。

2.2 阶段一:环境守卫和路径解析

脚本开头先定义临时的 omz_f 颜色函数,只在 stdout 是终端时输出 ANSI 格式;然后用 POSIX 风格检查阻止从 bash 等非 zsh shell 加载。如果不是 zsh,它还会尝试打印当前进程树,帮助用户定位“为什么 .zshrc 被错误地由 bash 执行”。

接着检查:

[[ "$(emulate)" = zsh ]] || return 1

这一步拒绝 zsh 的非默认 emulation mode。原因不是风格偏好,而是后续脚本依赖 zsh 的数组、参数展开、[[ ... ]] 和 option 语义;在兼容模式中继续执行会让错误更隐蔽。

路径推导顺序是:

  1. 如果没有 ZSH,用当前脚本路径的目录作为安装目录;
  2. 如果没有 ZSH_CUSTOM,使用 $ZSH/custom
  3. 如果没有 ZSH_CACHE_DIR,使用 $ZSH/cache
  4. 如果默认 cache 不可写,则退回 ${XDG_CACHE_HOME:-$HOME/.cache}/oh-my-zsh

这不是单纯的默认值设置,而是将“核心代码”和“可写状态”分开:只读安装目录也可以运行,前提是用户 home 下有可写缓存目录。

2.3 阶段二:缓存目录与补全路径

脚本创建 $ZSH_CACHE_DIR/completions,并把它插到 fpath 前面:

mkdir -p "$ZSH_CACHE_DIR/completions"
(( ${fpath[(Ie)$ZSH_CACHE_DIR/completions]} )) || \
  fpath=("$ZSH_CACHE_DIR/completions" $fpath)

这里有一个重要的优先级:生成的补全文件优先于仓库中的默认补全文件。像 Docker 这样的插件可以根据本机 CLI 版本生成补全,再把生成物写入 cache,而不修改仓库工作树。

之后,启动脚本把 $ZSH/{functions,completions}$ZSH_CUSTOM/{functions,completions} 加入 fpath。这是一个“先准备搜索路径,后初始化补全”的硬性顺序。

2.4 阶段三:更新检查先于主体加载

source "$ZSH/tools/check_for_upgrade.sh" 发生得很早。它会读取 :omz:update zstyle,必要时检查缓存状态文件,决定是否提醒或升级。

这样做的好处是用户每次打开 shell 都能得到更新反馈;代价是启动阶段可能发生网络请求和 Git 检查。源码用多种条件短路:没有 Git、目录不可写、非交互 stdout、非 Git 工作树等情况下直接跳过。

更新脚本还使用 $ZSH/log/update.lock 目录作为互斥锁,避免多个 shell 同时执行升级。第 8 章会拆开这条状态机。

2.5 阶段四:插件先进入 fpath

脚本定义 is_plugin:只要目录下存在 $name.plugin.zsh_$name,就认为它是可识别插件。

然后遍历用户的 plugins 数组:

for plugin ($plugins); do
  if is_plugin "$ZSH_CUSTOM" "$plugin"; then
    fpath=("$ZSH_CUSTOM/plugins/$plugin" $fpath)
  elif is_plugin "$ZSH" "$plugin"; then
    fpath=("$ZSH/plugins/$plugin" $fpath)
  else
    echo "[oh-my-zsh] plugin '$plugin' not found"
  fi
done

注意这里还没有 source 插件,只是把插件目录放进 fpath。这样 compinit 才能扫描到插件提供的 _command,而插件主动代码会在稍后执行。

2.6 阶段五:compinit 与 dump 文件

脚本计算一个默认的 ZSH_COMPDUMP

${ZDOTDIR:-$HOME}/.zcompdump-${SHORT_HOST}-${ZSH_VERSION}

然后用两条 metadata 判断缓存是否失效:

  • #omz revision: ...:Oh My Zsh Git HEAD;
  • #omz fpath: ...:当前补全路径。

任何一项变化都会删除旧 dump,让 compinit 重新扫描。这个设计把“软件版本变化”和“插件路径变化”都纳入缓存一致性判断,避免新增插件后补全仍然不可见。

默认路径执行 compinit -i,只从安全目录加载并忽略不安全目录;如果用户设置 ZSH_DISABLE_COMPFIX=true,则使用 compinit -u,允许不安全目录。

这是一个明确的安全/便利开关:默认拒绝可疑权限,用户可以自行承担风险以减少提示。

2.7 阶段六:按顺序 source 所有模块

_omz_source 是关键抽象。它接收一个相对路径,先把路径转换成 zstyle context:

路径 context
lib/git.zsh lib:git
plugins/docker/docker.plugin.zsh plugins:docker

然后检查 zstyle ":omz:${context}" aliases。如果用户禁用了别名,函数会在 source 前备份 alias 表,source 后恢复旧表,从而只去掉该文件新增加的 alias。

文件查找顺序固定为:

  1. $ZSH_CUSTOM/$filepath
  2. $ZSH/$filepath

加载顺序固定为:

lib/*.zsh → plugins/*/*.plugin.zsh → custom/*.zsh → selected theme

这里有两个结果:

  • custom 可以覆盖同名 lib 或 plugin 的实现;
  • 主题最后加载,可以直接消费 lib/plugin 定义的函数和变量。

2.8 阶段七:主题与最终收尾

根据 ZSH_THEME,脚本先找 $ZSH_CUSTOM/$theme.zsh-theme,再找 $ZSH_CUSTOM/themes/$theme.zsh-theme,最后找 $ZSH/themes/$theme.zsh-theme。找不到则只打印 warning,不会让整个 zsh 初始化失败。

最后,如果存在 LS_COLORS,把它转换为 completion list color zstyle,使补全列表和 ls 的颜色保持一致。

2.9 一次启动的故障定位顺序

如果启动失败,不要先猜主题。按源码阶段排查:

  1. echo $ZSH:安装路径是否正确?
  2. echo $ZSH_CUSTOMecho $ZSH_CACHE_DIR:是否可读/可写?
  3. zsh -xlic exit 2>trace.log:最后一个 source 文件是什么?
  4. zstyle -L ':omz:*':source 前后的 zstyle 是否符合预期?
  5. 临时设置 ZSH_THEME=""plugins=():区分 theme/plugin 问题和 core 问题。

启动链的核心不是“加载很多文件”,而是把依赖关系编码成加载顺序。下一章解释哪些变量和 zstyle 能在这条顺序上施加影响。