【笔记】Linux 用户环境的 Shell 初始化与目录边界
用户环境里有两套经常混在一起的机制:Shell 启动文件决定变量和交互行为何时进入进程;XDG Base Directory 约定应用把配置、数据和运行状态放在哪里。
一句话心智模型是:先沿进程父子关系判断配置何时被加载,再沿数据生命周期判断文件应该存放在哪里。
1. 环境不是从某个配置文件凭空出现的
进程通常继承父进程的环境变量。Shell 启动文件只是在 Shell 进程创建时,根据当前启动模式继续修改这份环境。
flowchart LR
A[登录管理器、终端或 SSH] --> B[启动 Zsh]
B --> C[按 login / interactive 状态读取启动文件]
C --> D[export 修改当前 Shell 环境]
D --> E[子进程继承环境]
这解释了两个常见现象:
- 变量写进
.zprofile后,登录 Shell 的后代通常都能继承; - 修改配置文件不会反向改变已经运行的父进程,只能重新启动相应 Shell,或在当前 Shell 中显式
source。
2. Login 与 Interactive 是两个独立维度
Login shell 表示它承担一次登录会话的 Shell 初始化或收尾;interactive shell 表示它要与用户交互、读取命令并显示提示符。两者不是同义词。
| 状态 | 常见启动方式 | 主要关注点 |
|---|---|---|
| login + interactive | zsh -l、配置为 login 的终端会话 | 环境初始化和交互配置都会运行 |
| non-login + interactive | 在现有终端执行 zsh | 只需要交互体验 |
| login + non-interactive | zsh -l -c 'command' | 登录环境,但不显示交互提示符 |
| non-login + non-interactive | zsh script.zsh、zsh -c 'command' | 自动化脚本 |
终端模拟器、SSH 服务和发行版如何启动 Shell 是外部行为,不能仅凭“打开了终端”或“通过 SSH”推断状态。可直接观察:
1
2
[[ -o login ]] && print login || print non-login
[[ -o interactive ]] && print interactive || print non-interactive
$- 会列出当前启用的单字符 Shell options,但阅读它不如用 [[ -o ... ]] 直接表达意图。$0 以 - 开头是传统 login shell 标记之一,显式 -l 同样可以建立 login shell。
3. Zsh 启动文件是一组条件钩子
在默认启用 RCS 和 GLOBAL_RCS 时,Zsh 的启动顺序可以概括为:
1
2
3
4
所有 Zsh: global zshenv → $ZDOTDIR/.zshenv
login Zsh: global zprofile → $ZDOTDIR/.zprofile
interactive Zsh:global zshrc → $ZDOTDIR/.zshrc
login Zsh: global zlogin → $ZDOTDIR/.zlogin
login shell 退出时顺序反过来:
1
$ZDOTDIR/.zlogout → global zlogout
$ZDOTDIR 未设置时默认为 $HOME。全局启动文件的实际目录是构建 Zsh 时配置的,Ubuntu/Debian 常见 /etc/zsh/,不能把 /etc/zshenv 当成所有系统的固定路径。
3.1 .zshenv:所有 Zsh 的共同入口
.zshenv 连非交互脚本也会读取,因此应保持无输出、低开销,并避免改变脚本依赖的交互选项。适合放:
ZDOTDIR等必须在后续启动文件之前生效的 Zsh 自身变量;- 确实要求每一个 Zsh 进程都具备的少量环境变量。
“变量很重要”不等于必须放 .zshenv。若程序不是由 Zsh 启动,它仍然看不到这里的变量。
3.2 .zprofile:login 初始化
.zprofile 适合一次 login shell 启动所需的环境准备。这里的“一次”指每个 login Zsh 执行一次;嵌套执行 zsh -l 仍会再次读取,并不是整个桌面或 SSH 会话的全局单例。
适合放需要让后代进程继承、但无需每次交互式子 Shell 重做的初始化。耗时命令仍应谨慎,因为 login shell 不只由图形终端产生。
3.3 .zshrc:交互行为
.zshrc 适合仅在用户操作命令行时需要的内容:
- prompt、补全和键绑定;
- alias、交互函数;
- 语法高亮、自动建议等插件。
在现有终端里执行 zsh 会再次读取 .zshrc,因此配置最好可重复执行,不要无限追加 PATH 或重复注册 hook。
3.4 .zlogin 与 .zlogout
.zlogin 在交互启动文件之后读取,但它只取决于 login 状态,不保证一定存在终端。避免在没有检查终端的情况下输出欢迎信息。
.zlogout 在 login Zsh 正常退出时读取。进程被 SIGKILL、机器掉电或程序强制终止时,不能依赖它完成关键数据提交。它适合尽力而为的收尾,不适合作为唯一清理机制。
3.5 .env 与 .aliases 不是 Zsh 协议
Zsh 不会自动读取 ~/.env 或 ~/.aliases。它们只有被某个官方启动文件显式 source 时才生效:
1
[[ -r "$HOME/.aliases" ]] && source "$HOME/.aliases"
.env 还常被应用框架解释为 dotenv 格式;dotenv 并不保证支持完整 Zsh 语法。把同一个文件同时当 Shell 脚本和 dotenv 文件使用,会形成隐蔽的语法与密钥泄漏风险。
4. .profile 属于兼容生态,不是 Zsh 启动文件
.profile 是 Bourne/POSIX Shell 传统入口。原生模式的 Zsh 不会因为自己是 login shell 就自动读取 ~/.profile;是否读取取决于系统启动链、兼容模式或用户自己的 source。
因此,从 Bash 切换到 Zsh 时不应直接删除所有 Bash/Profile 文件。先检查:
1
2
getent passwd "$USER"
rg -n 'source|\.|PATH|export' ~/.profile ~/.bash_profile ~/.bashrc ~/.zprofile ~/.zshrc 2>/dev/null
确认变量已迁移、没有其他程序依赖后,再决定是否保留。历史记录可删不代表初始化文件可无条件删;用户配置模板通常能从 /etc/skel 恢复,但个人内容未必可恢复。
5. XDG 解决的是文件位置,不是 Shell 加载顺序
XDG Base Directory Specification 为应用提供一组路径变量。变量未设置或不是绝对路径时,应用应使用规范默认值,而不是把相对路径偷偷拼到当前目录。
| 变量 | 默认值 | 生命周期与用途 |
|---|---|---|
XDG_CONFIG_HOME | $HOME/.config | 用户配置 |
XDG_DATA_HOME | $HOME/.local/share | 用户专属数据文件 |
XDG_STATE_HOME | $HOME/.local/state | 应跨重启保留、但通常不可移植的状态 |
XDG_CACHE_HOME | $HOME/.cache | 丢失后不影响数据正确性的非必要缓存 |
XDG_RUNTIME_DIR | 无静态默认值 | 当前登录期的 socket、FIFO 等运行时对象 |
XDG_CONFIG_DIRS | /etc/xdg | 按优先级搜索系统配置的目录列表 |
XDG_DATA_DIRS | /usr/local/share:/usr/share | 按优先级搜索系统数据的目录列表 |
XDG_RUNTIME_DIR 应由登录系统创建,属于当前用户、权限为 0700,并绑定登录生命周期。应用不应随意用 /tmp 或自造固定路径代替其安全语义。
6. Config、Data、State 与 Cache 的真正边界
用应用 foo 举例:
1
2
3
4
5
~/.config/foo/config.toml 用户选择的行为
~/.local/share/foo/library.db 应用持有的用户数据
~/.local/state/foo/history 跨启动保留的历史和状态
~/.cache/foo/index 可重新生成的索引
/run/user/1000/foo.sock 当前登录期 IPC
判断时不要使用“文件大小”或“是否文本”这样的表面特征:
- Config:改变它会改变应用应该怎样工作;
- Data:它是应用要保存和读取的用户数据;
- State:它记录应用过去怎样运行,跨重启有价值,但不要求在另一台机器上可移植;规范明确举例包括日志、历史、最近使用文件和当前视图;
- Cache:删除后应用仍能正确工作,只是需要重新计算或下载;
- Runtime:只服务当前登录期,系统重启或完整登出后不应继续依赖。
是否备份是用户策略,不是规范直接替你决定的。Data 通常优先级最高;Config 和部分 State 是否备份取决于恢复目标。
7. share 与“共享给别人”无关
XDG_DATA_HOME 默认是 ~/.local/share。这里的 share 继承 Unix 安装布局中 architecture-independent data 的含义:数据不依赖 CPU 架构,可以被同一安装环境中的程序使用;它不表示自动共享给其他账户。
系统级的 XDG_DATA_DIRS 是有序搜索路径。应用通常先看用户目录,再看系统目录,从而允许用户资源覆盖全局默认资源。具体子目录如 applications/、icons/、mime/ 由其他 freedesktop 规范定义,不是 Base Directory Specification 自己穷举的固定目录表。
8. ~/.local/bin 不是 XDG Base Directory 变量
~/.local/bin 是常见的用户级可执行文件目录,但它不由 XDG_* 变量定义,也不属于 XDG_CACHE_HOME。程序本体不能放进随时可重建的 cache。
同样,不能看到 ~/.local/share 就推导 ~/.local/lib、include 都由 XDG Base Directory Specification 定义。它们来自更广泛的 Unix/FHS/发行版和工具链约定。
9. Shell 配置与 XDG 怎样连接
如果希望把 Zsh 配置移到 XDG 风格目录,可以在最早读取的用户文件中设置:
1
export ZDOTDIR="${XDG_CONFIG_HOME:-$HOME/.config}/zsh"
但存在启动悖论:Zsh 必须先找到默认位置的 .zshenv,才能知道新的 ZDOTDIR。因此通常仍保留一个很小的 ~/.zshenv 作为跳板。
对于自己开发的应用,应由程序读取 XDG 变量并实现默认值,不应要求用户先在 .zshrc 中导出默认路径。图形应用和系统服务未必由交互式 Shell 启动。
10. 排障顺序
配置或文件位置不符合预期时,按下面顺序检查:
- 谁启动了当前进程,继承了什么环境?
- 当前 Zsh 是 login、interactive,还是两者兼有?
- 实际
ZDOTDIR和全局启动文件目录是什么? - 哪个文件显式
source了自定义片段? - 应用是否真的支持 XDG,还是仍使用自己的传统目录?
- XDG 变量是否是绝对路径?列表变量的覆盖顺序是否正确?
可用的观测命令:
1
2
3
4
print -r -- "ZDOTDIR=${ZDOTDIR:-$HOME}"
env | sort | rg '^(XDG_|PATH=|SHELL=)'
zsh -xlic exit
zsh -xfic exit
-x 会暴露启动过程中执行的命令,输出可能包含路径和环境值,不应在含密钥的环境里直接公开日志。
11. 复习索引
- Login 决定
.zprofile/.zlogin/.zlogout;interactive 决定.zshrc。 .zshenv影响每一个 Zsh,越早加载,越要精简。- 全局 Zsh 文件路径由构建配置决定,Ubuntu 常见
/etc/zsh/。 .env/.aliases是自定义片段,不会被 Zsh 自动读取。- XDG 目录按数据生命周期分,不按扩展名或大小分。
- State 跨重启但通常不可移植;Cache 可以重建;Runtime 绑定登录期。
~/.local/bin常见但不是 XDG Base Directory 变量。- Shell 只影响自己的后代,应用应自行实现 XDG 默认路径。