Bash 启动配置

bash (Bourne Again SHell) 是默认的 shell,有必要了解一下它不同启动方式下的配置过程。

当执行bash命令或者用户登录系统时,会陆续加载各种bash配置文件,还会设置或清空一系列变量,有时还会执行一些自定义的命令,这些行为都算是启动bash时的过程。在不同的系统中具体的逻辑还是不同的,目前只关注Ubuntu和CentOS。

启动过程有两个维度的分类方式:

  • 交互式和非交互式
    • 交互式的标准情景:输入一个命令,然后输出user@host:path$,等待用户输入;
    • 非交互式的标准情景:bash执行一个脚本,例如bash demo.sh
  • 登录和非登录:顾名思义,非登录情景可以加上--login-l选项来伪装为登录情况。

可以通过下面的方法进行判断:

  • 交互式的判断:
    • 交互式环境下的$-变量会含有字母i
    • 交互式定义了提示符$PS1,但是非交互式会清空这个变量,因此echo $PS1可以区分。
  • 登录和非登录的判断:shopt login_shell,返回onoff
1
echo $PS1;shopt login_shell

使用ssh命令时:

  • ssh远程登录:交互式、登录式;
  • ssh远程执行命令但不登录:非交互式、非登录式

使用bash命令启动shell时:

  • bash:交互式、非登录式
  • bash -l:交互式、登录式

使用bash命令执行脚本时:

  • bash demo.sh:非交互式、非登录式
  • bash -l demo.sh:非交互式、登录式

使用su命令切换用户时,加不加-有不同的效果:

  • su user2:切换到user2,打开的shell是交互式、非登录式
  • su - user2:切换到user2,打开的shell是交互式、登录式

在图形用户界面下打开终端时,默认为交互式、非登录式;但是可以在设置中改为交互式、登录式。

bash不同启动过程中涉及到的配置文件可能有

  • 系统级:
    • /etc/profile(通常在脚本中自动加载/etc/profile.d/*.sh
    • /etc/bash.bashrc
    • /etc/bashrc
  • 用户级:
    • ~/.bash_profile
    • ~/.bash_login
    • ~/.profile(这三个文件的角色几乎是等价的,只是出于兼容性进行的保留,系统通常只会自动生成其中的一个文件,并且只会加载第一个存在且可读的文件)
    • ~/.bashrc(通常用户只需要在这里进行修改自定义配置,各种启动方式通常都会直接或间接地加载这个文件)

bash退出时还涉及~/.bash_logout等文件,这里不做讨论。此外,~/.bash_history文件记录了bash的历史命令。(但是如果命令以空格开头,就不会记录)

对于一个交互式、登录式启动的shell,或者非交互式但是使用-l选项的情况:

  • 首先读取 /etc/profile
  • 然后依次搜索 ~/.bash_profile~/.bash_login~/.profile(在某些系统中,某一个文件会自动生成,其它文件则默认不存在),仅加载其中第一个存在到且可读的文件。

使用--noprofile可以跳过这个过程。如果这里使用的是sh名称,出于兼容性考虑,只会依次加载etc/profile~/.profile

对于一个交互式、非登录式启动的shell:

  • 读取/etc/bash.bashrc~/.bashrc

对于一个非交互式、非登录式的shell(通常是执行一个脚本),不会加载类似的配置文件,但是会尝试加载$BASH_ENV这个环境变量所代表的配置文件,如果存在的话。

实践中发现,CentOS会自动生成~/.bash_profile,Ubuntu会自动生成~/.profile。两者的作用是类似的,大意是:

  • 如果存在~/.bashrc,读取这个文件;
  • 如果存在~/.local/bin这个目录,添加到PATH中。具体的措施不太一样,有的会加在PATH的头部,有的则会加在PATH的最后。

因此,在默认情况下,无论是哪一种方式启动,~/.bashrc中的内容都会被加载执行一次。

系统通常都会自动生成~/.bashrc文件,其中的内容却大不相同:
对于CentOS,自动生成的文件中会尝试加载/etc/bashrc文件

1
2
3
4
# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi

对于Ubuntu,自动生成的文件头部有如下内容

1
2
3
4
5
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac

具体含义是:通过$-判断当前shell是不是交互式的,如果不是就直接结束,不再执行后面的命令。

上面这个例子说明,不同的系统中这些配置文件发挥的作用是很不一样的。

对于 Ubuntu,在启动时默认会输出一些信息,可以通过在家目录下创建一个空的 .hushlogin 文件来关闭这些信息。

后台任务

通常在shell中执行的任务都是前台任务,即任务会占用前台,在任务结束之前无法进行下一个任务。在命令结尾使用&可以将命令使用后台进程执行,例如

1
./test.sh &

其中的测试脚本在不断写入日志,每隔4秒写入一次

1
2
3
4
5
6
7
8
#!/bin/bash

logfile="time_log.txt"

while true; do
echo "$(date)" >> "$logfile"
sleep 4
done

使用jobs命令可以查看当前的后台任务,每一个后台任务都有独立的序号(注意不是pid),例如

1
[1]+  Running                 ./test.sh &

如果返回值为空,代表没有后台任务。后台任务的状态可能是Running(正在运行),也可能是Stopped(挂起,暂停),Terminated(终止),终止的任务稍后就会从jobs的输出中消失。

需要注意的是,后台任务仍然会向当前终端的输出流写入信息!这可能会混淆其他命令的输出,对于后台任务,最好还是将输出重定向比较稳妥

1
./test.sh > output.log 2>&1 &

如果当前的前台任务太长或者卡死了,我们可以使用ctrl+c强行结束前台任务,也可以使用ctrl+z将前台任务挂起,此时任务会转为后台任务,并且设置为暂停状态,此时jobs输出形如

1
[1]+  Stopped                 ./test.sh

使用fg %1命令可以将指定的后台任务(正在执行或挂起的)转到前台执行。
使用bg %1命令则可以将指定的后台挂起任务在后台执行,这些命令的百分号很重要)

1
2
3
bg %1

fg %1

如果缺省编号,默认操作对象是标记为+的最近任务,此外还有标记为-的任务,含义是如果+标记的任务结束,-标记的任务就会变成+标记。

使用kill命令可以终止后台任务,例如 kill %jobnumberkill PID

需要注意的是,在一个会话中,无论是前台任务还是后台任务都是从属于当前会话的,如果退出当前的登陆,后台任务也会被终止,
有两种方法来解决这种问题:

  • 使用nohup命令;
  • 使用setsid命令。

第一种是基于nohup的方法,可以使用nohup搭配 & 来解决会话退出导致后台任务终止的问题,此时对应的进程不会因为退出会话而被终止,例如

1
nohup ./test.sh  &

nohup命令默认会将输出重定向到当前位置下的nohup.out文件(如果当前位置无权限则会回到家目录下创建nohup.out文件),在事后可以使用nohup.out文件来查看这个后台任务的输出。

可以指定全部输出重定向到指定文件,例如

1
nohup ./test.sh > out.log 2>&1 &

nohup命令有提示信息,需要回车确认一下。

如果对一个已经正在运行的后台任务忘记使用nohup执行了,还可以使用disown命令进行补救,例如

1
2
./test.sh &
disown -h %1

第二种是基于setsid的方法,它的做法在原理上更加彻底,此时的进程和当前会话不再是从属关系,因此当前会话的终止不会影响该进程,例如

1
setsid ./test.sh &

使用小括号似乎有一样的效果

1
(./test.sh &)

对后台任务的进一步分析必须从Linux中的进程,信号等机制出发,但是我对此没啥兴趣,这里只是从实用的角度进行学习,浅尝辄止。

Here Document(heredoc)

Bash 允许在命令行中创建一个 here document,这里 document 的内容会作为 stdin 传递给命令。

heredoc = 把一段文本作为 stdin 输入给命令

基本语法

1
2
3
4
command <<WORD
line1
line2
WORD

要点:

  • <<WORD 作为开始
  • 中间内容作为 stdin
  • 单独一行的 WORD 结束
  • WORD 只是一个分隔标记符,可以自定义(EOFENDPY 等)

heredoc 可以根据内容是否被 shell 展开,分为两种情况:

  • 不加引号(会展开),可能发生:$VAR 变量替换,$(cmd) 命令替换,反引号替换等
  • 加引号(不展开,更安全)

不加引号例如

1
2
3
4
cat <<EOF
home: $HOME
date: $(date)
EOF

输出

1
2
home: /home/user
date: Sun Mar ...

加引号例如

1
2
3
4
cat <<'EOF'
home: $HOME
date: $(date)
EOF

输出

1
2
home: $HOME
date: $(date)

一个更实际的例子:给 Python 传多行代码并立刻执行(习惯上使用 'PY'

1
2
3
4
python3 - <<'PY'
import json
print({"a": 1})
PY

除此之外,下面的特殊写法还支持自动移除前导的 TAB 缩进:(不包括空格)

1
2
3
4
cat <<-EOF
line1
line2
EOF

补充:在大多数 Unix shell 中,快捷键 Ctrl+D 表示 EOF(End Of File)
如果当前程序正在读取 stdin,并且输入缓冲区为空,按 Ctrl+D 会告诉程序输入结束,常用于结束交互式输入。