Shell 启动脚本:理解 bash、zsh 的启动流程

如果你是一个经常使用 shell 的用户,几乎肯定会在主目录下有一个 .bash_profile.bashrc 脚本,通常包含各种调整,比如设置环境变量(将某个目录添加到 $PATH)、告诉 shell 做聪明的事情(如 set -o noclobber)以及为命令添加各种别名(如 alias please=sudo)。

(如果你真的很有条理,你会把所有点文件都放在某个仓库中,这样你就可以在所有工作的机器上保持设置同步。)

无论如何,我怀疑很少有人知道 .bash_profile.bashrc 这样的文件实际上什么时候被执行。当我刚开始时,我只是按照别人的建议把东西放在 .bashrc 中,然后当它不工作时,就放到 .bash_profile 中。我可以在这里停下来,只描述 bash 的启动过程(尽管它很愚蠢),但有一个复杂的情况是,我在几年前切换到了 zsh(并且没有回头),但偶尔会在没有安装 zsh 的机器上使用 bash。

Shell 启动脚本的复杂性

为了优雅地处理这种情况,我需要能够在它们自己的文件中指定特定于 bash 或 zsh 的内容,然后在通用启动文件中指定任何符合 POSIX 标准的 shell(如别名和环境变量)都能理解的内容。

我对这个问题的解决方案是定义一些新的点文件文件夹,每个 shell 一个(.bash/.zsh/.sh/),还有一个用于 shell 无关文件(.shell/):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.bash/
env
interactive
login
logout
.sh/
env
interactive
login
.shell/
env
interactive
login
logout
.zsh/
env
interactive
login
logout

不同类型的 Shell

"But!" 你说,"这些不同的文件在这里做什么?" 啊,我很高兴你问了。有两种类型的 shell:

  • [非]交互式 shell(你向它们输入 / shell 脚本)
  • [非]登录 shell(首次登录时运行的 shell / 子 shell)

所有 shell 都会首先运行 env,然后登录 shell 会运行 login,然后交互式 shell 会运行 interactive。完成后,登录 shell 会运行 logout

在哪里放置内容

这完全取决于它什么时候需要运行。

  • 如果它正在设置 / 修改环境变量,它应该放在 login
  • 如果它是别名或终端特定的环境变量(例如,GREP_COLOR),它应该放在 interactive
  • 在我的 .shell/env 文件中,我设置了 umask,还定义了一些有用的函数来修改冒号分隔的路径环境变量(如 $PATH

即使你不采用我方案中的其他任何东西,我也建议你看看我的函数在做什么,这与像 export PATH=$PATH:/path/to/dir 这样的东西不同。

这种特定模式太常见了,如果你考虑 $PATH(或任何你的变量,如 $LD_LIBRARY_PATH)没有设置的情况,它会非常危险。然后,值将是 :/path/to/dir,这通常意味着 /path/to/dir 和当前目录,这通常既是意外行为又是安全问题。

使用我的实现(见 .shell/env_functions),你可以从任何冒号分隔的环境变量中追加、前置和删除目录,当追加或前置时,你保证该目录只会在该变量中出现一次。

Shell 启动流程详解

Bash 启动流程

Bash 的启动流程相对复杂,因为它有多种模式:

  1. 登录 shell:当 bash 以 -l 参数启动或作为登录 shell 启动时
  2. 交互式 shell:当 bash 以交互模式启动时
  3. 非交互式 shell:当 bash 执行脚本时
  4. 远程 shell:当 bash 检测到通过 ssh 或 rsh 启动时

Zsh 启动流程

Zsh 的启动流程更加简洁和一致:

  1. 全局配置文件/etc/zshenv/etc/zprofile/etc/zshrc/etc/zlogout
  2. 用户配置文件~/.zshenv~/.zprofile~/.zshrc~/.zlogout

启动文件执行顺序

1
2
3
4
5
6
7
8
登录 shell:
/etc/zshenv → ~/.zshenv → /etc/zprofile → ~/.zprofile → /etc/zshrc → ~/.zshrc

交互式非登录 shell:
/etc/zshenv → ~/.zshenv → /etc/zshrc → ~/.zshrc

非交互式 shell:
/etc/zshenv → ~/.zshenv

实际应用建议

1. 环境变量管理

避免使用 export PATH=$PATH:/new/path 这种模式,因为它可能导致重复和空路径问题。相反,使用更安全的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 安全的路径添加函数
path_append() {
local var_name="$1"
local new_path="$2"
local current_path="${!var_name}"

if [[ -z "$current_path" ]]; then
export "$var_name"="$new_path"
elif [[ ":$current_path:" != *":$new_path:"* ]]; then
export "$var_name"="$current_path:$new_path"
fi
}

# 使用示例
path_append PATH "/usr/local/bin"

2. 条件加载

根据 shell 类型和交互性条件性地加载配置:

1
2
3
4
5
6
7
8
9
# 只在交互式 shell 中加载
if [[ -o interactive ]]; then
source ~/.shell/interactive
fi

# 只在登录 shell 中加载
if [[ -o login ]]; then
source ~/.shell/login
fi

3. 跨 Shell 兼容性

为了在不同 shell 之间保持兼容性,使用 POSIX 兼容的语法:

1
2
3
4
5
6
7
8
9
# 使用 POSIX 兼容的语法
case "$SHELL" in
*/bash)
source ~/.bash/specific
;;
*/zsh)
source ~/.zsh/specific
;;
esac

总结

理解 shell 启动脚本的执行顺序对于正确配置你的环境至关重要。通过采用模块化的方法,你可以:

  1. 保持配置的整洁:将不同类型的配置分离到不同的文件中
  2. 提高可维护性:每个文件都有明确的职责
  3. 增强可移植性:在不同机器和 shell 之间轻松迁移配置
  4. 避免常见陷阱:如路径重复、环境变量污染等问题

记住,shell 启动脚本的执行顺序可能因操作系统、shell 版本和编译选项而异。最好的方法是测试你自己的系统,并根据需要调整配置。


参考来源: Shell startup scripts - flowblok's blog