本文为 Bash Reference Manual 第3章:Basic Shell Features 第7节:Executing Commands 的读书笔记。
3.7.1 Simple Command Expansion
当 shell 执行一个简单命令时,它会按照从左到右的顺序执行以下扩展、赋值和重定向操作。
如果没有生成命令名称,变量赋值会影响当前的 shell 环境。在这种命令的情况下(即仅由赋值语句和重定向组成的命令),赋值语句会在重定向之前执行。否则,变量会被添加到执行命令的环境中,而不会影响当前的 shell 环境。如果任何赋值尝试向只读变量赋值,将会发生错误,命令以非零状态退出。
如果没有生成命令名称,则会执行重定向,但不会影响当前的 shell 环境。重定向错误会导致命令以非零状态退出。
如果在扩展后仍然有命令名称,执行将按以下描述进行。否则,命令将退出。如果某个扩展包含了命令替换,命令的退出状态就是最后一个执行的命令替换的退出状态。如果没有命令替换,命令将以零状态退出。
示例1:
$ a=1
$ a=2 echo $a
1
$ echo $a
1
依据上述规则,echo $a先被展开,并且a=2不会影响当前shell。
示例2:
$ a=2; echo $a
2
$ echo $a
2
依据上述规则,从左到右,a=2先执行。并且a=2影响当前shell。
3.7.2 Command Search and Execution
当一个命令被拆分成单词后,如果它拆分为一个简单命令和一个可选的参数列表,shell 将执行以下操作。
示例:
$ $(echo pwd)
/home/vagrant
$ $(echo pdw)
-bash: pdw: command not found
$ echo $?
127
$ type pwd
pwd is a shell builtin
$ type find
find is /usr/bin/find
$ type env
env is hashed (/usr/bin/env)
$ time sleep 5
real 0m5.011s
user 0m0.002s
sys 0m0.000s
$ echo $PATH
/home/vagrant/.local/bin:/home/vagrant/bin:/usr/share/Modules/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
$ which a.sh
/usr/bin/which: no a.sh in (/home/vagrant/.local/bin:/home/vagrant/bin:/usr/share/Modules/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin)
$ a.sh
-bash: a.sh: command not found
3.7.3 Command Execution Environment
shell 有一个执行环境,它包括以下内容:
- 由 shell 在调用时继承的打开文件,以及由 exec 内建命令提供的重定向所修改的文件。
- 由 cd、pushd 或 popd 设置的当前工作目录,或者由 shell 在调用时继承的目录。
- 通过 umask 设置的文件创建权限掩码,或从 shell 的父进程继承的权限掩码。
- 由 trap 设置的当前陷阱。
- 通过变量赋值设置的 shell 参数,或通过 set 设置,或者从 shell 父进程的环境中继承的参数。
- 执行期间定义的 shell 函数,或者从 shell 父进程的环境中继承的函数。
- 在调用时启用的选项(默认或通过命令行参数)或通过 set 启用的选项。
- 通过 shopt 启用的选项(参见 The Shopt Builtin)。
- 使用 alias 定义的 shell 别名(参见 Aliases)。
- 各种进程 ID,包括后台作业的 ID(参见 Lists of Commands)、$$ 的值以及 $PPID 的值。
3.4.2 Special Parameters中有对$$的说明。
($$) Expands to the process ID of the shell. In a subshell, it expands to the process ID of the invoking shell, not the subshell.
示例:
$ echo $$ $BASHPID
14556 14556
$ ( echo $$ $BASHPID)
14556 14821
当要执行一个简单命令(非内置命令或 shell 函数)时,它会在一个独立的执行环境中被调用,该环境包括以下内容。除非另有说明,否则这些值是从 shell 继承的。
- shell 已打开的文件,以及通过重定向命令指定的任何修改和新增部分。
- 当前工作目录。
- 文件创建模式掩码。
- 被标记为导出的 shell 变量和函数,以及为命令导出的变量,通过环境传递(参见环境)。
- shell 捕获的陷阱会重置为从其父 shell 继承的值,而被 shell 忽略的陷阱仍然被忽略。
💡 在这个独立环境中调用的命令无法影响 shell 的执行环境。
子 shell 是 shell 进程的一个副本。
命令替换、用括号分组的命令以及异步命令都会在一个子 shell 环境中调用,该环境是 shell 环境的副本,但 shell 捕获的陷阱会被重置为 shell 在调用时从其父继承的值。作为管道一部分调用的内置命令(可能最后一个元素除外,这取决于 lastpipe shell 选项的值,参见 The Shopt Builtin)也会在子 shell 环境中执行。对子 shell 环境所做的更改不会影响 shell 的执行环境。
当 shell 处于 POSIX 模式时,为执行命令替换而生成的子 shell 会继承父 shell 的 -e 选项的值。当不处于 POSIX 模式时,Bash 会在这些子 shell 中清除 -e 选项。有关如何在非 POSIX 模式下控制此行为,请参阅 inherit_errexit shell 选项的说明(见 Bash 内建命令)。
如果命令后跟 ‘&’ 且作业控制未激活,则命令的默认标准输入为空文件 /dev/null。否则,被调用的命令会继承调用 shell 的文件描述符,并受到重定向的影响。
3.7.4 Environment
当一个程序被调用时,它会获得一个名为环境的字符串数组。这是由名称-值对组成的列表,形式为 name=value。
Bash 提供了多种操作环境的方法。在调用时,shell 会扫描自身的环境,并为找到的每个名称创建一个参数,自动将其标记为可导出(export)到子进程。执行的命令会继承环境。export、‘declare -x’ 和 unset 命令通过添加和删除参数及函数来修改环境。如果环境中某个参数的值被修改,新的值会自动成为环境的一部分,替换掉旧值。任何执行命令继承的环境由 shell 的初始环境组成,其值可以在 shell 中修改,减去被 unset 和 ‘export -n’ 命令移除的任意键值对,再加上通过 export 和 ‘declare -x’ 命令添加的任意内容。
💡 只有export、‘declare -x’ 和 unset 命令可以修改环境。
示例:
$ VAR1=123
$ env|grep VAR1
$ set|grep VAR1
VAR1=123
$ export VAR2
$ env|grep VAR
$ set|grep VAR
VAR1=123
_=VAR2
$ export VAR2=456
$ env|grep VAR
VAR2=456
$ set|grep VAR
VAR1=123
VAR2=456
_=VAR2=456
$ unset VAR2
$ env|grep VAR
$ set|grep VAR
VAR1=123
_=VAR2
💡 set 和 env 命令最大的区别是,set输出shell变量,env输出环境变量。shell变量含环境变量。环境变量是可以继承的。
如果在简单命令之前出现任何参数赋值语句(如 Shell 参数中所述),这些变量赋值将在该命令执行期间成为其环境的一部分。这些赋值语句只影响该命令所看到的环境。如果这些赋值出现在对 shell 函数的调用之前,变量将在函数内部为局部变量,并导出给该函数的子进程。
如果设置了 -k 选项(参见 The Set Builtin),则所有参数赋值都会被放入命令的环境中,而不仅仅是放在命令名称之前的那些。
当 Bash 调用外部命令时,变量 $_ 会被设置为命令的完整路径名,并在命令的环境中传递给该命令。
示例:
$ export -p
declare -x DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"
declare -x DEBUGINFOD_IMA_CERT_PATH="/etc/keys/ima:"
declare -x HISTCONTROL="ignoredups"
declare -x HISTSIZE="1000"
declare -x HOME="/home/vagrant"
declare -x HOSTNAME="ol9-vagrant"
declare -x LANG="en_US.utf-8"
declare -x LESSOPEN="||/usr/bin/lesspipe.sh %s"
declare -x LOADEDMODULES=""
declare -x LOGNAME="vagrant"
...
$ export|grep VAR
$ export VAR=123
$ export|grep VAR
declare -x VAR="123"
$ export VAR1=456
$ export|grep VAR
declare -x VAR="123"
declare -x VAR1="456"
$ unset VAR
$ export|grep VAR
declare -x VAR1="456"
$ export -n VAR1
$ export|grep VAR
$
3.7.5 Exit Status
已执行命令的退出状态是由 waitpid 系统调用或等效函数返回的值。退出状态的范围在 0 到 255 之间,不过,如下所述,shell 可能会对大于 125 的值进行特殊处理。来自 shell 内建命令和复合命令的退出状态也限制在这个范围内。在某些情况下,shell 会使用特殊值来表示特定的失败模式。
对于 shell 来说,一个以零退出状态退出的命令表示成功。非零的退出状态则表示失败。这种看似违反直觉的方案之所以被采用,是为了提供一种明确的方式来表示成功,以及多种方式来表示不同的失败模式。
当一个命令由于致命信号而终止,其信号编号为 N 时,Bash 会使用 128 + N 作为退出状态。
如果未找到命令,则为执行该命令而创建的子进程返回状态为 127。如果找到命令但不可执行,则返回状态为 126。
如果命令由于扩展或重定向期间的错误而失败,则退出状态大于零。
退出状态被 Bash 条件命令(参见Conditional Constructs)和一些列表构造(参见Lists of Commands)使用。
所有 Bash 内置命令在成功时返回零的退出状态,失败时返回非零状态,因此它们可以被条件语句和列表结构使用。所有内置命令在用法不正确时通常返回退出状态 2,一般是无效选项或缺少参数导致。
💡 最后一个命令的退出状态可以通过特殊参数 $? 获取(参见 Special Parameters)。
Bash 本身会返回最后执行命令的退出状态,除非发生语法错误,此时会以非零值退出。另请参见 exit 内置命令。
示例:
$ pwd
/home/vagrant
$ echo $?
0
$ ppwd
-bash: ppwd: command not found
$ echo $?
127
$ ./a.sh
-bash: ./a.sh: Permission denied
$ echo $?
126
$ ls *.sh
a.sh
$ [[ -f a.sh ]] && wc -c a.sh
8 a.sh
$ [[ -f b.sh ]] && wc -c a.sh
$ ls *.sh
a.sh
$ [[ -f b.sh ]] || touch b.sh
$ ls -l *.sh
-rw-r–r–. 1 vagrant vagrant 8 Jan 6 07:25 a.sh
-rw-r–r–. 1 vagrant vagrant 0 Jan 6 07:58 b.sh
$ [[ -f a.sh ]] || touch a.sh
$ ls -l *.sh
-rw-r–r–. 1 vagrant vagrant 8 Jan 6 07:25 a.sh
-rw-r–r–. 1 vagrant vagrant 0 Jan 6 07:58 b.sh
3.7.6 Signals
当 Bash 处于交互模式时,如果没有设置任何陷阱,它会忽略 SIGTERM(这样 ‘kill 0’ 就不会终止一个交互式 shell),并捕获和处理 SIGINT(这样 wait 内置命令可以被中断)。当 Bash 收到 SIGINT 时,它会跳出任何正在执行的循环。在所有情况下,Bash 都会忽略 SIGQUIT。如果作业控制生效(参见作业控制),Bash 会忽略 SIGTTIN、SIGTTOU 和 SIGTSTP。
trap 内置命令用于修改 shell 的信号处理方式,如下所述。
Bash 执行的非内置命令,其信号处理程序默认设置为从其父进程继承的值,除非使用 trap 将它们设置为忽略,在这种情况下子进程也会忽略它们。当作业控制未生效时,异步命令除了这些继承的处理程序外,还会忽略 SIGINT 和 SIGQUIT。由于命令替换而运行的命令会忽略键盘生成的作业控制信号 SIGTTIN、SIGTTOU 和 SIGTSTP。
默认情况下,shell 在接收到 SIGHUP 信号时会退出。在退出之前,交互式 shell 会将 SIGHUP 信号重新发送给所有作业,无论是运行中的还是已停止的。shell 会向已停止的作业发送 SIGCONT 信号,以确保它们接收到 SIGHUP(有关运行中和已停止作业的更多信息,请参见作业控制)。要防止 shell 向特定作业发送 SIGHUP 信号,可以使用 disown 内建命令将其从作业表中移除(参见作业控制内建命令),或使用 disown -h 标记该作业不接收 SIGHUP。
disown 示例:
$ sleep 1000&
[1] 15089
$ ps -ef|grep 15080
vagrant 15080 1 0 08:25 pts/0 00:00:00 sleep 300
vagrant 15093 14556 0 08:29 pts/0 00:00:00 grep –color=auto 15080
$ ps -p 15089
PID TTY TIME CMD
15089 pts/0 00:00:00 sleep
$ jobs
[1]+ Running sleep 1000 &
$ disown %%
$ jobs
$ ps -p 15089
PID TTY TIME CMD
15089 pts/0 00:00:00 sleep
disown -h示例:
## 第1轮
$ sleep 1000 &
[1] 15295
$ kill -HUP $$
# 当前终端退出了,但退出前将HUP信号发送给了sleep命令
# 因此sleep命令也退出了
$ ps -ef|grep sleep|grep -v grep
$
## 第2轮
$ sleep 1000&
[1] 15351
$ disown -h %%
$ kill -HUP $$
# 当前终端退出了,由于设置了disown,因此shell退出时不会将HUP信号发送给sleep命令
# 此时从其他终端查看,sleep命令仍在运行
$ ps -p 15351
PID TTY TIME CMD
15351 ? 00:00:00 sleep
$ kill -HUP 15351
$ ps -p 15351
PID TTY TIME CMD
如果使用 shopt 设置了 huponexit shell 选项(参见 The Shopt Builtin),当交互式登录 shell 退出时,Bash 会向所有作业发送 SIGHUP 信号。
如果 Bash 正在等待一个命令完成,并且收到了已设置 trap 的信号,它不会立即执行 trap,而是要等到命令完成后才执行。如果 Bash 正在通过 wait 内置命令等待一个异步命令,并且收到了已设置 trap 的信号,wait 内置命令会立即以大于 128 的退出状态返回,随后 shell 会立刻执行 trap。
当作业控制未启用,并且 Bash 正在等待前台命令完成时,shell 会接收到由键盘生成的信号,例如 SIGINT(通常由 ^C 生成),用户通常打算将这些信号发送给该命令。这是因为 shell 和命令与终端处于同一进程组,而 ^C 会向该进程组中的所有进程发送 SIGINT。由于Bash在非交互式 shell 时默认不启用作业控制,因此这种情况在非交互式 shell 中最为常见。
当启用作业控制且 Bash 正在等待前台命令完成时,shell 不会接收到键盘生成的信号,因为它不在与终端相同的进程组中。这种情况在交互式 shell 中最为常见,在这种情况下,Bash 默认会尝试启用作业控制。有关进程组的更深入讨论,请参见作业控制。
当作业控制未启用,并且 Bash 在等待前台命令时收到 SIGINT,它会等待该前台命令终止,然后再决定如何处理 SIGINT:
当启用作业控制时,Bash 在等待前台命令时不会接收由键盘产生的信号,例如 SIGINT。交互式 shell 不会理会 SIGINT,即使前台命令因此而终止,它也只会记录其退出状态。如果 shell 不是交互式的,并且前台命令因 SIGINT 而终止,Bash 会假装它自己收到了 SIGINT(如上面的场景 1),以保持兼容性。
示例:
## 输出同 kill -l
$ trap -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
下面这个有趣的trap示例参考了这里,稍微简化了下语法:
#!/bin/sh
trap 'increment' SIGINT
increment()
{
echo "Caught SIGINT …"
((X+=500))
if ((X>2000))
then
echo "Okay, I'll quit …"
exit 1
fi
}
X=0
while :
do
echo "X=$X"
sleep 1
done
运行情况如下,按4次<Ctrl+C>后程序退出:
$ ./a.sh
X=0
X=0
X=0
^CCaught SIGINT ...
X=500
X=500
^CCaught SIGINT ...
X=1000
X=1000
^CCaught SIGINT ...
X=1500
X=1500
X=1500
^CCaught SIGINT ...
X=2000
X=2000
^CCaught SIGINT ...
Okay, I'll quit ...
还有一些没看的,先留存:
- Bash Guide for Beginners:Traps
- bash signals posts in Stack Exchange
网硕互联帮助中心




评论前必须登录!
注册