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 配置文件加载顺序 登录 Shell 配置文件加载顺序 启动登录 Shell /etc/profile(全局) 查找 ~/.bash_profile 加载 .bash_profile 查找 .bash_login 查找 .profile(兜底) 注意:找到第一个就停止,不会全部执行。.bash_profile 优先级最高,.profile 是最后兜底。

具体来说,登录 Shell 的加载顺序是这样的:

  1. 先加载 /etc/profile:这是全局配置文件,对所有用户生效。里面通常设置了 PATH、umask 等基础环境。
  2. 然后按优先级查找用户级配置文件:
    • 先找 ~/.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。