跳转至

第 4 章:插件系统 —— “目录约定”如何变成能力包

4.1 插件的两条腿

一个插件目录通常包含三类文件:

plugins/docker/
├── docker.plugin.zsh     # 主动代码:alias、函数、环境和动态逻辑
├── completions/_docker    # 被 compinit/autoload 使用的补全函数
└── README.md              # 用户文档,不参与启动执行

但并不是每个插件都三者齐全:

形态 识别条件 启动行为
主动插件 foo.plugin.zsh _omz_source 显式 source
补全插件 _foo 插件目录进入 fpath,由补全系统使用
文档/占位 只有 README 不会被 is_plugin 识别

is_plugin 同时检查 .plugin.zsh_$name,这说明“插件”在 Oh My Zsh 中不是单一执行文件,而是一个可提供 shell 行为或补全行为的目录协议。

4.2 从 plugins=(git docker) 到执行

flowchart LR
  A[plugins 数组] --> B{custom 中存在?}
  B -- 是 --> C[fpath 插入 custom/plugins/name]
  B -- 否 --> D{内置插件存在?}
  D -- 是 --> E[fpath 插入 plugins/name]
  D -- 否 --> W[打印 not found]
  C --> F[compinit 扫描补全]
  E --> F
  F --> G[_omz_source 主动入口]
  G --> H[备份 alias]
  H --> I[source plugin.zsh]
  I --> J[按 zstyle 恢复 alias]

这里有意把“进入 fpath”和“source 主动代码”分成两步:前者服务于补全初始化,后者服务于 alias/function/环境副作用。

4.3 为什么插件按用户数组顺序加载

源码按 for plugin ($plugins) 遍历,因此数组顺序决定 source 顺序。若两个插件定义同名函数或 alias,后加载的通常会覆盖先加载的;但不要把这种覆盖当成正式 API,因为插件可能用 typeset、条件分支或 zsh hook 改写行为。

推荐顺序:先加载提供基础函数/环境的插件,再加载依赖它们的别名或 prompt 插件;发现冲突时,用 type -a namewhich -a namealias name 检查最终来源。

4.4 custom 插件的“同名替换”陷阱

如果 $ZSH_CUSTOM/plugins/git/git.plugin.zsh 存在,_omz_source 会优先 source 它,内置文件不会再执行。这对修复或 fork 很有用,但如果只是想追加一个 alias,应该写:

# $ZSH_CUSTOM/aliases.zsh
alias gco='git checkout'

而不是复制一个同名 git.plugin.zsh。复制会形成“隐式 fork”:上游内置 git 插件更新后,你的副本不会自动获得新函数。

4.5 代表性插件:common-aliases

这个插件的主动代码很简单,集中提供 lllgreph 等常用别名,并用 $+commands[...] 检查外部命令是否存在。它体现了插件的低门槛:一个 .plugin.zsh 就可以改变交互体验。

同时也体现了全局副作用风险:alias rm='rm -i' 这类行为会影响所有后续命令。插件 README 必须明确列出 alias,用户也应该把启用插件当成一次“引入 shell 全局策略”。

4.6 代表性插件:docker

Docker 插件先定义大量快捷别名,然后检查 docker 命令是否存在;没有命令时直接 return,避免在未安装 Docker 的机器上继续调用。

它还根据 Docker 版本选择补全来源:

  • 默认优先使用 Docker 自带的 docker completion zsh
  • 旧版本或 legacy-completion zstyle 下,复制仓库内的 _docker
  • 生成结果写入 $ZSH_CACHE_DIR/completions/_docker

这展示了一个成熟插件的三层防线:先定义静态快捷方式,再做依赖检测,最后把与外部 CLI 版本相关的内容放入 cache。

4.7 插件作者应遵守的契约

plugins/<name>/
├── README.md
├── <name>.plugin.zsh
├── completions/_<command>   # 有补全时
└── functions/<function>     # 需要 autoload 时

主动脚本建议:

  • 对外部命令先用 (( $+commands[cmd] ))command -v 检查;
  • 对 zsh 版本用 is-at-least 分支;
  • 不在加载阶段执行昂贵网络请求;
  • 避免无条件覆盖用户已经定义的函数、alias 和环境变量;
  • 把可配置项放在 zstyle ':omz:plugins:<name>' ... 下;
  • README 说明新增 alias、函数、快捷键和依赖。

4.8 插件系统的边界

Oh My Zsh 不提供:

  • 依赖解析器;
  • 插件版本锁定;
  • 卸载时的副作用回滚;
  • 每个插件的沙箱;
  • 只读加载模式。

因此插件的真正 API 是“在某次 source 后,当前 shell 里留下什么”。这也是为什么扩展开发不能只看目录结构,还要看启动顺序和用户环境。