不用 oh-my-zsh:macOS 上一套轻量的 zsh + Starship 配置

为什么不用 oh-my-zsh

oh-my-zsh 不是变差了,它解决的问题依然真实:开箱即有合理的 history 配置、几百个插件的补全、一个能看的 prompt。对刚从 bash 迁过来的人,这省掉几小时摸索。

代价是启动时间和不透明度。OMZ 默认加载一大堆用不到的东西,冷启动通常在 200–500ms。单看不算什么,但每开一个 tmux pane、每跑一次 zsh -c,这个成本都要付一遍。更麻烦的是出问题时难以定位——它的 lib/ 目录里十几个文件都在改 setopt、alias、compinit 行为,你很难知道某个诡异表现是谁造成的。

而真正让人上瘾的那两个功能(自动建议、语法高亮),本来就是独立项目,根本不需要 OMZ。

什么时候 OMZ 仍然合理: 你不想在 shell 配置上花任何时间;或者你需要它某几个插件的复杂补全(kubectlgcloud 之类)而懒得单独找。这不丢人,只是如果你已经会读 zshrc,自己搭一套更划算。


安装

1
brew install zsh-autosuggestions zsh-syntax-highlighting starship fzf zoxide

macOS 自带 zsh 5.9,够用。除非需要 5.10+ 的新特性,不必 brew install zsh。Apple Silicon 上 Homebrew 前缀是 /opt/homebrew,写死 /usr/local 的老教程要注意。


完整的 .zshrc

顺序有讲究,先给出全貌:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# ---- 1. 环境变量、PATH ----
eval "$(/opt/homebrew/bin/brew shellenv)"
export PATH="$HOME/.local/bin:$PATH"

setopt extended_glob

# ---- 2. 补全路径要在 compinit 之前注册 ----
FPATH="$(brew --prefix)/share/zsh/site-functions:$FPATH"

# ---- 3. compinit(带缓存) ----
autoload -Uz compinit
if [[ -n ~/.zcompdump(#qN.mh+24) ]]; then compinit; else compinit -C; fi

# ---- 4. 插件 ----
source $(brew --prefix)/share/zsh-autosuggestions/zsh-autosuggestions.zsh
source $(brew --prefix)/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

# ---- 5. prompt 与工具初始化 ----
eval "$(starship init zsh)"
eval "$(zoxide init zsh)"
source <(fzf --zsh)

三条硬性约束

  1. fpath 的修改必须在 compinit 之前,否则 compinit 扫描时看不到那些补全函数。
  2. zsh-syntax-highlighting 必须最后 source,它要 hook 所有已定义的 widget;放前面会漏掉后续插件注册的东西。
  3. compinit 只调用一次。如果之前装过 OMZ 或别的框架,它们内部会自己调,重复调用会明显拖慢启动。

compinit 那行在干什么

~/.zcompdump(#qN.mh+24) 是 zsh 的 glob qualifier:

  • #q 声明这是一个 qualifier(需要 extended_glob
  • N = nullglob,文件不存在时不报错而是返回空
  • . = 只匹配普通文件
  • mh+24 = 修改时间超过 24 小时

所以逻辑是:缓存超过一天,跑完整的 compinit(重新扫描 + 权限安全检查);否则 compinit -C 跳过检查直接读缓存,省下几十毫秒。

测启动时间

1
for i in {1..10}; do /usr/bin/time zsh -i -c exit; done

想知道具体是谁慢,在 zshrc 顶部加 zmodload zsh/zprof、底部加 zprof,开一个新 shell 就能看到逐项耗时。


五个包分别在做什么

zsh-autosuggestions

打字时根据历史记录在光标后显示灰色建议。输入 git com 会淡淡地补出你上次跑的 git commit -m "fix ingress"End 接受整条,Ctrl+→ 只接受一个词。

这是我认为 zsh 体验里价值最高的单个插件。日常命令重复率极高,长参数的 kubectl -n xxx logs -f deploy/yyy 尤其受益。

1
2
3
ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8'         # 建议文字颜色,太淡看不清就调
ZSH_AUTOSUGGEST_STRATEGY=(history completion)  # 历史没命中时退回补全系统
ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=20             # 超长命令不触发,防卡顿

zsh-syntax-highlighting

实时给命令行上色:命令存在是绿色,不存在是红色,引号没闭合、路径不存在也会标出来。

真正的用处是回车前就发现错误。敲错命令名当场变红,不用等 command not found;粘贴一段带引号的长命令时,颜色立刻告诉你引号有没有配平。

fzf

模糊查找器。source <(fzf --zsh) 之后绑三个快捷键:

快捷键 作用
Ctrl+R 历史搜索,替代默认那个只能逐条回溯的版本
Ctrl+T 当前目录下模糊选文件,结果插入命令行
Alt+C 模糊 cd 到子目录

Ctrl+R 是改变最大的一个。另外它还能当通用管道用:

1
2
kubectl get pods | fzf
git branch | fzf | xargs git checkout

zoxide

学习型 cd。访问过的目录会记录频率和时间,之后 z uts 就直接跳到 ~/uts-git/uts-server,不用打全路径。zi 打开 fzf 界面挑。

用一段时间后它基本能取代大部分 cd。想让它直接接管 cd 这个名字:

1
eval "$(zoxide init zsh --cmd cd)"

Starship

跨 shell 的 prompt,Rust 写的,git 状态异步获取。下面单独展开。


Starship 配置

配置文件在 ~/.config/starship.toml,没有就新建。starship config 可以直接打开它。

显示用户名和主机名

Starship 默认把这两个藏起来了——username 只在 root 或 SSH 会话显示,hostname 只在 SSH 显示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[username]
show_always = true
format = "[$user]($style)"
style_user = "bold blue"
style_root = "bold red"

[hostname]
ssh_only = false
format = "[@$hostname]($style) in "
style = "bold green"

为什么要改 format 两个模块的默认格式各自都以 in 结尾,直接开启会得到 henry in hostname in ~/dir 这种重复。上面把 username 的尾巴去掉、用 @ 接上 hostname,只保留一个 in

SSH 时想更显眼(同时开一堆节点窗口时很有用):

1
2
3
4
[hostname]
ssh_only = false
ssh_symbol = "🌐 "
format = "[@$ssh_symbol$hostname]($style) in "

ssh_symbol 只在 SSH 会话里出现,本地不显示,一眼能分清自己在哪台机器上。

macOS 上 hostname 的坑: 默认可能显示成 Henrys-MacBook-Pro 或带 .local,连不同网络时还会变。固定下来:

1
2
3
sudo scutil --set HostName henry-mbp
sudo scutil --set LocalHostName henry-mbp
sudo scutil --set ComputerName henry-mbp

Starship 也支持 trim_at = "." 只保留第一个点之前的部分。

读懂默认 prompt

一个典型的输出:

1
hzhou@henry-mbp in some-repo on dev

拆开看:

片段 模块 含义
hzhou@henry-mbp username + hostname 上面配的
in some-repo directory 默认 truncate_to_repo = true,进 git 仓库后只显示仓库根往下的路径
on dev git_branch 分支名,前面是 nerd font 的分支图标

更值得注意的是没显示什么。 Starship 的 git 信息分成几个独立模块:

  • git_status — 脏文件、未跟踪、ahead/behind 计数。没出现说明工作区干净且与远端同步;有改动时会变成 [!2 ?1 ⇡3]
  • git_state — 只在 rebase / merge / bisect / cherry-pick 进行中才出现
  • git_commit — 只在 detached HEAD 时显示 commit hash
  • git_metrics — 默认关闭,开了显示 +120 -30 的增删行数

关掉不需要的模块

1
2
3
4
5
6
7
8
[package]
disabled = true

[gradle]
disabled = true

[java]
disabled = true

与其一个个列,不如反过来用 format 做白名单,只声明要的,其余一律不显示:

1
format = """$username$hostname$directory$git_branch$git_status$cmd_duration$line_break$character"""

这样以后进任何 Python / Node / Rust 项目都不会突然冒出新东西。代价是想加模块得手动改这一行。

starship explain 会列出当前 prompt 每个模块和它的来源,starship timings 显示各自耗时——超过 100ms 的基本都值得考虑关掉版本探测。

那个 Java 超时警告

1
[WARN] - (starship::utils): Executing command "/usr/bin/java" timed out.

Starship 渲染 prompt 时要执行 java -version 来拿版本号,默认 command_timeout 是 500ms,JVM 冷启动经常超过。

最省事的修法,在配置文件顶层(所有 [section] 之前)加:

1
command_timeout = 2000

必须放在文件最开头,放到某个 [xxx] 块下面 TOML 会把它当成那个块的字段。

但更好的做法是别让它跑 java -version 每次 cd 进 Java 项目都启一次 JVM 只为拿版本号,代价太大:

1
2
[java]
format = "[$symbol]($style)"   # 只留图标,不查版本

同类问题在 [nodejs][python][ruby] 上也有,只是没 JVM 这么夸张。

command_timeoutcmd_duration 不是一回事

名字像,管的东西完全不同:

  • command_timeout — Starship 自己的内部预算。它执行 java -version 这类探测命令时最多等多久,超了就放弃并打 WARN。这是输入参数。
  • cmd_duration — 纯展示模块。统计上一条命令跑了多久,超过 min_time 就显示出来。这是输出
1
2
3
4
5
command_timeout = 2000      # Starship 探测别人时,最多等 2 秒

[cmd_duration]
min_time = 5000             # 你的命令超过 5 秒,才值得报出来
format = "took [$duration]($style) "

顺带一提:如果已经把所有会执行外部命令的模块都关了,command_timeout 就没有作用了,可以删掉。

cmd_duration 值得留着——跑构建或长任务时能知道花了多久,不用手动掐表。


Starship 还能干什么

Kubernetes 上下文

1
2
3
4
5
6
7
8
[kubernetes]
disabled = false          # 默认关闭,必须显式打开
format = 'on [⛵ $context\($namespace\)](dimmed green) '

[[kubernetes.contexts]]
context_pattern = "prod.*"
style = "bold red"
symbol = "☠️ "

contexts 数组按 context 名字匹配改样式——生产环境标红,能实实在在防住误操作。它读的是 kubeconfig 里的当前 context,不执行任何命令,没有性能开销。

自定义模块

最灵活的部分,任何 shell 能拿到的信息都能塞进 prompt:

1
2
3
4
[custom.docker_host]
command = "echo $DOCKER_HOST"
when = "[ -n \"$DOCKER_HOST\" ]"
format = "[🐳 $output](blue) "

when 是守卫条件,返回非零就整个跳过,不会执行 command。写自定义模块时守卫一定要写好,否则每次渲染都跑一遍外部命令。

其他值得开的模块

模块 用处
[status] 上条命令的退出码,失败时显示 ✘127,比手动 echo $?
[shlvl] 嵌套 shell 层数,进了容器再开 shell 时不容易迷路
[jobs] 后台任务数
[time] 时间戳,回溯"这条命令是几点跑的"很有用
[battery] 低电量才显示,可按百分比设不同颜色

右侧 prompt

zsh 支持内容右对齐,不占左边空间:

1
right_format = "$time$battery"

转换字符

1
2
3
4
5
6
format = "$all$line_break$character"

[character]
success_symbol = "[❯](bold green)"
error_symbol = "[❯](bold red)"
vimcmd_symbol = "[❮](bold yellow)"

error_symbol 变红是个低成本的好设计——不用看输出就知道上条命令失败了。

现成的 preset

1
2
starship preset --list
starship preset nerd-font-symbols -o ~/.config/starship.toml

图标显示成豆腐块

装个 Nerd Font:

1
brew install --cask font-jetbrains-mono-nerd-font

然后在终端配置里指定字体。Ghostty 是 font-family = JetBrainsMono Nerd Font


小结

1
brew install zsh-autosuggestions zsh-syntax-highlighting starship fzf zoxide

三十行 zshrc,一个 starship.toml,启动时间 30–60ms。没有框架,没有黑盒,每一行都知道在干什么。

出问题时的排查顺序:zprof 看谁慢 → starship timings 看哪个模块慢 → starship explain 看 prompt 上的东西是谁画的。

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计