从很久之前就开始观察到我使用的终端模拟器(iTerm2)新建 tab 时变得很慢,但一直没有仔细去调整。今天终于忍无可忍,想要搞清楚是怎么回事。

为什么我的 zsh 变慢了

如果你是跟我一样的初级选手,对 shell 脚本并无超出实用范围的兴趣,那你也很可能是直接用 oh-my-zsh 直接搞一套插件下来进行定制的……这种模式很常见也很高效,无可厚非。oh-my-zsh 有一些问题(或者也可能是 zsh 本身有一些问题),例如 .zcompdump 文件突然损坏导致没有办法使用自动补全功能。这导致我最开始以为是其中自带的某个插件导致 zsh 启动缓慢。

~/.zshrc 头部和尾部分别加入以下内容来开启 profile:

1
2
3
4
# 在 ~/.zshrc 的头部加上这个,加载 profile 模块
zmodload zsh/zprof
# 在尾部加上这个,显示 profile 结果
zprof

然后运行 zsh,即可查看各个模块启动时间的占比。

这里我没有截图,所以只能说结论:我的结果与 Little Thing #2 — Speeding up ZSH 作者的结果是类似的,问题出在 nvm 插件。更准确地说,是 nvm 的 default version 功能拖慢了 zsh 的启动速度。

nvm 是什么

nvm 是一个用于管理 node 版本,方便切换环境和进行多版本测试的第三方环境管理器。如果你有使用过 pyenv 或者 conda,很容易理解 nvm 就是他们的 node 专属版。在 nvm 中你可以指定一个安装的 node 版本为默认版本。例如我可以指定 node 12 为默认版本,然而在某一个项目(例如 ~/github/my_node_project)中使用 node 14 版本;这样在这个项目之外的地方,直接运行 node 会启动 v12 的 node 交互终端。

从很久以前有一些用户就注意到 nvm 的这个功能会极大拖慢 shell 的启动速度,例如 #1277#2084#2251。但作者提出造成缓慢的原因在于 npm 运行本身就很慢,而他不接受除“在 npm 中进行修复”以外的其他方案。

弃用 nvm

最终我在 nvm 某个 issue 下一条被标记为 spam 的评论中发现了 nvs 这个管理器,作用与 nvm 类似,但是使用的是 link 模式,也就是通过软链接来设置默认 node 版本。这完全避免了在 shell 启动时去调用 npm。

1
2
3
4
5
6
7
8
# 确认 nvm 的安装路径,一般在 ~/.nvm
echo $NVM_DIR
# 卸载 nvm
rm -rf $NVM_DIR
# 安装 nvs
export NVS_HOME="$HOME/.nvs"
git clone https://github.com/jasongin/nvs "$NVS_HOME"
. "$NVS_HOME/nvs.sh" install

安装完成之后,重新启动一个 zsh,然后使用 nvs add xxxx 来安装某个版本的 node。要将某版本的 node 设置为默认版本,使用 nvs link xxxx。具体原理是:nvs 会创建软链接到 ~/.nvs/default/bin 中,而这个目录已被 nvs 加入 PATH 环境变量。

其实这篇文章到这里就该结束了,因为我的 zsh 启动慢的问题已经被解决。但之前一直有被人安利过 fish shell,于是就借机尝试下这个传说中很快的 shell。

fish shell

macOS 上安装相对简单,brew install fish 一句话搞定,但真正需要注意的是 fish 不兼容 bash 语法(简单撇了一眼主要应该是设置变量和脚本控制流的语法不一样)。这里简单记录下我常用的工具相关的配置。

设置,以及包管理器

fish 自带一个有 web 界面的配置工具 fish_config,直接运行就会打开浏览器页面,配置完成后回到终端按下回车退出。这个工具不是很实用,基本只能挑选主题配色和 prompt,以及查下命令历史。

fish 的配置文件在 ~/.config/fish/config.fish,默认是没有这个文件的,只有下文提到的内容需要这个文件。另外函数的定义在 ~/.config/fish/functions,文件名与函数名一致。

fish 包管理器推荐的是不会拖慢启动速度的 fisher,安装请参考官方文档。

Bash 兼容层

有两个 bash 兼容层,可用于运行 bash 脚本并将环境保留到 fish 中:

  • bass 是借助 Python 实现的 bash 兼容层,可以运行交互式程序
  • bax 是一个纯 fish shell 实现的 bash 兼容层,速度比 bass 快,但是不支持交互式程序

这两个兼容层都可以使用 fisher 安装:fisher add edc/bass jorgebucaran/fish-bax

注意在运行多条命令时,两个程序似乎有所区别。

nvs 支持

nvs 需要使用到 bash 兼容层。将这个函数保存在配置目录的 functions 目录中,文件名与函数名相同,后缀为 .fish。嫌麻烦的话直接 funced nvs 编辑完成按回车退出,然后 funcsave nvs 保存到文件。

1
2
3
function nvs
    bass source ~/.nvs/nvs.sh ';' nvs $argv
end

因为 nvs 包含交互式操作,所以这里使用的是 bass 这个 bash 兼容层。

此外,还需要将 ~/.nvs/default/bin 目录加入 PATH 环境变量,方法在下一小节。

PATH 环境变量

fish 推荐使用 fish_user_paths 来管理自定义 PATH 变量,修改即时生效。

1
2
3
4
5
6
7
8
9
# 例如将 Rust 相关工具所在路径加入 PATH
# -U 的意思是 universal
set -U fish_user_paths $fish_user_paths ~/.cargo/bin
# 将 fzf 工具所在路径加入 PATH
set -U fish_user_paths $fish_user_paths ~/.fzf/bin
# 查看加入的 PATH
echo $fish_user_paths
# 注意这里显示以空格分隔,这是正常的
/Users/xxx/.cargo/bin /Users/xxx/.fzf/bin

conda 支持

conda 支持 fish,但是需要在配置文件 ~/.config/fish/config.fish 中添加一些内容,运行 conda init fish 即可。要注意的是 conda 会覆盖 fish_right_prompt 函数,用于在右侧显示当前的 conda 环境名称。

命令耗时

对于可能运行比较久的命令,显示耗时信息是很有帮助的;但是用户通常不会记得手动加 time。fish 在执行完一个命令后会触发 fish_postexec 事件,并且耗时信息会存储在 CMD_DURATION 变量中,因此可以使用事件函数来展示耗时信息。

1
2
3
4
5
6
7
function show_command_duration --on-event fish_postexec
    if test $CMD_DURATION -gt 0
        set duration (echo "$CMD_DURATION 1000" | awk '{printf "%.3fs", $1 / $2}')
        echo "Cost: $duration"
        set CMD_DURATION 0
    end
end

注意这个函数的定义放在配置文件(~/.config/fish/config.fish)中才可以正常工作。事件函数如果放在 functions 目录下,要在被手动调用后才会自动被事件触发。

GPG agent

macOS GPG Tools 用户需要在配置文件中加入这些内容才能正常使用 GPG agent 处理 ssh 请求。

1
2
3
4
5
6
# Requires jorgebucaran/fish-bax plugin, which can be installed with fisher
set -x GPG_TTY (tty)
if test -f "~/.gpg-agent-info"
    and status is-interactive
    bax "source ~/.gpg-agent-info ; export GPG_AGENT_INFO ; export SSH_AUTH_SOCK"
end