换一台机器,git clone 完 dotfiles、source 一遍,然后发现提示符没有 git 分支、补全没生效、某个 alias 指向不存在的路径。改完这些,旧机器已漂移成另一套配置,两台开始互相"借鉴"对方的 bug。
重复几次之后,一半人彻底放弃定制,另一半把 dotfiles 养成几千行单体,每次开终端多等一秒。成因相同——没区分"哪一层在疼",也没定义"哪些配置值得带走"。这篇只给判据:四层各管什么、配置怎么流动、启动与信息密度各该付多少、何时停手。
1. 四层模型:先定位是哪一层在疼
| 层 | 职责 | 可替换项 | 换掉的影响半径 |
|---|---|---|---|
| 终端模拟器 | 渲染、键位、分屏、会话复用 | alacritty / kitty / wezterm | 配色与键位,小时级 |
| Shell | 解析、作业控制、启动流程 | bash / zsh / fish / nushell | rc 重写、脚本兼容,天级 |
| 提示符 | 状态呈现(目录、分支、退出码) | starship / powerlevel10k | 延迟与观感,小时级 |
| 工具链 | 实际执行的命令 | ripgrep / fzf / bat / lazygit | 语义与肌肉记忆,逐个替换 |
最容易糊掉的是 shell 与提示符:提示符框架挂在 shell 启动链上,"换提示符"往往实为改 shell 的启动时间。判据朴素——改完 zsh -i -c exit 的耗时变了,就是动了 shell 层。迁移成本约等于你会换掉的那几层的数量。
2. 可迁移性:结构、条件分支与机器裁剪
第一性问题不是放什么,而是哪些内容允许因机器而异;这条定了,结构自然长出来。
~/dotfiles/shell/{common,linux,darwin}.zsh # 入口是 common
~/dotfiles/tools/{fzf,git}.zsh # 一工具一文件
~/dotfiles/hosts/laptop.zsh # 只放这台机器独有
装载顺序固定为 common → OS 分支 → tools → hosts。顺序固定比内容精确更重要——配置被 source 的时机不同,是"这台好使那台不好使"的第一大来源。
# shell/common.zsh 末尾:唯一装载入口
source "$DOTFILES/shell/$(uname -s | tr 'A-Z' 'a-z').zsh"
for f in "$DOTFILES"/tools/*.zsh; do source "$f"; done
[[ -f "$DOTFILES/hosts/$(hostname -s).zsh" ]] && source "$DOTFILES/hosts/$(hostname -s).zsh"
command -v rg >/dev/null && alias g='rg --hidden --glob "!.git"' # 按能力探测
| 可迁移等级 | 内容 | 承载方式 | 失效表现 |
|---|---|---|---|
| 完全可迁移 | 别名、函数、工具配置 | 仓库 + 符号链接 | 链接失效,按旧行为跑 |
| 条件可迁移 | OS / 发行版差异 | if 分支或按文件拆 |
判断写错 → 静默不加载 |
| 半可迁移 | 工具版本差异 | 能力探测(command -v) |
参数不识别而报错 |
| 不可迁移 | 密钥、host 特定路径 | keychain / pass | 换机后命令缺参数 |
静默失败最贵:条件不满足时不报错,你只会觉得"某功能莫名没了"。对策是在 common.zsh 顶部 echo 一次环境判定结果。
3. 启动时间:构成与延迟加载
启动每次开窗口都付一次,绝对值不重要,频率才重要:一天开二十次,单次多 200ms 不至于动手,加上第四个插件才变成一秒。
| 耗时来源 | 典型量级 | 策略 |
|---|---|---|
| 插件同步装载 | 每插件数十至数百毫秒 | 延迟到首次调用 |
compinit 重建缓存 |
依赖 fpath 条目数,几十毫秒起 |
缓存 dump,按天刷新 |
| 版本管理器初始化 | 最贵的一类,可达数百毫秒 | 函数壳懒加载 |
# 函数壳替代同步初始化:首次调用才真正加载
nvm() { unset -f nvm; source "$HOME/.nvm/nvm.sh"; nvm "$@"; }
for i in 1 2 3; do /usr/bin/time -f '%e s' zsh -i -c exit; done # 基线
延迟加载把成本从"每次开终端"搬到"第一次用该工具",并可能让补全在首次调用时缺失。它是纯收益,当且仅当该工具不是高频命令:十次有八次第一条是 git,延迟它只是把卡顿挪了个位置。
膨胀的根因是每次遇到问题就往同一个文件加一行、从不删。两个刻度:两百行出现按机器分支 → 拆文件(按工具拆,不按机器拆);八百行以上或启动已被感知 → 冻结新增,只做删除与延迟加载。
4. 提示符:信息密度的取舍
提示符每敲一次回车付一次成本:10ms 的取数在一天上百次提交里可感知;信息太少又得用命令补,一次 git status 在中等仓库是百毫秒到秒级。
| 信息项 | 取数成本 | 何时值得上提示符 |
|---|---|---|
| 当前目录 | 近零 | 总是 |
| git 分支名 | 一次文件读 | 只在仓库内、异步更新 |
| git 状态(dirty/ahead) | 最贵,巨仓可达数百毫秒 | 大仓关闭或限扫描深度 |
| 上条退出码 / 耗时 | 近零 | 长命令与批处理价值高 |
| k8s 上下文 / venv | 近零 | 多集群切换时防误操作 |
# starship.toml:关掉最贵的一项,并给整体设上限
command_timeout = 300 # 单模块取数超时即放弃
[git_status]
disabled = true # 按仓库规模决定是否打开
判据:取数成本 ÷ 每天看它的次数,与"敲一条命令拿同样信息"比,前者小才上提示符。
5. 别名与函数的边界
| 形态 | 可组合 | 可调试 | 适用 |
|---|---|---|---|
| alias | 否,无法按位置追加语义 | 好(alias g 即见展开) |
固定参数的短命令 |
| shell 函数 | 是 | 一般(难单测、无类型) | 带少量判断的封装 |
| 独立脚本入 PATH | 是 | 最好(可单跑、可测试) | 超二十行、需复用 |
| 插件命令 | 是 | 差(黑盒) | 生态成熟、替代成本高 |
# alias 不做管道:想"列目录并过滤"必须用函数
alias ll='ls -alh --color=auto --group-directories-first'
lg() { ll "$@" | rg --color=always --smart-case . ; }
# 自检:functions lg 看是否被覆盖;whence -v lg 区分 alias/函数
分界线:只改默认参数用 alias,需传参或管道用函数,需被别的脚本调用就抽成独立文件。函数最大的隐患是只在交互式 shell 存在——同一逻辑在 cron 里跑不到,两份实现迟早不一致。
6. 补全与历史:两个按键路径上的成本
补全的延迟落在按键路径上,比启动更易被感知:等 300ms,你会觉得终端"黏"。
| 类型 | 来源 | 运行时成本 | 失效形态 |
|---|---|---|---|
| 静态补全 | 工具自带或手写 _xxx |
近零 | 参数变化后不同步 |
| 动态补全 | 每次触发调用工具自身 | 每次 fork,百毫秒级 | 版本差异导致补错子命令 |
| 模糊查找 | fzf 接管菜单 | 首次构建候选有延迟 | 候选过多反而慢 |
# 补全缓存:每天最多重建一次
autoload -Uz compinit
if [[ -n ${ZDOTDIR:-$HOME}/.zcompdump(#qN.mh+24) ]]; then
compinit -C # 用现有 dump,跳过安全目录检查(按 zsh 5.9 核对)
else compinit && compinit -C; fi
source <(fzf --zsh) # Ctrl-R 交给 fzf,保留时间序
setopt hist_ignore_dups hist_ignore_space share_history
判据:命令是每天几十次还是几次。前者值得在仓库放静态片段,后者用官方补全脚本即可。历史是唯一随使用增值的资产,也最易在换机时丢,检索方式按"记不住命令时先去哪里找"选。
7. 多机分叉、凭据与可复现
| 分叉来源 | 收敛手段 |
|---|---|
| host 特定路径(挂载点、代理端口) | 全进 hosts/<name>.zsh,仓库只放模板 |
| 工具版本差异 | 能力探测,不写死版本号 |
| 团队共享片段 | 独立目录,单一 source 顺序 |
| 一次性试验 | 分支或未跟踪文件,一周内决断:收编或删除 |
| 密钥与 token | 不进仓库,运行时从钥匙串读 |
# 凭据从钥匙串读;缺失时静默跳过,别让启动报错
command -v secret-tool >/dev/null 2>&1 && \
export API_TOKEN=$(secret-tool lookup service blog token api 2>/dev/null)
# macOS: security find-generic-password -s blog -a api -w ;通用: pass show blog/api
可复现是唯一能验证可迁移的指标:裸系统到能干活的耗时。十分钟内 = 一条 install.sh 加包管理器;半天 = 手工装多个工具;一天以上 = 插件也手工配,说明配置并未真正可复现。
set -euo pipefail # 幂等:已存在就跳过,冲突就停下
command -v rg >/dev/null || "$PKG" install ripgrep
for f in shell/common.zsh tools/*.zsh; do
t="$HOME/.config/dotfiles/$f"; mkdir -p "$(dirname "$t")"
[[ -e $t && ! -L $t ]] && mv "$t" "$t.bak.$(date +%s)" # 先备份再链
ln -sfn "$PWD/$f" "$t"
done
zsh -i -c 'echo shell ok' # 断言:配置在新环境里成立
脚本的价值不在装软件,而在把隐性知识变成可执行断言:最后那行失败即说明配置在新环境跑不起来。团队共享同理——别人用了会更快或更少出错吗? 项目别名与格式化钩子归团队,提示符外观与历史归个人。
8. 排错:四种"配置在别的机器上不成立"
| 现象 | 原因 | 处理 |
|---|---|---|
| 新机器 source 后命令不存在 | 条件分支静默跳过 | 顶部 echo 环境判定 |
| 每次启动越来越慢 | 插件同步加载 / compinit 全量重建 |
zprof 定位,改延迟加载或缓存 dump |
| 提示符在巨仓里卡顿 | 每次渲染都取 git 全树状态 | 关掉该项或设 command_timeout |
多机 Ctrl-R 找不到昨天的命令 |
HISTFILE 不同或写入策略冲突 |
统一路径,检查 inc_append_history |
9. 边界:什么情况本就不该折腾框架
| 场景 | 不必做 | 理由 |
|---|---|---|
| 日常只用几个固定命令 | 插件框架与主题生态 | 维护成本高于省下的按键 |
| 机器一年换不到一次 | 精细的延迟加载 | 一次性成本摊不开 |
| 两百行以内且从未出问题 | 重构、模块化、写文档 | 复杂度会自己长回来 |
| 团队无人维护共享仓库 | 强推统一 dotfiles | 会变成没人敢改的隐性依赖 |
收束:价值来自「重复次数 × 迁移次数」,不来自行数。 换机频率低、命令集小时,花在脚本化上比花在主题上回报高。
10. 自测
- 你怎么判断一次改动动的是 shell 层还是提示符层?
- 为什么"按能力探测"比"按机器名分支"更耐迁移?
- 配置出现"改一处坏两处"时,第一个该做的动作是什么?
- 延迟加载在什么条件下是纯收益,什么条件下反而更卡?
- 什么信号说明某条信息该放进提示符,而不是留在命令里?
- 新机器到可用的耗时,为什么比 dotfiles 行数更能说明可迁移性?