zsh 的 PATH 该写哪个文件:.zshenv / .zprofile / .zshrc 加载时机,及 ssh/pdsh 报 command not found 的根因
一次
pdsh批量巡检里 6 台机器 5 台报command not found: fastfetch的排查沉淀。软件早就装好了,问题出在 PATH 写错了文件。
TLDR
.zprofile 和 .zshrc 不能合并——它们由「是否登录」×「是否交互」两个开关组合触发,不是同一件事写两遍:
| 场景 | .zshenv |
.zprofile |
.zshrc |
.zlogin |
|---|---|---|---|---|
| 登录 + 交互(Terminal/iTerm 新窗口) | ✅ | ✅ | ✅ | ✅ |
登录 + 非交互(zsh -lc) |
✅ | ✅ | ❌ | ✅ |
非登录 + 交互(zsh -i、tmux 默认) |
✅ | ❌ | ✅ | ❌ |
非登录 + 非交互(ssh host cmd、pdsh、脚本) |
✅ | ❌ | ❌ | ❌ |
只有 .zshenv 被所有场景读取——所以 PATH 写这里:
# ~/.zshenv 顶部
typeset -U path PATH # 让 path/PATH 自动去重,重复 prepend 不累积
export PATH="/opt/local/bin:/opt/local/sbin:$PATH" # MacPorts
export PATH="$HOME/.local/bin:$PATH"
先分清「没装」还是「没在 PATH」——这是最容易误判的一步:
ls -l /opt/local/bin/fastfetch # 有输出 = 装了;没输出 = 真没装
which fastfetch # 有输出 = 在 PATH;没输出 = PATH 问题
问题
一条批量巡检命令,6 台机器只有 1 台成功:
pdsh -w host1,host2,host3,host4,host5,host6 'source .zshrc; fastfetch'
# host1..host5: zsh:1: command not found: fastfetch
# host6: 正常输出
诡异之处:失败那 5 台都装了 fastfetch(/opt/local/bin/fastfetch,校验和一致),成功那台反而是唯一没迁到 MacPorts的。
为什么「装了却找不到」
MacPorts 安装器把 PATH 写进了 ~/.zprofile,注释还留着它自己的签名:
# MacPorts Installer addition on 2026-09-04_at_19:01:13
export PATH="/opt/local/bin:/opt/local/sbin:$PATH"
而 pdsh/ssh host cmd 走的是非登录 + 非交互 shell,.zprofile 和 .zshrc 都不读,只读 .zshenv——那里面没有 /opt/local/bin,于是找不到。
成功那台之所以正常,恰恰因为它是唯一没迁的:fastfetch 在 Homebrew 的 /opt/homebrew/bin,而它的 .zshrc 里有 eval "$(/opt/homebrew/bin/brew shellenv)",source .zshrc 正好生效。
关键区分:路径存在 ≠ 在 PATH 里。 迁移类操作只搬了二进制,没搬 PATH,就会出现「装了却 command not found」。
为什么三个方向都不能合并
| 合并方向 | 后果 |
|---|---|
并进 .zshrc |
非交互场景(pdsh、脚本、zsh -lc)全废——正是本文这个 bug。且 .zshrc 里有 oh-my-zsh、compinit、PROMPT,在脚本和 $(...) 里执行会拖慢启动、污染 stdout、让别名泄漏进脚本 |
并进 .zprofile |
非登录交互 shell(tmux、zsh -i)丢掉 alias / 补全 / 提示符 |
并进 .zshenv |
所有场景都读,于是 oh-my-zsh 和补全在每个子 shell、每次命令替换里跑一遍,最糟 |
结论:职责不能合并,但重复内容可以抽走——公共环境变量抽到 .zshenv,.zprofile 留登录专属,.zshrc 只留交互专属。
「交互」不等于「登录」——最隐蔽的一坑
日常用 Terminal / iTerm 开的是登录 shell,所以 .zprofile 生效,问题长期不暴露。但这两类场景会静默失效:
# tmux 默认起非登录 shell、zsh -i 同理 —— 都不读 .zprofile
zsh -ic 'which fastfetch'
# 修复前: zsh:1: command not found: fastfetch
# 修复后: /opt/local/bin/fastfetch
同一个 bug 还会让交互式别名一起失效(如 alias ff='fastfetch -c examples/25'),表现为「明明在终端里好好的,tmux 里就是不行」。
重复 prepend 会累积——用 typeset -U 根治
PATH 重复不会报错,只会静默膨胀。实测某台机器 $HOME/.local/bin 出现 2 次、/opt/homebrew/bin 3 次,来源是 .zshrc 里多轮安装器各自追加:
# 同一路径,不同写法,各写一遍
export PATH="$HOME/.local/bin:$PATH"
export PATH="/Users/chedong/.local/bin:$PATH"
zsh 的 path 数组与 PATH 联动,typeset -U 让赋值自动去重。放在 .zshenv 最顶部,之后所有 export PATH=... 都受保护:
typeset -U path PATH
(同一手法对 fpath 也适用:typeset -U fpath,用于抑制补全函数目录重复扫描。)
验证:四种场景必须一致
改完 PATH 后,用这段脚本一次性确认四种场景解析到同一个二进制:
pdsh -w host1,host2,host3 'h=$(hostname)
a=$(zsh -c "which fastfetch" 2>&1 | tail -1) # 非交互
b=$(zsh -lc "which fastfetch" 2>&1 | tail -1) # 登录非交互
c=$(zsh -ic "which fastfetch" 2>&1 | tail -1) # 交互非登录
d=$(zsh -lic "which fastfetch" 2>&1 | tail -1) # 登录交互
if [ "$a" = "$b" ] && [ "$b" = "$c" ] && [ "$c" = "$d" ]; then
printf " %-14s 一致 OK %s\n" "$h" "$a"
else
printf " %-14s 不一致! %s | %s | %s | %s\n" "$h" "$a" "$b" "$c" "$d"
fi'
顺带查 PATH 是否有重复项(应无输出):
echo $PATH | tr ':' '\n' | grep -v '^$' | sort | uniq -d
已知坑速查
| 症状 | 原因 | 处置 |
|---|---|---|
ssh host cmd / pdsh 报 command not found,但登录终端正常 |
PATH 写在 .zprofile(登录专属) |
挪到 .zshenv |
tmux / zsh -i 里命令或 alias 失效 |
同上,非登录交互 shell 不读 .zprofile |
同上 |
确认「装了」但 which 找不到 |
二进制在,PATH 没搬 | ls -l /opt/.../bin/<cmd> 与 which 对比 |
| PATH 里同一目录出现多次 | 多轮安装器重复 prepend | typeset -U path PATH |
| MacPorts 装完新终端才有 | 安装器只写 .zprofile |
见上 |
命令速查
# 判断没装还是没在 PATH
ls -l /opt/local/bin/<cmd> # 装了?
which <cmd> # 在 PATH?
echo $PATH | tr ':' '\n' | grep -c local # PATH 里有几份
# 四种场景一致性
zsh -c 'which <cmd>' # 非交互(ssh/pdsh/脚本)
zsh -lc 'which <cmd>' # 登录非交互
zsh -ic 'which <cmd>' # 交互非登录(tmux)
zsh -lic 'which <cmd>' # 登录交互(Terminal 新窗口)
# 让 PATH 对所有场景生效
# ~/.zshenv 顶部:
# typeset -U path PATH
# export PATH="/opt/local/bin:/opt/local/sbin:$PATH"
# 语法自检(改完必跑)
zsh -n ~/.zshenv && zsh -n ~/.zshrc
参考资料
- 前置背景:Homebrew 停止 Intel 支持:迁移到 MacPorts 的完整笔记——本文是其中「PATH 优先级」一节的修正:PATH 只写
~/.zprofile覆盖不到非交互场景 - zsh 启动文件加载顺序:
man zsh,见STARTUP/SHUTDOWN FILES一节