源码解读——ohmyzsh
最近在做一个 shell 项目——NOX,在开发过程中遇到了一些设计方面的问题。为了能够得到一些灵感,我将注意力放到了 github 上 star 数量最高的 shell 项目——ohmyzsh。本文将记录我阅读 ohmyzsh 源码后,对于其设计的一些理解。
概述
Ohmyzsh(下文简称 OMZ) 是一个由开发者社区驱动的开源框架,用于管理我们的 zsh 配置。OMZ 提供了大量的插件,能够有效提升我们的命令行工作效率。此外,OMZ 提供了大量的主题,通过主题配置我们可以将终端打造得极具极客范!
目录结构
我们首先来看一下 OMZ 的目录结构,如下图所示。根据
.gitignore
中的定义,我们可以大概能够猜测到:
cache
目录用于存放 OMZ 运行时产生的缓存文件。cache
目录不加入 git 管理。custom
目录用于存放用户自定义的一些插件或主题,该目录下默认有两个子目录plugins
和themes
分别用户存放插件和主题。比如:我们可以将https://github.com/zsh-users/zsh-completions
克隆到custom/plugins
目录下,作为自定义的插件集合。custom
目录不加入 git 管理。lib
目录下的一系列文件定义了诸多 shell 工具方法。下文我们将具体进行介绍。log
目录应该用户存放 OMZ 运行时产生的日志缓存文件。log
目录不加入 git 管理。plugins
目录存放了 OMZ 项目所管理的插件。该目录下的子目录数量庞大,其中每一个子目录对应一个工具,如:xcode
目录对应 Xcode,该目录下存放的文件可分为两类:一是以_
为前缀的自动补全文件;二是以.plugin.zsh
为后缀的插件文件。我们可以通过设置.zshrc
中的plugins
数组变量来选择加载的插件。templates
目录存放了一个 zsh 配置文件的模板。themes
目录存放了大量主题,文件命名方式遵循主题名称
+.zsh-theme
的规则。我们可以通过设置.zshrc
中的ZSH_THEME
变量来选择主题。tools
目录包含了一系列 OMZ 系统相关的重要脚本,如:安装、卸载、检查更新、执行更新等。
安装方式
从 tools/install.sh
脚本中,我们能够看出 OMZ
的安装过程可以分为以下五个步骤:
- 设置变量
- 环境检查
- 设置 OMZ
- 设置 zshrc
- 设置 shell
设置变量
第一步 设置变量 中设置的变量包含两类:基础设置相关的变量、执行选项相关的变量。
基础设置相关的变量包括以下这些:
ZSH
:表示 OMZ 仓库的路径,默认是$HOME/.oh-my-zsh
。REPO
:表示仓库名称,默认是ohmyzsh/ohmyzsh
。REMOTE
:表示 OMZ 仓库的远程地址,默认是https://github.com/${REPO}.git
。BRANCH
:表示安装 OMZ 时所对应的仓库分支,默认是master
。
执行选项相关的变量包括以下这些:
CHSH
:表示安装 OMZ 时是否会切换默认的 shell,默认是yes
。RUNZSH
:表示是否会在安装后运行 zsh,默认是yes
。KEEP_ZSHRC
:表示是否会替换已有的.zshrc
,默认是no
。
在执行 install.sh
脚本时,我们可以通过传入特定选项来设置执行选项相关的变量。如:
--skip-chsh
选项:将CHSH
设置为no
。--unattended
选项:将CHSH
和RUNZSH
均设置为no
。--keep-zshrc
选项:将KEEP_ZSHRC
设置为yes
。
环境检查
第二步 环境检查 中主要判断了两种情况:
- zsh 未安装的情况下,退出 OMZ 的安装
- OMZ 已安装的情况下,退出 OMZ 的安装
设置 OMZ
第三步 设置 OMZ 的任务主要是根据第一步中所设置的变量
REPO
、REMOTE
、ZSH
,通过
git
将远程仓库克隆到本地。
设置 zshrc
第四步 设置 zshrc,顾名思义,就是设置
~/.zshrc
文件。这里会用到第一步中所设置的变量
ZSH
和 KEEP_ZSHRC
。
当 KEEP_ZSHRC
为 yes
时,则跳过本步骤。
当 KEEP_ZSHRC
为 no
时,会先将原有的
~/.zshrc
备份为
~/.zshrc.pre-oh-my-zsh
,用于卸载时进行恢复。然后将
templates
目录下的 zshrc.zsh-template
拷贝为
~/.zshrc
,同时将 ZSH
变量的设置导入至
~/.zshrc
中。
~/.zshrc
设置完之后,其内部默认开启的配置并不多,如下所示。 1
2
3
4
5
6
7
8# Path to your oh-my-zsh installation.
export ZSH=$HOME/.oh-my-zsh
ZSH_THEME="robbyrussell"
plugins=(git)
source $ZSH/oh-my-zsh.sh~/.zshrc
中定义的命令。OMZ 默认在
~/.zshrc
中设置了
ZSH
、ZSH_THEME
、plugins
三个变量,此外还定义了每次启动 zsh 时执行 oh-my-zsh.sh
脚本,对 OMZ 进行初始化。
设置 shell
第五步 设置 shell
的任务主要是根据第一步中所设置的变量 CHSH
来决定是否将默认的 shell 切换成 zsh。
OMZ 初始化
在上一节 安装方式 的 设置 zshrc
小结中,我们提到每次启动 zsh 时都会执行 oh-my-zsh.sh
脚本对
OMZ 进行初始化。那么这个初始化过程具体包含哪些内容呢?如下所示为
oh-my-zsh.sh
的源代码。
OMZ 初始化的主要步骤包括:
- 默认路径设置,如:
ZSH
、ZSH_CACHE_DIR
、fpath
、ZSH_CUSTOM
、ZSH_COMPDUMP
等 - 检查更新
- 初始化补全系统,包括:预设
fpath
变量、创建.zcompdump
文件等 - 加载 lib
- 加载插件
- 加载自定义配置
- 加载主题
详细初始化过程可以阅读如下所示的源代码。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125# 1. 如果 `ZSH` 变量没有设置,则将 `ZSH` 设置为当前 `oh-my-zsh.sh` 脚本所在的目录路径
[[ -z "$ZSH" ]] && export ZSH="${${(%):-%x}:a:h}"
# 2. 如果 `ZSH_CACHE_DIR` 变量没有设置,则将 `ZSH_CACHE_DIR` 设置为 `cache` 目录路径,用于存放缓存文件。
if [[ -z "$ZSH_CACHE_DIR" ]]; then
ZSH_CACHE_DIR="$ZSH/cache"
fi
# 3. 检查更新
if [ "$DISABLE_AUTO_UPDATE" != "true" ]; then
source $ZSH/tools/check_for_upgrade.sh
fi
# 初始化 OMZ
# 4. 为 `fpath` 变量增加两个索引路径,即使这两个路径在当前仓库中并不存在。`fpath` 类似于 `PATH`,zsh 启动时会加载 `fpath` 路径下的所有脚本文件。
fpath=($ZSH/functions $ZSH/completions $fpath)
# 5. 根据 `fpath` 变量加载所有后面会用到的库函数
autoload -U compaudit compinit
# 6. 如果 `ZSH_CUSTOM` 变量没有设置,则将 `ZSH_CUSTOM` 设置为 `custom` 目录路径,用于存放自定义配置文件和插件。
if [[ -z "$ZSH_CUSTOM" ]]; then
ZSH_CUSTOM="$ZSH/custom"
fi
is_plugin() {
local base_dir=$1
local name=$2
builtin test -f $base_dir/plugins/$name/$name.plugin.zsh \
|| builtin test -f $base_dir/plugins/$name/_$name
}
# 7. 将 `~/.zshrc` 中 `plugins` 变量所指定的插件路径加入到 `fpath` 变量中。`ZSH_CUSTOM` 中的自定义插件的优先级比 OMZ 提供的插件的优先级更高。所有的插件会在执行 `compinit` 之前加载完成。
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
# 8. 设置 `SHORT_HOST` 变量
if [[ "$OSTYPE" = darwin* ]]; then
# macOS's $HOST changes with dhcp, etc. Use ComputerName if possible.
SHORT_HOST=$(scutil --get ComputerName 2>/dev/null) || SHORT_HOST=${HOST/.*/}
else
SHORT_HOST=${HOST/.*/}
fi
# 9. 设置 `ZSH_COMPDUMP` 变量,`.zcompdump` 文件用于加速 `compinit` 的执行(在 zsh 中,`compinit` 用于初始化 shell 补全)
if [ -z "$ZSH_COMPDUMP" ]; then
ZSH_COMPDUMP="${ZDOTDIR:-${HOME}}/.zcompdump-${SHORT_HOST}-${ZSH_VERSION}"
fi
# 10. 构建 zcompdump OMZ metadata,本质上就是用 OMZ git 仓库当前的 commit 号进行区分
zcompdump_revision="#omz revision: $(builtin cd -q "$ZSH"; git rev-parse HEAD 2>/dev/null)"
zcompdump_fpath="#omz fpath: $fpath"
# 11. 如果 OMZ zcompdump metadata 发生变化,则删除 `.zcompdump` 文件
if ! command grep -q -Fx "$zcompdump_revision" "$ZSH_COMPDUMP" 2>/dev/null \
|| ! command grep -q -Fx "$zcompdump_fpath" "$ZSH_COMPDUMP" 2>/dev/null; then
command rm -f "$ZSH_COMPDUMP"
zcompdump_refresh=1
fi
# 12. 执行 compinit,生成 `.zcompdump` 文件
if [[ $ZSH_DISABLE_COMPFIX != true ]]; then
source $ZSH/lib/compfix.zsh
# If completion insecurities exist, warn the user
handle_completion_insecurities
# Load only from secure directories
compinit -i -C -d "${ZSH_COMPDUMP}"
else
# If the user wants it, load from all found directories
compinit -u -C -d "${ZSH_COMPDUMP}"
fi
# 13. 如果 `.zcompdump` 文件不存在,则创建,并写入 zcompdump metadata
if (( $zcompdump_refresh )); then
# Use `tee` in case the $ZSH_COMPDUMP filename is invalid, to silence the error
# See https://github.com/ohmyzsh/ohmyzsh/commit/dd1a7269#commitcomment-39003489
tee -a "$ZSH_COMPDUMP" &>/dev/null <<EOF
$zcompdump_revision
$zcompdump_fpath
EOF
fi
unset zcompdump_revision zcompdump_fpath zcompdump_refresh
# 14. 加载 `lib` 目录下的所有以 `.zsh` 为后缀的配置文件
for config_file ($ZSH/lib/*.zsh); do
custom_config_file="${ZSH_CUSTOM}/lib/${config_file:t}"
[ -f "${custom_config_file}" ] && config_file=${custom_config_file}
source $config_file
done
# 15. 加载 `~/.zshrc` 中 `plugins` 变量所定义的全部插件
for plugin ($plugins); do
if [ -f $ZSH_CUSTOM/plugins/$plugin/$plugin.plugin.zsh ]; then
source $ZSH_CUSTOM/plugins/$plugin/$plugin.plugin.zsh
elif [ -f $ZSH/plugins/$plugin/$plugin.plugin.zsh ]; then
source $ZSH/plugins/$plugin/$plugin.plugin.zsh
fi
done
# 16. 加载 `custom` 目录下的所有自定义配置文件
for config_file ($ZSH_CUSTOM/*.zsh(N)); do
source $config_file
done
unset config_file
# 17. 加载 `~/.zshrc` 中 `ZSH_THEME` 变量所定义的主题
if [ ! "$ZSH_THEME" = "" ]; then
if [ -f "$ZSH_CUSTOM/$ZSH_THEME.zsh-theme" ]; then
source "$ZSH_CUSTOM/$ZSH_THEME.zsh-theme"
elif [ -f "$ZSH_CUSTOM/themes/$ZSH_THEME.zsh-theme" ]; then
source "$ZSH_CUSTOM/themes/$ZSH_THEME.zsh-theme"
else
source "$ZSH/themes/$ZSH_THEME.zsh-theme"
fi
fi
更新策略
由于安装 OMZ 时,OMZ 会在 ~/.zshrc
中加入一行代码
source $ZSH/oh-my-zsh.sh
,从而使得每次启动 zsh 会都会执行
oh-my-zsh.sh
中的代码。oh-my-zsh.sh
中通过
DISABLE_AUTO_UPDATE
变量来判断是否执行更新检查,然后由
check_for_upgrade.sh
来决定是否进行更新,如下所示:
1
2
3if [ "$DISABLE_AUTO_UPDATE" != "true" ]; then
source $ZSH/tools/check_for_upgrade.sh
fi
DISABLE_AUTO_UPDATE
默认未进行初始化,所以每次启动 zsh
都会进行更新检查,用户可以通过在 ~/.zshrc
去掉对
DISABLE_AUTO_TITLE="true"
的注释来关闭更新检查。
下面,我们依次来介绍 OMZ 中的更新检查和更新执行。
更新检查
OMZ 更新检查的主要逻辑包括以下这些步骤:
- 读取
cache/.zsh-update
中的LAST_EPOCH
变量,如果文件不存在或LAST_EPOCH
的值为空,则写入新值并退出。LAST_EPOCH
表示上一次更新的时间(单位:从 1970年1月1日至当前的天数)。 - 读取更新间隔值
UPDATE_ZSH_DAYS
,默认值为 13。如果上次LAST_EPOCH
的更新时间与当前相隔 13 天及以上,那么继续后面的步骤;否则,退出。 - 根据
DISABLE_UPDATE_PROMPT
的值决定是否请求用户同意更新。采用交互式的方式请求用户同意。如果同意更新,则执行更新,更新完成后更新cache/.zsh-update
中上次更新的时间LAST_EPOCH
。
更新执行
具体的更新操作非常简单,就是通过 git pull --rebase
的方式获取最新的代码。
插件
插件是 OMZ 所提供的最重要的功能之一。插件主要位于 OMZ 的
plugins
目录下,每一个插件都有一个维护者,因此 OMZ
是一个社区驱动的开源项目。
那么插件究竟干了些什么呢?我们以
plugins/git/git.plugin.zsh
为例,进行分析。
总体而言,插件提供了三方面的功能。
- 函数
- alias
- 自动补全
对于函数,插件中提供了如下所示的一些函数,这些函数我们可以在 shell 中直接调用。
1 | function current_branch() { |
对于 alias,插件中则定义大量缩写以供用户快速调用命令,如下所示:
1
2
3
4
5
6
7
8
9
10
11alias gc='git commit -v'
alias gc!='git commit -v --amend'
alias gcn!='git commit -v --no-edit --amend'
alias gca='git commit -v -a'
alias gca!='git commit -v -a --amend'
alias gcan!='git commit -v -a --no-edit --amend'
alias gcans!='git commit -v -a -s --no-edit --amend'
alias gcam='git commit -a -m'
alias gcsm='git commit -s -m'
alias gcb='git checkout -b'
...
与此同时,插件还为函数定义了自动补全函数,如下所示定义
ggpnp
函数的自动补全与 git checkout
相同。
1
2
3
4
5
6
7
8function ggpnp() {
if [[ "$#" == 0 ]]; then
ggl && ggp
else
ggl "${*}" && ggp "${*}"
fi
}
compdef _git ggpnp=git-checkout
此外,OMZ 还会提供特定的自动补全文件,一般以 _
为前缀,后跟具体的命令名称,如:_pod
为 pod
提供了自动补全定义。
我们可以对 ~/.zshrc
的 plugins
变量进行修改,选择我们想要加载的插件。
关于 zsh 自动补全的细节,本文不作具体介绍,可以参考 《Zsh 自动补全脚本入门》 一文。
主题
主题主要用于修改 zsh 提示符和某些程序的外观,还有一些其他的行为,如:终端的 tab 和窗口标题。
主题通过设置各种变量来控制 zsh
提示符的外观,包括:PROMPT
、RPRPROMT
、LSCOLORS
、LS_COLORS
等变量;此外,主题还能够安装 hook 方法,从而提供更深层次的修改。
我们可以对 ~/.zshrc
的 ZSH_THEME
变量进行修改,选择我们想要的主题。
关于主题的具体实现,后面有时间写一篇文章来进行介绍。
总结
总体而言,从设计角度而言,OMZ 非常简单,它使用清晰的目录结构管理插件、主题。OMZ 的安装和更新也非常简单,没有复杂的机制。由于 OMZ 提供了巨量的插件和主题,能够极大地提升用户的工作效率,从而受到了用户的热烈追捧。