3. Shell环境初始化:交互式与非交互式Shell,登录Shell与非登录Shell
Shell脚本启动时,环境变量是怎么加载的?.bashrc、.bash_profile、.profile 到底谁先执行?
说实话,这个问题我当年也踩过坑。有一次写了个自动化部署脚本,本地跑得好好的,放到服务器上就报 command not found。查了半天,发现是 Shell 类型搞错了,环境变量根本没加载。
今天咱们就把这个事彻底理清楚。
3.1 两个维度,四种组合
Shell 的初始化行为,由两个维度决定:
- 交互式 vs 非交互式:有没有终端跟你对话
- 登录 vs 非登录:是不是通过登录进入的
两两组合,一共四种情况。每种情况加载的配置文件都不一样。
| 类型 | 典型场景 | 加载的配置文件 |
|---|---|---|
| 交互式 + 登录 | SSH 登录、终端登录(tty1) | /etc/profile → ~/.bash_profile / ~/.bash_login / ~/.profile |
| 交互式 + 非登录 | 在桌面打开终端、su 切换用户 |
~/.bashrc |
| 非交互式 + 登录 | 通过 ssh command 执行单条命令 |
同交互式登录,但 BASH_ENV 可能生效 |
| 非交互式 + 非登录 | Shell 脚本执行、bash -c |
继承父进程环境,不加载任何配置文件 |
核心结论:.bashrc 只在交互式非登录 Shell 中加载。.bash_profile 只在登录 Shell 中加载。两者不是互斥,而是分工不同。
3.2 登录 Shell 的加载顺序
登录 Shell 的加载顺序,我画了张图,你一看就明白:
具体来说,登录 Shell 的加载顺序是这样的:
- 先加载
/etc/profile:这是全局配置文件,对所有用户生效。里面通常设置了PATH、umask等基础环境。 - 然后按优先级查找用户级配置文件:
- 先找
~/.bash_profile,找到就加载,后面的跳过 - 没找到,再找
~/.bash_login - 还没找到,最后找
~/.profile
- 先找
我的习惯:我一般只在 .bash_profile 里写一行 source ~/.bashrc,然后所有配置都放 .bashrc 里。这样不管是登录 Shell 还是非登录 Shell,都能加载到同一套配置。
3.3 非登录 Shell 的加载顺序
非登录 Shell 就简单多了。它只加载 ~/.bashrc,没有别的花头。
但这里有个坑:.bashrc 里通常也会调用 /etc/bashrc 或 /etc/bash.bashrc(不同发行版文件名不一样)。所以全局配置还是能生效的。
# 典型的 .bashrc 开头
# 由 bash 非登录交互式 Shell 加载
# 加载全局配置
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
# 用户自定义别名和函数
alias ll='ls -lh'
alias grep='grep --color=auto'
# 自定义提示符
PS1='[\u@\h \W]\$ '
3.4 非交互式 Shell 的特殊情况
非交互式 Shell 通常不加载任何配置文件。但有一个例外:BASH_ENV 环境变量。
我记得有一次排查 CI/CD 流水线的问题,脚本里调用了 nvm 但一直找不到命令。后来发现是因为非交互式 Shell 不会加载 .bashrc,而 nvm 的初始化代码写在 .bashrc 里。
解决办法是在脚本开头显式 source:
#!/bin/bash
# 非交互式脚本,需要手动加载环境
source ~/.bashrc
# 或者设置 BASH_ENV
export BASH_ENV=~/.bashrc
我曾经踩过的坑:在 crontab 里执行脚本时,环境变量经常不生效。因为 cron 启动的 Shell 是非交互式非登录的,.bashrc 和 .bash_profile 都不会加载。解决方案是在脚本里显式 source 需要的配置文件,或者在 crontab 里设置 SHELL=/bin/bash 并加载 BASH_ENV。
3.5 各发行版的差异
不同 Linux 发行版对配置文件的管理方式略有不同。我整理了一张表:
| 发行版 | 登录 Shell 加载 | 非登录 Shell 加载 | 备注 |
|---|---|---|---|
| CentOS / RHEL | /etc/profile → ~/.bash_profile |
~/.bashrc(内部调用 /etc/bashrc) |
.bash_profile 默认会 source .bashrc |
| Ubuntu / Debian | /etc/profile → ~/.profile |
~/.bashrc(内部调用 /etc/bash.bashrc) |
默认没有 .bash_profile,用 .profile 兜底 |
| macOS | /etc/profile → ~/.bash_profile |
~/.bashrc |
终端默认是登录 Shell,所以 .bash_profile 优先 |
3.6 如何判断当前 Shell 类型
写脚本时,有时候需要判断当前 Shell 是什么类型。可以用这几个方法:
# 判断是否是登录 Shell
shopt -q login_shell
echo $? # 0 是登录 Shell,1 是非登录 Shell
# 判断是否是交互式 Shell
if [[ $- == *i* ]]; then
echo "交互式 Shell"
else
echo "非交互式 Shell"
fi
# 或者用 PS1 判断
if [ -n "$PS1" ]; then
echo "交互式"
fi
3.7 最佳实践总结
说了这么多,最后给几条我自己的实践经验:
- 所有配置放
.bashrc,然后在.bash_profile里 source 它。这样登录和非登录 Shell 都能加载。 - 全局配置放
/etc/profile.d/目录下,以.sh结尾。系统会自动加载这个目录下的所有脚本。 - 脚本开头显式 source,不要依赖 Shell 自动加载。特别是 crontab、systemd 服务、CI/CD 流水线里。
- 用
set -o posix可以禁用BASH_ENV等非 POSIX 特性,让脚本行为更可预测。
一句话总结:登录 Shell 走 .bash_profile,非登录 Shell 走 .bashrc,非交互式 Shell 啥也不走。想让配置通用,就在 .bash_profile 里 source .bashrc。