前言

Zplugin 是个冷门但是却强大无比的 zsh 插件管理器,它拥有一个 killer feature —— Turbo mode,可以让插件在后台加载。这意味这你可以先加载最重要的插件,比如语法高亮和自动建议,剩下的可以统统放到后台加载,让你的 zsh 尽快进入可用状态。

利用这个机制,zplugin 可以将 zsh 的启动时间缩短到几十毫秒——以我的配置为例,只需要 35 毫秒左右。而使用传统的插件管理器比如 antigen,需要近 200 毫秒才能加载完成。

这里有一张图,对比了不同插件管理器的速度(来源:Comparison of ZSH frameworks and plugin managers

可以看到 zplugin 在插件数目变多时速度……似乎更快了???
什么鬼,这个大概是实验误差吧,也有可能是第一次启动时编译了自身所以后面变快了。不过 zplugin 的速度是毋庸置疑的,哪怕这个插件需要数十秒来加载,只要放在后台加载,一样不影响你的 zsh 启动。

然而!!这个工具虽然强大,却没多少名气,让人倍感惋惜。

所以写下这篇文章,希望能有更多人了解到这个工具。
不过本文只涉及了它强大功能的冰山一角,深入研究推荐阅读 Zplugin Wiki 和 Zplugin 的 README

(如果你很懒不想研究的话,文末有一份完整的示例配置,安装完成后可以直接使用)

安装

自动安装

官方推荐的安装方式,一键完成。不过让我很没有安全感,我倾向于手动安装。

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/zdharma/zplugin/master/doc/install.sh)"

手动安装

首先 clone repo 到随便哪个位置

1
git clone https://github.com/zdharma/zplugin.git ~/.zplugin/bin

然后在你的 ~/.zshrc 顶端添加如下语句

1
source ~/.zplugin/bin/zplugin.zsh

安装完成,非常简单。

配置

我本打算从头写一篇教程,但是感觉自己要么讲得太多了,要么又讲得太少了。干脆直接翻译一下文档的 Introduction 部分吧(

在本篇文档中,你将会学会如何:

  • 使用 Oh My Zsh 和 Prezto
  • 管理补全
  • 使用 Turbo mode
  • 使用 ice 修饰词比如 as"program"

等等

基本插件加载

1
2
zplugin load zdharma/history-search-multi-word
zplugin light zsh-users/zsh-syntax-highlighting

以上命令展示了两种最基本的加载插件的方式。

  • load 会启用报告功能——你可以通过 zplugin report {plugin-spec} 跟踪插件具体做了什么,也可以使用 zplugin unload {plugin-spec} 卸载插件。
  • light无需跟踪和报告,可以提升加载速度,但是会导致失去查看插件报告和动态卸载插件的能力。

开启 Turbo mode 后跟踪插件所耗费的时间可以忽略不计

使用 Oh My Zsh & Prezto

为了加载 Oh My Zsh 和 Prezto 插件,可以使用 snippet 功能加载代码片段。代码片段是指通过 curlwget 等工具下载的单个文件。后面直接跟 URL 即可(会自动判断下载工具)。举例:

1
2
zplugin snippet 'https://github.com/robbyrussell/oh-my-zsh/raw/master/plugins/git/git.plugin.zsh'
zplugin snippet 'https://github.com/sorin-ionescu/prezto/blob/master/modules/helper/init.zsh'

此外,对于 Oh My Zsh 和 Prezto,你还可以使用缩写 OMZ::PZT::

1
2
zplugin snippet OMZ::plugins/git/git.plugin.zsh
zplugin snippet PZT::modules/helper/init.zsh

此外的此外,snippet 还支持 Subversion 协议,which GitHub 也支持。这可以让你加载包含多个文件的代码片段(比如 Prezto module 就有可能包含两个或者更多的文件,像 init.zshalias.zsh)。默认会被 source 的文件有:*.plugin.zsh, init.zsh, *.zsh-theme

1
2
3
# URL 指向目录
zplugin ice svn
zplugin snippet PZT::modules/docker

代码片段和性能

通过 curlwget 等工具和 SVN ,你可以几乎完全避免加载 Oh My Zsh 和 Prezto 或者是其他框架的代码。这可以提高 Zplugin 的性能,而且更快更紧凑(指占用内存更小并且加载时间更短)。

一些 Ice 修饰词

命令 zplugin ice 为下一条命令提供了 Ice 修饰词(详见 README ice-modifiers 一节)。啥意思呢:“ice” 是指一些被添加物(就像被添加到饮料或者咖啡里面的冰块)——在 Zplugin 中这意味着 ice 是被添加到下一条命令中的修饰词,冰块会融化(所以不会持续起作用)——在 Zplugin 中这意味着修饰词只对下一条命令起效。举例来说使用 pick ice 可以显示地选择被执行的文件:(译注:绕半天,其实就是可选参数)

1
2
zplugin ice svn pick"init.zsh"
zplugin snippet PZT::modules/git

ice 修饰词的内容可以简单地放在 "...", '...'$'...' 中。不需要在 ice 修饰词名称的后面加上 ":" (尽管你这么做也没问题,而且加 = 也是可以的。比如 pick="init.zsh"pick=init.zsh 都是可行的)。
这样可以让 vimemacs 之类的编辑器和 zsh-users/zsh-syntax-highlightingzdharma/fast-syntax-highlighting 能够高亮 ice 修饰词的内容。

as”program”

插件并不一定是需要被 source 的脚本,也可以是需要添加到 $PATH 中的命令。为了实现这种效果,需要以 program 为参数调用 as ice (或者以 command 为参数也可以)

1
2
zplugin ice as"program" cp"httpstat.sh -> httpstat" pick"httpstat"
zplugin light b4b4r07/httpstat

上面的代码会将插件目录添加到 $PATH 中,并复制文件 httpstat.shhttpstat ,并为 pick 选中的文件(本例中为 httpstat) 添加正确的可执行权限(+x)。还有一个修饰词 mv,它和 cp 的工作方式类似,只不过是移动文件而不是复制。mv的优先级比cp低。

cpmv ice (还有其它的比如 atclone)只会在插件(或代码片段)被安装的时候运行。要想再次运行它们的话需要先使用 zplugin delete PZT::modules/osx 这类命令来删除插件)

atpull”…”

复制文件相比移动来说是个更佳选择,它便于进行后续更新——因为 repo 中的原始文件并不会被修改,所以 git 不会报告冲突。不过,要想使用 mv 也是可以的,只要你正确使用了 atpull (一个在插件更新(update)的时候被调用的 ice):

1
2
3
zplugin ice as"program" mv"httpstat.sh -> httpstat" \
pick"httpstat" atpull'!git reset --hard'
zplugin light b4b4r07/httpstat

atpull 后面的命令以感叹号开头,意味着它会在 git pullmv 之前被执行。此外 atpull, mv, cp 都只会在获取到新的提交的时候被执行。

总而言之,当用户执行 zplugin update b4b4r07/httpstat 来升级这个插件的时候,如果有新 commit,首先执行的是 git reset --hard——它会恢复原来的 httpstat.sh,然后 git pull被执行并拉取新的 commit(进行快进),然后 mv 再次被执行将命令名称修改为 httpstat 而不是 httpstat.sh。这样 mv 可以用于永久性更新插件的内容而且不会阻碍插件使用 git (或 subversion )更新。

在 zsh 的交互式会话中,为了避免感叹号被展开,请使用 '...' 而不是 "..." 来包裹 atpull ice 的内容

通过 snippet 安装命令

也可以使用 snippet 添加命令。比如:

1
2
3
4
zplugin ice mv"httpstat.sh -> httpstat" \
pick"httpstat" as"program"
zplugin snippet \
https://github.com/b4b4r07/httpstat/blob/master/httpstat.sh

注:Snippet 也支持 atpull,所以可以这样写 atpull'!svn revert' 。还有 atinit,可以在每次加载插件或 snippet 的时候被执行。

通过 snippet 安装补全

completion 为参数调用 as ice,可以让 snippet 命令直接加载一个补全文件,比如:

1
2
zplugin ice as"completion"
zplugin snippet https://github.com/docker/cli/blob/master/contrib/completion/zsh/_docker

补全管理

Zplugin 允许禁用/启动任意插件的任意一条补全。试着安装一个提供了补全的流行插件:

1
2
zplugin ice blockf
zplugin light zsh-users/zsh-completions

第一条命令(blockf ice)将会阻断传统的添加补全的方式。zplugin 会使用它自己的方式(基于符号链接而不是往 $fpath 里加一堆目录)。Zplugin 将会自动安装它下载的插件的补全。想要卸载这些补全并且重新安装的话,可以使用:

1
2
zplugin cuninstall zsh-users/zsh-completions   # 卸载
zplugin creinstall zsh-users/zsh-completions # 安装

列出补全

注: zplg 是一个可以在交互式会话中使用的别名

要以表格形式查看每个插件都提供哪些补全和插件的名字,请使用

1
zplg clist

这个命令特别适用于 zsh-users/zsh-completions 这类提供了大量补全的插件——表格每行将会展示三个补全(这样可以占用的终端页面的大小)就像这样

1
2
3
4
5
...
atach, bitcoin-cli, bower zsh-users/zsh-completions
bundle, caffeinate, cap zsh-users/zsh-completions
cask, cf, chattr zsh-users/zsh-completions
...

你也可以通过给 clist 添加参数来提高每行显示的补全的数目,比如 zplg clist 6 将会显示:

1
2
3
4
5
...
bundle, caffeinate, cap, cask, cf, chattr zsh-users/zsh-completions
cheat, choc, cmake, coffee, column, composer zsh-users/zsh-completions
console, dad, debuild, dget, dhcpcd, diana zsh-users/zsh-completions
...

启用和禁用补全

补全可以被禁用,这样就可以调用 zsh 的原始补全。这个命令非常简单,它只需要补全的名称作为参数

1
2
3
4
$ zplg cdisable cmake
Disabled cmake completion belonging to zsh-users/zsh-completions
$ zplg cenable cmake
Enabled cmake completion belonging to zsh-users/zsh-completions

就这么简单。还有一个命令 zplugin csearch,可以搜索所有的插件目录列出所有可用的补全并且展示它们是否被启用。

#csearch screenshot

这就实现了对补全的完全控制。

子目录的 SVN 支持

通常,为了使用 GitHub 项目的子目录作为 snippet,需要在 URL中添加 /trunk/{path-to-dir},比如

1
2
3
4
5
6
zplugin ice svn
zplugin snippet https://github.com/zsh-users/zsh-completions/trunk/src

# 对于 Oh My Zsh 和 Prezto, 可以直接使用 OMZ:: 和 PZT:: 前缀
# 不需要添加 `/trunk/`, (不过路径需要指向一个目录而不是文件)
zplugin ice svn; zplugin snippet PZT::modules/docker

snippet 也会默认自动安全可用补全,就像 plugin 一样。

Turbo Mode (Zsh >= 5.3)

wait ice 允许你将插件的加载过程延迟到 .zshrc 加载完成并且 prompt 已经显示出来以后。就像 Windows 一样——在启动过程中,即使后台依然在加载数据,它也会显示桌面。尽管这有缺点,不过总比黑屏十分钟要好。
然而,在 Zplugin 中,这个方法没有缺点——窗口不会延迟、冻结等等——在插件被加载的过程中,你的命令行完全处于可用状态,即使插件数量有十多二十个。

Turbo mode 将会加速 zsh 的启动过程 50%~73% 之多。比如原先是 200ms,现在就只需要 50ms!

这个功能需要 Zsh 5.3 及以上版本。为了使用 Turbo mode,可以参照以下方式为你的插件添加 wait ice:

1
2
3
PS1="READY > "
zplugin ice wait'!0'
zplugin load halfo/lambda-mod-zsh-theme

上面的代码让 psprint/zprompts 插件在 zshrc 处理完毕后的 0 秒后启动。它会在基本的命令提示符 READY > 出现后的大概 1ms 后启动。我已经使用这种方式来设置我的命令提示符两年多了,没有丝毫问题。 只提供 wait 一个词也是可以的,它的效果等同于 wait'0'(同样 wait'!'等同于 wait'!0'

感叹号让 Zplugin 在插件加载完毕后重设命令提示符,对于延迟加载主题来说是很有必要的。Prezto 主题也是一样,下面的例子使用了更长的延迟

1
2
zplg ice svn silent wait'!1' atload'prompt smiley'
zplg snippet PZT::modules/prompt

延迟加载 zsh-users/zsh-autosuggestions

1
2
zplugin ice wait lucid atload'_zsh_autosuggest_start'
zplugin light zsh-users/zsh-autosuggestions

解释:Autosuggesstions 使用了 precmd 钩子,它会在处理完 zshrc 之后(刚好在第一个命令提示符出现之前)被调用。然而 Turbo mode 会在 zshrc 加载完成 1s 后再加载它,使得在第一个命令提示符下 precmd 将不会被安装并调用。这就会让 autosuggesstions 在第一个命令提示符下处于不可用状态。但 atload ice 可以修复这个问题,它可以在插件加载完成后调用同样的函数,就像 precmd 做的那样,这样就可以获得一致的体验。

lucid ice 可以隐藏 Turbo mode 下插件加载完成的提示,类似 Loaded zsh-users/zsh-autosuggestions

Turbo Mode 加载复杂的命令提示符

某些高级主题的初始化过程是通过 precmd 钩子完成的(一些需要在每个命令提示符出现之前被调用的函数)。这个钩子被通过 zsh 函数 add-zsh-hook 以将函数名添加到 $precmd_functions 数组中的方式被安装。

为了使命令提示符在用 Turbo mode 半路加载完主题后被完全初始化,需要使用 atload'' ice 调用这个 hook。

首先,检查 $precmd_function 数字来获取钩子函数的名称。举例来说,在 robobenklein/zinc 主题中,将会有两个函数:prompt_zinc_setupprompt_zinc_precmd:

1
2
[email protected] > ~ > print $precmd_functions                       < ✔ < 22:21:33
_zsh_autosuggest_start prompt_zinc_setup prompt_zinc_precmd

然后,把他们添加到 atload'' ice 中

1
2
3
zplugin ice wait'!' lucid nocd \
atload'!prompt_zinc_setup; prompt_zinc_precmd'
zplugin load robobenklein/zinc

atload'!...' 中的感叹号会让 Zplugin 跟踪这个函数以便卸载插件,详见 这儿。这个对于接下来会提到的设置多个命令提示符会有用。

按条件自动加载/卸载

loadunload ice 允许你定义插件什么时候需要被激活或者禁用。举例:

1
2
3
4
5
6
7
8
9
10
# 处于 ~/tmp 时加载

zplugin ice load'![[ $PWD = */tmp* ]]' unload'![[ $PWD != */tmp* ]]' \
atload"!promptinit; prompt sprint3"
zplugin load psprint/zprompts

# 不在 ~/tmp 时加载

zplugin ice load'![[ $PWD != */tmp* ]]' unload'![[ $PWD = */tmp* ]]'
zplugin load russjohnson/angry-fly-zsh

两个命令提示符,每个都在不同的目录下生效,这个技术可以用来定义不同的插件组,比如定义一个 $PLUGINS 和可能的值比如 cpp, web, admin,并且设置 load/unload 条件来激活 cppweb 中不同的插件。

load/unloadwait 的不同之处是它始处于激活状态,而不是只在只在第一次加载时有效。

需要注意的是,要使卸载插件功能正常工作,你需要跟踪插件的加载过程(所以需要使用 zplugin load ... 而不是 zplugin light ...)。跟踪过程有轻微的性能损耗,不能在开启了 Turbo mode 后并不后影响 zsh 的启动时间。

可以参见 WIki 的 multi prompts 一节,它包含了一个使用多个命令提示符的跟现实的例子,和作者自己目前所使用的类似。

Zplugin Module

这也是一个强力功能,不过在 Turbo Mode 的光环下就黯然失色了。

它的功能是自动编译被 source 的脚本,虽然我并没有观察到这个功能起效。。。

但是它还有一个功能——查看每个被 source 过的脚本的执行时间,这个功能十分强大,可以迅速找出拖慢你 zsh 启动的元凶。

安装方式

未安装 Zplugin

没有安装 Zplugin 时也是可以使用这个模块的,对于某些不带编译功能的插件管理器来说有一定帮助。

1
sh -c "$(curl -fsSL https://raw.githubusercontent.com/zdharma/zplugin/master/doc/mod-install.sh)"

脚本执行完会提示你添加两行代码到 ~/.zshrc 顶部

已安装 Zplugin

1
zplugin module build

同样,脚本执行完会提示你添加两行代码到 ~/.zshrc 顶部

用法

使用 zpmod source-study 就可以查看每个脚本的执行时间了,加上 -l 参数还可以显示脚本的完整路径。十分好用!

关于 Oh My Zsh 碎碎念

有不少 zsh 用户都嫌弃 Oh My Zsh(以下简称 OMZ),主要嫌弃它的速度太慢。(还有很蛋疼的一点就是想找点 zsh 语法的教程看看,结果搜索结果清一色的 OMZ 配置教程,只有一篇文章是在讲 zsh 本身的。行吧,我去 RTFM 了。)

这个慢主要体现在三点:

1. 粘贴代码太慢

这个确实没得洗,OMZ 默认启用了一个非常烦人的自动 URL 转义的功能。这个功能会极大拖慢粘贴的速度,而且这个功能在长达数年的时间里都是无法被除了修改源码以外的方式关闭的(oh-my-zsh#5569),直到今年五月份才终于加入了一个用于控制这个功能开关的变量,然而似乎还是默认开启的。这个功能不知道劝退了多少 OMZ 的用户……

总之,如果你是 OMZ 的忠实粉丝,看完本文后依然坚持使用 OMZ,建议立即更新并使用 DISABLE_MAGIC_FUNCTIONS=true 关闭这个功能。

2. 命令提示符响应太慢

这个其实是可以洗的——你别选那么花哨的主题就行了啊。

拖慢命令提示符响应速度的大头是 git 信息的统计。这个功能其实挺不错的,然而会导致在首次进入大 repo 时卡顿,尤其是在 HDD 上,甚至能卡数秒,难以忍受。

那么 OMZ 有没有不显示git 信息的主题呢?

答案是没有……

那么简单的主题你好意思提交到 OMZ 上去吗!这样的主题只能自己写了,比如我用了挺久的 loli 主题 (然而没有任何 loli 要素)。

不过看完这篇文章你肯定会想,既然插件可以异步加载,git 信息可不可以异步加载呢?

答案是可以!一个著名主题 Pure 就是这么做的,兼顾了美观与实用性,即使再大的 repo 也能够秒进。

3. 启动速度太慢

这个和 OMZ 没啥关系,主要是你插件太多了。除了 Zplugin 外,其他插件管理器/框架都存在这个问题。

解决方案就是换 Zplugin(其实还有一个实现了多线程加载的 Zplug,就是文章开头那张图中最慢的那一个……想法很好,但实现很挫)

Zplugin 很强,但是我不想抛弃 OMZ 怎么办

前面已经说了,Zplugin 可以直接加载 OMZ 的插件,所以 OMZ 的好处你都能享受到。

当然,你会发现即使加载了 OMZ 插件,使用体验还是和 OMZ 不一致。
因为 zsh 的很多功能是默认没有开启的,你可以手动开启,或者如果你和我一样懒的话,还可以直接用 snippet 选择性加载 OMZ 的部分功能。举例如下:

1
2
3
4
zplg snippet OMZ::lib/completion.zsh
zplg snippet OMZ::lib/history.zsh
zplg snippet OMZ::lib/key-bindings.zsh
zplg snippet OMZ::lib/theme-and-appearance.zsh

上面的代码加载了 OMZ 对补全、历史、键位绑定、主题等的设置,对于我来说基本上和 OMZ 的使用体验就一致了。如果还想要什么功能的话,可以直接去 OMZ 的 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
source ~/.zplugin/bin/zplugin.zsh

# 快速目录跳转
zplg ice lucid wait='1'
zplg light skywind3000/z.lua

# 语法高亮
zplg ice lucid wait='0' atinit='zpcompinit'
zplg light zdharma/fast-syntax-highlighting

# 自动建议
zplg ice lucid wait="0" atload='_zsh_autosuggest_start'
zplg light zsh-users/zsh-autosuggestions

# 补全
zplg ice lucid wait='0'
zplg light zsh-users/zsh-completions

# 加载 OMZ 框架及部分插件
zplg snippet OMZ::lib/completion.zsh
zplg snippet OMZ::lib/history.zsh
zplg snippet OMZ::lib/key-bindings.zsh
zplg snippet OMZ::lib/theme-and-appearance.zsh
zplg snippet OMZ::plugins/colored-man-pages/colored-man-pages.plugin.zsh
zplg snippet OMZ::plugins/sudo/sudo.plugin.zsh

zplg ice svn
zplg snippet OMZ::plugins/extract

zplg ice lucid wait='1'
zplg snippet OMZ::plugins/git/git.plugin.zsh

# 加载 pure 主题
zplg ice pick"async.zsh" src"pure.zsh"
zplg light sindresorhus/pure

安装完 Zplugin 后,可以将以上代码粘贴到 ~/.zshrc 中,然后建议挂着代理启用 zsh(因为会从 GitHub clone,所以第一次启动会比较慢,根据你的网络状况可能需要几十秒到几十分钟不等

本来还有一段安装一些常用工具的代码,不过一方面这些工具最好的安装方式应该是通过系统包管理工具安装,另一方面加上这段可能会让网络不好的朋友非常痛苦,故单独提出来放在下面。

需要的话可以把这段放到 source 后面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ---- (可选)加载了一堆二进制程序 ----
zplg light zplugin/z-a-bin-gem-node

zplg as="null" wait="1" lucid from="gh-r" for \
mv="exa* -> exa" sbin ogham/exa \
mv="*/rg -> rg" sbin BurntSushi/ripgrep \
mv="fd* -> fd" sbin="fd/fd" @sharkdp/fd \
sbin="fzf" junegunn/fzf-bin

# 加载它们的补全等
zplg ice mv="*.zsh -> _fzf" as="completion"
zplg snippet 'https://github.com/junegunn/fzf/blob/master/shell/completion.zsh'
zplg snippet 'https://github.com/junegunn/fzf/blob/master/shell/key-bindings.zsh'
zplg ice as="completion"
zplg snippet 'https://github.com/robbyrussell/oh-my-zsh/blob/master/plugins/fd/_fd'
zplg ice mv="*.zsh -> _exa" as="completion"
zplg snippet 'https://github.com/ogham/exa/blob/master/contrib/completions.zsh'

# 不需要花里胡哨的 ls,我们有更花里胡哨的 exa
DISABLE_LS_COLORS=true
alias ls=exa
# 配置 fzf 使用 fd
export FZF_DEFAULT_COMMAND='fd --type f'
# ---- 加载完了 ----