GNU Bash 参考手册(中文版)(四)

5 Shell 变量

本章描述 Bash 所使用的 Shell 变量。Bash 会自动为若干变量赋予默认值。

5.1 Bourne Shell 变量

Bash 以与 Bourne Shell 相同的方式使用某些 Shell 变量。在某些情况下,Bash 会为该变量赋予默认值。

CDPATH(CDPATH)

以冒号分隔的目录列表,用作 cd 内建命令的搜索路径。

译者注CDPATHPATH 一样是冒号分隔的目录列表,但只供 cd 内建命令查找目录使用,查找规则也相同——空目录名(相邻冒号或首尾冒号)同样表示当前目录。与 PATH 不同的是,Bash 不会自动为它设置默认值,系统也没有统一约定。实测:在交互式 Shell 中,cd 通过 CDPATH 找到目标目录时会打印出新目录的完整路径名(在本机 Bash 5.3.9 上验证):

# 译者注示例:cd 借助 CDPATH 找到目录,并打印新目录路径
$ mkdir -p /tmp/cp-demo/sub && CDPATH=/tmp/cp-demo cd sub
/tmp/cp-demo/sub

HOME(HOME)

当前用户的主目录;cd 内建命令的默认目标。该变量的值也用于波浪号展开(参见第 3.5.2 节(波浪号展开))。

译者注cd 不带参数时回到 $HOMEcd - 则回到上一个工作目录(OLDPWD,见第 5.2 节)。波浪号展开中,~ 单独出现时展开为 $HOME 的值,~user 则查用户数据库,两者不完全等价。该变量通常由登录进程设置,但脚本中可以自由修改它——例如用 HOME=$(mktemp -d) 给命令造一个干净的临时主目录。

IFS(IFS)

用于分隔字段的字符列表;Shell 在展开过程中拆分单词时,以及 read 内建命令把一行拆分为单词时都会用到它。关于单词拆分的描述,参见第 3.5.7 节(单词拆分)。

译者注IFS 的默认值是空格、制表符、换行三个字符,即 $' \t\n'(可运行 printf '%q' "$IFS" 验证)。注意:展开结果的单词拆分只把「IFS 中的空白字符」(空格与制表符)的连续序列折叠成一个分隔符,而 IFS 中的其他字符每个都是独立分隔符。修改 IFS 会直接影响 read 的字段拆分,例如解析 /etc/passwd 时常用 IFS=:

# 译者注示例:默认 IFS 与自定义 IFS 对 read 拆分的影响
$ read -r a b <<< "foo:bar"; printf '[%s] [%s]\n' "$a" "$b"
[foo:bar] []
$ IFS=: read -r a b <<< "foo:bar"; printf '[%s] [%s]\n' "$a" "$b"
[foo] [bar]

MAIL(MAIL)

如果该值被设置为一个文件名或目录名,且 MAILPATH 变量未设置,Bash 会在指定文件或 Maildir 格式的目录中有新邮件到达时告知用户。

MAILPATH(MAILPATH)

以冒号分隔的文件名列表,Shell 会周期性地检查其中是否有新邮件。每个列表条目都可以通过「?」把文件名与消息分隔开,以指定新邮件到达邮件文件时打印的消息。在消息文本中使用时,$_ 会展开为当前邮件文件的名称。

译者注:这是 Bash 的「有邮件时提示」机制:Shell 每隔 MAILCHECK 秒(见第 5.2 节,默认 60 秒)检查一次。设置 MAILPATHMAIL 不再生效;MAIL 可指向 Maildir 目录(每封邮件一个文件的格式),MAILPATH 则指向普通文件。消息文本中的 $_ 在打印时展开为文件名,例如:

# 译者注示例:为新邮件提示自定义消息(其中 $_ 是邮件文件名)
$ MAILPATH='/tmp/mbox?你有一封新邮件($_)'

OPTARG(OPTARG)

getopts 内建命令处理的上一个选项参数的值。

OPTIND(OPTIND)

getopts 内建命令将要处理的下一个参数的索引。

译者注OPTIND 在每次 Shell 启动时初始化为 1(可在 bash -c 'echo $OPTIND' 中验证),OPTARG 在每次 getopts 迭代时被更新:选项带参数时它是该参数,不带参数时被取消设置或为空。选项解析完后,常用 shift $((OPTIND - 1)) 把位置参数中的选项部分移掉。示例:

# 译者注示例:getopts 循环中 OPTARG 与 OPTIND 的变化
$ set -- -a foo -b
$ while getopts "a:b" o; do echo "选项 $o, 参数[$OPTARG], 下一个索引 $OPTIND"; done
选项 a, 参数[foo], 下一个索引 3
选项 b, 参数[], 下一个索引 4

PATH(PATH)

以冒号分隔的目录列表,Shell 在其中查找命令。PATH 值中的零长度(空)目录名表示当前目录。空目录名可以表现为两个相邻的冒号,也可以表现为开头或结尾的冒号。默认路径与系统相关,由安装 bash 的管理员设置。常见值为 /usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin

译者注:两个要点。其一,查找按列表顺序进行,先找到者胜出,因此把目录排在前面可以「劫持」同名命令(这也常用于在脚本开头放入自己的工具目录)。其二,空目录名表示当前目录,等价于把 . 写进路径——把当前目录放进 PATH 存在安全隐患(攻击者可在目录里放同名的恶意程序),现代系统普遍用前导冒号或空元素以外的写法来避免,或者干脆用绝对路径。空元素的验证示例:

# 译者注示例:PATH 中的空目录名(此处为尾部冒号)表示当前目录
$ mkdir -p /tmp/pwdemo && printf '#!/bin/sh\necho "当前目录中找到"\n' > /tmp/pwdemo/tool && chmod +x /tmp/pwdemo/tool
$ cd /tmp/pwdemo && PATH="/bin:" && tool
当前目录中找到

PS1(PS1)

主提示符字符串。默认值为 \s-\v\$ 。显示 PS1 之前会展开的转义序列的完整列表,参见第 6.9 节(控制提示符)。

译者注:四个提示符变量的分工:PS1 主提示符(等待新命令),PS2 辅助提示符(命令续行、here-document 内),PS3select 的提示符(默认 #? ),PS4set -x 跟踪输出的前缀(默认 + )。默认值 \s-\v\$ 中,\s 是 Shell 名(bash),\v 是版本号(如 5.3),\$ 在普通用户下显示 $、在 root(有效 UID 为 0)下显示 #。提示符中常用 \u(用户名)、\h(主机名)、\w(当前目录)等转义,详见第 6.9 节(控制提示符)。

PS2(PS2)

辅助提示符字符串。默认值为 > PS2 在显示前以与 PS1 相同的方式展开。

译者注PS2 出现在一条命令尚未结束的时候——例如多行命令的续行、here-document 的正文输入过程中。PS1 中可用的转义序列在 PS2 中同样适用,可据此自定义续行提示(如显示当前行号等)。

5.2 Bash 变量

这些变量由 Bash 设置或使用,但其他 Shell 通常不会对它们作特殊处理。

Bash 使用的一些变量在其他章节中介绍:用于控制作业控制设施的变量(参见第 7.3 节(作业控制变量))。

__

$_,即下划线。)该变量有若干种含义,视上下文而定。Shell 启动时,$_ 被设置为用于调用 Shell、或执行中的 Shell 脚本的路径名(按环境或参数列表传入的值)。此后,它展开为前台执行的上一条简单命令在展开之后的最后一个参数。它还会被设置为用于调用每条已执行命令的完整路径名,并放入导出给该命令的环境中。检查邮件时,$_ 展开为邮件文件的名称。

译者注$_ 最常见的用途是在交互式 Shell 中取「上一条命令的最后一个参数」,例如 mkdir dir && cd "$_" 一步完成建目录并进入。要注意它的更新时机:只有「在 Shell 顶层前台执行」的简单命令才会更新 $_——管道中的命令在子 Shell 里运行,对 $_ 的更新传不回父 Shell(实测 echo a | cat 之后 $_ 保持原值,既不是 a 也不是 cat);赋值语句之后 $_ 则被置空。验证示例:

# 译者注示例:$_ 由前台顶层执行的简单命令更新
$ echo one two three; echo "$_"
one two three
three
$ echo a | cat >/dev/null; echo "[$_]"   # 管道不会更新 $_
[three]
$ x=5; echo "[$_]"                       # 赋值语句后 $_ 为空
[]

BASH(BASH)

用于执行当前 Bash 实例的完整路径名。

译者注$BASH 给出当前 bash 可执行文件的完整路径,通常与 shebang 里写的解释器一致。判断「当前 Shell 是不是 bash」时,用 $BASH_VERSION 是否为空比用 $BASH 更可靠——其他 Shell(如 zsh、dash)中 BASH 通常未定义,而 $SHELL 更是只反映登录 Shell(见第 5.2 节 SHELL 条目)。

BASHOPTS(BASHOPTS)

以冒号分隔的已启用 Shell 选项列表。列表中的每个单词都是 shopt 内建命令 -s 选项的合法参数(参见第 4.3.2 节(shopt 内建命令))。出现在 BASHOPTS 中的选项,是 shopt 报告为「on」的那些选项。如果 Bash 启动时该变量已在环境中,Shell 会在读取任何启动文件之前启用列表中的每个选项。如果该变量被导出,子 Shell 将启用列表中的每个选项。该变量是只读的。

译者注BASHOPTS 与第 5.2 节的 SHELLOPTS 是两套并列的选项列表:前者对应 shopt 控制的选项,后者对应 set -o 控制的选项。因为变量本身只读,不能直接赋值来启用选项,但把它导出后,子 Shell 会在启动时自动启用列表中的选项——这是把一组 shopt 选项「广播」给所有子进程的惯用方法。示例:

# 译者注示例:BASHOPTS 是只读的;shopt 开启的选项会出现在列表中
$ BASHOPTS=foo
bash: BASHOPTS: readonly variable
$ shopt -s globstar; [[ $BASHOPTS == *globstar* ]] && echo "globstar 已开启"
globstar 已开启

BASHPID(BASHPID)

展开为当前 Bash 进程的进程 ID。在某些情况下这与 $$ 不同,例如不需要重新初始化 Bash 的子 Shell 中。对 BASHPID 的赋值没有效果。如果 BASHPID 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注$$ 在子 Shell 中保持父进程的 PID 不变,而 BASHPID 每次引用都会查询实际进程,因此在子 Shell、命令替换、管道组件等确实派生新进程的场合,两者不再相等。判断「当前是否发生了 fork」最直接的办法就是比较二者:

# 译者注示例:子 Shell 中 $$ 保持不变,而 BASHPID 跟随新进程
$ [ "$$" = "$BASHPID" ] && echo "顶层: 相同" || echo "顶层: 不同"
顶层: 相同
$ ( [ "$$" = "$BASHPID" ] && echo "子Shell: 相同" || echo "子Shell: 不同" )
子Shell: 不同

BASH_ALIASES(BASH_ALIASES)

一个关联数组变量,其成员与 alias 内建命令维护的内部别名列表相对应(参见第 4.1 节(Bourne Shell 内建命令))。添加到该数组的元素会出现在别名列表中;不过,取消设置数组元素目前不会把别名从别名列表中删除。如果 BASH_ALIASES 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注BASH_ALIASES 提供了以编程方式读写别名的接口。要注意别名的展开发生在语法分析阶段(解析到别名名称所在单词时即展开),因此「同一条命令行里先给 BASH_ALIASES 赋值、后使用该别名」不会生效——赋值发生在展开之后。非交互式 Shell 默认不展开别名,需要先开启 expand_aliases 选项:

# 译者注示例:向 BASH_ALIASES 添加元素等价于定义别名
$ bash -O expand_aliases -c $'BASH_ALIASES[hx]="echo 你好"\nhx'
你好

BASH_ARGC(BASH_ARGC)

一个数组变量,其值是当前 Bash 执行调用栈中每一帧的参数个数。当前子程序(Shell 函数或通过 .source 执行的脚本)的参数个数位于栈顶。执行子程序时,传入的参数个数被压入 BASH_ARGC。只有在扩展调试模式下 Shell 才设置 BASH_ARGC(关于 shopt 内建命令的 extdebug 选项的说明,参见第 4.3.2 节(shopt 内建命令))。在 Shell 已经开始执行子程序之后才设置 extdebug,或未设置 extdebug 时引用该变量,可能导致不一致的值。对 BASH_ARGC 的赋值没有效果,且它不能被取消设置。

BASH_ARGV(BASH_ARGV)

一个数组变量,包含当前 Bash 执行调用栈中的所有参数。最后一次子程序调用的最后一个参数位于栈顶;最初调用的第一个参数位于栈底。执行子程序时,Shell 把提供的参数压入 BASH_ARGV。只有在扩展调试模式下 Shell 才设置 BASH_ARGV(关于 shopt 内建命令的 extdebug 选项的说明,参见第 4.3.2 节(shopt 内建命令))。在 Shell 已经开始执行脚本之后才设置 extdebug,或未设置 extdebug 时引用该变量,可能导致不一致的值。对 BASH_ARGV 的赋值没有效果,且它不能被取消设置。

译者注BASH_ARGCBASH_ARGV 是一对用于栈回溯的变量,需要 shopt -s extdebug 才会被填充。数组第一个元素对应栈顶(最近一次调用)。实测示例:函数 f 被以 f a b c 调用时,BASH_ARGC(3 0)——栈顶帧有 3 个参数,栈底的脚本主帧有 0 个参数;BASH_ARGV(c b a)——栈顶是最后一次调用的最后一个参数,栈底是最初调用的第一个参数:

# 译者注示例:extdebug 模式下用 BASH_ARGC/BASH_ARGV 查看调用栈的参数
$ shopt -s extdebug
$ f() { echo "argc=${BASH_ARGC[*]}"; echo "argv=${BASH_ARGV[*]}"; }
$ f a b c
argc=3 0
argv=c b a

BASH_ARGV0(BASH_ARGV0)

引用该变量时,它展开为 Shell 或 Shell 脚本的名称(与 $0 相同;特殊参数 0 的说明参见第 3.4.2 节(特殊参数))。给 BASH_ARGV0 赋值会把 $0 设置为相同的值。如果 BASH_ARGV0 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注$0 本身不能赋值,而 BASH_ARGV0 提供了修改它的合法途径。交互式 Shell 中修改 $0 会影响 ps 里显示的命令名,这在某些伪装场景或需要「以另一个名字运行」时有用。实测:赋值后 $0 立即同步变化:

# 译者注示例:给 BASH_ARGV0 赋值会同步修改 $0
$ BASH_ARGV0=foo; echo $0
foo

BASH_CMDS(BASH_CMDS)

一个关联数组变量,其成员与 hash 内建命令维护的内部命令哈希表相对应(参见第 4.1 节(Bourne Shell 内建命令))。向该数组添加元素会使它们出现在哈希表中;不过,取消设置数组元素目前不会从哈希表中删除命令名。如果 BASH_CMDS 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注:命令哈希表是 Bash 对「在 PATH 中查到的命令的完整路径」的缓存,命中后不再逐目录查找,可显著加快循环中反复执行同一命令的速度。用 hash 查看与清理(hash -r 清空)。实测:执行过一次 ls 后,BASH_CMDS 中即出现 [ls]="/usr/bin/ls" 之类的条目(本机为 /usr/bin/ls)。

BASH_COMMAND(BASH_COMMAND)

展开为当前正在执行、或即将执行的命令,除非 Shell 正在执行作为陷阱(trap)结果的命令——此时它是陷阱发生时正在执行的命令。如果 BASH_COMMAND 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注BASH_COMMANDDEBUGERREXIT 等陷阱配合,可以知道「陷阱是在哪条命令上触发的」。注意在陷阱动作本身内部,BASH_COMMAND 反映的是触发陷阱的那条命令,而不是陷阱动作中的命令。实测:

# 译者注示例:DEBUG 陷阱中 BASH_COMMAND 显示触发陷阱的命令
$ trap 'echo "trap 中执行: $BASH_COMMAND"' DEBUG
$ echo hi
trap 中执行: echo hi
hi
$ trap - DEBUG
trap 中执行: trap - DEBUG

BASH_COMPAT(BASH_COMPAT)

该值用于设置 Shell 的兼容级别。关于各种兼容级别及其效果的说明,参见第 6.12 节(Shell 兼容模式)。该值可以是对应于所需兼容级别的小数(如 4.2)或整数(如 42)。如果 BASH_COMPAT 被取消设置或设置为空字符串,兼容级别被设置为当前版本的默认值。如果 BASH_COMPAT 被设置为不是合法兼容级别的值,Shell 打印一条错误消息,并把兼容级别设置为当前版本的默认值。合法值的一个子集对应于第 6.12 节(Shell 兼容模式)中描述的兼容级别。例如,4.2 和 42 是合法值,它们对应于 compat42 这个 shopt 选项,并把兼容级别设置为 42。当前版本也是合法值。

译者注:兼容模式用于让 Bash 5.x 模拟旧版本(如 3.2、4.2)的某些行为,帮助老脚本平滑迁移。值 4.242 等价,与 shopt -s compat42 的效果相同;「当前版本也是合法值」意味着可以写 BASH_COMPAT=5.3(或 53)显式声明版本。实测验证:

# 译者注示例:BASH_COMPAT=42 等价于开启 compat42 选项
$ BASH_COMPAT=42 bash -c 'shopt -p compat42'
shopt -s compat42

BASH_ENV(BASH_ENV)

如果调用 Bash 执行 Shell 脚本时该变量已设置,它的值会被展开并用作启动文件的名称,在执行脚本之前读取。Bash 不使用 PATH 来搜索生成的文件名。参见第 6.2 节(Bash 启动文件)。

译者注BASH_ENV 是非交互式 Shell 的「启动文件」入口——交互式 Shell 的对应机制是 ~/.bashrc(由 Bash 自身定位),而 BASH_ENV 需要显式设置。注意与 ENV(第 5.2 节)的分工:ENV 只影响 POSIX 模式下的交互式 Shell,BASH_ENV 影响非交互式 Shell。实测:Bash 在执行脚本前先读取 BASH_ENV 指定的文件:

# 译者注示例:BASH_ENV 指定的启动文件在脚本执行前被读取
$ printf 'echo "BASH_ENV 生效:脚本执行前的初始化"\n' > /tmp/env.sh
$ printf 'echo "脚本正文"\n' > /tmp/real.sh
$ BASH_ENV=/tmp/env.sh bash /tmp/real.sh
BASH_ENV 生效:脚本执行前的初始化
脚本正文

BASH_EXECUTION_STRING(BASH_EXECUTION_STRING)

-c 调用选项的命令参数。

译者注:即 bash -c '命令字符串' 中的那个「命令字符串」。可用来区分「通过 -c 执行的脚本」与「通过文件名执行的脚本」——后一种情况下该变量不存在。实测:

# 译者注示例:BASH_EXECUTION_STRING 就是 -c 后面的命令字符串
$ bash -c 'echo "[$BASH_EXECUTION_STRING]"'
[echo "[$BASH_EXECUTION_STRING]"]

BASH_LINENO(BASH_LINENO)

一个数组变量,其成员是 FUNCNAME 的每个对应成员被调用处的源文件行号。${BASH_LINENO[$i]}${FUNCNAME[$i]} 被调用处所在源文件(${BASH_SOURCE[$i+1]})中的行号(如果在另一个 Shell 函数内引用,则为 ${BASH_LINENO[$i-1]})。要获取当前行号,请使用 LINENO。对 BASH_LINENO 的赋值没有效果,且它不能被取消设置。

译者注BASH_LINENOBASH_SOURCEFUNCNAME 三个数组按相同的索引对齐,共同刻画调用栈;caller 内建命令(参见第 4.2 节(Bash 内建命令))正是基于它们打印「文件:行号 + 函数名」的。完整的栈回溯示例见第 5.2 节 FUNCNAME 条目的译者注。

BASH_LOADABLES_PATH(BASH_LOADABLES_PATH)

以冒号分隔的目录列表,enable 命令在其中查找可动态加载的内建命令。

译者注:与 enable -f 文件 名字(参见第 4.2 节(Bash 内建命令))配合使用:设置该变量后,enable -f 可以只给文件名而不写完整路径。加载式内建命令是 Bash 随发行版附带的动态共享库(如 sleeprealpath 等),需要时才加载,以节省启动开销。

BASH_MONOSECONDS(BASH_MONOSECONDS)

每次引用该变量时,它展开为系统单调时钟(monotonic clock)返回的值(若系统提供)。如果没有单调时钟,则等价于 EPOCHSECONDS。如果 BASH_MONOSECONDS 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注:Bash 5.3 新增。单调时钟只增不减,不受 datentpdate 等修改系统时间的影响,因此适合测量时间间隔(而 EPOCHSECONDS/date +%s 是墙上时钟,系统时间被回拨时会「倒退」)。实测(数值随运行而异):

# 译者注示例:BASH_MONOSECONDS 随时间单调递增
$ bash -c 'echo "$BASH_MONOSECONDS"; sleep 1; echo "$BASH_MONOSECONDS"'
17110
17111

BASH_REMATCH(BASH_REMATCH)

一个数组变量,其成员由 [[ 条件命令的 =~ 二元操作符赋值(参见第 3.2.5.2 节(条件结构))。索引 0 的元素是与整个正则表达式匹配的字符串部分。索引 N 的元素是与第 N 个括号子表达式匹配的字符串部分。

译者注BASH_REMATCH 是脚本里做正则提取的标准手段:=~ 本身只返回匹配与否的退出状态,匹配的文本要靠 BASH_REMATCH 取回。索引 0 是整个匹配,从 1 开始才是各括号捕获组。实测:

# 译者注示例:[[ =~ ]] 的匹配结果存放在 BASH_REMATCH 中
$ [[ "abc123" =~ ([a-z]+)([0-9]+) ]]; echo "${BASH_REMATCH[0]} | ${BASH_REMATCH[1]} | ${BASH_REMATCH[2]}"
abc123 | abc | 123

BASH_SOURCE(BASH_SOURCE)

一个数组变量,其成员是 FUNCNAME 数组变量中对应 Shell 函数名被定义处的源文件名。Shell 函数 ${FUNCNAME[$i]} 定义在文件 ${BASH_SOURCE[$i]} 中,并从 ${BASH_SOURCE[$i+1]} 被调用。对 BASH_SOURCE 的赋值没有效果,且它不能被取消设置。

译者注${BASH_SOURCE[0]} 是当前函数(或脚本)所在文件的路径,在函数里想「知道自己写在哪一个文件」时非常有用,例如拼出与脚本同目录的配置文件路径:dirname "${BASH_SOURCE[0]}"。与 FUNCNAMEBASH_LINENO 联合的栈回溯示例见 FUNCNAME 条目的译者注。

BASH_SUBSHELL(BASH_SUBSHELL)

每当 Shell 开始在某个子 Shell 或子 Shell 环境中执行时,该变量加 1。初始值为 0。如果 BASH_SUBSHELL 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注( ) 组合命令、$( ) 命令替换、管道组件等都会进入子 Shell 环境,进入一层 BASH_SUBSHELL 就加 1。注意:子 Shell 环境并不总是派生新进程(现代 bash 对 ( ) 末尾的最后一条命令可能做优化),所以「在子 Shell 中」与「在新进程中」不完全等价——需要判断是否真的 fork 时,与 BASHPID(第 5.2 节)一起比较最可靠。实测:

# 译者注示例:每进入一层子 Shell 环境,BASH_SUBSHELL 加 1
$ echo "顶层 $BASH_SUBSHELL"; ( echo "子Shell $BASH_SUBSHELL" )
顶层 0
子Shell 1

BASH_TRAPSIG(BASH_TRAPSIG)

设置为正在执行的陷阱动作所对应的信号编号。关于信号编号和陷阱执行的信息,参见 trap 的说明(参见第 4.1 节(Bourne Shell 内建命令))。

译者注:Bash 5.3 新增。当多个信号共用一个陷阱动作(如 trap 'handler' INT TERM)时,在陷阱动作内部通过 BASH_TRAPSIG 就能区分这次触发来自哪个信号——例如 case $BASH_TRAPSIG in 2) ... ;; 15) ... ;; esac 分别处理。注意 DEBUGRETURN 等伪信号没有对应的信号编号。

BASH_VERSINFO(BASH_VERSINFO)

一个只读数组变量(参见第 6.7 节(数组)),其成员保存当前 Bash 实例的版本信息。赋给数组成员的值如下:

BASH_VERSINFO[0]

主版本号(即 "release")。

BASH_VERSINFO[1]

次版本号(即 "version")。

BASH_VERSINFO[2]

补丁级别。

BASH_VERSINFO[3]

构建版本。

BASH_VERSINFO[4]

发布状态(如 beta)。

BASH_VERSINFO[5]

MACHTYPE 的值。

译者注:与 BASH_VERSION(一个字符串)相比,BASH_VERSINFO 把版本拆成了六个可单独判断的字段,写兼容性判断更方便——例如 (( ${BASH_VERSINFO[0]} > 4 )) || (( ${BASH_VERSINFO[0]} == 4 && ${BASH_VERSINFO[1]} >= 4 )) 判断「是否 Bash 4.4 及以上」。本机实测(数值随版本而异):

# 译者注示例:BASH_VERSINFO 与 BASH_VERSION 对应,可逐项读取版本信息
$ echo "$BASH_VERSION"; echo "${BASH_VERSINFO[@]}"
5.3.9(1)-release
5 3 9 1 release x86_64-pc-linux-gnu

BASH_VERSION(BASH_VERSION)

展开为描述当前 Bash 实例版本的字符串(如 5.3.9(1)-release)。

BASH_XTRACEFD(BASH_XTRACEFD)

如果被设置为对应于合法文件描述符的整数,Bash 会把启用 set -x 时生成的跟踪输出写到该文件描述符,而不是标准错误。这样可以把跟踪输出与诊断和错误消息分开。当 BASH_XTRACEFD 被取消设置或被赋予新值时,该文件描述符会被关闭。取消设置 BASH_XTRACEFD 或给它赋空字符串,会使跟踪输出发送到标准错误。注意,把 BASH_XTRACEFD 设置为 2(标准错误文件描述符)然后再取消设置,将导致标准错误被关闭。

译者注:脚本调试时 set -x 的跟踪输出和脚本自身的错误消息混在标准错误里,用 BASH_XTRACEFD 可以把跟踪单独写进日志文件,屏幕上只留正常输出。最后一句的坑要当心:把 BASH_XTRACEFD 设为 2 后取消设置,会顺手关掉标准错误,之后的错误消息全部静默。实测:

# 译者注示例:把 set -x 的跟踪输出单独写入日志文件
$ exec 3>/tmp/xtrace.log; BASH_XTRACEFD=3; set -x; echo hi; set +x
hi
$ BASH_XTRACEFD=; exec 3>&-
$ cat /tmp/xtrace.log
+ echo hi
+ set +x

CHILD_MAX(CHILD_MAX)

设置 Shell 需要记住的已退出子进程状态值的个数。Bash 不允许该值降到低于 POSIX 规定的最小值,并且有一个当前为 8192 的最大值,不能超过。最小值与系统相关。

译者注:Bash 5.3 新增。Shell 需要记住每个已退出子进程的退出状态(供 $?、作业表等使用),若子进程退出后状态未被及时回收,就会在系统进程表中留下僵尸(zombie)状态。该变量限制 Shell 同时记住的此类状态个数,防止长期运行的大脚本让状态无限累积。实测:赋值本身不校验范围(赋 0、负数、8193 甚至非数值都会被原样接受),因此「不小于 POSIX 规定的最小值、不超过 8192」是 Shell 实际使用该值时遵守的界限,而非赋值时的检查。

COLUMNS(COLUMNS)

select 命令在打印选择列表时用它确定终端宽度。如果启用了 checkwinsize 选项(参见第 4.3.2 节(shopt 内建命令)),或在交互式 Shell 中收到 SIGWINCH 信号时,会自动设置。

译者注COLUMNSLINES(见第 5.2 节)是「终端尺寸」变量,主要影响 select 的选择列表排版(每行几列)与某些全屏程序。非交互式 Shell 中二者不会自动设置,需要时可用 stty size 查询终端尺寸并自行赋值。窗口大小改变时,交互式 Shell 收到 SIGWINCH 信号,若开启了 checkwinsize 选项(默认开启),每次命令执行后都会重新检查并更新这两个变量。

COMP_CWORD(COMP_CWORD)

${COMP_WORDS} 中一个单词的索引,该单词包含当前光标位置。该变量仅在可编程补全设施调用的 Shell 函数中可用(参见第 8.6 节(可编程补全))。

COMP_KEY(COMP_KEY)

用于调用当前补全函数的按键(或按键序列的最后一个键)。该变量仅在可编程补全设施调用的 Shell 函数和外部命令中可用(参见第 8.6 节(可编程补全))。

COMP_LINE(COMP_LINE)

当前命令行。该变量仅在可编程补全设施调用的 Shell 函数和外部命令中可用(参见第 8.6 节(可编程补全))。

COMP_POINT(COMP_POINT)

当前光标位置相对于当前命令开头的索引。如果当前光标位置在命令末尾,该变量的值等于 ${#COMP_LINE}。该变量仅在可编程补全设施调用的 Shell 函数和外部命令中可用(参见第 8.6 节(可编程补全))。

COMP_TYPE(COMP_TYPE)

被设置为一个整数值,对应于导致补全函数被调用的补全类型:<TAB> 表示普通补全,? 表示连续按 Tab 后列出补全,! 表示部分单词补全时列出候选,@ 表示单词未被修改时列出补全,% 表示菜单补全。该变量仅在可编程补全设施调用的 Shell 函数和外部命令中可用(参见第 8.6 节(可编程补全))。

译者注COMP_TYPE 的取值就是对应字符的 ASCII 码:普通补全 <TAB> 为 9(制表符的 ASCII 码),! 为 33,% 为 37,? 为 63,@ 为 64。补全函数据此可以区分「用户按了一次 Tab」还是「连按两次列出全部候选」(9 与 63),从而决定返回简短还是详尽的候选。

COMP_WORDBREAKS(COMP_WORDBREAKS)

Readline 库在执行单词补全时视为单词分隔符的字符集合。如果 COMP_WORDBREAKS 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注:补全时命令行按这些字符切分成单词。默认值来自 Readline 库——本机 Bash 5.3.9 上实测为 $' \t\n"\'@><=;|&(:'(空格、制表符、换行、"'@><=;|&(:)。把它改小可以让补全不因某些字符中断——例如去掉 : 后,PATH=/usr/bin: 之后的路径补全会连冒号后的部分一起处理。

COMP_WORDS(COMP_WORDS)

一个数组变量,由当前命令行的各个单词组成。命令行按 Readline 的拆分方式拆分为单词,使用上面描述的 COMP_WORDBREAKS。该变量仅在可编程补全设施调用的 Shell 函数中可用(参见第 8.6 节(可编程补全))。

译者注:这组 COMP_* 变量只在补全函数被调用的瞬间存在(补全结束后即失效),且只有交互式 Shell 的可编程补全(complete/compgen,参见第 8.7 节(可编程补全内建命令))会调用它们。调试补全函数的惯用方法是把内容写进文件:

# 译者注示例:补全函数内部查看 COMP_* 变量(按 Tab 后查看日志)
$ _mycomp() { printf 'cword=%s line=%s point=%s\n' "$COMP_CWORD" "$COMP_LINE" "$COMP_POINT" >> /tmp/comp.log; }
$ complete -F _mycomp mycmd

COMPREPLY(COMPREPLY)

一个数组变量,Bash 从其中读取由可编程补全设施调用的 Shell 函数生成的候选补全(参见第 8.6 节(可编程补全))。每个数组元素包含一个候选补全。

译者注:补全函数的核心输出通道:函数往 COMPREPLY 里填候选,Bash 随后把它们显示或补到命令行上。最简单的补全函数只做一件事——根据用户输入过滤后填数组:

# 译者注示例:把固定候选填入 COMPREPLY 即可实现命令补全
$ _fruit() { COMPREPLY=(apple apricot banana); }
$ complete -F _fruit fruit

COPROC(COPROC)

一个数组变量,创建它来保存无名协程的输出与输入文件描述符(参见第 3.2.6 节(协程))。

译者注coproc 命令 启动无名协程时,其标准输出与标准输入的文件描述符保存在 COPROC[0]COPROC[1];用 coproc 名字 { 命令; } 指定名字时则保存在 名字[0]/名字[1]。随后即可用 read -u "${COPROC[0]}" 从协程读取、用 printf ... >&"${COPROC[1]}" 向协程写入——文件描述符编号每次会话不同,务必通过数组取值,不要硬编码。

DIRSTACK(DIRSTACK)

一个数组变量,保存目录栈的当前内容。目录在栈中的顺序与 dirs 内建命令显示的顺序一致。对该数组变量成员的赋值可以用于修改已在栈中的目录,但添加和删除目录必须使用 pushdpopd 内建命令。对该变量的赋值不会改变当前目录。如果 DIRSTACK 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注:目录栈(参见第 6.8 节(目录栈))的内容可以像普通数组一样读取:${DIRSTACK[@]} 列出全部,${DIRSTACK[1]}cd - 会去的目录。注意栈顶(索引 0)永远是当前目录,直接给 DIRSTACK 数组赋值不会改变当前目录,只能由 pushd/popd/cd 实际改变。实测:

# 译者注示例:DIRSTACK 保存目录栈内容,栈顶(当前目录)在前
$ cd /tmp; pushd / >/dev/null; echo "${DIRSTACK[@]}"; popd >/dev/null
/ /tmp

EMACS(EMACS)

如果 Shell 启动时 Bash 在环境中找到该变量,且其值为 t,Bash 就假定 Shell 运行在 Emacs 的 shell 缓冲区中,并禁用行编辑。

ENV(ENV)

在 POSIX 模式下调用交互式 Shell 时,该变量像 BASH_ENV 一样被展开并执行(参见第 6.2 节(Bash 启动文件)与第 6.11.2 节(Bash 的 POSIX 模式))。

译者注ENV 是 POSIX 标准规定的交互式 Shell 启动文件机制。分工要分清:ENV 只在 POSIX 模式下对交互式 Shell 生效;BASH_ENV 用于非交互式 Shell(见第 5.2 节 BASH_ENV 条目);~/.bashrc 则是 Bash 自己的交互式启动文件。因此 ENV=~/.posixrc bash --posix 时,交互式会话会先读取 .posixrc

EPOCHREALTIME(EPOCHREALTIME)

每次引用该参数时,它展开为自 Unix 纪元(Epoch)以来的秒数,是一个微秒精度的浮点值(Epoch 的定义见 C 库函数 time 的文档)。对 EPOCHREALTIME 的赋值被忽略。如果 EPOCHREALTIME 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

EPOCHSECONDS(EPOCHSECONDS)

每次引用该参数时,它展开为自 Unix 纪元以来的秒数(Epoch 的定义见 C 库函数 time 的文档)。对 EPOCHSECONDS 的赋值被忽略。如果 EPOCHSECONDS 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注:二者都是「每次引用都重新查询系统时钟」的动态参数,因此 echo $EPOCHSECONDS $EPOCHSECONDS 可能打印两个不同的值(两次展开之间恰好跨过整秒)。EPOCHREALTIME 带微秒级小数,适合测量毫秒级间隔;EPOCHSECONDS 是整数秒,适合计算「今天过去了多少秒」之类的整秒运算。实测:

# 译者注示例:EPOCHREALTIME 是带微秒小数的浮点数,EPOCHSECONDS 是整数秒
$ [[ $EPOCHREALTIME == *.* ]] && echo "EPOCHREALTIME 是浮点数"
EPOCHREALTIME 是浮点数
$ echo ${#EPOCHSECONDS}   # 当前纪元秒数为 10 位数
10

EUID(EUID)

当前用户的数值有效用户 ID。该变量是只读的。

译者注EUID 是有效用户 ID,UID(见第 5.2 节)是真实用户 ID:运行 setuid 程序时二者不同,普通会话中二者相等。脚本判断「是否以 root 运行」的惯用法是 [[ $EUID -eq 0 ]]。三者均为只读,赋值直接报错:

# 译者注示例:EUID、UID、PPID 都是只读变量,赋值会报错
$ EUID=1
bash: EUID: readonly variable

EXECIGNORE(EXECIGNORE)

以冒号分隔的 Shell 模式列表(参见第 3.5.8.1 节(模式匹配)),定义通过 PATH 进行命令搜索时应忽略的文件名集合。其完整路径名与这些模式之一匹配的文件,在补全和通过 PATH 查找执行命令时,不被视为可执行文件。这不会影响 [test[[ 命令的行为。命令哈希表中的完整路径名不受 EXECIGNORE 约束。使用该变量忽略设置了可执行位、但并非可执行文件的共享库文件。模式匹配遵循 extglob Shell 选项的设置。

译者注:典型用途是忽略「被误加了可执行位的共享库」:PATH 里若混有含 .so 文件的目录,命令查找与补全会把它们当作可执行文件列出来,设置 EXECIGNORE='*.so' 即可过滤。注意两点:一是哈希表里已有的条目不受影响(需先 hash -r 清空);二是 [test[[ 不受其约束——因为它们要判断的是文件属性,而不是「能否通过 PATH 找到」。实测:

# 译者注示例:EXECIGNORE 让命令查找忽略匹配模式的可执行文件
$ mkdir -p /tmp/xe && printf '#!/bin/sh\necho hi\n' > /tmp/xe/libfoo.so && chmod +x /tmp/xe/libfoo.so
$ PATH=/tmp/xe:/usr/bin command -v libfoo.so
/tmp/xe/libfoo.so
$ PATH=/tmp/xe:/usr/bin EXECIGNORE='*.so' command -v libfoo.so; echo "rc=$?"
rc=1

FCEDIT(FCEDIT)

fc 内建命令默认使用的编辑器。

译者注fc(参见第 4.1 节(Bourne Shell 内建命令))用编辑器打开历史命令进行编辑重放。编辑器选择顺序:FCEDIT 未设置时依次回退到 EDITORvi。例如在脚本里 FCEDIT=/usr/bin/nano 即可让 fc 用 nano。

FIGNORE(FIGNORE)

执行文件名补全时要忽略的后缀的冒号分隔列表。后缀与 FIGNORE 中某个条目匹配的文件名,会从匹配文件名列表中排除。示例值为 .o:~

译者注:补全时按后缀过滤候选,示例值 .o:~ 会隐藏目标文件(.o)、备份文件(~ 结尾)等不想看到的名字。注意它只影响补全的候选列表,不影响 * 之类的文件名展开;条目不必以点开头(例如写 ,v 即可忽略 RCS 版本文件)。

FUNCNAME(FUNCNAME)

一个数组变量,包含当前执行调用栈中所有 Shell 函数的名称。索引 0 的元素是当前正在执行的 Shell 函数的名称。最底层的元素(索引最大的那个)是 "main"。该变量仅在 Shell 函数执行时存在。对 FUNCNAME 的赋值没有效果。如果 FUNCNAME 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

该变量可以与 BASH_LINENOBASH_SOURCE 一起使用。FUNCNAME 的每个元素在 BASH_LINENOBASH_SOURCE 中都有对应元素,共同描述调用栈。例如,${FUNCNAME[$i]} 是从文件 ${BASH_SOURCE[$i+1]} 的第 ${BASH_LINENO[$i]} 行被调用的。caller 内建命令使用这些信息显示当前调用栈。

译者注:这是 Bash 栈回溯的核心。${FUNCNAME[0]} 是当前函数名;数组在函数外不存在(实测:顶层 echo "${FUNCNAME[*]}" 输出为空)。与 BASH_SOURCEBASH_LINENO 联合即可打印完整调用链——caller 内建命令内部正是这样实现的。实测:

# 译者注示例:FUNCNAME 与 BASH_SOURCE、BASH_LINENO 联合描绘调用栈
$ cat > /tmp/stack.sh <<'EOF'
top() { middle; }
middle() { bottom; }
bottom() {
  for i in "${!FUNCNAME[@]}"; do
    if [ -n "${BASH_SOURCE[$i+1]}" ]; then
      printf '%s 被 %s 的第 %s 行调用\n' "${FUNCNAME[$i]}" "${BASH_SOURCE[$i+1]}" "${BASH_LINENO[$i]}"
    fi
  done
}
top
EOF
$ bash /tmp/stack.sh
bottom 被 /tmp/stack.sh 的第 2 行调用
middle 被 /tmp/stack.sh 的第 1 行调用
top 被 /tmp/stack.sh 的第 10 行调用

FUNCNEST(FUNCNEST)

一个大于 0 的数值定义最大函数嵌套层级。超过该嵌套层级的函数调用会导致当前命令中止。

译者注:防递归失控的保险丝。递归函数(如遍历目录树的函数)一旦终止条件写错就会无限嵌套,最终耗尽栈空间导致整个 Shell 崩溃;设置 FUNCNEST=50 之类的小值后,超限时 Bash 会中止当前命令并打印错误,Shell 本身不受影响。实测:

# 译者注示例:FUNCNEST 限制函数嵌套深度,超限后当前命令中止
$ FUNCNEST=3; f() { f; }; f
bash: f: maximum function nesting level exceeded (3)

GLOBIGNORE(GLOBIGNORE)

以冒号分隔的模式列表,定义文件名展开时要忽略的文件名集合。如果被文件名展开模式匹配的文件名也匹配 GLOBIGNORE 中的某个模式,它会从匹配列表中移除。模式匹配遵循 extglob Shell 选项的设置。

译者注:与 FIGNORE(只影响补全)不同,GLOBIGNORE 作用于 *?[...] 等文件名展开的结果。两个实用细节:其一,模式与展开匹配的文件名「完整名」比较,写 *.log 即可;其二,设置 GLOBIGNORE 后,* 等模式会开始匹配以点开头的文件(相当于隐含启用了 dotglob 行为),除非它们也匹配 GLOBIGNORE——实测如下:

# 译者注示例:GLOBIGNORE 从文件名展开结果中剔除匹配的文件
$ cd /tmp/gdemo && touch a.txt b.txt c.log
$ GLOBIGNORE='*.txt'; echo *
c.log
$ touch .hidden; echo *    # 设置 GLOBIGNORE 后,* 也会匹配隐藏文件
.hidden c.log

GLOBSORT(GLOBSORT)

控制文件名展开结果的排序方式。该变量的值指定文件名展开结果的排序标准和排序顺序。如果该变量未设置或设置为空字符串,文件名展开使用历史行为:按名称排序,按 LC_COLLATE Shell 变量决定的升序字典序。

如果已设置,合法值以可选的 +(被忽略)或 -(把排序顺序从升序反转为降序)开头,后跟一个排序说明符。合法的排序说明符是 namenumericsizemtimeatimectimeblocks,分别按名称、名称的数值(而非字典序)、文件大小、修改时间、访问时间、inode 变更时间和块数对文件排序。如果任何非名称键比较相等(例如两个文件大小相同),排序以名称作为次级排序键。

例如,值 -mtime 按修改时间降序排序结果(最新的在前)。

numeric 说明符把仅由数字组成的名称当作数字,用它们的数值排序(例如 "2" 排在 "10" 之前)。使用 numeric 时,含非数字字符的名称排在所有全数字名称之后,并沿用传统行为按名称排序。

排序说明符 nosort 完全禁用排序;Bash 按从文件系统读取的顺序返回结果,忽略任何前导 -

如果排序说明符缺失,默认为 NAME,因此值 + 等价于空字符串,值 - 按名称降序排序。

任何非法值都恢复历史排序行为。

译者注:Bash 5.3 新增,此前文件名展开的结果一律按字典序排列。GLOBSORT 的格式是「可选的正负号 + 排序键」,如 GLOBSORT=-mtime(按修改时间降序)、GLOBSORT=size(按大小升序)。numeric 对「1、2、10」这类名字尤其好用——字典序会排成 1 10 2,数值序才是 1 2 10。实测:

# 译者注示例:GLOBSORT 控制文件名展开的排序(Bash 5.3 新增)
$ cd /tmp/gsort && touch 2 10 1
$ echo *                # 默认按字典序
1 10 2
$ ( GLOBSORT=numeric; echo * )
1 2 10

GROUPS(GROUPS)

一个数组变量,包含当前用户所属的组列表。对 GROUPS 的赋值没有效果。如果 GROUPS 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注:与 id -nG 输出的组名列表对应,但 GROUPS 的元素是数值组 ID。它由登录过程初始化,反映「当前用户」的组成员关系,而不是「当前进程」的——即使进程用 setpriv 之类换了附加组,该数组也不会自动更新(判断实际生效的组请用 id 命令)。

histchars(histchars)

控制历史展开、快速替换和记号化的两到三个字符(参见第 9 章(交互式使用历史记录))。第一个字符是「历史展开」字符,即开始一次历史展开的字符,通常是 !。第二个字符是「快速替换」字符,通常是 ^。当它作为行首字符出现时,历史替换会重复上一条命令,把一个字符串替换为另一个字符串。可选的第三个字符是「历史注释」字符,通常是 #,当一个单词的首字符是它时,表示该行其余部分是注释。历史注释字符会禁用该行其余单词的历史替换。它不一定会让 Shell 解析器把该行其余部分当作注释。

译者注histchars 是第 5 章中唯一不是全大写的变量(而且它由 Bash 自动设置,默认值就是 !^#)。三个字符的含义:! 启动历史展开(!! 重放上一条命令、!42 重放编号 42 的命令),^ 是快速替换(^旧^新 重放上一条命令并替换字符串),# 作为历史注释字符。把它改成其他字符后,对应的功能改用新字符触发;注意历史注释字符未必被解析器当作注释处理。

HISTCMD(HISTCMD)

当前命令的历史编号,即它在历史列表中的索引。对 HISTCMD 的赋值没有效果。如果 HISTCMD 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注:交互式 Shell 中每条命令有一个递增的历史编号,!编号 即可重放对应命令(参见第 9.3 节(历史展开)),HISTCMD 让你在脚本逻辑里拿到「当前命令是第几条」。非交互式 Shell 不维护历史列表,该值为 0(本机实测)。注意在命令执行时它已经是「当前这条」的编号,而不是上一条的。

HISTCONTROL(HISTCONTROL)

一个冒号分隔的值列表,控制命令如何保存到历史列表中。如果值列表包含 ignorespace,以空格字符开头的行不保存到历史列表。值 ignoredups 使与上一条历史记录匹配的行不被保存。值 ignorebothignorespaceignoredups 的简写。值 erasedups 使所有与当前行匹配的先前行在保存该行之前从历史列表中移除。任何不在上述列表中的值都被忽略。如果 HISTCONTROL 未设置,或不包含合法值,Bash 把所有由 Shell 解析器读取的行保存到历史列表,具体受 HISTIGNORE 值约束。如果多行复合命令的第一行被保存,第二行及后续行不再测试,无论 HISTCONTROL 的值如何都会被加入历史。如果第一行未被保存,命令的第二行及后续行也不会被保存。

译者注:四个值可以冒号组合,例如 HISTCONTROL=ignoreboth:erasedups 同时忽略「以空格开头」与「与上一条重复」的行,并在保存新行前清除历史上所有与它相同的旧行——这是最常见的隐私配置之一。注意 ignorespace 只在行首空格时生效,行中空格不算;多行复合命令以「第一行是否保存」为准整体决定。

HISTFILE(HISTFILE)

命令历史保存到的文件名。Bash 赋予默认值 ~/.bash_history。如果 HISTFILE 未设置或为空,Shell 退出时不保存命令历史。

译者注:历史在退出时写入该文件、启动时读入。会话结束时想丢弃本次历史,可以在退出前 unset HISTFILE 或把 HISTSIZE 设为 0。注意它只控制「默认的历史文件」:history -w 其他文件(参见第 9.2 节(Bash 历史记录内建命令))可以随时把历史写到任意文件。

HISTFILESIZE(HISTFILESIZE)

历史文件包含的最大行数。给该变量赋值时,历史文件会在必要时截断,通过移除最旧的条目,使其包含的历史条目总行数不超过该数目。如果历史列表包含多行条目,为避免留下不完整的历史条目,历史文件可能包含比该最大值更多的行。Shell 退出、或由 history 内建命令写文件后,历史文件也会被截断到该大小。如果值为 0,历史文件被截断为零大小。非数值和小于零的数值会抑制截断。读取任何启动文件后,Shell 把默认值设置为 HISTSIZE 的值。

译者注HISTFILESIZE(写入文件的最大行数)与 HISTSIZE(会话内历史列表保留的最大条数,默认 500)是两回事:前者管磁盘上的历史文件,后者管内存中的历史列表,后者是前者的默认值。把 HISTFILESIZE 设为 0 会立刻把历史文件清空,负数或非数值则完全不截断——注意这与 HISTSIZE=0(不记录)含义不同。

HISTIGNORE(HISTIGNORE)

一个冒号分隔的模式列表,用于决定哪些命令行应保存到历史列表。如果一条命令行匹配 HISTIGNORE 值中的某个模式,它不会保存到历史列表。每个模式锚定在行首,且必须匹配整行(Bash 不会隐式追加 *)。每个模式都在 HISTCONTROL 指定的检查应用之后,对行进行测试。除了常规的 Shell 模式匹配字符外,& 匹配上一条历史行。反斜杠转义 &;在尝试匹配之前,反斜杠会被移除。如果多行复合命令的第一行被保存,第二行及后续行不再测试,无论 HISTIGNORE 的值如何都会被加入历史。如果第一行未被保存,命令的第二行及后续行也不会被保存。模式匹配遵循 extglob Shell 选项的设置。

HISTIGNORE 涵盖 HISTCONTROL 的部分功能。模式 &ignoredups 相同,模式 [ ]*ignorespace 相同。用冒号组合这两个模式,就提供了 ignoreboth 的功能。

译者注:按模式过滤不想进历史的命令,典型配置如 HISTIGNORE='ls *:cd *:exit'。注意模式必须匹配整行且锚定行首——ls * 能匹配「ls 加空格加任意内容」,但单独的 ls(没有参数)不会被匹配,需要另外写 lsls* 之类的模式。特殊字符 & 代表「上一条历史行」,配合 extglob 可以写出相当精细的过滤规则。

HISTSIZE(HISTSIZE)

历史列表上要记住的最大命令数。如果值为 0,命令不保存到历史列表。小于零的数值使每条命令都保存到历史列表(没有限制)。读取任何启动文件后,Shell 把默认值设置为 500。

译者注:默认值 500 在读取启动文件之后设置,因此 ~/.bashrc 里修改 HISTSIZE 会覆盖默认值(本机交互式 Shell 实测默认 HISTSIZE=500)。三种取值对应三种行为:0 完全不记录(连历史展开 !! 都无法使用),负数不限量,正数是上限。区分 HISTSIZE(会话内条数)与 HISTFILESIZE(写入文件的行数)见 HISTFILESIZE 条目的译者注。

HISTTIMEFORMAT(HISTTIMEFORMAT)

如果该变量已设置且非空,其值被用作 strftime(3) 的格式字符串,用于打印 history 内建命令显示的每条历史条目关联的时间戳。如果该变量已设置,Shell 会把时间戳写入历史文件,以便跨 Shell 会话保留。这使用历史注释字符来区分时间戳与其他历史行。

译者注:格式串遵循 C 库的 strftime(3) 约定:%F%Y-%m-%d 的简写(日期),%T%H:%M:%S 的简写(时刻),%c 是本地化的完整日期时间。设置后,历史文件里每条记录前面会多一行带注释字符的时间戳(如 #1722868532),从而跨会话保留;若某条记录写入时尚未设置该变量,则没有时间戳。实测(编号与时间随会话而异):

# 译者注示例:设置 HISTTIMEFORMAT 后,history 输出带时间戳
$ HISTTIMEFORMAT='%F %T '
$ history 3
  312  2026-08-06 22:22:12 echo 第一条
  313  2026-08-06 22:22:12 echo 第二条
  314  2026-08-06 22:22:12 history 3

HOSTFILE(HOSTFILE)

包含一个文件名,格式与 /etc/hosts 相同,Shell 需要补全主机名时应读取该文件。可能的补全主机名列表可以在 Shell 运行期间改变;值改变后下一次尝试主机名补全时,Bash 会把新文件的内容添加到现有列表。如果 HOSTFILE 已设置但没有值,或命名的文件不可读,Bash 尝试读取 /etc/hosts 获取可能的补全主机名列表。当 HOSTFILE 被取消设置时,Bash 清空主机名列表。

译者注:主机名补全只发生在交互式 Shell 中(补全形如 user@主机名 的单词时,在 @ 之后补全)。默认情况下 Bash 读取 /etc/hosts~/.ssh/known_hosts 作为主机名来源;设置 HOSTFILE 可以让公司内网的主机名列表(/etc/hosts 格式的文件)参与补全,且运行中修改后无需重启 Shell 即可增量生效。

HOSTNAME(HOSTNAME)

当前主机名。

译者注:由 Shell 自动设置(本机实测即 hostname 命令的输出)。注意它在非交互式 Shell 中也可能未定义,脚本里要显示主机名时用 hostname 命令或 $HOSTNAME 均需自行判断可用性。

HOSTTYPE(HOSTTYPE)

描述 Bash 运行所在机器的字符串。

IGNOREEOF(IGNOREEOF)

控制 Shell 收到作为唯一输入的 EOF 字符时的动作。如果已设置,该值是 Bash 退出前可以在输入行首读取的连续 EOF 字符个数。如果变量已设置但没有数值,或值为空,则默认为 10。如果变量未设置,则 EOF 表示 Shell 输入的结束。这仅对交互式 Shell 生效。

译者注:这是防止误按 Ctrl-D 把交互式 Shell 关掉的保险:IGNOREEOF=10(或只写 IGNOREEOF,默认取 10)表示连续按 10 次 Ctrl-D 后 Bash 才会退出;未设置时按一次 Ctrl-D 就退出。只对交互式 Shell 生效,非交互式脚本里的 EOF(标准输入结束)不受影响。

INPUTRC(INPUTRC)

Readline 初始化文件的名称,覆盖默认的 ~/.inputrc

译者注:Readline 初始化文件控制命令行编辑的行为(按键绑定、vi/emacs 模式、补全风格等,参见第 8.3 节(Readline 初始化文件))。在脚本或程序里想要「用一套与用户无关的 Readline 配置」时,可以先设置 INPUTRC 再调用 bash。注意该变量是只读的,只能通过环境传入。

INSIDE_EMACS(INSIDE_EMACS)

如果 Shell 启动时 Bash 在环境中找到该变量,它假定 Shell 运行在 Emacs 的 shell 缓冲区中,并可能根据 TERM 的值禁用行编辑。

译者注:现代 Emacs(24.4 及以后)通过 INSIDE_EMACS 向子进程通报自身环境,其值形如 27.2,comint(版本号加模式名);旧式机制则是 EMACS=t(见第 5.2 节 EMACS 条目)。Bash 看到该变量后认为终端由 Emacs 管理:若 TERMdumb 则禁用行编辑,否则仍启用。在 Emacs 的 M-x shell 里运行 bash 时,这些变量保证行编辑不会与 Emacs 自身的按键处理冲突。

LANG(LANG)

用于确定未用 LC_ 开头的变量专门选择的任何类别(locale category)的区域设置。

LC_ALL(LC_ALL)

该变量覆盖 LANG 和任何其他指定区域设置类别的 LC_ 变量的值。

译者注:区域设置(locale)按类别拆分,如 LC_CTYPE(字符与大小写)、LC_COLLATE(排序)、LC_MESSAGES(消息语言)、LC_NUMERIC(数字格式)、LC_TIME(日期格式)。优先级从高到低:LC_ALL > 具体的 LC_* 类别变量 > LANG——因此 LC_ALL=C 可以强制任何脚本回到 C(POSIX)区域设置,这对输出英文错误消息、保证排序可复现非常有用(本手册所有示例即是在 LC_ALL=C 下验证的);而单独设置 LANG=zh_CN.UTF-8 时,任何 LC_*LC_ALL 都会覆盖它。当前区域设置可用 locale 命令查看。

LC_COLLATE(LC_COLLATE)

该变量决定对文件名展开结果排序时使用的排序顺序,并决定文件名展开和模式匹配中范围表达式、等价类和排序序列的行为(参见第 3.5.8 节(文件名展开))。

LC_CTYPE(LC_CTYPE)

该变量决定文件名展开和模式匹配中字符的解释方式和字符类的行为(参见第 3.5.8 节(文件名展开))。

译者注LC_CTYPE 直接影响脚本的「字符级」行为:[a-z] 之类的范围表达式、[[:upper:]] 之类的字符类是否按预期匹配,取决于当前区域设置的字符分类。在 C/POSIX 区域设置下,[a-z] 只匹配 a 到 z 的 26 个小写字母;而在某些区域设置(如 en_US.UTF-8)下,按排序序列它可能还会匹配 AÀ 等字符——这是「同样的脚本换台机器行为不同」的常见根源,必要时显式设置 LC_COLLATE=C

LC_MESSAGES(LC_MESSAGES)

该变量决定翻译 $ 前缀的双引号字符串时使用的区域设置(参见第 3.1.2.5 节(区域设置相关的翻译($"...")))。

LC_NUMERIC(LC_NUMERIC)

该变量决定数字格式化使用的区域设置类别。

LC_TIME(LC_TIME)

该变量决定日期和时间格式化使用的区域设置类别。

LINENO(LINENO)

当前正在执行的脚本或 Shell 函数中的行号。行号从 1 开始。不在脚本或函数中时,该值不保证有意义。如果 LINENO 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注LINENO 在脚本和函数中给出「当前代码所在的行号」。两个细节:其一,在函数体内它表示函数体代码所在的行号,而不是调用处的行号——调用位置请用 BASH_LINENO(见第 5.2 节);其二,在交互式 Shell 的提示符下,该值不保证有意义(手册原文如此,实测输出随输入方式变化)。实测:

# 译者注示例:LINENO 在脚本与函数中给出当前行号
$ cat > /tmp/lineno.sh <<'EOF'
echo "脚本第 $LINENO 行"
f() {
  echo "函数内第 $LINENO 行"
}
f
EOF
$ bash /tmp/lineno.sh
脚本第 1 行
函数内第 3 行

LINES(LINES)

select 命令在打印选择列表时用它确定列数。如果启用了 checkwinsize 选项(参见第 4.3.2 节(shopt 内建命令)),或在交互式 Shell 中收到 SIGWINCH 信号时,会自动设置。

MACHTYPE(MACHTYPE)

一个字符串,以标准的 GNU CPU-COMPANY-SYSTEM(CPU-厂商-系统)格式完整描述 Bash 运行所在的系统类型。

译者注MACHTYPEconfigure 脚本所用的系统三元组格式,比 HOSTTYPE/OSTYPE 更完整:x86_64(CPU)、pc(厂商)、linux-gnu(系统及库)。本机实测值为 x86_64-pc-linux-gnu。它与 uname -m(只给 CPU 架构,如 x86_64)不同——需要「架构 + 系统」组合信息时,MACHTYPE 一次给全。写脚本时可用 ${MACHTYPE%%-*} 之类的参数展开提取架构部分。

MAILCHECK(MAILCHECK)

Shell 应以多长的间隔(秒)检查 MAILPATHMAIL 变量中指定文件的邮件。默认是 60 秒。到检查邮件的时间时,Shell 会在显示主提示符之前进行检查。如果该变量未设置,或被设置为不小于零的数字以外的值,Shell 禁用邮件检查。

译者注:与 MAIL/MAILPATH(见第 5.1 节)配套:默认每 60 秒在显示 PS1 之前检查一次新邮件。未设置、负数或非数值都会禁用检查(实测交互式 Shell 默认 MAILCHECK=60)。注意该机制只对交互式 Shell 生效,且只在你设置了 MAILMAILPATH 时才做实际检查。

MAPFILE(MAPFILE)

一个数组变量,创建它来保存 mapfile 内建命令在未提供变量名时读取的文本。

译者注mapfile(参见第 4.2 节(Bash 内建命令))把文本逐行读入数组,是「把文件逐行读进数组」的标准工具(read 逐行读的循环远不如它高效)。不指定数组名时默认存入 MAPFILE。默认每个元素会保留行尾换行符,通常配合 -t 去掉。实测:

# 译者注示例:mapfile 未指定数组名时结果存入 MAPFILE
$ mapfile -t <<< $'x\ny'
$ declare -p MAPFILE
declare -a MAPFILE=([0]="x" [1]="y")

OLDPWD(OLDPWD)

cd 内建命令设置的上一工作目录。

译者注cd -(或 cd "$OLDPWD")即可在最近两个目录间往返,cd - 还会打印目标目录。注意:退出目录后 OLDPWD 指向退出前的目录;脚本里用它之前先确认非空(首次执行 cd 之前它未设置)。

OPTERR(OPTERR)

如果设置为值 1,Bash 显示 getopts 内建命令生成的错误消息。每次调用 Shell 时 OPTERR 被初始化为 1。

译者注getopts 遇到「未声明的选项」或「缺少参数」时,默认会向标准错误打印如 bash: illegal option -- z 的消息。脚本希望自行处理这些错误(比如按自己的格式报错)时,可在 getopts 循环前 OPTERR=0 关闭内置消息,然后靠退出状态和 ? 分支自行处理。注意 OPTERR 是普通变量(可赋值),与只读的 OPTIND 不同。

OSTYPE(OSTYPE)

描述 Bash 运行所在操作系统的字符串。

PIPESTATUS(PIPESTATUS)

一个数组变量(参见第 6.7 节(数组)),包含最近一次前台执行的管道中各命令的退出状态值列表,该管道也可以只由一条简单命令组成(参见第 3.2 节(Shell 命令))。Bash 在执行多元素管道、带计时和取反的管道、简单命令、用 ( 操作符创建的子 Shell、[[(( 复合命令之后,以及在导致 Shell 中止命令执行的错误条件之后,设置 PIPESTATUS

译者注$? 只给出管道中最后一条命令的状态,PIPESTATUS 则给出每一条的状态——echo ${PIPESTATUS[@]} 是脚本里最常见的用法。两个易错点:其一,PIPESTATUS 在每一条前台命令执行后都会被重置,所以要取状态必须紧跟管道之后(上一条 echo 自身也会重置它——false | true; echo ${PIPESTATUS[@]}; echo ${PIPESTATUS[@]} 第一次输出 1 0,第二次输出 0);其二,管道中每条命令在子 Shell 中运行,其中修改的变量不会影响外层。实测:

# 译者注示例:PIPESTATUS 保存管道中各命令的退出状态
$ false | true; echo ${PIPESTATUS[@]}
1 0

POSIXLY_CORRECT(POSIXLY_CORRECT)

如果 Bash 启动时该变量在环境中,Shell 会在读取启动文件之前进入 POSIX 模式(参见第 6.11.2 节(Bash 的 POSIX 模式)),就像提供了 --posix 调用选项一样。如果它在 Shell 运行期间被设置,Bash 启用 POSIX 模式,就像执行了命令 set -o posix 一样。当 Shell 进入 POSIX 模式时,如果该变量尚未设置,它会设置该变量。

译者注:三种进入 POSIX 模式的方式等价:bash --posixset -o posix、启动时环境里带 POSIXLY_CORRECT。用环境变量传入的好处是不需要改脚本内容——POSIXLY_CORRECT=1 ./script 即可让脚本在 POSIX 语义下运行(行为差异见第 6.11 节(Bash 与 POSIX))。实测:Shell 进入 POSIX 模式后会回填该变量,且 set -o posix 选项随之开启:

# 译者注示例:环境变量 POSIXLY_CORRECT=1 使 Bash 进入 POSIX 模式
$ POSIXLY_CORRECT=1 bash -c 'shopt -qo posix && echo "POSIX 模式已开启"'
POSIX 模式已开启

PPID(PPID)

Shell 父进程的进程 ID。该变量是只读的。

译者注PPID 是「启动当前 Shell 的进程」的 PID——交互式 Shell 的父进程通常是登录进程或终端模拟器,脚本的父进程则是调用它的程序。用 ps -p $PPID 可以查看父进程是谁。注意它与 $!(最近一个后台作业的 PID)无关,且只读(赋值报 PPID: readonly variable)。

PROMPT_COMMAND(PROMPT_COMMAND)

如果该变量已设置,并且是数组,每个已设置元素的值都被解释为一条命令,在打印主提示符($PS1)之前执行。如果该变量已设置但不是数组变量,它的值被用作要执行的命令。

译者注:在每次显示 PS1 之前执行其中的命令,是「往提示符里塞动态信息」的标准手段。数组形式(Bash 5.1 起支持)下每个元素按顺序执行,可以在不覆盖既有配置的前提下追加自己的命令。典型用法:提示符里显示当前时间、git 分支等。例如把下面这行放进 ~/.bashrc,之后每次提示符出现前都会打印分隔线:

# 译者注示例:PROMPT_COMMAND 在每次主提示符显示前执行
$ PROMPT_COMMAND='echo "——— 新的提示符即将显示 ———"'

PROMPT_DIRTRIM(PROMPT_DIRTRIM)

如果设置为大于零的数字,该值用作展开 \w\W 提示符转义时保留的尾部目录分量数(参见第 6.9 节(控制提示符))。被移除的字符用省略号替代。

译者注:在很深的目录树里工作时,\w 会打印出长得吓人的路径。设 PROMPT_DIRTRIM=2 后,\w 只保留最后两级目录,前面被截掉的部分以省略号代替(例如某个长路径显示为 .../src/main 之类)。它只影响提示符里的 \w/\W,不影响 pwd$PWD

PS0(PS0)

该参数的值像 PS1 一样展开,交互式 Shell 在读取一条命令之后、执行该命令之前显示它。

译者注PS0 默认未设置(实测交互式 Shell 中 PS0 为空)。它是「读入命令与执行命令之间」的提示:可用于在长命令执行前显示一条标记(例如 PS0='\t 开始执行…\n'),或配合 PROMPT_COMMAND 做「回车后、执行前」的计时起点。只在交互式 Shell 中生效,且必须是能展开的字符串。

PS3(PS3)

该变量的值用作 select 命令的提示符。如果该变量未设置,select 命令以 #? 提示。

PS4(PS4)

该参数的值像 PS1 一样展开,展开后的值是设置 -x 选项(参见第 4.3.1 节(set 内建命令))时回显命令行之前打印的提示符。展开值的第一个字符会根据需要重复多次,以表示多级间接。默认值是 +

译者注set -x 打开跟踪后,每条要执行的命令都会以 PS4 为前缀回显到标准错误。默认 + ;「第一个字符按嵌套层级重复」的层级来自命令替换等嵌套执行环境——实测:echo "$(echo nested)" 这类双重嵌套下,内层命令的前缀是 ++。若想让跟踪行同时显示行号,可设 PS4='+ ${BASH_SOURCE}:${LINENO}: '。实测:

# 译者注示例:PS4 默认是 '+ ';嵌套(如命令替换)时首字符会重复
$ PS4='+ '; set -x
$ echo "$(echo nested)"
++ echo nested
+ echo nested
nested
$ set +x

PWD(PWD)

cd 内建命令设置的当前工作目录。

译者注$PWDpwd 内建命令(两者在手册中常被混用)都给出当前目录的「逻辑」路径——即按 cd 时的路径拼出来的结果;而 pwd -P(及 $PWD 的物理版本)解析符号链接后的真实路径。脚本里写日志或提示时二者都可用,但涉及符号链接目录的「真实位置」判断时要用 pwd -P

RANDOM(RANDOM)

每次引用该参数时,它展开为 0 到 32767 之间的一个随机整数。给 RANDOM 赋值会初始化(播种)随机数序列。用相同的常量值给随机数生成器播种会产生相同的值序列。如果 RANDOM 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注:三个要点。其一,每次引用都重新生成,所以 echo $RANDOM $RANDOM 是两次取值;其二,赋值即播种,且同一种子在同一 Bash 版本中产生完全相同的序列——这既是缺点(可预测,不适合做密码学用途),也是优点(测试时可复现);其三,范围只有 0–32767,取更大范围可组合两次结果或用 $((RANDOM % N)) 取模(注意模运算有微小偏差,精确均分可用取整写法)。实测种子可复现:

# 译者注示例:对 RANDOM 赋值即播种,相同种子产生相同序列
$ RANDOM=42; echo $RANDOM $RANDOM $RANDOM
17772 26794 1435
$ RANDOM=42; echo $RANDOM
17772

READLINE_ARGUMENT(READLINE_ARGUMENT)

bind -x 定义的 Readline 命令(参见第 4.2 节(Bash 内建命令))在调用时接收到的数字参数。

READLINE_LINE(READLINE_LINE)

Readline 行缓冲区的内容,供 bind -x 使用(参见第 4.2 节(Bash 内建命令))。

READLINE_MARK(READLINE_MARK)

Readline 行缓冲区中「标记」(mark,保存的插入点)的位置,供 bind -x 使用(参见第 4.2 节(Bash 内建命令))。插入点与标记之间的字符通常称为「区域」(region)。

READLINE_POINT(READLINE_POINT)

Readline 行缓冲区中插入点的位置,供 bind -x 使用(参见第 4.2 节(Bash 内建命令))。

译者注bind -x(参见第 8.4 节(可绑定的 Readline 命令))可以把任意按键绑定到 Shell 命令;这些变量让被绑定的命令能读到「当前正在编辑的命令行的状态」——READLINE_LINE 是整行内容,READLINE_POINT 是光标(插入点)位置,READLINE_MARK 是标记位置,READLINE_ARGUMENT 是按下按键前输入的数字参数(如 M-5 后按绑定键)。被绑定的命令可以直接改写 READLINE_LINE/READLINE_POINT 来修改命令行。示例:绑定 C-t 在光标处报告行内容:

# 译者注示例:用 bind -x 与 READLINE_LINE/READLINE_POINT 读取当前命令行
$ bind -x '"\C-t": echo "行内容: $READLINE_LINE 插入点: $READLINE_POINT"'

REPLY(REPLY)

read 内建命令的默认变量;read 未提供变量名参数时,设置为读到的行。

译者注read 不给变量名时读入的内容存进 REPLYselect 命令(参见第 3.2.5.2 节(条件结构))把用户选择项也存进 REPLY。因此 readselect 联用时,REPLY 是两者共用的「返回值通道」——例如 select x in 甲 乙 丙; do case $REPLY in ... esacREPLY 是用户输入的数字,$x 才是选项本身。

SECONDS(SECONDS)

该变量展开为 Shell 启动以来的秒数。对该变量的赋值把计数重置为所赋的值,展开值变为所赋的值加上赋值以来的秒数。Shell 调用时的秒数和当前时间总是通过以一秒分辨率查询系统时钟确定。如果 SECONDS 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注:测量脚本片段耗时的最简工具:SECONDS=0 清零后做事,最后读 $SECONDS。注意它是一秒分辨率,测量亚秒级耗时请用 EPOCHREALTIME(见第 5.2 节)。实测:

# 译者注示例:SECONDS 从 Shell 启动起计秒,赋值后重新计数
$ SECONDS=0; sleep 2; echo $SECONDS
2

SHELL(SHELL)

这个环境变量展开为 Shell 的完整路径名。如果 Shell 启动时它未设置,Bash 给它赋当前用户登录 Shell 的完整路径名。

译者注:最常见的误解——用 $SHELL 判断「当前跑的是哪个 Shell」。SHELL 只是一个环境变量,由登录进程设置为你登录时默认 Shell 的路径;它不会因为你在 zsh 里敲 bash 而更新(实测:bash 中 $SHELL 仍是登录 Shell 的路径,除非显式导出覆盖)。判断当前 Shell 请用 $BASH_VERSION(是否 bash)或 $0(名字)。另外,cron、systemd 等环境的 $SHELL 可能为空或指向别的程序,脚本里不要默认它存在。

SHELLOPTS(SHELLOPTS)

以冒号分隔的已启用 Shell 选项列表。列表中的每个单词都是 set 内建命令 -o 选项的合法参数(参见第 4.3.1 节(set 内建命令))。出现在 SHELLOPTS 中的选项,是 set -o 报告为「on」的那些选项。如果 Bash 启动时该变量已在环境中,Shell 会在读取任何启动文件之前启用列表中的每个选项。如果该变量被导出,子 Shell 将启用列表中的每个选项。该变量是只读的。

译者注SHELLOPTSBASHOPTS(见第 5.2 节)对应 set -oshopt 两套选项。变量只读,但可以 export——导出后子 Shell 启动时自动启用其中的选项,这是「把 errexitpipefail 等选项传给所有子脚本」的惯用方法(子 Shell 会据此启用选项,而不是被强制)。实测:

# 译者注示例:导出 SHELLOPTS 后,子 Shell 会继承其中的选项
$ set -o pipefail
$ export SHELLOPTS
$ bash -c 'echo "$SHELLOPTS"'
braceexpand:hashall:interactive-comments:pipefail

SHLVL(SHLVL)

每启动一个新的 Bash 实例加 1。它用于统计 Bash Shell 的嵌套深度。

译者注:登录时通常为 1,每嵌套一层 bash(或 susudo 里再起 shell)就加 1,实测在 bash -c 'echo $SHLVL' 中比父 Shell 大 1。注意它统计的是「bash 实例嵌套」,不是「进程树深度」,且各发行版登录流程可能重置它;某些环境里 SHLVL 可能未设置(为空),脚本判断时应按 $((SHLVL + 1)) 这类写法容错。

SRANDOM(SRANDOM)

每次引用该变量时,它展开为一个 32 位伪随机数。在支持 /dev/urandomarc4random 的系统上,随机数生成器是非线性的,因此每个返回的数与它前面的数没有关系。该随机数生成器无法播种,因此对该变量的赋值没有效果。如果 SRANDOM 被取消设置,它会失去其特殊属性,即使之后重新设置也一样。

译者注SRANDOM(Bash 5.1 新增)是 RANDOM 的高质量版本:32 位范围(0 到 2^32-1)、基于 /dev/urandomarc4random(支持的系统上),且不可播种——每次引用都返回与前值无关的数。两者取舍:需要可复现的伪随机序列(如测试)用 RANDOM,需要不可预测、范围更大的随机数(如生成临时文件名后缀)用 SRANDOM。实测:

# 译者注示例:SRANDOM 每次引用返回 32 位伪随机数,且不可播种
$ echo $SRANDOM
1292587805
$ [[ $SRANDOM -ge 0 && $SRANDOM -lt $((2**32)) ]] && echo "在 32 位无符号整数范围内"
在 32 位无符号整数范围内

TIMEFORMAT(TIMEFORMAT)

该参数的值用作格式字符串,指定带 time 保留字前缀的管道的计时信息应如何显示。% 字符引入一个转义序列,展开为一个时间值或其他信息。转义序列及其含义如下;方括号表示可选部分。

%%

字面量 %

%[P][l]R

经过的时间(秒)。

%[P][l]U

用户态消耗的 CPU 秒数。

%[P][l]S

系统态消耗的 CPU 秒数。

%P

CPU 占用百分比,按(%U + %S)/ %R 计算。

可选的 P 是指定精度(小数点后的位数)的数字。值为 0 时不输出小数点或小数。time 最多打印小数点后六位;大于 6 的 P 值改为 6。如果未指定 P,time 打印小数点后三位。

可选的 l 指定更长的格式,包含分钟,形如 MMmSS.FFs。P 的值决定是否包含小数。

如果该变量未设置,Bash 表现得好像它的值是 $'\nreal\t%3lR\nuser\t%3lU\nsys\t%3lS'。如果值为空,Bash 不显示任何计时信息。显示格式字符串时会追加一个尾部换行。

译者注time 保留字(参见第 3.2.3 节(管道))默认输出三行(real/user/sys,各含分钟与毫秒);TIMEFORMAT 可以改成单行或自定义文本,%R 是经过时间(real time),%U 用户态 CPU 时间,%S 系统态 CPU 时间,%P 是 CPU 占用率。例如想「只报经过时间、精确到毫秒」:TIMEFORMAT='%3R 秒'。实测(time true 耗时近乎为零,输出稳定):

# 译者注示例:自定义 TIMEFORMAT 控制 time 的输出格式
$ TIMEFORMAT='耗时 %3R 秒'; time true
耗时 0.000 秒

TMOUT(TMOUT)

如果设置为大于零的值,read 内建命令把它用作默认超时时间(参见第 4.2 节(Bash 内建命令))。当输入来自终端时,如果 TMOUT 秒后没有输入到达,select 命令(参见第 3.2.5.2 节(条件结构))终止。

在交互式 Shell 中,该值被解释为发出主提示符后等待一行输入的时间(秒)。如果完整的一行输入没有在等待了该秒数后到达,Bash 终止。

译者注TMOUT 有三个作用点:read 的默认超时(等价于每次 read 都带 -t $TMOUT)、select 的超时退出、交互式 Shell 的空闲自动退出。最后一个用途常被用来做「终端锁」:在服务器上设置 TMOUT=300 后,空闲 5 分钟会自动退出登录,防止终端无人看管。注意它在子 Shell 里也有效,脚本里慎用——read 意外超时返回非零会让 set -e 下的脚本直接退出。实测 read 的默认超时:

# 译者注示例:TMOUT 作为 read 内建命令的默认超时时间
$ printf 'hello\n' | TMOUT=3 bash -c 'read x && echo "read 得到: $x"'
read 得到: hello

TMPDIR(TMPDIR)

如果已设置,Bash 使用它的值作为目录名,在该目录中创建供 Shell 使用的临时文件。

译者注:Bash 自身(如 here-document 的临时文件、fc 编辑历史时等)创建临时文件时优先使用 TMPDIR,未设置时使用系统默认(通常为 /tmp)。注意:它只影响 Bash 自己创建的临时文件,不影响 mktemp(mktemp 有自己的一套优先级)。给脚本设置 TMPDIR 时确保目录存在且可写,否则命令会失败。

UID(UID)

当前用户的数值真实用户 ID。该变量是只读的。

译者注UID 是真实用户 ID,EUID(见第 5.2 节)是有效用户 ID,普通会话中两者相等。该变量只读,且只在 bash 中存在——写可移植脚本时,需要用户 ID 应优先用 id -u 命令,以兼容其他 Shell。

6 Bash 功能

本章描述 Bash 独有的功能。

6.1 调用 Bash

bash [long-opt] [-ir] [-abefhkmnptuvxdBCDHP] [-o OPTION] [-O SHOPT_OPTION] [ARGUMENT ...]
bash [long-opt] [-abefhkmnptuvxdBCDHP] [-o OPTION] [-O SHOPT_OPTION] -c STRING [ARGUMENT ...]
bash [long-opt] -s [-abefhkmnptuvxdBCDHP] [-o OPTION] [-O SHOPT_OPTION] [ARGUMENT ...]

set 内建命令一起使用的所有单字符选项(参见第 4.3.1 节(set 内建命令))都可以在调用 Shell 时用作选项。此外,还有几个可以使用的多字符选项。这些选项必须出现在命令行中单字符选项之前,才能被识别。

选项 说明
--debugger 安排在 Shell 启动前执行调试器配置文件。开启扩展调试模式(shopt 内建命令的 extdebug 选项的描述见第 4.3.2 节(shopt 内建命令))。
--dump-po-strings 以 GNU gettext 的 PO(portable object,可移植对象)文件格式,在标准输出上打印所有带 `
--- ---
--debugger 安排在 Shell 启动前执行调试器配置文件。开启扩展调试模式(shopt 内建命令的 extdebug 选项的描述见第 4.3.2 节(shopt 内建命令))。
前缀的双引号字符串的列表。除输出格式外与 -D 等价。
--dump-strings -D 等价。
--help 在标准输出上显示用法信息并以成功状态退出。
--init-file FILENAME--rcfile FILENAME 在交互式 Shell 中执行 FILENAME 中的命令(而不是 ~/.bashrc 中的命令)。
--login -l 等价。
--noediting 当 Shell 交互时,不使用 GNU Readline 库(参见第 8 章(命令行编辑))读取命令行。
--noprofile 当 Bash 以登录 Shell 调用时,不加载系统级启动文件 /etc/profile,也不加载任何个人初始化文件 ~/.bash_profile~/.bash_login~/.profile
--norc 在交互式 Shell 中不读取 ~/.bashrc 初始化文件。如果 Shell 以 sh 名称调用,此行为默认开启。
--posix 开启 POSIX 模式;在 Bash 的默认行为与 POSIX 标准不一致的地方,改变 Bash 的行为以符合标准。其目的是让 Bash 表现为该标准的严格超集。Bash 的 POSIX 模式的描述见第 6.11.2 节(Bash 的 POSIX 模式)。
--restricted -r 等价。使 Shell 成为受限 Shell(参见第 6.10 节(受限 Shell))。
--verbose -v 等价。在读取 Shell 输入行时将其打印出来。
--version 在标准输出上显示当前 Bash 实例的版本信息并以成功状态退出。

译者注set 内建命令的单字符选项(-a-e-f-h-k-m-n-t-u-v-x-C-B-H-P 等,完整清单参见第 4.3.1 节)都可以在调用 Bash 时直接使用。其中 -x(xtrace)在调试脚本时最常用:它会在执行每条命令前把命令(展开后)打印到标准错误。

# 译者注示例:用 -x 跟踪执行(前两行是 xtrace 输出,最后一行是命令的真实输出)
$ bash -xc 'x=1; echo $x'
+ x=1
+ echo 1
1

另请注意多字符选项与单字符选项的相对位置:手册要求多字符选项必须出现在单字符选项之前才能被识别,实测也是如此——把 --posix 放到 -x 之后会直接报「无效的选项」并显示用法信息:

# 译者注示例:多字符选项放错位置
$ bash -x --posix -c 'echo hi'
bash: --: 无效的选项

bash --posix -x -c 'echo hi' 就能正常执行。

此外,还有几个单字符选项可以在调用时提供,但 set 内建命令没有这些选项。

选项 说明
-c 从第一个非选项参数 COMMAND_STRING 读取并执行命令,然后退出。如果 COMMAND_STRING 之后还有参数,第一个参数被赋给 $0,其余参数被赋给位置参数。对 $0 的赋值设置了 Shell 的名称,该名称会用在警告和错误消息中。
-i 强制 Shell 以交互方式运行。交互式 Shell 在第 6.3 节(交互式 Shell)中描述。
-l 使本 Shell 表现得如同它是由 login 直接调用的一样。当 Shell 交互时,这等价于用 exec -l bash 启动一个登录 Shell。当 Shell 非交互时,它会读取并执行登录 Shell 的启动文件。exec bash -lexec bash --login 将用 Bash 登录 Shell 替换当前 Shell。登录 Shell 的特殊行为见第 6.2 节(Bash 启动文件)。
-r 使 Shell 成为受限 Shell(参见第 6.10 节(受限 Shell))。
-s 如果出现此选项,或者选项处理结束后没有剩余参数,Bash 就从标准输入读取命令。此选项允许在调用交互式 Shell 或通过管道读取输入时设置位置参数。
-D 在标准输出上打印所有带 $ 前缀的双引号字符串的列表。当当前区域设置(locale)不是 CPOSIX 时,这些字符串是需要进行语言翻译的字符串(参见第 3.1.2.5 节(区域设置相关的翻译($"...")))。此选项隐含 -n 选项;不会执行任何命令。
[-+]O [SHOPT_OPTION] SHOPT_OPTION 是 shopt 内建命令接受的 Shell 选项之一(参见第 4.3.2 节(shopt 内建命令))。如果提供了 SHOPT_OPTION,-O 设置该选项,+O 取消该选项。如果没有提供 SHOPT_OPTION,Bash 在标准输出上打印 shopt 所接受的全部 Shell 选项的名称和值。如果调用选项是 +O,输出采用可以重新用作输入的格式。
-- -- 表示选项的结束,并禁用进一步的选项处理。-- 之后的任何参数都被当作 Shell 脚本文件名(参见第 3.8 节(Shell 脚本))以及传给该脚本的参数。
- -- 等价。

译者注-c 的命令字符串之后紧跟的第一个参数会被赋给 $0(而不是 $1),这一点经常被搞混——$0 在这里是「Shell 的名称」,会出现在警告和错误消息中:

# 译者注示例:-c 时 $0 与位置参数的赋值
$ bash -c 'echo "shell: $0"; echo "args: $1 $2"' myshell aa bb
shell: myshell
args: aa bb

译者注-s 让 Bash 从标准输入读取命令,常用于给通过管道传来的命令设置位置参数:

# 译者注示例:通过管道向 bash -s 传入位置参数
$ echo 'echo "args: $1 $2"' | bash -s a b
args: a b

译者注-D 用于提取脚本中需要翻译的 $"..." 字符串(参见第 3.1.2.5 节),供 GNU gettext 翻译流程使用。它隐含 -n,不会真的执行命令:

# 译者注示例:-D 列出带 $ 前缀的双引号字符串
$ bash -D -c 'echo $"Hello"'
"Hello"

--dump-po-strings 则直接输出 gettext 的 PO(可移植对象)格式,可以喂给翻译工具:

# 译者注示例:--dump-po-strings 的 PO 格式输出
$ bash --dump-po-strings -c 'echo $"Hello"'
#: -c:1
msgid "Hello"
msgstr ""

所谓「登录 Shell」(login shell),是指其第零号参数($0)的第一个字符是 - 的 Shell,或者以 --login 选项调用的 Shell。

所谓「交互式 Shell」(interactive shell),是指不带非选项参数启动(除非指定了 -s)、未指定 -c 选项、且标准输入和标准错误都连接到终端(由 isatty(3) 判定)的 Shell,或者以 -i 选项启动的 Shell。更多信息参见第 6.3 节(交互式 Shell)。

译者注:这里给出了「登录 Shell」与「交互式 Shell」的精确定义,两个概念是正交的:登录 Shell 由「$0 首字符为 - 或带 --login」决定,交互式 Shell 由「标准输入与标准错误均为终端且未指定 -c/-s(或无参数),或带 -i」决定。于是存在四种组合:交互且登录(login 终端会话、带终端的 ssh host)、交互但非登录(终端里普通的 bash)、非交互但登录(如 bash -l 脚本名)、非交互且非登录(如 ssh host command,但该情形按第 6.2 节的网络调用规则读取 ~/.bashrc)。「登录 Shell 的 $0 首字符是 -」指的是 login 程序以 -bash 作为 argv[0] 启动 Bash 的情形;实测 bash -l -c 'echo $0' 输出的仍是 bash(argv[0] 未被修改),但 shoptlogin_shell 选项为真。判断方法见第 6.3.2 节。

如果在选项处理之后还有参数剩余,且既没有提供 -c 也没有提供 -s 选项,则第一个参数被当作包含 Shell 命令的文件的名称(参见第 3.8 节(Shell 脚本))。以这种方式调用 Bash 时,$0 被设置为该文件的名称,位置参数被设置为其余参数。Bash 从该文件读取并执行命令,然后退出。Bash 的退出状态是脚本中最后执行的命令的退出状态。如果没有执行任何命令,退出状态为 0。Bash 首先尝试在当前目录中打开该文件;如果找不到,则在 PATH 的各个目录中搜索该脚本。

译者注:以「脚本文件 + 参数」的方式调用 Bash 时:$0 是脚本文件名,其余参数依次成为 $1$2……;脚本的退出状态是其中最后一条命令的退出状态;文件名不带斜杠且当前目录找不到时,会按 PATH 搜索:

# 译者注示例:脚本的 $0、位置参数、退出状态与 PATH 查找
$ printf 'echo "$0: $1"\nexit 7\n' > /tmp/bin/demo
$ chmod +x /tmp/bin/demo
$ bash -- /tmp/bin/demo hello
/tmp/bin/demo: hello
$ echo $?
7
$ PATH=/tmp/bin:$PATH bash demo hi
demo: hi

注意 bash -- /tmp/bin/demo hello-- 之后的内容全部当作脚本名与参数;而 bash demo hi 通过 PATH 找到了脚本,$0 显示为 demo

6.2 Bash 启动文件

本节描述 Bash 如何执行其启动文件。如果其中任何文件存在但无法读取,Bash 会报告错误。文件名中的波浪号按上文「波浪号展开」中描述的方式展开(参见第 3.5.2 节(波浪号展开))。

交互式 Shell 在第 6.3 节(交互式 Shell)中描述。

译者注:Bash 启动文件的加载顺序可以总结为下表(结合第 6.1 节的调用选项理解):

调用方式 读取的启动文件
交互式登录 Shell /etc/profile~/.bash_profile / ~/.bash_login / ~/.profile(按序取第一个存在且可读的)→ 退出时执行 ~/.bash_logout
非交互式 + --login 登录启动文件同上;退出时不读 ~/.bash_logout(除非执行 exit 内建命令)
交互式非登录 Shell ~/.bashrc(可用 --rcfile FILE 替换)
非交互式 Shell(如运行脚本) BASH_ENV 所指文件(若另带 --login 则还读登录文件)
sh 名称调用 登录时:/etc/profile~/.profile;交互式:仅 ENV 所指文件;非交互式:不读任何文件
POSIX 模式(--posix 交互式:仅 ENV 所指文件
sshd/rshd 等经网络调用 即使非交互也读 ~/.bashrc(以 sh 调用时除外)
有效 UID/GID 与真实 UID/GID 不同 不读任何启动文件(除非带 -p

因为交互式登录 Shell 不读 ~/.bashrc,而交互式非登录 Shell 不读 ~/.profile,惯例是在 ~/.bash_profile 里显式加载 ~/.bashrc(见下文),使个性化设置只维护一份。

以交互式登录 Shell 调用,或带 --login 选项调用

当 Bash 以交互式登录 Shell 调用,或作为带 --login 选项的非交互式 Shell 调用时,它首先从文件 /etc/profile 读取并执行命令(如果该文件存在)。读取该文件之后,它按顺序查找 ~/.bash_profile~/.bash_login~/.profile,并从第一个存在且可读的文件读取并执行命令。--noprofile 选项会抑制此行为。

当交互式登录 Shell 退出,或非交互式登录 Shell 执行 exit 内建命令时,Bash 会从文件 ~/.bash_logout 读取并执行命令(如果该文件存在)。

以交互式非登录 Shell 调用

当 Bash 作为非登录的交互式 Shell 运行时,它从 ~/.bashrc 读取并执行命令(如果该文件存在)。--norc 选项会抑制此行为。--rcfile FILE 选项使 Bash 使用 FILE 而不是 ~/.bashrc

因此,典型的 ~/.bash_profile 会在任何登录专用的初始化之后(或之前)包含下面这一行:

if [ -f ~/.bashrc ]; then . ~/.bashrc; fi

译者注:这正是惯用做法:~/.bash_profile 负责登录会话的初始化,~/.bashrc 负责所有交互式会话的初始化,前者在开头(或结尾)「拉」后者一次,让两类会话共享同一份配置:

# 译者注示例:让登录 Shell 也加载 ~/.bashrc
if [ -f ~/.bashrc ]; then . ~/.bashrc; fi

注意 .(source)是在当前 Shell 中执行文件,与执行子 Shell 脚本不同——文件里的 cd、变量赋值、export 都会影响当前 Shell,这正是需要这种「拉取」的原因。

非交互式调用

当 Bash 非交互式启动(例如为了运行 Shell 脚本)时,它会在环境中查找变量 BASH_ENV,如果该变量存在则展开其值,并把展开后的值用作要读取和执行的文件的名称。Bash 的行为就像执行了下面这条命令:

if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi

但不会使用 PATH 变量的值来查找该文件名。

如上所述,如果非交互式 Shell 以 --login 选项调用,Bash 会尝试从登录 Shell 的启动文件中读取并执行命令。

译者注BASH_ENV 只在非交互式 Shell 中生效,而且只把展开后的值当作文件名直接打开,不会像脚本调用那样到 PATH 里搜索:

# 译者注示例:非交互式 Shell 加载 BASH_ENV 所指文件
$ echo 'echo "BASH_ENV loaded"' > /tmp/benv.bash
$ BASH_ENV=/tmp/benv.bash bash -c 'echo inside'
BASH_ENV loaded
inside
# 译者注示例:BASH_ENV 不按 PATH 搜索(文件 bnv 明明在 PATH 中的 /tmp/bin 下)
$ echo 'echo found-via-path' > /tmp/bin/bnv
$ cd /tmp && PATH=/tmp/bin:$PATH BASH_ENV=bnv bash -c 'echo inside'
inside

第二条命令只输出了 inside,说明 bnv 没有被加载——Bash 按原值 bnv 直接打开文件失败后便静默跳过。

sh 名称调用

如果 Bash 以 sh 名称调用,它会尽可能模仿历史上各版本 sh 的启动行为,同时也要符合 POSIX 标准。

当以交互式登录 Shell 调用,或以带 --login 选项的非交互式 Shell 调用时,它首先尝试按顺序从 /etc/profile~/.profile 读取并执行命令。--noprofile 选项会抑制此行为。

当以 sh 名称作为交互式 Shell 调用时,Bash 查找变量 ENV,如果已定义则展开其值,并把展开后的值用作要读取和执行的文件的名称。由于以 sh 名称调用的 Shell 不会尝试从任何其他启动文件读取并执行命令,--rcfile 选项没有效果。

sh 名称调用的非交互式 Shell 不会尝试读取任何其他启动文件。

当以 sh 名称调用时,Bash 在读取启动文件之后进入 POSIX 模式。

以 POSIX 模式调用

当 Bash 以 POSIX 模式启动(例如通过 --posix 命令行选项)时,它遵循 POSIX 标准对启动文件的规定。在这种模式下,交互式 Shell 展开 ENV 变量,并从展开值所指的文件读取并执行命令。不读取其他任何启动文件。

译者注ENVBASH_ENV 正好相反:它只对交互式 Shell 生效,而且只在「以 sh 名称调用」或「POSIX 模式」两种情形下被检查。普通的交互式 Bash(既非 sh 也非 POSIX 模式)不读 ENV

# 译者注示例:POSIX 模式交互式 Shell 读取 ENV 所指文件
$ echo 'echo ENV-loaded' > /tmp/penv.bash
$ printf 'echo bye\n' | ENV=/tmp/penv.bash bash --posix -i 2>/dev/null
ENV-loaded
bye

而在普通模式下设置 ENV 没有任何效果(下面只输出了 x):

# 译者注示例:普通模式忽略 ENV
$ ENV=/tmp/penv.bash bash -c 'echo x'
x

由远程 Shell 守护进程调用

Bash 会尝试确定自己是否在标准输入连接到网络连接的情况下运行,例如由历史悠久的、如今已很少见的远程 Shell 守护进程(通常是 rshd)或安全 Shell 守护进程 sshd 执行时。如果 Bash 确定它以非交互方式在这种状态下运行,它会从 ~/.bashrc 读取并执行命令(如果该文件存在且可读)。如果以 sh 名称调用,Bash 不读取该文件。--norc 选项会抑制此行为,--rcfile 选项使 Bash 使用不同的文件代替 ~/.bashrc;不过 rshdsshd 通常都不会用这些选项调用 Shell,也不允许指定这些选项。

译者注:这一条解释了 ssh host command 的行为:远程命令的标准输入连接的是网络 socket 而不是终端,Bash 识别出这种情形后,即使是非交互式也会读取 ~/.bashrc。不过要注意:远程执行的是一条命令,而非交互式 Shell 默认不展开别名(参见第 6.6 节),所以不要指望 ~/.bashrc 里定义的别名在远程命令中生效。

有效 UID/GID 与真实 UID/GID 不同时调用

如果 Bash 启动时有效用户(组)ID 与真实用户(组)ID 不相等,且没有提供 -p 选项,则不读取任何启动文件,Shell 函数不从环境继承,环境中的 SHELLOPTSBASHOPTSCDPATHGLOBIGNORE 变量(如果出现)被忽略,并且有效用户 ID 被设置为真实用户 ID。如果在调用时提供了 -p 选项,启动行为相同,但不会重置有效用户 ID。

译者注:这一条是 setuid 场景的安全规则:当 Bash 以 setuid 程序的身份被启动(有效 UID ≠ 真实 UID)时,不读取任何启动文件、不继承环境中的 Shell 函数、忽略 SHELLOPTSBASHOPTSCDPATHGLOBIGNORE 四个变量,并把有效 UID 重置为真实 UID,防止借启动文件或环境变量提权。-p(privileged,特权模式)保留有效 UID,但仍保持上述保护性启动行为(依旧不读启动文件)。

6.3 交互式 Shell

6.3.1 什么是交互式 Shell?

交互式 Shell 是指不带非选项参数启动(除非指定了 -s)、未指定 -c 选项、其输入和错误输出都连接到终端(由 isatty(3) 判定)的 Shell,或者以 -i 选项启动的 Shell。

交互式 Shell 通常从用户的终端读取,并写入用户的终端。

-s 调用选项可以在交互式 Shell 启动时用来设置位置参数。

6.3.2 当前 Shell 是否交互?

要在启动脚本中判断 Bash 是否交互式运行,可以测试 - 这个特殊参数的值。当 Shell 交互时,它包含 i。例如:

case "$-" in
*i*)	echo This shell is interactive ;;
*)	echo This shell is not interactive ;;
esac

或者,启动脚本可以检查变量 PS1:它在非交互式 Shell 中未设置,在交互式 Shell 中已设置。因此:

if [ -z "$PS1" ]; then
        echo This shell is not interactive
else
        echo This shell is interactive
fi

译者注$- 中包含当前生效的选项集合(参见第 4.3.1 节),其中的 i 表示交互式。但 $- 只回答「是否交互」,回答「是否登录 Shell」要用 shoptlogin_shell 选项:

# 译者注示例:用 shopt 的 login_shell 选项判断登录 Shell
$ bash -c 'shopt -q login_shell && echo login || echo not-login'
not-login
$ bash --noprofile -lc 'shopt -q login_shell && echo login || echo not-login'
login

PS1 判断交互式的做法有个小缺陷:用户(或脚本)可以显式设置 PS1,从而干扰判断;$- 更可靠。

6.3.3 交互式 Shell 的行为

当 Shell 交互式运行时,它会在几个方面改变自己的行为。

  1. Bash 按照第 6.2 节(Bash 启动文件)中的描述读取并执行启动文件。
  2. 默认启用作业控制(参见第 7 章(作业控制))。作业控制生效时,Bash 忽略键盘产生的作业控制信号 SIGTTINSIGTTOUSIGTSTP
  3. 在打印主提示符 $PS1 之前,Bash 会把 PROMPT_COMMAND 数组变量中已设置元素的值作为命令执行(参见第 5.2 节(Bash 变量))。
  4. Bash 在读取命令的第一行之前展开并显示 PS1,在读取多行命令的第二行及后续行之前展开并显示 PS2。Bash 在读取一条命令之后、执行它之前展开并显示 PS0。完整的提示字符串转义序列列表参见第 6.9 节(控制提示符)。
  5. Bash 使用 Readline(参见第 8 章(命令行编辑))从用户的终端读取命令。
  6. 读取命令时在标准输入上收到 EOF,Bash 不再立即退出,而是检查 set -oignoreeof 选项的值(参见第 4.3.1 节(set 内建命令))。
  7. Bash 默认开启命令历史(参见第 9.1 节(Bash 的历史记录设施))和历史展开(参见第 9 章(交互式使用历史记录))。启用历史记录的 Shell 退出时,Bash 会把命令历史保存到 $HISTFILE 所指的文件中。
  8. 默认执行别名展开(参见第 6.6 节(别名))。
  9. 在没有任何陷阱(trap)的情况下,Bash 忽略 SIGTERM(参见第 3.7.6 节(信号))。
  10. 在没有任何陷阱的情况下,SIGINT 被捕获并处理(参见第 3.7.6 节(信号))。SIGINT 会中断某些 Shell 内建命令。
  11. 交互式登录 Shell 在退出时,如果 huponexit Shell 选项已启用,会向所有作业发送 SIGHUP(参见第 3.7.6 节(信号))。
  12. -n 选项没有效果,无论是在调用时使用,还是使用 set -n 时(参见第 4.3.1 节(set 内建命令))。
  13. Bash 会根据 MAILMAILPATHMAILCHECK Shell 变量的值定期检查邮件(参见第 5.2 节(Bash 变量))。
  14. 在启用 set -u 之后,因引用未绑定(unbound)的 Shell 变量而产生的展开错误不会导致 Shell 退出(参见第 4.3.1 节(set 内建命令))。
  15. ${VAR:?WORD} 展开中因 VAR 未设置或为空而产生的展开错误不会导致 Shell 退出(参见第 3.5.3 节(Shell 参数展开))。
  16. Shell 内建命令遇到的重定向错误不会导致 Shell 退出。
  17. 在 POSIX 模式下运行时,特殊内建命令返回错误状态不会导致 Shell 退出(参见第 6.11.2 节(Bash 的 POSIX 模式))。
  18. 失败的 exec 不会导致 Shell 退出(参见第 4.1 节(Bourne Shell 内建命令))。
  19. 解析器语法错误不会导致 Shell 退出。
  20. 如果启用了 cdspell Shell 选项,Shell 会对 cd 内建命令的目录参数尝试简单的拼写纠正(参见第 4.3.2 节(shopt 内建命令)中对 cdspell 选项的描述)。cdspell 选项只在交互式 Shell 中生效。
  21. Shell 会检查 TMOUT 变量的值,如果在打印 $PS1 之后指定秒数内没有读取到命令,就退出(参见第 5.2 节(Bash 变量))。

译者注:第 14、15、16、18、19 条共同说明了「交互式 Shell 出错不易退出」。用 set -u 对比最直观:非交互式 Shell 遇到未绑定变量会立即退出,后面的命令不会执行:

# 译者注示例:非交互式 Shell 中 set -u 后引用未绑定变量
$ bash -c 'set -u; echo "$UNDEF"; echo after'
bash: 行 1: UNDEF: 未绑定的变量

echo after 没有执行。)而在交互式 Shell 中,同样的错误只会打印一行错误并继续等待下一条命令。类似地,exec 失败、重定向错误、语法错误在交互式 Shell 中都不会让它退出。

译者注:配合 ignoreeofTMOUT 可以给交互式会话加「保险」:执行 set -o ignoreeof 后,误按 Ctrl-D 不会退出 Shell,只会得到提示;再把 TMOUT=600 写进 ~/.bashrc,闲置 10 分钟没有输入命令时 Shell 会自动退出——常用于无人值守终端的超时保护。

6.4 Bash 条件表达式

条件表达式由 [[ 复合命令(参见第 3.2.5.2 节(条件结构))以及 test[ 内建命令(参见第 4.1 节(Bourne Shell 内建命令))使用。test[ 命令根据参数的数目决定自身行为;这两个命令的其他命令特定动作参见它们各自的描述。

表达式可以是一元或二元的,由下面列出的主元(primary)构成。一元表达式常用于检查文件或 Shell 变量的状态。二元操作符用于字符串、数字和文件属性的比较。

当在表达式中使用某些文件名时,Bash 会特殊处理它们。如果 Bash 运行所在的操作系统提供了这些特殊文件,Bash 使用它们;否则 Bash 在内部模拟它们,行为如下:如果某个主元的 FILE 参数形如 /dev/fd/N,Bash 检查文件描述符 N。如果某个主元的 FILE 参数是 /dev/stdin/dev/stdout/dev/stderr 之一,Bash 分别检查文件描述符 0、1 或 2。

当与 [[ 一起使用时,<> 操作符使用当前区域设置(locale)按字典序排序。test 命令使用 ASCII 顺序。

除非另有说明,作用于文件的主元会跟随符号链接,对链接的目标进行操作,而不是链接本身。

主元 说明
-a FILE 若 FILE 存在则为真。
-b FILE 若 FILE 存在且是块特殊文件(block special file)则为真。
-c FILE 若 FILE 存在且是字符特殊文件则为真。
-d FILE 若 FILE 存在且是目录则为真。
-e FILE 若 FILE 存在则为真。
-f FILE 若 FILE 存在且是普通文件则为真。
-g FILE 若 FILE 存在且设置了 set-group-id 位则为真。
-h FILE 若 FILE 存在且是符号链接则为真。
-k FILE 若 FILE 存在且设置了「粘滞」(sticky)位则为真。
-p FILE 若 FILE 存在且是命名管道(FIFO)则为真。
-r FILE 若 FILE 存在且可读则为真。
-s FILE 若 FILE 存在且大小大于零则为真。
-t FD 若文件描述符 FD 已打开且指向终端则为真。
-u FILE 若 FILE 存在且设置了 set-user-id 位则为真。
-w FILE 若 FILE 存在且可写则为真。
-x FILE 若 FILE 存在且可执行则为真。
-G FILE 若 FILE 存在且属于有效组 ID 则为真。
-L FILE 若 FILE 存在且是符号链接则为真。
-N FILE 若 FILE 存在且自上次访问以来已被修改则为真。
-O FILE 若 FILE 存在且属于有效用户 ID 则为真。
-S FILE 若 FILE 存在且是套接字(socket)则为真。
FILE1 -ef FILE2 若 FILE1 和 FILE2 指向相同的设备号和 inode 号则为真。
FILE1 -nt FILE2 若 FILE1 比 FILE2 更新(按修改日期),或者 FILE1 存在而 FILE2 不存在则为真。
FILE1 -ot FILE2 若 FILE1 比 FILE2 更旧,或者 FILE2 存在而 FILE1 不存在则为真。
-o OPTNAME 若 Shell 选项 OPTNAME 已启用则为真。选项列表出现在第 4.3.1 节(set 内建命令)中 -o 选项的描述里。
-v VARNAME 若 Shell 变量 VARNAME 已设置(已被赋过值)则为真。如果 VARNAME 是以 @* 作下标的索引数组变量名,则当数组有任何已设置元素时返回真。如果 VARNAME 是以 @* 作下标的关联数组变量名,则当该键有已设置的元素时返回真。
-R VARNAME 若 Shell 变量 VARNAME 已设置且是名称引用(name reference)则为真。
-z STRING 若 STRING 的长度为零则为真。
-n STRINGSTRING 若 STRING 的长度非零则为真。
STRING1 == STRING2STRING1 = STRING2 若两个字符串相等则为真。与 [[ 命令一起使用时,会执行上文所述的模式匹配(参见第 3.2.5.2 节(条件结构))。为了 POSIX 兼容,与 test 命令一起使用时应使用 =
STRING1 != STRING2 若两个字符串不相等则为真。
STRING1 < STRING2 若 STRING1 按字典序排在 STRING2 之前则为真。
STRING1 > STRING2 若 STRING1 按字典序排在 STRING2 之后则为真。
ARG1 OP ARG2 OP-eq-ne-lt-le-gt-ge 之一。这些算术二元操作符分别当 ARG1 等于、不等于、小于、小于等于、大于或大于等于 ARG2 时返回真。ARG1 和 ARG2 可以是正整数或负整数。与 [[ 命令一起使用时,ARG1 和 ARG2 按算术表达式求值(参见第 6.5 节(Shell 算术))。由于 [[ 命令对 ARG1 和 ARG2 执行的展开可能产生空字符串,算术表达式求值会把这些空字符串当作求值为 0 的表达式。

译者注:条件表达式的三种用法要分清:[(即 test 命令,参见第 4.1 节)是普通命令,参数之间必须有空格,<> 等符号必须转义或加引号;[[ ]] 是 Shell 保留字(参见第 3.2.5.2 节),支持模式匹配和 &&/|| 等;(( )) 专做算术求值,结果非零即为真:

# 译者注示例:三种判断形式的对比
$ [ 1 -eq 1 ] && echo A
A
$ [[ 1 -eq 1 ]] && echo B
B
$ (( 1 + 1 == 2 )) && echo C
C

[ 里忘记转义 < 的后果是把 < 当成了重定向操作符:

# 译者注示例:在 [ 里忘记转义 < 的后果
$ cd /tmp && [ 1 < 2 ]
bash: 2: 没有那个文件或目录

(Bash 试图把文件 2 重定向为 [ 的标准输入。)[[ ]] 中则可以放心直接写 <

译者注-a FILE-e FILE 完全等价,但 -a 已被 POSIX 标准废弃:一来它容易与 [ 表达式1 -a 表达式2 ] 中的「逻辑与」操作符混淆,二来它在不同实现里行为不一。新代码请一律使用 -e[[ -e 文件 ]]

译者注[[ ]]</> 按当前区域设置(locale)的排序规则比较,test/[ ] 固定按 ASCII 码顺序比较。两者在大多数区域设置下结果一致,但 LC_COLLATE 会影响字母大小写的相对次序,依赖排序结果时要留意:

# 译者注示例:两种排序方式
$ [ "abc" \< "abd" ] && echo ASCII
ASCII
$ [[ "abc" < "abd" ]] && echo locale
locale

注意在 [ ]<> 必须写成 \<\>(或加引号),否则会被当作重定向操作符(参见第 3.6 节(重定向))。

译者注-v 判断「变量是否已赋值」。数组可以配合 @/* 下标使用;注意对关联数组而言,@* 是普通的字面键,-v m[@] 只检查名为 @ 的键是否存在:

# 译者注示例:-v 与索引数组、关联数组
$ a=(1 2); [[ -v a[@] ]] && echo "indexed: has elements"
indexed: has elements
$ declare -A m=([foo]=1)
$ [[ -v m[foo] ]] && echo "m[foo] set"
m[foo] set
$ [[ -v m[@] ]] && echo "m[@] set" || echo "m[@] not set"
m[@] not set
$ declare -A m2=([@]=x); [[ -v m2[@] ]] && echo "literal @ key set"
literal @ key set

译者注[[ ]] 的算术二元操作符(-eq 等)把两侧当作算术表达式求值,因此可以直接写算式和变量名;展开结果为空字符串时按 0 处理:

# 译者注示例:[[ ]] 中直接写算术表达式
$ x=2
$ [[ x+1 -eq 3 ]] && echo yes
yes
$ [[ 5 -eq 3+2 ]] && echo yes
yes
$ [[ $empty -eq 0 ]] && echo zero
zero

(最后一个例子中 $empty 未设置,展开为空字符串,按 0 参与比较。)

译者注/dev/fd/N/dev/stdin/dev/stdout/dev/stderr 这些「特殊文件名」即使操作系统不提供对应的设备文件,Bash 也会在内部映射到相应的文件描述符。例如:

# 译者注示例:检查标准输入
$ [ -t 0 ] && echo tty || echo not-tty
not-tty
$ [ -e /dev/fd/0 ] && echo fd0-open
fd0-open

(在管道或脚本中标准输入不是终端,所以 -t 0 为假;-e /dev/fd/0 检查标准输入是否已打开。)

6.5 Shell 算术

Shell 允许对算术表达式求值,方式包括:作为一种 Shell 展开,或使用 (( 复合命令、letdeclare 内建命令、算术 for 命令、[[ 条件命令,或 declare 内建命令的 -i 选项。

求值在可用的最大定宽整数(fixed-width integer)上进行,不检查溢出,但除以 0 会被捕获并标记为错误。运算符及其优先级、结合性和取值都与 C 语言相同。下面的运算符按同级优先级分组列出,各层按优先级从高到低排列。

运算符 含义
ID++ ID-- 变量后置自增、后置自减
++ID --ID 变量前置自增、前置自减
- + 一元负号、一元正号
! ~ 逻辑取反、按位取反
** 幂运算
* / % 乘法、除法、取余
+ - 加法、减法
<< >> 左移、右移(按位)
<= >= < > 比较
== != 相等、不等
& 按位与
^ 按位异或
| 按位或
&& 逻辑与
|| 逻辑或
expr ? if-true-expr : if-false-expr 条件运算符
= *= /= %= += -= <<= >>= &= ^= |= 赋值
expr1 , expr2 逗号

Shell 变量允许作为操作数;参数展开在表达式求值之前执行。在表达式内部,Shell 变量也可以不带参数展开语法、直接按名称引用。也就是说,你可以在算术表达式中使用 X(X 是 Shell 变量名),Shell 会把它的值作为表达式求值并使用结果。在表达式中按名称引用的、为空的或未设置的 Shell 变量求值为 0。

变量的值在被引用时按算术表达式求值;用 declare -i 赋予了「整数」(integer)属性的变量被赋值时也按算术表达式求值。空值求值为 0。Shell 变量即使没有开启「整数」属性,也可以用在表达式中。

整数常量遵循 C 语言定义,不带后缀,也没有字符常量。以 0 开头的常量按八进制数解释。前导 0x0X 表示十六进制。否则,数字采用 [BASE#]N 形式,其中可选的 BASE 是 2 到 64 之间的十进制数,表示算术进制,N 是该进制下的数字。如果省略 BASE#,则使用十进制。指定 N 时,如果需要非数字字符,大于 9 的「数字」依次由小写字母、大写字母、@_ 表示。如果 BASE 小于等于 36,小写字母和大写字母可以互换使用来表示 10 到 35 之间的数。

运算符按优先级顺序求值。括号中的子表达式最先求值,可以覆盖上述优先级规则。

译者注(( ))let$(( )) 三者分工不同:(( ))let 把表达式当作命令执行,以「结果非零」为真;$(( )) 是算术展开,把求值结果放进命令行。优先级与 C 语言完全一致,括号可以改变计算顺序:

# 译者注示例:三种算术写法与优先级
$ echo $(( 2 + 3 * 4 ))
14
$ echo $(( (2 + 3) * 4 ))
20
$ x=5; (( x += 2 )); echo $x
7
$ let 'x = x * 3'; echo $x
21
$ echo $(( 2**10 ))
1024

另外,declare -i 给变量加上「整数」属性后,之后的赋值都会按算术表达式求值:

# 译者注示例:declare -i 的算术赋值
$ declare -i z=3
$ z+=2; echo $z
5

译者注:整数常量的写法与 C 语言一致:010 是八进制(等于十进制的 8),0x10 是十六进制(16)。此外 Bash 还支持 BASE#N 形式的任意进制(2–64),大于 9 的「数字」依次为小写字母、大写字母、@_

# 译者注示例:各种进制的整数常量
$ echo $(( 010 ))
8
$ echo $(( 0x10 ))
16
$ echo $(( 2#1010 ))
10
$ echo $(( 64#_ ))
63
$ echo $(( 010 + 0x10 + 2#1010 + 64#_ ))
97

译者注:在算术表达式中按名称引用的变量,若未设置或为空,按 0 处理——因此可以放心引用可能未定义的变量:

# 译者注示例:未设置变量按 0 参与运算
$ unset y; echo $(( y + 1 ))
1

6.6 别名

「别名」(Alias)允许把一个字符串替换到输入中某个位置上、可以成为简单命令第一个单词的单词上。别名有名称和相应的值,使用 aliasunalias 内建命令来设置和取消(参见第 4 章(Shell 内建命令))。

如果 Shell 读到一个处于正确位置的未加引号的单词,它会检查该单词是否与某个别名名称匹配。如果匹配,Shell 用别名值替换该单词,并像读入的是别名值而不是原单词一样继续读取。在尝试别名替换之前,Shell 不会查看该单词之后的任何字符。

/$`= 字符以及上面列出的任何 Shell 元字符或引号字符都不能出现在别名名称中。替换文本可以包含任何有效的 Shell 输入,包括 Shell 元字符。替换文本的第一个单词会被检查是否为别名,但与正在展开的别名完全相同的单词不会再次展开。这意味着,例如,你可以把 ls 别名为 "ls -F",而 Bash 不会尝试递归展开替换文本。

如果别名值的最后一个字符是「空白」(blank),那么 Shell 会检查该别名之后的下一个命令单词是否进行别名展开。

别名用 alias 命令创建和列出,用 unalias 命令删除。

csh 不同,这里没有在替换文本中使用参数的机制。如果需要参数,请改用 Shell 函数(参见第 3.3 节(Shell 函数))。

当 Shell 不交互时,别名不会被展开,除非用 shopt 设置 expand_aliases Shell 选项(参见第 4.3.2 节(shopt 内建命令))。

关于别名的定义和使用的规则有些令人困惑。Bash 在执行某行上的任何命令或某个复合命令之前,总是至少读取一整条完整的输入行,以及组成复合命令的所有行。别名在命令被读取时展开,而不是在执行时展开。因此,与另一条命令出现在同一行的别名定义,要等到 Shell 读取下一行输入时才生效;复合命令中的别名定义要等到 Shell 解析并执行完整个复合命令后才生效。该行上别名定义之后的命令,或复合命令其余部分中的命令,不受新别名的影响。函数执行时也存在这个问题:别名在函数定义被读取时展开,而不是在函数执行时展开,因为函数定义本身也是一条命令。因此,在函数中定义的别名要等到该函数执行之后才可用。为了安全起见,始终把别名定义放在单独的一行,并且不要在复合命令中使用 alias

几乎在所有用途上,Shell 函数都比别名更可取。

译者注:别名在「读取」时展开而不是「执行」时展开,这带来两个常见误区。其一,同一行里定义的别名对该行上的后续命令不生效(Bash 会先把整行读完再执行):

# 译者注示例:同一行定义并立即使用别名——不生效
$ bash -c $'shopt -s expand_aliases\nalias hi="echo hello"; hi'
bash: 行 2: hi: 未找到命令

把定义放到单独一行后就正常了:

# 译者注示例:别名定义分行
$ bash -c $'shopt -s expand_aliases\nalias hi="echo hello"\nhi'
hello

其二,非交互式 Shell 默认不展开别名(交互式 Shell 默认展开,见第 6.3.3 节第 8 条),脚本里要用必须先用 shopt -s expand_aliases 开启。

译者注alias ls="ls -F" 之所以安全,是因为规则规定「与正在展开的别名完全相同的单词不会再次展开」,所以不会无限递归。另一个细节:如果别名值的最后一个字符是空格,Shell 还会继续检查下一个命令单词是否为别名,可用于级联:

# 译者注示例:别名值末尾空格带来的级联效果
$ bash -c $'shopt -s expand_aliases\nalias e=echo\nalias x=echo\ne x'
x
$ bash -c $'shopt -s expand_aliases\nalias e="echo "\nalias x=echo\ne x'
echo

第一条命令中 e 的别名值末尾没有空格,x 不被展开,命令变成 echo x;第二条命令中 e 的别名值以空格结尾,x 被展开成 echo,命令变成 echo echo

译者注:别名不能带参数(那是 csh 别名才有的能力)。需要参数、循环、分支等逻辑时请使用 Shell 函数(参见第 3.3 节(Shell 函数))。手册的结论很直白:几乎任何用途下,函数都优于别名。

6.7 数组

Bash 提供一维的索引数组和关联数组变量。任何变量都可以当作索引数组使用;declare 内建命令显式声明数组。数组的大小没有上限,成员也不要求连续索引或连续赋值。索引数组用必须展开为整数的算术表达式来引用(参见第 6.5 节(Shell 算术)),并且从零开始编号;关联数组使用任意字符串。除非另有说明,索引数组的下标必须是非负整数。

Shell 对索引数组下标执行参数和变量展开、算术展开、命令替换和引号去除。由于这可能会产生空字符串,下标索引会把这些空字符串当作求值为 0 的表达式。

Shell 对关联数组下标执行波浪号展开、参数和变量展开、算术展开、命令替换和引号去除。空字符串不能用作关联数组的键。

如果用下面这个语法给任何变量赋值,Bash 会自动创建一个索引数组:

NAME[SUBSCRIPT]=VALUE

SUBSCRIPT 被视为算术表达式,必须求值为大于等于零的数。要显式声明索引数组,使用

declare -a NAME

(参见第 4.2 节(Bash 内建命令))。也接受语法

declare -a NAME[SUBSCRIPT]

其中的 SUBSCRIPT 被忽略。

关联数组用下面这种方式创建:

declare -A NAME

可以用 declarereadonly 内建命令为数组变量指定属性。每个属性适用于数组的所有成员。

数组用以下形式的复合赋值来赋值:

NAME=(VALUE1 VALUE2 ... )

其中每个 VALUE 可以是 [SUBSCRIPT]=STRING 形式。索引数组赋值除了 STRING 之外不需要任何东西。

列表中的每个 VALUE 都要经受上文所述的 Shell 展开(参见第 3.5 节(Shell 展开)),但包含方括号和下标的、是有效变量赋值的 VALUE 不做花括号展开和单词拆分,这与单独的变量赋值一样。

对索引数组赋值时,如果提供了可选的下标,就赋给该下标;否则被赋值的元素下标是该语句中上一个被赋值的下标加一。索引从零开始。

对关联数组赋值时,复合赋值中的单词可以是赋值语句(此时下标是必需的),也可以是被解释为交替键值序列的单词列表:NAME=(KEY1 VALUE1 KEY2 VALUE2 ... )。它们与 NAME=( [KEY1]=VALUE1 [KEY2]=VALUE2 ... ) 的处理完全相同。列表中的第一个单词决定其余单词如何解释;一个列表中的所有赋值必须是同一类型。使用键/值对时,键不能缺失或为空;末尾缺失的值按空字符串处理。

declare 内建命令也接受这种语法。单个数组元素可以用上文介绍的 NAME[SUBSCRIPT]=VALUE 语法赋值。

对索引数组赋值时,如果 NAME 的下标是负数,该数字被解释为相对于 NAME 最大索引加一的位置,因此负下标从数组末尾往回数,-1 下标引用最后一个元素。

使用复合赋值语法赋值时,+= 操作符向数组变量追加;参见上文第 3.4 节(Shell 参数)。

数组元素用 ${NAME[SUBSCRIPT]} 引用。花括号是必需的,以避免与 Shell 的文件名展开操作符冲突。如果 SUBSCRIPT 是 @*,该单词展开为数组 NAME 的所有成员,除非在内建命令或单词展开的描述中另有说明。这两个下标只有在单词出现在双引号内时才不同。如果单词加了双引号,${NAME[*]} 展开为单个单词,每个数组成员的值由 IFS 变量的第一个字符分隔;${NAME[@]} 把 NAME 的每个元素展开为单独的单词。当没有数组成员时,${NAME[@]} 展开为空。如果双引号展开出现在某个单词内部,第一个参数的展开与原始单词开头的部分连接,最后一个参数的展开与原始单词末尾的部分连接。这与特殊参数 @* 的展开类似。

${#NAME[SUBSCRIPT]} 展开为 ${NAME[SUBSCRIPT]} 的长度。如果 SUBSCRIPT 是 @*,展开结果是数组中元素的个数。

如果用来引用索引数组元素的下标求值为小于零的数,它被解释为相对于数组最大索引加一的位置,因此负下标从数组末尾往回数,-1 下标引用最后一个元素。

不带下标引用数组变量等价于以下标 0 引用。任何使用有效下标对变量的引用都是有效的;如有必要,Bash 会创建数组。

如果某个下标被赋过值,该数组变量就被认为已设置。空字符串是有效值。

除了值之外,还可以取得数组的键(下标)。${!NAME[@]}${!NAME[*]} 展开为数组变量 NAME 中已赋值的下标。在双引号内的处理方式与特殊参数 @* 在双引号内的展开类似。

unset 内建命令用来销毁数组。unset NAME[SUBSCRIPT] 取消设置下标 SUBSCRIPT 处的数组元素。索引数组的负下标按上文所述解释。取消设置数组变量的最后一个元素不会取消设置该变量本身。unset NAME(NAME 是数组)删除整个数组。当 SUBSCRIPT 是 *@ 时,unset NAME[SUBSCRIPT] 的行为因数组类型而异:当 NAME 是关联数组时,它删除键为 *@ 的元素;如果 NAME 是索引数组,unset 删除所有元素,但不删除数组本身。

当把带下标的变量名作为命令的参数(例如 unset)而不使用上文所述的单词展开语法时(例如 unset a[4]),该参数要经受 Shell 的文件名展开。如果不想要路径名展开,请给参数加引号(例如 unset 'a[4]')。

declarelocalreadonly 内建命令都接受 -a 选项指定索引数组,-A 选项指定关联数组。如果同时提供两个选项,-A 优先。read 内建命令接受 -a 选项,把从标准输入读到的单词列表赋给数组,并且可以把标准输入读到的值赋给单个数组元素。setdeclare 内建命令以可以重新用作输入的方式显示数组值。其他内建命令也接受数组名称参数(例如 mapfile);细节参见各内建命令的描述。Shell 提供了许多内建的数组变量。

译者注:关联数组必须先 declare -A 声明才能使用(索引数组不声明也能用)。遍历关联数组要用 "${!arr[@]}" 取得键列表:

# 译者注示例:关联数组的声明与遍历
$ declare -A user=([name]=Alice [city]=Beijing)
$ for k in "${!user[@]}"; do echo "$k=${user[$k]}"; done | sort
city=Beijing
name=Alice

复合赋值还有等价的「键 值 键 值……」交替写法:

# 译者注示例:键值交替的复合赋值
$ declare -A m4=(a 1 b 2)
$ for k in "${!m4[@]}"; do echo "$k=${m4[$k]}"; done | sort
a=1
b=2

译者注"${a[@]}""${a[*]}" 的区别只在双引号内体现:前者每个元素各成一个单词,后者合并成一个单词、以 IFS 变量的第一个字符连接:

# 译者注示例:@ 与 * 的差别
$ a=(x y z)
$ printf '<%s>\n' "${a[@]}"
<x>
<y>
<z>
$ ( IFS=,; echo "${a[*]}" )
x,y,z

正因如此,遍历数组、把数组作为参数传递时,永远使用带双引号的 ${a[@]}

译者注:负下标从数组末尾倒数,-1 是最后一个元素;${#a[@]} 是元素个数;"${!a[@]}" 列出已赋值的下标:

# 译者注示例:负下标、元素个数与下标列表
$ a=(x y z)
$ echo "${a[-1]}"
z
$ echo "${#a[@]}"
3
$ printf '%s ' "${!a[@]}"; echo
0 1 2
$ echo "${a[@]: -2}"
y z

"${a[@]: -2}" 是子串切片:取最后两个元素。切片的负偏移 - 前必须有空格或括号,否则会被当作选项。)

译者注unset a[4] 中的 a[4] 是普通参数而不是展开语法,它会先经过文件名展开:若当前目录恰好有匹配 a[4] 这个模式的 a4 之类的文件,就会出问题。所以规范写法是加引号:

# 译者注示例:给下标加引号,避免文件名展开
$ a=(1 2 3 4); unset 'a[1]'; echo "${a[@]}"
1 3 4

另外注意:unset a 删除整个数组;对索引数组 unset a[@]/unset a[*] 只清空全部元素、保留数组本身,而对关联数组则只删除键为 @* 的那个元素。

译者注:任何变量都可以直接当索引数组用,b[0]=x 会自动创建数组;下标必须是非负整数。复合赋值从 0 开始连续填充,b+=(...) 在末尾追加:

# 译者注示例:自动创建与追加
$ b[0]=x; b[2]=z; echo "${b[@]}"
x z
$ b+=(w); echo "${b[@]}"
x z w

(第一个命令中 b[1] 从未赋值,"${b[@]}" 打印为 x z。)

6.8 目录栈

目录栈是最近访问过的目录的列表。pushd 内建命令在更改当前目录的同时把目录加入栈中,popd 内建命令从栈中删除指定的目录并把当前目录改为被删除的目录。dirs 内建命令显示目录栈的内容。当前目录始终是目录栈的「栈顶」。

目录栈的内容也以 DIRSTACK Shell 变量的值的形式可见。

6.8.1 目录栈内建命令

dirs

dirs [-clpv] [+N | -N]

不带选项时,显示当前记住的目录列表。目录通过 pushd 命令加入列表;popd 命令从列表中删除目录。当前目录始终是栈中的第一个目录。

如果提供了选项,含义如下:

选项 含义
-c 通过删除所有元素来清空目录栈。
-l 使用完整路径名显示;默认的显示格式用波浪号表示主目录。
-p 使 dirs 逐行打印目录栈,每行一项。
-v 使 dirs 逐行打印目录栈,并为每项加上其在栈中的索引前缀。
+N 显示第 N 个目录(从 dirs 不带选项打印的列表左边数起),从零开始。
-N 显示第 N 个目录(从 dirs 不带选项打印的列表右边数起),从零开始。

popd

popd [-n] [+N | -N]

从目录栈中删除元素。元素从 0 开始编号,从 dirs 列出的第一个目录起算;也就是说,popd 等价于 popd +0

不带参数时,popd 删除栈顶目录并切换到新的栈顶目录。

如果提供了参数,含义如下:

参数 含义
-n 删除栈中目录时抑制正常的目录切换,只操作栈本身。
+N 从栈中删除第 N 个目录(从 dirs 打印的列表左边数起),从零开始。
-N 从栈中删除第 N 个目录(从 dirs 打印的列表右边数起),从零开始。

如果目录栈的栈顶元素被修改,且未提供 -n 选项,popd 使用 cd 内建命令切换到栈顶目录。如果 cd 失败,popd 返回非零值。

否则,如果指定了无效选项、目录栈为空,或 N 指定了不存在的目录栈项,popd 返回失败状态。

如果 popd 命令成功,Bash 运行 dirs 显示目录栈的最终内容,返回状态为 0。

pushd

pushd [-n] [+N | -N | DIR]

把目录加到目录栈的顶部,或者旋转栈,使新的栈顶成为当前工作目录。不带参数时,pushd 交换目录栈的前两个元素。

如果提供了参数,含义如下:

参数 含义
-n 旋转或加入目录时抑制正常的目录切换,只操作栈本身。
+N 旋转栈,使第 N 个目录(从 dirs 打印的列表左边数起,从零开始)位于栈顶。
-N 旋转栈,使第 N 个目录(从 dirs 打印的列表右边数起,从零开始)位于栈顶。
DIR 使 DIR 成为栈顶。

栈被修改后,如果未提供 -n 选项,pushd 使用 cd 内建命令切换到栈顶目录。如果 cd 失败,pushd 返回非零值。

否则,如果未提供参数,pushd 返回零,除非目录栈为空。旋转目录栈时,pushd 返回零,除非目录栈为空或 N 指定了不存在的目录栈元素。

如果 pushd 命令成功,Bash 运行 dirs 显示目录栈的最终内容。

译者注:目录栈的典型操作如下(当前目录始终在栈顶,即最左边):

# 译者注示例:pushd / popd / dirs -v
$ mkdir -p /tmp/d1 /tmp/d2 && cd /tmp/d1
$ pushd /tmp/d2
/tmp/d2 /tmp/d1
$ pushd /tmp
/tmp /tmp/d2 /tmp/d1
$ dirs -v
 0  /tmp
 1  /tmp/d2
 2  /tmp/d1
$ popd
/tmp/d2 /tmp/d1
$ dirs
/tmp/d2 /tmp/d1

dirs -v 为每项编号,配合 pushd +N/popd +N 可以按编号旋转或删除(+N 从左边数、-N 从右边数,均从 0 起)。栈的内容同时以 DIRSTACK 变量的形式存在。

6.9 控制提示符

此外,下表描述了可以出现在提示符变量 PS0PS1PS2PS4 中的特殊字符:

转义序列 含义
\a 响铃字符。
\d 日期,格式为「星期 月 日」(如 "Tue May 26")。
\D{FORMAT} FORMAT 传给 strftime(3),结果插入提示字符串;空的 FORMAT 产生区域设置相关的本地时间表示。花括号是必需的。
\e 转义字符。
\h 主机名,到第一个 . 为止。
\H 主机名。
\j 当前由 Shell 管理的作业数。
\l Shell 终端设备名称的基本名(如 "ttys0")。
\n 换行符。
\r 回车符。
\s Shell 的名称:$0 的基本名(最后一个斜杠之后的部分)。
\t 时间,24 小时制 HH:MM:SS 格式。
\T 时间,12 小时制 HH:MM:SS 格式。
\@ 时间,12 小时制 am/pm 格式。
\A 时间,24 小时制 HH:MM 格式。
\u 当前用户的用户名。
\v Bash 版本号(如 2.00)。
\V Bash 发行版,版本号 + 补丁级别(如 2.00.0)。
\w PWD Shell 变量($PWD)的值,$HOME 缩写为波浪号(使用 $PROMPT_DIRTRIM 变量)。
\W $PWD 的基本名,$HOME 缩写为波浪号。
\! 本命令的历史编号。
\# 本命令的命令编号。
\$ 如果有效 uid 是 0,则为 #,否则为 $
\NNN ASCII 码为八进制值 NNN 的字符。
\\ 反斜杠。
\[ 开始一段不可打印字符序列。可用于把终端控制序列嵌入提示符。
\] 结束一段不可打印字符序列。

命令编号和历史编号通常不同:一条命令的历史编号是它在历史列表中的位置,历史列表可能包含从历史文件恢复的命令(参见第 9.1 节(Bash 的历史记录设施));而命令编号是它在当前 Shell 会话中已执行命令序列中的位置。

字符串被解码之后,还要经受参数展开、命令替换、算术展开和引号去除,具体取决于 promptvars Shell 选项的值(参见第 4.3.2 节(shopt 内建命令))。如果字符串中被转义的部分出现在命令替换内,或包含对单词展开特殊的字符,这可能会产生不想要的副作用。

译者注:最常见的提示符是 \u@\h:\w\$ ,即「用户名@主机名:当前目录$」;\$ 在 root 下自动显示为 #,一眼可辨:

# 译者注示例:经典提示符(root 用户时末尾显示 #)
$ PS1='\u@\h:\w\$ '

给提示符加颜色时,\[\] 必须把不可打印的终端控制序列包起来,否则 Readline 计算光标位置会出错,编辑长命令时光标会乱跳:

# 译者注示例:绿色路径 + 复位符的彩色提示符(\e[32m 绿色,\e[00m 复位)
$ PS1='\[\e[32m\]\w\[\e[00m\]\$ '

译者注\D{...} 把括号里的格式串交给 strftime(3) 处理:%F 相当于 %Y-%m-%d%T 相当于 %H:%M:%S

# 译者注示例:带日期时间的提示符
$ PS1='\D{%F %T}\$ '

另外注意命令编号(\#)与历史编号(\!)的区别:历史编号是命令在整个历史列表中的位置,会话启动时会把 HISTFILE 里的旧命令并入历史(参见第 9.1 节),所以历史编号往往不是从 1 开始;命令编号只统计当前会话内已执行的命令。执行 history -r 重新读入历史后,两者会明显不一致。

译者注:提示符字符串解码之后还会再进行一轮参数展开、命令替换等(受 promptvars 选项控制,参见第 4.3.2 节)。很多人喜欢在提示符里嵌 $(git branch ...) 之类的命令替换来显示 Git 分支——注意它每次显示提示符时都会执行一次,命令太慢会明显拖慢输入;不需要这轮展开时,可用 shopt -u promptvars 关闭。

6.10 受限 Shell

如果 Bash 以名称 rbash 启动,或者在调用时提供了 --restricted-r 选项,Shell 就会进入受限(restricted)模式。受限 Shell 用于建立一个比标准 Shell 受到更多控制的环境。除了以下被禁止或不再执行的操作外,受限 Shell 的行为与 bash 完全一致:

  • 使用 cd 内建命令更改目录。
  • 设置或取消设置 SHELLPATHHISTFILEENVBASH_ENV 变量的值。
  • 指定包含斜杠的命令名。
  • 指定包含斜杠的文件名作为 . 内建命令的参数。
  • 使用 . 内建命令的 -p 选项来指定搜索路径。
  • 指定包含斜杠的文件名作为 history 内建命令的参数。
  • 指定包含斜杠的文件名作为 hash 内建命令 -p 选项的参数。
  • 在启动时从 Shell 环境导入函数定义。
  • 在启动时解析 Shell 环境中的 SHELLOPTS 值。
  • 使用 >>|<>>&&>>> 重定向操作符重定向输出。
  • 使用 exec 内建命令用另一个命令替换 Shell。
  • 使用 enable 内建命令的 -f-d 选项添加或删除内建命令。
  • 使用 enable 内建命令启用被禁用的 Shell 内建命令。
  • 指定 command 内建命令的 -p 选项。
  • set +rshopt -u restricted_shell 关闭受限模式。

译者注:进入受限模式有两种方式:把 rbash 作为命令名调用(argv[0]rbash),或在调用时加 --restricted(或 -r)选项。注意限制是在「读取任何启动文件之后」才生效的——启动文件先于限制运行,因此 ~/.bashrc 中的命令(包括修改 PATH 之类的操作)在读取阶段仍可正常执行。

这些限制在读取任何启动文件之后强制执行。

当执行的命令被认定是一个 Shell 脚本时(参见第 3.8 节(Shell 脚本)),rbash 会关闭为执行该脚本而派生的 Shell 中的任何限制。

译者注:注意最后这条:受限 Shell 执行的 Shell 脚本会在一个不受限的 Shell 中运行。因此「不允许受限 Shell 执行 Shell 脚本」本身就是受限环境的安全配置措施之一。实测(Bash 5.3):

# 译者注示例:受限 Shell 中违禁操作的报错
$ bash -r -c 'cd /tmp'
bash: 第 1 行: cd: 受限
$ bash -r -c 'echo hi > /tmp/x'
bash: 行 1: /tmp/x: 受限:无法重定向输出
$ bash -r -c 'set +r'
bash: 第 1 行: set: +r: 无效的选项

受限 Shell 模式只是有用的受限环境的一个组成部分。它还应辅以以下措施:把 PATH 设置为只允许执行少数经过验证的命令的值(允许 Shell 转义的命令尤其脆弱);登录后把当前目录更改为 $HOME 以外的不可写目录;不允许受限 Shell 执行 Shell 脚本;以及清理环境中会导致某些命令改变其行为的变量(例如 VISUALPAGER)。

现代系统提供了实现受限环境的更安全方式,例如「jails(监狱)」、「zones(区域)」或「containers(容器)」。

译者注:受限 Shell 只是一种「自我限制」机制,并不是安全边界——它不能阻止用户通过允许的命令逃逸(例如许多编辑器、less 等命令本身提供进入子 Shell 的转义途径),也不能防范恶意用户。需要真正隔离的环境时,请使用 chroot、jail、容器或虚拟机等机制。

6.11 Bash 与 POSIX

6.11.1 什么是 POSIX?

POSIX 是基于 Unix 的一系列标准的名称。许多 Unix 服务、工具和函数都是该标准的一部分,从基本的系统调用和 C 库函数,到常见的应用程序和工具,再到系统管理和维护。

POSIX 的 Shell 与实用工具(Shell and Utilities)标准最初由 IEEE 工作组 1003.2(POSIX.2)制定。1003.2 标准的第一版于 1992 年发布。它后来与最初的 IEEE 1003.1 工作组合并,目前由 Austin Group(IEEE、The Open Group 与 ISO/IEC SC22/WG15 的联合工作组)维护。如今,Shell 与实用工具是构成 IEEE Std 1003.1-2024 的文档集中的一个分册,因此早先的 POSIX.2(1992 年)现在是当前统一 POSIX 标准的一部分。

译者注:简单梳理一下这段历史:POSIX.1(IEEE 1003.1)最初只规定 C 语言系统接口,POSIX.2(IEEE 1003.2,1992 年)才规定 Shell 和实用工具。1990 年代末两者合并为一个标准,由 Austin Group 维护,后来的版本以「IEEE Std 1003.1-YYYY」的形式逐年修订发布(即大家常说的「POSIX 标准」)。Bash 手册引用的是 2024 年版。

Shell 与实用工具分册专注于命令解释器接口以及通常在命令行或由其他程序执行的实用工具程序。该标准在网络上免费提供,网址为 https://pubs.opengroup.org/onlinepubs/9799919799/utilities/contents.html

Bash 关注的是 POSIX Shell 与实用工具分册所定义的 Shell 行为方面。Shell 命令语言当然已被标准化,包括基本的流程控制和程序执行结构、I/O 重定向和管道(流水线)、参数处理、变量展开和引号机制。

特殊内建命令(special builtins)必须作为 Shell 的一部分实现以提供所需的功能,被规定为 Shell 的组成部分;evalexport 就是这样的例子。其他实用工具出现在 POSIX 中不专门讲 Shell 的章节里,它们通常(在某些情况下必须)作为内建命令实现,例如 readtest。POSIX 还规定了 Shell 交互式行为的各个方面,包括作业控制和命令行编辑。只有 vi 风格的行编辑命令被标准化了;emacs 编辑命令因遭到反对而被排除在外。

译者注:POSIX 只标准化了 vi 风格的行编辑命令。当年委员会对把 emacs 的编辑命令写入标准存在争议(部分成员认为 emacs 编辑风格与标准委员会的主流意见不合,且 EMACS 的按键约定被其开发者视为应受到版权保护),最终 emacs 风格未被纳入标准。但这并不妨碍 Bash 默认使用 emacs 风格的行编辑(见第 8 章),因为 POSIX 并不强制具体的编辑界面。

6.11.2 Bash 的 POSIX 模式

尽管 Bash 是 POSIX Shell 规范的一种实现,但在某些领域 Bash 的默认行为与规范不同。Bash 的「POSIX 模式(posix mode)」在这些领域改变 Bash 的行为,使其更严格地符合标准。

--posix 命令行选项启动 Bash,或在 Bash 运行期间执行 set -o posix,会在 Bash 默认行为与 POSIX 不同的领域把行为改为与 POSIX 规定的一致,从而使 Bash 更接近 POSIX 标准。

当以 sh 调用时,Bash 在读取启动文件后进入 POSIX 模式。

译者注:进入 POSIX 模式的三种途径:bash --posix 命令行选项、交互式执行 set -o posix(也可以用 set -o posix 关闭后重新打开,或用 set +o posix 退出该模式),以及以 sh 为命令名调用 Bash。注意许多 Linux 发行版把 /bin/sh 链接到 dash,只有把 /bin/sh 链接到 bash 的系统上「以 sh 调用」才等价于 Bash 的 POSIX 模式。

以下是 POSIX 模式生效时发生的变化:

  1. Bash 确保 POSIXLY_CORRECT 变量已被设置。

译者注:实测 bash --posix -c 'echo "$POSIXLY_CORRECT"' 输出 yPOSIXLY_CORRECT 是 Bash 兼容性处理的历史遗留:该变量被设置(为任意值)时,Bash 就会启用一部分与 POSIX 模式重叠的行为(例如 kill -l 的输出格式)。因此不要在启动文件中手动设置它,否则会意外触发与 POSIX 模式类似的行为。

  1. Bash 读取并执行 POSIX 启动文件($ENV),而不是普通的 Bash 启动文件(参见第 6.2 节(Bash 启动文件))。

  2. 别名展开始终启用,即使在非交互式 Shell 中也是如此。

  3. 出现在可识别保留字的上下文中的保留字不进行别名展开。

  4. 在初次解析命令替换时就执行别名展开。默认(非 POSIX)模式通常在启用别名展开时把它推迟到命令替换执行时。这意味着命令替换不会展开在命令替换初次解析之后才定义的别名(例如,作为函数定义的一部分定义的别名)。

译者注:这条的差异在于「解析时机」。例如在 x=$(alias hi='echo hello'; hi) 中,如果别名 hi 与命令替换在同一轮解析中定义,非 POSIX 模式会等到命令替换实际执行(即子 Shell 中重新解析时)才展开别名,而 POSIX 模式在初次解析时就展开。实践中别名与命令替换混用容易出问题,建议在函数或脚本中避免这种写法。

  1. time 保留字可以单独用作简单命令。以这种方式使用时,它显示 Shell 及其已完成子进程的计时统计。TIMEFORMAT 变量控制计时信息的格式。

译者注time 单独使用即「对整个 Shell 会话计时」:bash --posix -c 'time' 会输出本 Shell 的 user/sys 时间(实测输出两行:usersys)。如果 time 后跟的是以 - 开头的单词,则第 7 条说明的解析规则会把 time 当成普通命令名,所以 time -p cmd 之类的写法在 POSIX 模式下不被识别为保留字。

  1. 如果下一个记号以 - 开头,解析器不把 time 识别为保留字。

  2. 在解析和展开双引号内的 ${...} 展开时,单引号不再特殊,不能用于引住闭花括号或其他特殊字符,除非该操作符是定义为执行模式删除(pattern removal)的操作符之一。在这种情况下,单引号不必成对出现。

译者注:模式删除操作符指 ${var#模式}${var##模式}${var%模式}${var%%模式}。例如 ${v%'}'} 中模式 '}' 里的单引号可以引住闭花括号而不必成对,因为解析器知道这里需要把 '}' 当成模式的一部分。其他操作符(如 ${v:-...})在 POSIX 模式下则不能这样做。这与下文 6.12 节 compat41/compat42 条目中引用的 POSIX 解释 221 有关。

  1. 重定向操作符不对重定向中的单词执行文件名展开,除非 Shell 是交互式的。

  2. 重定向操作符不对重定向中的单词执行单词拆分。

  3. 函数名不得与某个 POSIX 特殊内建命令同名。

  4. 波浪号展开只对命令名之前的赋值语句执行,而不是对行中的所有赋值语句执行。

译者注:即 v=~/dir cmd(赋值紧跟在命令名之前)中 ~ 会被展开,而其他位置的赋值语句(例如 cmd v=~/dir 之类不以赋值开头的写法)中的 ~ 在 POSIX 模式下不再展开。此差异主要体现在赋值语句与命令的先后关系上,实际脚本里最稳妥的写法是把 ~ 显式写成 $HOME

  1. 虽然变量间接引用可用,但它不能应用于 #? 特殊参数。

  2. 在展开结果被双引号括起的模式上下文中展开 * 特殊参数时,不再把 $* 当作被双引号括起来对待。

  3. 当双引号字符(")出现在将要进行展开的 here-document 主体内的反引号命令替换中时,它会被特殊对待。这意味着,例如,双引号字符前面的反斜杠会转义该双引号,并且反斜杠会被删除。

  4. 命令替换不再设置 ? 特殊参数。没有命令字的简单命令的退出状态仍然是求值该命令中的变量赋值和重定向时发生的最后一次命令替换的退出状态,但这要等到所有赋值和重定向完成之后才会发生。

  5. PATH 变量各元素中作为首字符出现的字面波浪号不再按上文「波浪号展开」所述的方式展开(参见第 3.5.2 节(波浪号展开))。

  6. 命令查找先于 Shell 函数找到 POSIX 特殊内建命令,包括 typecommand 内建命令打印的输出。

  7. 即使一个名称中包含斜杠的 Shell 函数在进入 POSIX 模式之前就已定义,Shell 也不会执行名称中包含一个或多个斜杠的函数。

  8. 当哈希表中的命令不再存在时,Bash 会重新搜索 $PATH 以找到新位置。这也可以通过 shopt -s checkhash 实现。

  9. Bash 不会把未设置执行位的命令插入命令哈希表,即使它在 $PATH 搜索中把它作为(最后的)结果返回。

  10. 作业以非零状态退出时,作业控制代码和内建命令打印的消息为 Done(status)

  11. 作业停止时,作业控制代码和内建命令打印的消息为 Stopped(SIGNAME),其中 SIGNAME 例如是 SIGTSTP

译者注:默认模式与 POSIX 模式的作业状态消息格式不同,实测(交互式会话,Bash 5.3):

# 译者注示例:默认模式下按 Ctrl-Z 挂起前台作业
$ sleep 10
^Z
[1]+  Stopped                    sleep 10
# 译者注示例:POSIX 模式下同样的操作,消息变为 Stopped(SIGTSTP)
$ bash --posix
$ sleep 10
^Z
[1]+  Stopped(SIGTSTP)           sleep 10

同理,第 22 条:作业以非零状态退出时,POSIX 模式打印 Done(状态码)(例如被 SIGTERM 终止时是 Done(143)),而默认模式打印 Terminated 之类的传统消息。消息文本还会受区域设置影响(以上为 LANG=C 下的输出)。

  1. 如果 Shell 是交互式的,Bash 不会在以 ; 或换行符分隔的命令列表中的命令之间执行作业通知。非交互式 Shell 在列表中的前台作业完成后打印状态消息。

  2. 如果 Shell 是交互式的,Bash 会等到下一个提示符才打印状态发生变化的后台作业或由于信号而终止的前台作业的状态。非交互式 Shell 在前台作业完成后打印状态消息。

  3. 在通过 waitjobs 内建命令通知用户作业终止后,Bash 把该作业从作业表中永久移除。在通知用户其终止后,Bash 把该作业从作业列表中移除,但只要给 wait 提供 PID 参数,其状态仍然可以通过 wait 获取。

  4. vi 编辑模式在运行 v 命令时会直接调用 vi 编辑器,而不检查 $VISUAL$EDITOR

  5. 提示符展开启用 POSIX 对 PS1PS2 的展开:! 展开为历史编号、!! 展开为 !;并且无论 promptvars 选项的设置如何,Bash 都会对 PS1PS2 的值执行参数展开。

  6. 默认历史文件是 ~/.sh_history(这是 Shell 赋给 $HISTFILE 的默认值)。

译者注:实测(交互式 Shell):默认模式下 $HISTFILE~/.bash_history--posix 模式下是 ~/.sh_history。这也解释了为什么在 POSIX 模式下历史记录与普通 Bash 互不相通。

  1. 即使启用了 histexpand 选项,! 字符也不会在双引号字符串内引入历史展开。

  2. 打印 Shell 函数定义时(例如用 type),除非必要,Bash 不打印 function 保留字。

  3. 如果算术展开中的语法错误导致表达式无效,非交互式 Shell 退出。

  4. 如果发生参数展开错误,非交互式 Shell 退出。

  5. 如果 POSIX 特殊内建命令返回错误状态,非交互式 Shell 退出。致命错误是 POSIX 标准中列出的那些错误,包括传入不正确的选项、重定向错误、命令名之前的赋值语句的变量赋值错误等。

  6. 如果赋值语句之后没有命令名时发生变量赋值错误,非交互式 Shell 以错误状态退出。例如,试图给只读变量赋值时就会发生变量赋值错误。

  7. 如果特殊内建命令之前的赋值语句发生变量赋值错误,非交互式 Shell 以错误状态退出;但其他任何简单命令不会这样。对于任何其他简单命令,Shell 中止该命令的执行,并在顶层继续执行(「Shell 不得对发生错误的命令进行任何进一步的处理」)。

  8. 如果 for 语句中的迭代变量或 select 语句中的选择变量是只读变量或名称无效,非交互式 Shell 以错误状态退出。

  9. 如果 . FILENAME 中的 FILENAME 未找到,非交互式 Shell 退出。

  10. 如果用 .source 内建命令读取的脚本中存在语法错误,或者用 eval 内建命令处理的字符串中存在语法错误,非交互式 Shell 退出。

  11. 如果 exportreadonlyunset 内建命令的参数不是有效标识符,并且它们不是对 Shell 函数进行操作,非交互式 Shell 退出。这些错误强制退出,因为这些是特殊内建命令。

译者注:第 32–40 条可以概括为一句话:在 POSIX 模式下,非交互式 Shell 遇到「致命错误」时直接退出,而不是像默认模式那样报错后继续。这正体现了 POSIX 对特殊内建命令的严格语义。实测(Bash 5.3):

# 译者注示例:POSIX 模式下给只读变量赋值,非交互 Shell 立即退出
$ bash --posix -c 'readonly x=1; x=2; echo "不会执行到这里"'
bash: 行 1: x: 只读变量

(Shell 以非零状态退出,实测退出状态为 127;echo 未执行。)

  1. POSIX 特殊内建命令之前的赋值语句在内建命令完成后仍保留在 Shell 环境中。

  2. command 内建命令不会阻止接受赋值语句作为参数的内建命令把赋值语句作为赋值语句展开;在非 POSIX 模式下,声明类命令前面加上 command 时会失去其赋值语句展开的特性。

  3. 启用 POSIX 模式具有设置 inherit_errexit 选项的效果,因此为执行命令替换而派生的子 Shell 会从父 Shell 继承 -e 选项的值。未启用 inherit_errexit 选项时,Bash 会在这样的子 Shell 中清除 -e 选项。

译者注:通俗地说:set -e(出错即退出)默认在命令替换的子 Shell 中被关闭,所以 echo $(false) 不会让子 Shell 因 false 而退出;启用 inherit_errexit(或在 POSIX 模式下)后子 Shell 会继承 -e$(false) 内部执行到错误命令时子 Shell 会以非零状态退出(这通常会进一步影响外层命令的退出状态判断)。这正是 inherit_errexit 选项存在的意义。

  1. 启用 POSIX 模式具有设置 shift_verbose 选项的效果,因此传给 shift 的、超过位置参数个数的数值参数会产生错误消息。

  2. 启用 POSIX 模式具有设置 interactive_comments 选项的效果(参见第 3.1.3 节(注释))。

  3. 如果通过搜索 PATH 找不到文件名参数,.source 内建命令不会在当前目录中搜索该文件名。

  4. alias 内建命令显示别名定义时,除非提供了 -p 选项,否则不在前面显示 alias

  5. bg 内建命令使用规定格式描述放入后台的每个作业,该格式不包含该作业是当前作业还是前一作业的指示。

  6. cd 内建命令在逻辑模式下被调用,且由 $PWD 和作为参数提供的目录名构造的路径名不指向现有目录时,cd 将失败,而不是回退到物理模式。

  7. cd 内建命令无法更改目录,因为由 $PWD 和作为参数提供的目录名构造的路径名在规范化后超过 PATH_MAX 时,cd 将尝试使用提供的目录名。

  8. 启用 xpg_echo 选项后,Bash 不再尝试把传给 echo 的任何参数解释为选项。echo 在转换转义序列后显示每个参数。

译者注:实测(Bash 5.3):

# 译者注示例:POSIX 模式下 echo -n 仍按选项处理(-n 被解释为“不换行”)
$ bash --posix -c 'echo -n hi'
hi
# 译者注示例:启用 xpg_echo 后,-n 不再被当作选项,而是原样打印
$ bash --posix -c 'shopt -s xpg_echo; echo -n hi'
-n hi
  1. exportreadonly 内建命令按 POSIX 要求的格式显示输出。

  2. 列出历史时,fc 内建命令不包含历史条目是否被修改过的指示。

  3. fc 使用的默认编辑器是 ed

  4. fc 把多余的参数当作错误而不是忽略它们。

  5. 如果传给 fc -s 的参数过多,fc 打印错误消息并返回失败。

  6. kill -l 的输出把所有的信号名称打印在同一行上,用空格分隔,不带 SIG 前缀。

  7. kill 内建命令不接受带 SIG 前缀的信号名称。

译者注:实测(Bash 5.3):

# 译者注示例:默认模式按多列打印信号名,带 SIG 前缀
$ bash -c 'kill -l' | head -1
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
# 译者注示例:POSIX 模式单行打印,不带 SIG 前缀(实际为一行,此处省略中间部分)
$ bash --posix -c 'kill -l'
HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM ...

第 58 条也有直观的验证:默认模式 kill -l SIGINT 输出 2,而 bash --posix -c 'kill -l SIGINT' 报错 无效的信号说明符

  1. 如果任何 pid 或作业参数无效,或者向其中任何一个发送指定信号失败,kill 内建命令返回失败状态。在默认模式下,只要信号被成功发送到任一指定进程,kill 就返回成功。

  2. printf 内建命令使用 double(通过 strtod)转换对应于浮点转换说明符的参数,而不是在可用时使用 long doubleL 长度修饰符强制 printf 在可用时使用 long double

译者注double 通常只有约 15–17 位十进制有效数字,long double 在 x86 平台上有 18–21 位。默认模式下 %f 等浮点说明符会优先使用 long double 转换(精度更高),POSIX 模式则一律用 double;需要高精度时可以在转换说明符中加 L 长度修饰符(如 %Lf)强制使用 long double

  1. pwd 内建命令会验证它打印的值与当前目录相同,即使没有要求用 -P 选项检查文件系统。

  2. read 内建命令可能被已设置陷阱的信号中断。如果 Bash 在执行 read 时收到被捕获的信号,陷阱处理程序会执行,read 返回大于 128 的退出状态。

译者注:实测(Bash 5.3):read 等待输入时收到被捕获的 SIGINT,陷阱执行,read 返回 130(128 + 2):

# 译者注示例:read 被被捕获的信号中断时返回大于 128 的状态
$ bash --posix -c 'trap "echo TRAPPED" INT; read x; echo "rc=$?"' < <(sleep 3) &
$ sleep 0.3; kill -INT $!; wait
TRAPPED
rc=130

在默认模式下实测行为不同:陷阱同样会执行,但 read 返回 1 而不是大于 128——退出状态是否大于 128 正是区分两种模式的可观察点。

  1. 当不带选项调用 set 内建命令时,它不显示 Shell 函数名和定义。

  2. 当不带选项调用 set 内建命令时,它显示不带引号的变量值,除非这些值包含 Shell 元字符,即使结果包含不可打印字符。

  3. test 内建命令在求值 <> 二元操作符时使用当前区域设置(locale)比较字符串。

  4. test 内建命令的 -t 一元主表达式需要参数。历史版本的 test 在某些情况下把该参数做成可选的,Bash 试图为向后兼容而适应这些情况。

  5. trap 内建命令显示不带前导 SIG 的信号名称。

  6. trap 内建命令不检查第一个参数是否为可能的信号规格并在是的情况下把信号处理恢复为原始处置,除非该参数完全由数字组成并且是有效的信号编号。如果用户想把给定信号的处理程序重置为原始处置,应该用 - 作为第一个参数。

  7. 不带参数的 trap -p 显示处置被设置为 SIG_DFL 的信号以及在 Shell 启动时被忽略的信号,而不仅仅是设置了陷阱的信号。

  8. typecommand 内建命令不会报告已找到不可执行的文件,尽管如果这样的文件是在 $PATH 中找到的唯一的同名文件,Shell 会尝试执行它。

  9. ulimit 内建命令对 -c-f 选项使用 512 字节的块大小。

  10. 指定了 -v 选项的 unset 内建命令在试图取消设置 readonly 或「不可取消设置(non-unsettable)」变量时返回致命错误,这会导致非交互式 Shell 退出。

  11. 当要求取消设置在命令前面的赋值语句中出现的变量时,unset 内建命令也尝试取消设置当前或先前作用域中的同名变量。这实现了规定的「如果赋值变量被该工具进一步修改,该工具所做的修改应当持久」的行为。

  12. 当在 SIGCHLD 上设置了陷阱时,SIGCHLD 的到达不会中断 wait 内建命令并导致它立即返回。对于每个退出的子进程,陷阱命令运行一次。

  13. wait 内建命令返回已退出后台进程的状态后,Bash 把该状态从此类状态列表中移除。

还有额外的 POSIX 行为,即使在 POSIX 模式下,Bash 默认也不实现。具体来说:

  1. POSIX 要求单词拆分是面向字节的。也就是说,IFS 值中的每个字节都可能拆分一个单词,即使该字节是 IFS 中多字节字符的一部分,或者是单词中多字节字符的一部分。Bash 允许 IFS 值中出现多字节字符,把有效的多字节字符当作单个分隔符,并且不会拆分有效的多字节字符,即使组成该字符的字节之一出现在 IFS 中。这是 POSIX 解释 1560,又经 issue 1924 进一步修改。

译者注:举例说明「面向字节的拆分」:假设区域设置为 UTF-8,IFS 的值包含「你」字的第一个字节(0xE4)。按 POSIX 的字面要求,IFS 中的这个字节本身就会把单词拆开——即使它只是「你」这个多字节字符的一部分。Bash 的实际行为更合理:它把整个「你」字当作一个分隔符,而不会在某个组成字节处拆分其他词。这两者仅在 IFS 或待拆分单词中含多字节字符时才有差异。

  1. 如果 FCEDIT 未设置,fc 内建命令会检查 $EDITOR 作为编辑历史条目的程序,而不是直接默认使用 ed。如果 EDITOR 也未设置,fc 使用 ed

  2. 如上所述,Bash 要求启用 xpg_echo 选项,echo 内建命令才能完全符合标准。

通过在建置时向 configure 指定 --enable-strict-posix-default,Bash 可以配置为默认即符合 POSIX 标准(参见第 10.8 节(可选功能))。

6.12 Shell 兼容模式

Bash-4.0 引入了「Shell 兼容级别(shell compatibility level)」的概念,表示为 shopt 内建命令的一组选项(compat31compat32compat40compat41 等)。同一时刻只有一个当前兼容级别——每个选项互斥。兼容级别旨在让用户在他们把脚本迁移到使用当前特性和行为期间,能够选择以前版本中与较新版本不兼容的行为。它旨在作为一种临时解决方案。

本节不提及对特定版本而言属于标准的行为(例如,设置 compat32 意味着对正则匹配操作符右侧的引号处理会引住单词中的特殊正则字符,这是 bash-3.2 及后续版本的默认行为)。

译者注:这段举例说明兼容级别的粒度:在 bash-3.2 及以后,「对 [[=~ 操作符右侧(rhs)加引号」一直是默认行为,所以它不算 compat32 的「变化点」,本节不重复列举。兼容级别只描述「在该版本中发生改变的行为」。

如果用户启用比如 compat32,它可能影响直到并包括当前兼容级别的其他兼容级别的行为。其想法是:每个兼容级别控制在该版本的 Bash 中发生改变的行为,但该行为可能早在更旧的版本中就已经存在。例如,[[ 命令改用基于区域设置(locale)的比较发生在 bash-4.1,而更早的版本使用基于 ASCII 的比较,因此启用 compat32 也会启用基于 ASCII 的比较。这种粒度可能不足以满足所有用途,因此用户应谨慎使用兼容级别。请阅读特定功能的文档以了解当前行为。

Bash-4.3 引入了一个新的 Shell 变量:BASH_COMPAT。赋给该变量的值(一个十进制版本号,如 4.2,或与 compatNN 选项对应的整数,如 42)决定兼容级别。

译者注:也就是说,BASH_COMPAT=4.2BASH_COMPAT=42 等价,都对应 compat42。实测(Bash 5.3):

# 译者注示例:BASH_COMPAT 的两种取值写法
$ BASH_COMPAT=4.2 bash -c 'echo "$BASH_COMPAT"'
4.2
$ BASH_COMPAT=42 bash -c 'echo "$BASH_COMPAT"'
42

从 bash-4.4 开始,Bash 开始弃用较旧的兼容级别。最终,这些选项将被移除,取而代之的是 BASH_COMPAT

Bash-5.0 是为前一版本提供单独 shopt 选项的最后一个版本。BASH_COMPAT 是在 bash-5.0 之后的版本中控制兼容级别的唯一机制。

下面的条目描述了每个兼容级别设置控制的行为变化。compatNN 标签用作使用下列机制之一把兼容级别设置为 NN 的简写。对于 bash-5.0 之前的版本,可以使用相应的 compatNN shopt 选项设置兼容级别。对于 bash-4.3 及更高版本,首选 BASH_COMPAT 变量,而对于 bash-5.1 及更高版本,必须使用它。

compat31

  • [[ 命令正则匹配操作符(=~)右侧(rhs)的引号处理没有特殊效果。

译者注:默认行为是:对 =~ 右侧加引号后,该部分按字面匹配(不再作为正则表达式)。而 compat31 下引号没有任何效果。实测(Bash 5.3):

# 译者注示例:compat31 下,带引号的 "a|b" 仍按正则解释(| 是“或”)
$ bash -O compat31 -c '[[ "b" =~ "a|b" ]] && echo MATCH || echo NO'
MATCH
# 译者注示例:默认模式下,引号使 "a|b" 按字面匹配,因此 b 匹配失败
$ bash -c '[[ "b" =~ "a|b" ]] && echo MATCH || echo NO'
NO

compat40

  • [[ 命令的 <> 操作符在比较字符串时不考虑当前区域设置;它们使用 ASCII 排序。bash-4.1 之前的 Bash 版本使用 ASCII 排序规则和 strcmp(3);bash-4.1 及更高版本使用当前区域设置的排序序列和 strcoll(3)。

译者注:如本节正文所述,兼容级别有「累积」效应:启用 compat32 会连带启用 compat40 的 ASCII 比较行为(因为基于 locale 的比较是 4.1 才引入的,4.0 及更早版本都是 ASCII 比较)。因此设置兼容级别时要注意它不是「只回退一个版本」,而是回退到该级别所代表的全部旧行为。

compat41

  • 在 POSIX 模式下,time 后面可以跟选项,并且仍然被识别为保留字(这是 POSIX 解释 267)。
  • 在 POSIX 模式下,解析器要求双引号 ${...} 参数展开的 WORD 部分中出现偶数个单引号,并特殊处理它们,以便单引号内的字符被视为已引号(这是 POSIX 解释 221)。

compat42

  • 双引号模式替换中的替换字符串不进行引号去除,而在 bash-4.2 之后的版本中会进行。
  • 在 POSIX 模式下,单引号在展开双引号 ${...} 参数展开的 WORD 部分时被视为特殊字符,可用于引住闭花括号或其他特殊字符(这是 POSIX 解释 221 的一部分);在以后的版本中,单引号在双引号单词展开中不再特殊。

compat43

  • 单词展开错误被视为导致当前命令失败的非致命错误,即使在 POSIX 模式下也是如此(默认行为是使它们成为导致 Shell 退出的致命错误)。
  • 执行 Shell 函数时,循环状态(while/until 等)不会被重置,因此该函数中的 breakcontinue 会中断或继续调用上下文中的循环。Bash-4.4 及更高版本会重置循环状态以防止这种情况。

compat44

  • Shell 设置 BASH_ARGVBASH_ARGC 使用的值,使它们即使在没有启用扩展调试模式的情况下也能展开为 Shell 的位置参数。
  • 子 Shell 从其父上下文继承循环,因此 breakcontinue 会导致子 Shell 退出。Bash-5.0 及更高版本会重置循环状态以防止退出。
  • exportreadonly 等设置属性的内建命令之前的变量赋值,即使 Shell 不处于 POSIX 模式,也会继续影响调用环境中同名的变量。

compat50(使用 BASH_COMPAT 设置)

  • Bash-5.1 改变了 $RANDOM 的生成方式,引入稍微多一点的随机性。如果 Shell 兼容级别设置为 50 或更低,则恢复为 bash-5.0 及以前版本的方法,因此通过给 RANDOM 赋值来为随机数生成器播种,会产生与 bash-5.0 中相同的序列。
  • 如果命令哈希表为空,bash-5.1 之前的 Bash 版本会打印一条说明性的消息,即使在生成可复用为输入的输出时也是如此。Bash-5.1 在提供 -l 选项时抑制该消息。

译者注:实测(Bash 5.3):同样的种子 42,在默认(5.3)与兼容级别 50 下产生的序列不同,而兼容级别 50 下的结果与 bash-5.0 相同:

# 译者注示例:默认模式下给 RANDOM 播种
$ bash -c 'RANDOM=42; echo $RANDOM; RANDOM=42; echo $RANDOM'
17772
17772
# 译者注示例:兼容级别 50 下,序列与 bash-5.0 一致
$ bash -c 'BASH_COMPAT=50; RANDOM=42; echo $RANDOM; RANDOM=42; echo $RANDOM'
17766
17766

compat51(使用 BASH_COMPAT 设置)

  • unset 内建命令在给定类似 a[@] 的参数时会取消设置整个数组 a。Bash-5.2 会取消设置键为 @ 的元素(关联数组),或删除所有元素但不取消设置数组(索引数组)。
  • 算术命令(((...)))和算术 for 语句中的表达式可以展开多次。
  • [[ 条件命令中用作算术操作符参数的表达式可以展开多次。
  • 子串参数花括号展开中的表达式可以展开多次。
  • $(( ... )) 单词展开中的表达式可以展开多次。
  • 用作索引数组下标的算术表达式可以展开多次。
  • test -v 在给定 A[@](A 是现有的关联数组)时返回真,如果该数组有任何已设置的元素。Bash-5.2 会查找并报告名为 @ 的键。
  • ${PARAMETER[:]=VALUE} 单词展开会在执行任何变量特定的转换(例如转换为小写)之前返回 VALUE。Bash-5.2 会返回赋值给变量的最终值。
  • 解析命令替换的行为就像扩展通配(extglob,参见第 4.3.2 节(shopt 内建命令))已启用,因此解析包含 extglob 模式的命令替换(比如作为 Shell 函数的一部分)不会失败。这假定意图是在命令执行和单词展开之前启用 extglob。如果到命令执行时 extglob 还没有启用,它会在单词展开时失败。

译者注:第一条实测(Bash 5.3):

# 译者注示例:默认(5.3)行为:a[@] 只清空元素,数组仍然存在
$ bash -c 'a=(x y z); unset "a[@]"; declare -p a'
declare -a a=()
# 译者注示例:compat51 行为:整个数组被取消设置,declare 报“未找到”
$ bash -c 'BASH_COMPAT=51; a=(x y z); unset "a[@]"; declare -p a'
bash: 第 1 行: declare: a: 未找到

compat52(使用 BASH_COMPAT 设置)

  • test 内建命令在给定五个或更多参数时使用其历史算法解析带括号的子表达式。
  • 如果给 bind 内建命令提供了 -p-P 选项,bind 把选项处理之后剩余的任何参数视为可绑定的命令名,并显示绑定到这些命令的任何键序列,而不是把参数视为要绑定的键序列。
  • 交互式 Shell 在 source 脚本时通知用户已完成的作业。较新的版本把通知推迟到脚本执行完成之后。
posted @ 2026-08-07 04:40  立体风  阅读(17)  评论(0)    收藏  举报