文章

【笔记】Linux 用户环境的 Shell 初始化与目录边界

【笔记】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 + interactivezsh -l、配置为 login 的终端会话环境初始化和交互配置都会运行
non-login + interactive在现有终端执行 zsh只需要交互体验
login + non-interactivezsh -l -c 'command'登录环境,但不显示交互提示符
non-login + non-interactivezsh script.zshzsh -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 启动文件是一组条件钩子

在默认启用 RCSGLOBAL_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/libinclude 都由 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. 排障顺序

配置或文件位置不符合预期时,按下面顺序检查:

  1. 谁启动了当前进程,继承了什么环境?
  2. 当前 Zsh 是 login、interactive,还是两者兼有?
  3. 实际 ZDOTDIR 和全局启动文件目录是什么?
  4. 哪个文件显式 source 了自定义片段?
  5. 应用是否真的支持 XDG,还是仍使用自己的传统目录?
  6. 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 默认路径。

12. 核验入口

本文由作者按照 CC BY 4.0 进行授权