Linux Bash Shell 探究
本篇文章参考自:
Linux也算是用了好几年了吧,Shell(主要是Bash、Zsh)也一直在用,之前其实看到过交互式Shell、非交互式Shell、登录Shell的内容,但当时什么都不懂,理解也不深,只是脑子里留下了一个印象而已;现在无意中又碰到了这个话题,作为一个知识的奴隶,补充一下这方面的知识。
注:实验环境为 - CentOS 7 - Bash
1 Login & Non-Login
经常看到一个概念
Login Shell或者叫登录 Shell,但很难从名称理解它的含义,从网上也很少找到详细的解释,今天来整理一下Login Shell的概念以及如何区分Login Shell和Non-Login Shell。先来看看平常我们使用
SSH协议登录Linux服务器时的样子:
Connecting to 10.10.8.137:22...
Connection established.
To escape to local shell, press Ctrl+Alt+].
Last login: Wed Oct 18 21:55:30 2023 from 10.10.8.1
[root@localhost ~]#
可以看到有一个
Last login的时间和IP。这时,有两种命令可以断开
SSH连接:logout:在用户登录Shell会话时,用于安全地注销用户并终止该会话。exit:退出当前Shell环境,并返回到上一级Shell或关闭终端会话。
[root@localhost ~]# logout
Connection closed.
Disconnected from remote host(CentOS 7) at 00:28:31.
[root@localhost ~]# exit
logout
Connection closed.
Disconnected from remote host(CentOS 7) at 00:28:49.
可以看到两者的具体作用上有细微差别,比如在使用 bash 命令开启一个子
Shell时,却不能使用logout退出,只能使用exit:
[root@localhost ~]# bash
[root@localhost ~]# ps -f
UID PID PPID C STIME TTY TIME CMD
root 52446 52442 0 15:52 pts/1 00:00:00 -bash
root 52486 52446 0 15:52 pts/1 00:00:00 bash
root 52515 52486 0 15:52 pts/1 00:00:00 ps -f
[root@localhost ~]# logout
bash: logout: not login shell: use `exit'
这是为什么?很简单,因为
bash的子Shell是一个Non-login,对于一个非登录Shell来说,这个Shell没有用户登录又何来用户登出呢。
1.1 Login Shell
在
sudo命令中有个参数-i,用于运行一个Login Shell:
[root@localhost ~]# sudo --help | grep 'login'
-i, --login run login shell as the target user; a command may also be specified
[root@localhost ~]# sudo -i echo $0
-bash
在
su命令中也有类似的参数:
[root@localhost ~]# su --help | grep 'login'
-, -l, --login make the shell a login shell
[root@localhost ~]# su - centos7
Last login: Thu Oct 19 00:37:52 CST 2023 on pts/2
[centos7@localhost ~]$ echo $0
-bash
关于
Login Shell这部分定义,可以从Bash手册里看到:
A login shell is one whose first character of argument zero is a -, or one started with the --login option.
## 登录 Shell 是 &0 的第一个字符是 - 或以 ——login 选项开头的 Shell。
可以看到
-与--login两个条件二者取其一。
1.1.1 Login Shell 条件一
先看前面半句:
first character of argument zero is a -,即一个Shell的argument zero的第一个字符是个连字符 -。在这里
argument zero其实指的是$0。
注:$0 是 Shell 的一个参数,这个参数保存的是 bash 脚本的名称,bash 初始化的时候会设置 $0 这个变量。
在
bash中打印一下:
[root@localhost ~]# echo $0
-bash
可以看到,打印出来脚本名称是
-bash,第一个字符是 -,说明这个是一个Login Shell。使用
ps命令查看进程,也可以看到bash的进程名称为-bash:
[root@localhost ~]# ps -f
UID PID PPID C STIME TTY TIME CMD
root 5586 5578 0 00:32 pts/2 00:00:00 -bash
root 5957 5586 0 00:43 pts/2 00:00:00 ps -f
如果打印出来第一个字符不是 -,例如:
[root@localhost ~]# bash
[root@localhost ~]# ps -f
UID PID PPID C STIME TTY TIME CMD
root 2133 2125 0 20:50 pts/0 00:00:00 -bash
root 2205 2133 0 20:52 pts/0 00:00:00 bash
root 2238 2205 0 20:52 pts/0 00:00:00 ps -f
不过,这也不能说明这不是
Login Shell还要看后半句。
1.1.2 Login Shell 条件二
Shell启动的时候有--login参数,尝试一下:
[root@localhost ~]# bash --login
[root@localhost ~]# ps -f
UID PID PPID C STIME TTY TIME CMD
root 2133 2125 0 20:50 pts/0 00:00:00 -bash
root 2255 2133 0 20:53 pts/0 00:00:00 bash --login
root 2287 2255 0 20:53 pts/0 00:00:00 ps -f
[root@localhost ~]# logout
[root@localhost ~]# ps -f
UID PID PPID C STIME TTY TIME CMD
root 2133 2125 0 20:50 pts/0 00:00:00 -bash
root 2288 2133 0 20:54 pts/0 00:00:00 ps -f
可以使用
logout命令,用Docker试一试:
[root@localhost ~]# docker run -it centos:7 /bin/bash --login
[root@3341ceb0da2a /]# ps -f
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 12:55 pts/0 00:00:00 /bin/bash --login
root 19 1 0 12:55 pts/0 00:00:00 ps -f
[root@3341ceb0da2a /]# logout
[root@localhost ~]#
可以看到,如果带上
--login,那它就是一个Login shell,可以使用logout退出Shell。
1.2 Login & Non-Login 区别
Login Shell(登录Shell)和Non-Login Shell(非登录Shell)是两种不同类型的Shell环境,它们在启动时加载的配置文件以及行为方面存在一些区别。Login Shell(登录Shell):当用户登录到系统时,通常会启动一个登录
Shell。登录
Shell会加载一系列的配置文件,用于设置用户的环境(具体文件在第三章说明)。
Non-Login Shell(非登录Shell):当用户在已登录的会话中打开新的终端窗口或启动一个新的
Shell时,会使用非登录Shell。非登录
Shell通常是为了执行用户的命令而启动的,不需要执行登录过程的设置。非登录
Shell也会加载一系列的配置文件,用于设置用户的环境(具体文件在第三章说明)。
2 Interactive & Non-Interactive
交互模式就是在终端上执行,
Shell等待你的输入,并且立即解释执行你提交的命令。这种模式被称作交互式,是因为
Shell与用户进行交互。这种模式也是大多数用户非常熟悉的:登录、执行一些命令、退出,当你退出后,
Shell也终止了。
注:通常来说,Login Shell 都是 Interactive Shell。
同理,
Shell也可以运行在另外一种模式:非交互式模式。非交互模式以
Shell Script(非交互)方式执行。在这种模式下,
Shell不与你进行交互,而是读取存放在文件中的命令,并且依此解释执行它们。当它读到文件的结尾
Shell也就终止了。
2.1 Interactive Shell
关于
Interactive Shell这部分定义,可以从Bash手册里看到:
An interactive Shell is one started without non‐option arguments (unless -s is specified) and without the -c option, whose standard input and error are both connected to terminals (as determined by isatty(3)), or one started with the -i option. PS1 is set and $- includes i if bash is interactive, allowing a Shell script or a startup file to test this state.
## 一个交互式 Shell 是在没有非选项参数(除非指定了 -s 选项)和 -c 选项的情况下启动的 Shell,其标准输入和错误都连接到终端,或者使用 -i 选项启动的Shell。如果 bash 是交互式的,PS1 被设置并且 $- 包含 i,允许 Shell 脚本或启动文件测试此状态。
上面那句话出现了一个单词:
non‐option arguments(非选项参数),这是什么?
2.1.1 option & argument
在
Linux中,命令行使用空格分割为一个个的argument,例如:

以上图为例,命令执行时,在
ls脚本的内部,可以使用$0来获取到ls这个字符串。同理,
$1可以获取到-l,使用$2来获取到-a选项等等。arguments就是$0、$1、$2、…,以空格分割命令行后的每个部分。像
-l和-a这种可以改变ls命令的行为的,叫做选项(即option),它们一般会被写在命令使用文档中:

那非选项参数是哪个?
上述命令中参数
/path/to/folder既不是-l选项的值,也不是-a选项的值,它不属于任何一个option,所以它是一个non-option arguments。
2.1.2 Interactive Shell 条件一
看下手册里面的几个单词:
PS1 is set,这个PS1是啥?$PS1用于自定义命令提示符的外观和内容,以便增强Shell的可视化和交互性。
说明是个
Interactive Shell的话,变量$PS1应该是有输出值的:
[root@localhost ~]# echo $PS1
[\u@\h \W]\$
使用
bash命令的-c选项来执行上述命令:
注:bash -c 是在当前 Shell 进程中启动一个新的 Bash 子进程,并在该子进程中执行指定的命令或脚本。
[root@localhost ~]# bash -c "echo \$PS1"
可以理解为,执行
bash -c "echo \$PS1"命令的时候,实际上由当前 Shell 启动了一个非交互式Shell,然后让这个非交互式去执行echo $PS1命令。
2.1.3 Interactive Shell 条件二
再看下手册里面的几个单词:
$- includes i,这个$-又是个啥?$-是一个特殊变量,用于表示当前Shell的选项和状态。当
Bash Shell是交互式的时候,$-会包含字母i,表示Shell是交互式的。
说明是个
Interactive Shell的话,变量$-的输出值应该包含i:
[root@localhost ~]# echo $-
himBH
使用
bash命令的-c选项来执行上述命令:
[root@localhost ~]# bash -c 'echo $-'
hBc
2.1.4 Interactive Shell 条件三
前面已知了两个条件,还有一个容易被忽略的非选项参数。
既然已经知道了
non-option arguments的含义,我们来简单验证一下:
[root@localhost ~]# cat demo
#!/bin/bash
echo $-
[root@localhost ~]# bash demo
hB
[root@localhost ~]# bash
[root@localhost ~]# echo $-
himBH
还可以使用
bash -i进行交互式Shell创建:
[root@localhost ~]# bash -i demo
himB
注:bash -i 是在当前 Shell 进程中启动一个新的 Bash 子进程,并使该子进程成为一个交互式 Shell。
2.2 总结
3 Linux Bash Env-Conf
这里我们可以想一个问题,以前我想的少,现在想得多了:
Linux 的环境配置文件包括
/etc/profile、~/bash_profile、~/.bashrc等,它们通常会在用户登录时被执行,这些配置文件是每次登录都会调用吗?为什么有时候
/etc/profile没有生效?这些配置文件的执行顺序是怎么样的?
注:这里主要基于 Bash,其他 Shell 可能不适用。
3.1 Bash 配置文件调用逻辑
可以参考大佬的一张图:

总结一下就是:
如果一个
Shell是Login Shell时,/etc/profile会被调用,随后按顺序查找~/.bash_profile、~/.bash_login、~/.profile,并执行最先找到的一个。如果是交互式的
Non-Login Shell,则会调用~/.bashrc。如果是非交互式的
Non-Login Shell,则会执行环境变量$BASH_ENV中的脚本(如果存在,我也没有实践过)。
3.2 特殊情况
当然,凡是无绝对,上面这些被调用的脚本可能去调用其它脚本。
例如:
/etc/profile可能去调用/etc/profile.d/目录中的脚本。在
CentOS中~/.bash_profile会调用~/.bashrc,查看下代码:
[root@localhost ~]# head -n 6 ~/.bash_profile
## .bash_profile
## Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
所以在
CentOS中,Login Shell的配置文件加载是这样的:/etc/profile~/.bash_profile~/.bashrc
启动
Shell时可以使用--noprofile、--norc等选项使Shell不去调用对应的配置文件或者指定调用的配置文件,很少用到不展开讨论。如果启动
Shell时使用sh命令(sh是/bin/bash的一个软链接)情况又不同,如下:

使用
.或者source去执行一个脚本,是在当前Shell中执行脚本,不会启动新的Shell,所以不会去加载任何配置文件,没有上面这些流程。
注:
. 和 source 命令都用于在当前 Shell 环境中执行脚本,使脚本中的变量、函数、别名等定义在当前 Shell 中生效。
source 命令是 . 命令的一个 Bash 内建命令别名。