第 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 name、which -a name 和 alias name 检查最终来源。
4.4 custom 插件的“同名替换”陷阱¶
如果 $ZSH_CUSTOM/plugins/git/git.plugin.zsh 存在,_omz_source 会优先 source 它,内置文件不会再执行。这对修复或 fork 很有用,但如果只是想追加一个 alias,应该写:
而不是复制一个同名 git.plugin.zsh。复制会形成“隐式 fork”:上游内置 git 插件更新后,你的副本不会自动获得新函数。
4.5 代表性插件:common-aliases¶
这个插件的主动代码很简单,集中提供 ll、l、grep、h 等常用别名,并用 $+commands[...] 检查外部命令是否存在。它体现了插件的低门槛:一个 .plugin.zsh 就可以改变交互体验。
同时也体现了全局副作用风险:alias rm='rm -i' 这类行为会影响所有后续命令。插件 README 必须明确列出 alias,用户也应该把启用插件当成一次“引入 shell 全局策略”。
4.6 代表性插件:docker¶
Docker 插件先定义大量快捷别名,然后检查 docker 命令是否存在;没有命令时直接 return,避免在未安装 Docker 的机器上继续调用。
它还根据 Docker 版本选择补全来源:
- 默认优先使用 Docker 自带的
docker completion zsh; - 旧版本或
legacy-completionzstyle 下,复制仓库内的_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 里留下什么”。这也是为什么扩展开发不能只看目录结构,还要看启动顺序和用户环境。