Bash 启动时的配置文件加载

Bash 启动时的配置文件加载

参考博客:bash 启动时加载配置文件过程 - 骏马金龙 - 博客园,讲得通俗易懂,好文

是否是交互式的 shell?是否是登录式的 shell?

开启一个 shell 进程可以是交互式的(比如在 Linux 桌面上打开终端),有些时候是非交互的(比如执行一个 shell 脚本),因此总的来说 bash/shell 进程的启动类型可分为交互式 shell 和非交互式 shell。

从另一个角度来说,bash/shell 进程还分为登录式 shell(SSH 登录)和非登录式 shell(subash 命令没有后带上 --login-l 参数时都是进入非登录式的 shell 进程)。

判断是否交互式、是否登录式

判断是否为交互式 shell 有两种简单的方法:

方法一:判断变量 -,如果值中含有字母 i,表示交互式。

$ echo $-
himBH

执行脚本

#!/bin/bash
echo $-

输出

hB

方法二:判断变量 PS1,如果值非空,则为交互式,否则为非交互式,因为非交互式会清空该变量。

$ echo $PS1
\s-\v\$

执行脚本

#!/bin/bash
echo $PS1

输出


判断是否为登录式的方法也很简单,只需执行 shopt login_shell 即可。值为 on 表示为登录式,否则为非登录式。

在《Bash 的模式拓展》的 shopt 命令 小节提到过

直接通过 bash 命令开启的子 shell 进程,没有带上 -l 参数,所以式非登录式的,带上 -l 参数之后即式登陆式的

$ shopt login_shell
login_shell     on
$ bash
$ shopt login_shell
login_shell     off
$ exit
exit
$ bash -l
$ shopt login_shell
login_shell     on

所以,要判断是交互式以及登录式的情况,可简单使用如下命令:

$ echo $PS1;shopt login_shell
\s-\v\$
login_shell     on
# 或者
$ echo $-;shopt login_shell
himBH
login_shell     on

几种常见 bash/shell 进程启动方式与交互式和登录式的对应

判断方式很简单,直接以不同的方式启动进程然后执行 echo $-;shopt login_shell 即可

bash/shell 进程的环境配置文件的加载

无论是否交互、是否登录,bash 总要配置其运行环境。bash 环境配置主要通过加载 bash 环境配置文件来完成。但是否交互、是否登录将会影响加载哪些配置文件,除了交互、登录属性,有些特殊的属性也会影响读取配置文件的方法。

bash/shell 进程的环境配置文件主要有:

还有一个退出的时候会执行的脚本文件

登录式的 shell 进程的配置文件的加载

交互式登录 shell 或非交互式登录 shell 启动时,将先读取 /etc/profile,再依次搜索 ~/.bash_profile(重点)、~/.bash_login(默认不存在)和 ~/.profile(默认不存在),并仅加载第一个搜索到且可读的文件。当退出时,将执行 ~/.bash_logout 中的命令。

比较典型的交互式的登录 shell 是通过 SSH 登录

比较典型的非交互式登录 shell 就是 bash -l 执行 shell 脚本

先简单看看 /etc/profile,其中的注释提示说,/etc/profile 是系统范围的环境和启动程序,如果是想在登录时设置函数和别名,请修改 /etc/bashrc,如果你想要自定当前的计算机环境,直接修改 /etc/profile 不是一个好主意,除非你知道你正在做的事情。另一种修改环境的方式:在 /etc/profile.d/ 目录下创建一个自定义的 shell 脚本要好得多,而且将来系统升级,可能 /etc/profile 会更新,到时候你在这个文件中的自定义修改会被覆盖,因此,修改系统级环境最合适的方式,还是在 /etc/profile.d/ 目录下创建一个自定义的 shell 脚本

/etc/profile/etc/bashrc/etc/profile.d/*.sh 都是系统级别的配置文件,其中的修改会影响系统中所有的用户

简单分析一下 /etc/profile 的内容,其中重点也就这一小段儿,这段代码的作用是加载 /etc/profild.d 目录下的所有可读的脚本。注意,这一段是在 /etc/profile 的最后,加载 /etc/profile.d/*.sh 之前 /etc/profile 中的环境配置已经加载完。

# 遍历 /etc/profile.d 路径下的脚本,
# 其中 sh.local 是一个默认的脚本,也就是可以直接把自定义逻辑加到这里
for i in /etc/profile.d/*.sh /etc/profile.d/sh.local ; do
    # -r 判断文件是否可读,不可读就不读了 
    if [ -r "$i" ]; then
        # ${-#*i} 的意思是 删除 $- 这个变量中从开头到字母i都删除,只保留i后面的部分
        # 因为 交互式 shell 中 $- 带有 i,非交互式 shell 中,$- 没有有 i
        # 因此 "${-#*i}" != "$-" 会在交互式 shell 中成立
        if [ "${-#*i}" != "$-" ]; then 
            # 如果是交互式 shell,直接执行这个脚本,执行结果会输出到标准输出
            . "$i"
        else
            # 如果是非交互式的 shell,就没必要输出到标准输出了,输出到 /dev/null 算了
            . "$i" >/dev/null
        fi
    fi
done

然后再去加载 ~/.bash_profile,其中的注释提示,~/.bash_profile 是只针对特定用户也就是此家目录对应的用户才会生效的环境配置和启动程序。这其实将系统级别的配置和用户级别的配置做了区分,而且先加载系统级别的配置,再加载用户级别的配置可以方便用户自定义环境变量和加载行为,甚至重写系统的环境变量,而不影响其他用户。

为什么加载完 /etc/profile 会去加载 ~/.bash_login~/.bash_login 不存在再去加载 ~/.bash_login,~/.bash_login 不存在再去加载 ~/.profile,为什么是这个顺序,在哪里写了,我还不知道,只知道这个顺序是固定的,以后有时间再去研究 TODO

简单分析一下 ~/.bash_profile,它执行 ~/.bashrc,然后再执行 ~/.bash_profile 中的自定义配置

# -f 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。
if [ -f ~/.bashrc ]; then
        # 如果 ~/.bashrc 是一个文件,则加载这个文件
        . ~/.bashrc
fi

我们再去看看 ~/.bashrc,这个文件跟 ~/.bash_profile 文件一样,是是只针对特定用户也就是此家目录对应的用户才会生效的环境配置和启动程序。

这个文件会先执行 /etc/bashrc,然后再执行 ~/.bashrc 中的自定义配置

# -f 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。
if [ -f /etc/bashrc ]; then
        # 如果 ~/.bashrc 是一个文件,则加载这个文件
        . /etc/bashrc
fi

简单看下 /etc/bashrc,注释说,/etc/bashrc 保存的是系统范围内的方法和别名,如果你想设置环境相关的东西,则需要去修改 /etc/profile。如果你想要自定当前的计算机环境,直接修改 /etc/bashrc 不是一个好主意,除非你知道你正在做的事情。另一种修改环境的方式:在 /etc/profile.d/ 目录下创建一个自定义的 shell 脚本要好得多,而且将来系统升级,可能 /etc/bashrc 会更新,到时候你在这个文件中的自定义修改会被覆盖,因此,修改系统级环境最合适的方式,还是在 /etc/profile.d/ 目录下创建一个自定义的 shell 脚本

/etc/bashrc 中的源码就不去细看了,总结起来就是,/etc/bashrc 会在交互式 shell 环境下进行一些设置,同时在非登录式 shell 中加载 /etc/profile.d/ 目录下的 sh 文件。

所以对于登录式 shell 的配置文件加载进程是:

实践

我们在各个场景下进行登录也验证了我们的想法。

为了测试各种情形读取哪些配置文件,先分别向这几个配置文件中写入几个 echo 语句,用以判断该配置文件是否在启动 shell 进程时被读取加载了。

echo "echo '/etc/profile goes'" >>/etc/profile
echo "echo '~/.bash_profile goes'" >>~/.bash_profile
echo "echo '~/.bashrc goes'" >>~/.bashrc
echo "echo '/etc/bashrc goes'" >>/etc/bashrc
echo "echo '/etc/profile.d/test.sh goes'" >>/etc/profile.d/test.sh
chmod +x /etc/profile.d/test.sh

SSH 远程登陆时:

/etc/profile.d/test.sh goes
/etc/profile goes
/etc/bashrc goes
~/.bashrc goes
~/.bash_profile goes

可以看到是符合我们做的图里的顺序的,但是乍一看,/etc/profile.d/*.sh 明明在 /etc/profile 的前面啊,上面的图中,/etc/profile.d/*.sh 确实在 /etc/profile 的后面,明明不一致啊?

其实原因很简单,因为我们将 echo '/etc/profile goes' 放到了 /etc/profile 的最后面,最终的加载顺序是:/etc/profile 中的默认配置 -> /etc/profile.d/*.sh -> 用户后来添加的配置(echo '/etc/profile goes')。

直接在 /etc/profile 的最后面直接添加自定义系统配置也是我们一般情况下的做法,其实这样是不好的,因为这样实际上就是在加载 /etc/profile.d/*.sh 之后仍然设置环境变量,这样就有可能会覆盖 /etc/profile.d 中设置的变量,这就与 /etc/profile/etc/profile.d/*.sh 的设计相违背,当然你也可以将配置放到加载 /etc/profile.d/*.shfor 循环前面,但是这样未免也太麻烦了,因此,最简单的方式就是按照规范来,不修改 /etc/profile,直接在 /etc/profile.d 中添加脚本,比如专门创建一个脚本 Java.sh 用来设置 Java 变量,这样最直观和方便。

其他场景:

$ bash -l
/etc/profile.d/test.sh goes
/etc/profile goes
/etc/bashrc goes
~/.bashrc goes
~/.bash_profile goes
$ su -
Last login: Thu Aug 17 23:23:56 CST 2023 from 171.113.65.105 on pts/0
/etc/profile.d/test.sh goes
/etc/profile goes
/etc/bashrc goes
~/.bashrc goes
~/.bash_profile goes

可以看到输出的日志都是相同的。但是在执行带有登录参数的脚本的时候,有点不同

ts.sh 内容如下:

#!/bin/bash -l
echo ts.sh exec

执行前已经赋予了可执行权限,执行

$ ./ts.sh
/etc/profile goes
/etc/bashrc goes
~/.bashrc goes
~/.bash_profile goes
ts.sh exec

可以看到没有输出 /etc/profile.d/test.sh goes,这是因为在 /etc/profile 中执行 /etc/profile.d/*.sh 的时候,如果是当前 shell 是非交互式的,那么 /etc/profile.d/*.sh 的执行结果将重定向到 /dev/null,也就是不会输出到标准输出,也就是终端中。这个我们在对 /etc/profile 进行源码分析的时候已经提到过了。

交互式非登录式的 shell 进程的配置文件的加载

交互式非登录 shell 的 bash 启动时,将读取 ~/.bashrc,不会读取 /etc/profile~/.bash_profile~/.bash_login~/.profile。根据我们在上一个小节中的分析,~/.bashrc 会先执行 /etc/bashrc/etc/bashrc 会先执行 /etc/profile.d/*.sh,因此最终加载图如下:

实践

简单验证:

$ su
/etc/profile.d/test.sh goes
/etc/bashrc goes
~/.bashrc goes
$ bash
/etc/profile.d/test.sh goes
/etc/bashrc goes
~/.bashrc goes

交互式非登录式的 shell 进程的配置文件的加载

非交互式、非登录式 shell 启动 bash 时,不会加载前面所说的任何 bash 环境配置文件,但会在当前这个非交互式、非登录式 shell 进程环境中搜索变量 BASH_ENV,如果变量存在,则先加载变量中保存的指定的文件,然后再执行目标脚本。

# -n  检测字符串长度是否不为 0,不为 0 返回 true。
if [ -n "$BASH_ENV" ];then
    . "$BASH_ENV"
fi

执行脚本且没带 -l 参数的时候,开启的就是非交互式的非登录式的 shell 进程,而且几乎所有的 shell 脚本都不会特意在第一行带上 -l 参数,因此 shell 脚本不会加载任何 bash 环境配置文件,除非手动配置了变量 BASH_ENV

当然也有例外,请看下一个小节

实践

ts.sh 内容如下:

#!/bin/bash -l
echo ts.sh exec

/opt/info.sh

#!/bin/bash
echo script exec

开始的时候,没有配置 BASH_ENV 变量:

$ ./ts.sh
ts.sh exec

然后用 export 配置 BASH_ENV 变量,并执行脚本

$ export BASH_ENV=/opt/info.sh
$ ./ts.sh
script exec
ts.sh exec

可以看到,先执行的 info.sh 再执行的 ts.sh

远程 shell 方式启动的 bash

比如 ssh 执行脚本,它虽然属于非交互、非登录式,但会加载 ~/.bashrc,所以还会加载 /etc/bashrc,由于是非登录式,所以最终还会加载 /etc/profile.d/*.sh,只不过因为是非交互式而使得执行的结果全部重定向到了 /dev/null 中。

其实我没搞定为什么它不是登录式的,这不是输入了密码吗?

不过这个场景用的也不多,以后有时间再去深究吧,TODO

# ssh localhost echo haha
root@localhost's password:
/etc/bashrc goes
~/.bashrc goes
haha

总结

了解了这些不同场景下加载的配置文件,我们可以正确地自定环境变量了。