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

4 Shell 内建命令

内建命令(builtin)包含在 Shell 自身之中。当一个内建命令的名字被用作简单命令(simple command)的第一个单词时(参见第 3.2.2 节(简单命令)),Shell 直接执行该命令,而不去调用另一个程序。内建命令是实现某些无法通过独立工具程序获得、或通过它们实现很不方便的功能所必需的。

译者注:本节及第 4.2 节介绍的是编译进 Shell 自身的「常驻」内建命令。除此之外,Bash 还支持按需加载(lazy loading)的加载式内建命令(loadable builtin):它们以共享库(shared object)形式随 Bash 一并提供,首次需要时才动态载入——用 enable 不带选项、或 enable -fBASH_LOADABLES_PATH 指定的目录加载(参见第 4.2 节),用 enable -d 卸载。这一机制使 Shell 不必在启动时初始化全部内建命令,从而缩短启动时间、降低常驻内存占用。

本节简要描述 Bash 从 Bourne Shell 继承而来的内建命令,以及 Bash 所特有或已由 Bash 扩展的内建命令。

还有若干内建命令在其它章节中介绍:为作业控制设施提供 Bash 接口的内建命令(参见第 7.2 节(作业控制内建命令))、目录栈(参见第 6.8.1 节(目录栈内建命令))、命令历史(参见第 9.2 节(Bash 历史记录内建命令))以及可编程补全设施(参见第 8.7 节(可编程补全内建命令))。

许多内建命令已被 POSIX 或 Bash 扩展。

除另有说明外,每个文档说明为接受以 - 开头的选项的内建命令,都接受 -- 以表示选项的结束。:, true, falsetest/[ 内建命令不接受选项,也不对 -- 作特殊处理。exitlogoutreturnbreakcontinueletshift 内建命令接受并处理以 - 开头的参数,而不要求 --。其它接受参数但未被说明为接受选项的内建命令,会把以 - 开头的参数解释为非法选项,因此需要 -- 来阻止这种解释。

4.1 Bourne Shell 内建命令

下面的 Shell 内建命令继承自 Bourne Shell。这些命令均按 POSIX 标准的规定实现。

:(冒号)

: [ARGUMENTS]

除了展开 ARGUMENTS 并执行重定向之外,不做任何事。返回状态为零。

译者注:(冒号)又称空命令(null command),常用于需要「什么都不做」的位置:一是占位,二是构造死循环 while :; do ...; done(等价于 while true)。注意它仍会对参数做展开并执行重定向,与 # 注释不同。示例:

# 译者注示例:冒号命令的两种典型用法
$ : hello world; echo $?
0
$ while :; do echo "死循环也可以这样写"; break; done
死循环也可以这样写

.(句点)

. [-p PATH] FILENAME [ARGUMENTS]

. 命令在当前 Shell 上下文中读取并执行来自 FILENAME 参数的命令。

如果 FILENAME 不含斜杠,. 会查找它。若提供了 -p. 把 PATH 视为以冒号分隔的目录列表,在其中查找 FILENAME;否则,. 使用 PATH 变量中的目录来查找 FILENAME。FILENAME 不需要具有可执行权限。当 Bash 不处于 POSIX 模式时,若在 $PATH 中找不到 FILENAME,它会继续搜索当前目录;但提供了 -p 时不会搜索当前目录。如果 sourcepath 选项(参见第 4.3.2 节(shopt 内建命令))被关闭,. 不会搜索 PATH

如果提供了任何 ARGUMENTS,它们将成为执行 FILENAME 时的位置参数;否则位置参数保持不变。

如果启用了 -T 选项,. 会继承任何 DEBUG 陷阱;若未启用,则在调用 . 期间保存并恢复任何 DEBUG 陷阱字符串,而且 . 在执行期间会取消设置 DEBUG 陷阱。如果未设置 -T,且被载入(sourced)的文件修改了 DEBUG 陷阱,那么新值会在 . 完成后继续保留。返回状态是 FILENAME 中最后一条已执行命令的退出状态;如果没有执行任何命令,则为零。如果找不到 FILENAME 或无法读取它,返回状态非零。此内建命令等价于 source

译者注.(句点)与 source 完全等价。容易误解的是查找规则:FILENAME 不含斜杠时按 $PATH 搜索(Bash 非 POSIX 模式下最后才查当前目录;提供 -p 或关闭 sourcepath 选项后不查当前目录)。传入的 ARGUMENTS 会成为被载入脚本的位置参数。示例:

# 译者注示例:source 一个脚本并传入位置参数
$ cat > /tmp/hello.src <<'EOF'
echo "你好,$1!"
EOF
$ . /tmp/hello.src 世界
你好,世界!

break

break [N]

forwhileuntilselect 循环中退出。若提供了 N,break 退出第 N 层外层循环。N 必须大于或等于 1。返回状态为零,除非 N 小于 1。

译者注break 只能出现在循环体内,break N 一次退出 N 层循环;退出后脚本继续执行循环之后的命令。示例:

# 译者注示例:break 2 一次跳出两层循环
$ for i in 1 2 3; do
>   for j in a b; do
>     [ "$i" = 2 ] && [ "$j" = a ] && { echo "退出于 i=$i,j=$j"; break 2; }
>     echo "i=$i j=$j"
>   done
> done
i=1 j=a
i=1 j=b
退出于 i=2,j=a

cd

cd [-L] [-@] [DIRECTORY]
cd -P [-e] [-@] [DIRECTORY]

将当前工作目录更改为 DIRECTORY。若未提供 DIRECTORY,则使用 HOME Shell 变量的值作为 DIRECTORY。如果 Shell 变量 CDPATH 存在,且 DIRECTORY 不以斜杠开头,cd 会把它用作搜索路径:cd 依次在 CDPATH 中的每个目录名下查找 DIRECTORY,CDPATH 中的备选目录名以冒号(:)分隔。CDPATH 中的空目录名表示与当前目录相同的意思。

-P 选项表示不跟随符号链接:cd 在遍历 DIRECTORY 的过程中、于处理 DIRECTORY 中的 .. 实例之前解析符号链接。

默认情况下,或提供了 -L 选项时,DIRECTORY 中的符号链接在 cd 处理完 DIRECTORY 中的 .. 实例之后才解析。

如果 .. 出现在 DIRECTORY 中,cd 的处理方式是:删除紧挨其前的路径名分量(一直删到斜杠或 DIRECTORY 的开头),并验证到该处为止已处理的 DIRECTORY 部分在删除该路径名分量后仍是有效的目录名。如果不再是有效的目录名,cd 返回非零状态。

如果 -e 选项与 -P 一起提供,且目录更改成功后 cd 无法确定当前工作目录,则返回非零状态。

在支持的系统上,-@ 选项会把与文件关联的扩展属性(extended attributes)呈现为一个目录。

如果 DIRECTORY 是 -,它会在尝试更改目录之前被转换为 $OLDPWD

如果 cd 使用了 CDPATH 中的非空目录名,或者 - 是第一个参数,并且目录更改成功,cd 会把新工作目录的绝对路径名写到标准输出。

如果目录更改成功,cd 会把 PWD 环境变量的值设置为新目录名,并把 OLDPWD 环境变量设置为更改前的当前工作目录值。

目录成功更改时返回状态为零,否则非零。

译者注cd 的几个常用细节:

  • cd - 等价于 cd "$OLDPWD",成功后会打印新目录的绝对路径,适合在两个目录之间来回切换;
  • cd -P(物理路径,解析符号链接)与默认的 cd -L(逻辑路径)在路径中含符号链接时结果不同;
  • CDPATH 提供冒号分隔的搜索路径,用其中的目录成功切换时也会打印绝对路径。
    示例:
# 译者注示例:cd - 回到上一个目录
$ cd /tmp && cd /usr/local/bin && cd - && pwd
/tmp
/tmp
# 译者注示例:借助 CDPATH 搜索目录
$ mkdir -p /tmp/cdtest
$ CDPATH=/tmp bash -c 'cd cdtest; pwd'
/tmp/cdtest
/tmp/cdtest
$ rmdir /tmp/cdtest

continue

continue [N]

continue 恢复外层 forwhileuntilselect 循环的下一次迭代。若提供了 N,Bash 恢复第 N 层外层循环的执行。N 必须大于或等于 1。返回状态为零,除非 N 小于 1。

译者注continue Nbreak N 对应:continue 2 在嵌套循环中会跳过两层循环的当前迭代。示例(跳过奇数):

# 译者注示例:continue 跳过奇数
$ for i in 1 2 3 4; do
>   [ $((i % 2)) -eq 1 ] && continue
>   echo "$i 是偶数"
> done
2 是偶数
4 是偶数

eval

eval [ARGUMENTS]

ARGUMENTS 被拼接成一条命令,各部分以空格分隔。Bash 随后读取并执行这条命令,并把它的退出状态作为 eval 的退出状态返回。如果没有参数或只有空参数,返回状态为零。

译者注eval 先把参数展开、拼接成字符串,再当作命令重新解析执行——这种「二次求值」能力强大但危险:拼接内容若包含不可信输入,可能执行任意命令。能用数组、函数或直接变量展开解决的场景,尽量不用 eval。示例:

# 译者注示例:eval 拼接命令后执行
$ x='hello'; y='world'; eval "echo $x $y"
hello world
# 更安全的写法:直接展开变量
$ x='hello'; y='world'; echo "$x $y"
hello world

exec

exec [-cl] [-a NAME] [COMMAND [ARGUMENTS]]

若提供了 COMMAND,它将替换掉 Shell,而不创建新进程。COMMAND 不能是 Shell 内建命令或函数。ARGUMENTS 成为 COMMAND 的参数。若提供了 -l 选项,Shell 会在传给 COMMAND 的第零个参数开头放一个连字符(-)。这正是 login 程序的做法。-c 选项使 COMMAND 在空环境中执行。若提供了 -a,Shell 把 NAME 作为第零个参数传给 COMMAND。

若 COMMAND 因某种原因无法执行,非交互式 Shell 会退出,除非启用了 execfail Shell 选项——此时它返回非零状态。交互式 Shell 在文件无法执行时返回非零状态。若 exec 失败,子 Shell 无条件退出。

若未指定 COMMAND,则可以用重定向来影响当前 Shell 环境。如果没有重定向错误,返回状态为零;否则返回状态非零。

译者注exec 的三种典型用法:

  1. 在脚本末尾 exec cmd,让外部命令替换 Shell 进程(省去一层子进程,脚本退出码直接由 cmd 决定);
  2. 不指定命令,仅对当前 Shell 做重定向(如 exec 2>err.log,此后该 Shell 的错误输出都进文件);
  3. -a 改变 $0(进程名)、-c 清空环境。
    注意 COMMAND 不能是内建命令或函数。示例(在子 Shell 中把标准错误丢弃):
# 译者注示例:exec 重定向当前 Shell 的标准错误
$ bash -c 'exec 2>/dev/null; echo hi; ls /no/such/file'
hi

exit

exit [N]

退出 Shell,向 Shell 的父进程返回状态 N。若省略 N,退出状态为最后一条已执行命令的退出状态。任何 EXIT 陷阱都会在 Shell 终止之前执行。

译者注exit N 的 N 取 0–255 之间的整数(超出部分截取低 8 位)。不带参数退出时返回最后一条命令的状态——因此脚本末尾用显式 exit 0exit $? 表达意图更稳妥。EXIT 陷阱在 Shell 终止前执行,常用于清理临时文件:

# 译者注示例:EXIT 陷阱在退出时清理临时文件
$ bash -c 'trap "rm -f /tmp/trap-exit-test; echo 已清理临时文件" EXIT; : > /tmp/trap-exit-test; echo 创建完成'
创建完成
已清理临时文件

export

export [-fn] [-p] [NAME[=VALUE]]

将每个 NAME 标记为随环境(environment)传递给之后执行的命令。若提供了 -f 选项,NAME 指 Shell 函数;否则 NAME 指 Shell 变量。

-n 选项表示取消每个名称的导出:不再把它标记为导出。若未提供 NAME,或只给了 -p 选项,export 会在标准输出上显示所有已导出变量的名称列表。-p-f 一起使用显示已导出的函数。-p 选项以可重新用作输入的形式显示输出。

export 允许在导出或取消导出的同时设置变量值:在变量名后跟 =VALUE 即可。这会在修改导出属性的同时把变量值设置为 VALUE。

返回状态为零,除非提供了非法选项、某个名称不是合法的 Shell 变量名,或给 -f 提供了不是 Shell 函数的名称。

译者注export 只对之后启动的命令生效,已运行的命令不会得到新环境。VAR=value 前缀形式只对该条命令生效,不改变 Shell 自身的环境。示例:

# 译者注示例:前缀赋值只对该条命令生效
$ FOO=1 env | grep -c '^FOO='
1
$ env | grep -c '^FOO='
0

false

false

什么也不做;返回非零状态。

getopts

getopts OPTSTRING NAME [ARG ...]

getopts 供 Shell 脚本或函数解析位置参数并取得选项及其参数。OPTSTRING 包含要识别的选项字符;如果某字符后跟冒号,则该选项预期带有一个参数,参数应与选项字符用空白分隔。冒号(:)和问号(?)不能用作选项字符。

每次调用时,getopts 把下一个选项放入 Shell 变量 NAME(若 NAME 不存在则先初始化),并把下一个待处理参数的索引放入变量 OPTIND。每次 Shell 或 Shell 脚本启动时 OPTIND 都初始化为 1。当选项需要参数时,getopts 把该参数放入变量 OPTARG

Shell 不会自动重置 OPTIND;在同一 Shell 调用内多次调用 getopts 以解析新的一组参数时,必须手动重置。

当到达选项末尾时,getopts 以大于零的返回值退出。OPTIND 被设置为第一个非选项参数的索引,NAME 被设置为 ?

getopts 通常解析位置参数;但如果提供了更多参数作为 ARG 值,getopts 改而解析这些参数。

getopts 可以用两种方式报告错误。若 OPTSTRING 的第一个字符是冒号,getopts 使用静默(silent)错误报告。正常操作下,getopts 遇到非法选项或缺少选项参数时会打印诊断消息。若变量 OPTERR 被设置为 0,即使 optstring 的第一个字符不是冒号,getopts 也不显示任何错误消息。

getopts 检测到非法选项,它把 ? 放入 NAME;非静默模式下还会打印错误消息并取消设置 OPTARG。静默模式下,getopts 把找到的选项字符赋给 OPTARG,不打印诊断消息。

若必需的参数未找到且 getopts 非静默,它把 NAME 的值设为问号(?),取消设置 OPTARG,并打印诊断消息。静默模式下,它把 NAME 的值设为冒号(:),并把 OPTARG 设为找到的选项字符。

getopts 在找到选项(无论是否在 OPTSTRING 中列出)时返回真;遇到选项末尾或出错时返回假。

译者注getopts 是 POSIX 规定的选项解析方式,与外部命令 getopt 不同:它是内建命令,逐字符解析,没有引号处理问题。要点:

  • OPTSTRING 中 a: 表示选项 -a 需要一个参数(参数与选项之间以空白分隔);
  • 以冒号开头(如 :a:)进入静默模式,出错时 NAME 被置为 :?
  • 同一脚本内再次解析新的一组参数前,要手动执行 OPTIND=1
  • 解析结束后 shift $((OPTIND - 1)) 可去掉已处理的选项,留下剩余的位置参数。
    典型用法:
# 译者注示例:getopts 解析选项与位置参数
$ cat > /tmp/args.sh <<'EOF'
while getopts "a:b:v" opt; do
  case $opt in
    a) echo "选项 a 的参数: $OPTARG" ;;
    b) echo "选项 b 的参数: $OPTARG" ;;
    v) echo "开启 -v" ;;
    *) echo "非法选项: $opt" ;;
  esac
done
shift $((OPTIND - 1))
echo "剩余参数: $@"
EOF
$ bash /tmp/args.sh -a 1 -v -b "hello world" tail args
选项 a 的参数: 1
开启 -v
选项 b 的参数: hello world
剩余参数: tail args

hash

hash [-r] [-p FILENAME] [-dt] [NAME]

每次调用 hash,它都会记住作为 NAME 参数指定的命令的完整文件名,这样在后续调用中就不必再搜索它们。命令通过在 $PATH 列出的目录中搜索来找到。任何先前与 NAME 关联的记忆文件名都会被丢弃。-p 选项禁止路径搜索,hash 直接把 FILENAME 作为 NAME 的位置。

-r 选项使 Shell 忘记所有记住的位置。给 PATH 变量赋值也会清除所有哈希文件名。-d 选项使 Shell 忘记每个 NAME 记住的位置。

若提供了 -t 选项,hash 打印每个 NAME 对应的完整路径名。若 -t 带有多个 NAME 参数,hash 会在对应的哈希完整路径之前打印每个 NAME。-l 选项以可重新用作输入的形式显示输出。

若未提供任何参数,或只提供了 -lhash 打印已记住命令的信息。-t-d-p 选项(作用于 NAME 参数的选项)互斥,只会有一个生效。若提供了多个,-t 优先级高于 -p,而两者都高于 -d

返回状态为零,除非某个 NAME 未找到或提供了非法选项。

译者注:Shell 会把执行过的外部命令的路径缓存进哈希表,下次免于再搜索 $PATH(给 PATH 赋值或 hash -r 都会清空缓存)。在 Bash 5.3 中,hash -t 只报告已缓存的命令——从未执行过的命令会提示 not found。示例:

# 译者注示例:命令路径的哈希缓存
$ grep --version >/dev/null   # 执行后进入哈希表
$ hash -t grep
/usr/bin/grep
$ hash -d grep                # 忘记该项缓存
$ hash -t grep
bash: hash: grep: not found
$ hash -r                     # 清空整个哈希表

pwd

pwd [-LP]

打印当前工作目录的绝对路径名。若提供了 -P 选项,或启用了 set 内建命令的 -o physical 选项(参见第 4.3.1 节(set 内建命令)),打印的路径名将不包含符号链接。若提供了 -L 选项,打印的路径名可以包含符号链接。返回状态为零,除非在确定当前目录名称时出错或提供了非法选项。

译者注$PWD 通常与 pwd 一致,但 $PWD 是「逻辑路径」;进入符号链接目录后两者可能不同——pwd -L(默认,逻辑路径)显示链接路径,pwd -P(物理路径)解析符号链接。示例:

# 译者注示例:pwd -L 与 pwd -P 的区别
$ ln -sfn /tmp /tmp/pwd-link-test
$ cd /tmp/pwd-link-test && pwd -L
/tmp/pwd-link-test
$ pwd -P
/tmp
$ rm -f /tmp/pwd-link-test

readonly

readonly [-aAf] [-p] [NAME[=VALUE]] ...

将每个 NAME 标记为只读。这些名称的值此后不能通过赋值或 unset 更改。若提供了 -f 选项,每个 NAME 指一个 Shell 函数。-a 选项表示每个 NAME 指一个索引数组变量;-A 选项表示每个 NAME 指一个关联数组变量。若同时提供两个选项,-A 优先。若未提供 NAME 参数,或提供了 -p 选项,打印所有只读名称的列表。其它选项可用于把输出限制为只读名称集合的子集。-p 选项以可重新用作输入的形式显示输出。

readonly 允许在修改只读属性的同时设置变量值:在变量名后跟 =VALUE 即可。这会在修改只读属性的同时把变量值设置为 VALUE。

返回状态为零,除非提供了非法选项、某个 NAME 参数不是合法的 Shell 变量名或函数名,或给 -f 提供了不是 Shell 函数的名称。

译者注:只读属性一旦设置,在当前 Shell 内无法撤销(重新启动 Shell 才可)——export 有对应的 export -n,而 readonly 没有对应的逆操作。给只读变量赋值或 unset 都会报错:

# 译者注示例:只读变量不可再赋值,也不可 unset
$ readonly NAME=dev
$ NAME=prod
bash: NAME: readonly variable
$ unset NAME
bash: unset: NAME: cannot unset: readonly variable

return

return [N]

停止执行 Shell 函数或被载入的文件,并向其调用者返回值 N。若未提供 N,返回值为最后一条已执行命令的退出状态。若 return 由陷阱处理程序执行,用于确定状态的最后一条命令是陷阱处理程序执行之前的最后一条命令。若 returnDEBUG 陷阱期间执行,用于确定状态的最后一条命令是调用 return 之前陷阱处理程序执行的最后一条命令。

当用 return 终止由 .source)内建命令执行的脚本时,它返回 N 或脚本内最后一条已执行命令的退出状态,作为脚本的退出状态。若提供了 N,返回值取其最低的 8 位。

任何与 RETURN 陷阱关联的命令都会在函数或脚本结束后、执行恢复之前执行。

return 收到非数值参数,或在函数之外且不在 .source 执行的脚本期间使用时,返回状态非零。

译者注return 只能用在 Shell 函数或 ./source 载入的脚本里;在普通脚本顶层使用会报错。return Nexit N 一样,N 超出 0–255 时取低 8 位。示例:

# 译者注示例:函数用 return 返回不同状态
$ f() { [ "$1" = ok ] && return 0 || return 3; }
$ f ok; echo $?
0
$ f bad; echo $?
3

shift

shift [N]

把位置参数向左移动 N 位:位置参数 N+1 ... $# 被改名为 $1 ... $#-N。由数字 $# 向下直到 $#-N+1 表示的参数被取消设置。N 必须是非负数且小于等于 $#。若未提供 N,假定为 1。若 N 为零或大于 $#,位置参数不变。返回状态为零,除非 N 大于 $# 或小于零。

译者注shift 常配合 getopts 或在循环中逐项消费位置参数。N 的上限是 $#:N 大于 $# 时参数不变但返回状态非零;shift $# 可一次性清空所有位置参数。示例:

# 译者注示例:shift 左移位置参数
$ set -- a b c d
$ shift 2
$ echo "$1 $#"
c 2
$ shift 2
$ echo "剩余 $# 个参数"
剩余 0 个参数

test[

test EXPR

求值条件表达式 EXPR,并返回状态 0(真)或 1(假)。每个操作符和操作数都必须是一个单独的参数。表达式由下文 Bash 条件表达式(参见第 6.4 节(Bash 条件表达式))中描述的基本条件组成。test 不接受任何选项,也不接受并忽略表示选项结束的 -- 参数。使用 [ 形式时,命令的最后一个参数必须是 ]

表达式可以使用下列操作符组合,按优先级从高到低列出。求值方式取决于参数的个数;见下文。test 在参数为五个或更多时使用操作符优先级。

! EXPR

若 EXPR 为假则为真。

( EXPR )

返回 EXPR 的值。可用于覆盖正常的操作符优先级。

EXPR1 -a EXPR2

若 EXPR1 和 EXPR2 都为真则为真。

EXPR1 -o EXPR2

若 EXPR1 或 EXPR2 任一为真则为真。

test[ 内建命令根据参数的个数,用一组规则求值条件表达式。

0 个参数

表达式为假。

1 个参数

当且仅当参数非空时表达式为真。

2 个参数

若第一个参数是 !,当且仅当第二个参数为空时表达式为真。若第一个参数是某个一元条件操作符(参见第 6.4 节(Bash 条件表达式)),则一元测试为真时表达式为真。若第一个参数不是合法的一元操作符,表达式为假。

3 个参数

按下列顺序应用条件:

  1. 若第二个参数是某个二元条件操作符(参见第 6.4 节(Bash 条件表达式)),表达式的结果是使用第一和第三个参数作为操作数的二元测试的结果。参数为三个时,-a-o 被视为二元操作符。
  2. 若第一个参数是 !,值为用第二和第三个参数进行的两参数测试的否定。
  3. 若第一个参数恰好是 ( 且第三个参数恰好是 ),结果是第二个参数的单参数测试。
  4. 否则表达式为假。

4 个参数

按下列顺序应用条件:

  1. 若第一个参数是 !,结果是其余参数组成的三参数表达式的否定。
  2. 若第一个参数恰好是 ( 且第四个参数恰好是 ),结果是第二和第三个参数的两参数测试。
  3. 否则,按上面列出的规则依优先级解析并求值表达式。

5 个或更多参数

按上面列出的规则依优先级解析并求值表达式。

若 Shell 处于 POSIX 模式,或表达式属于 [[ 命令,<> 操作符使用当前区域设置(locale)排序。若 Shell 不处于 POSIX 模式,test[ 命令按 ASCII 顺序做字典序排序。

历史遗留的、4 个及以上参数时按操作符优先级解析的方式,在遇到看起来像基本条件的字符串时会产生歧义。POSIX 标准已弃用 -a-o 基本条件以及用括号括起表达式的写法。脚本不应再使用它们。更可靠的做法是把 test 调用限制为单个基本条件,并把 -a-o 的用法替换为 Shell 的 &&|| 列表操作符。例如,使用

test -n string1 && test -n string2

而不是

test -n string1 -a -n string2

译者注test 只有 -eq-ne-gt-ge-lt-le 这六个整数比较操作符;<>(在 [ ... ] 中须转义为 \<\>)和 =!= 做的是字符串比较。把整数当字符串比较是经典陷阱:"10""9" 按字典序比较时,先比第一个字符 '1''9''1' < '9'),所以 [ 10 \> 9 ] 为假。示例:

# 译者注示例:整数比较与字符串比较的差异
$ [ 10 -gt 9 ]; echo "整数比较 10 -gt 9: $?"
整数比较 10 -gt 9: 0
$ [ 10 \> 9 ]; echo "字符串比较 10 \> 9: $?"
字符串比较 10 \> 9: 1
$ [ 2 \> 10 ]; echo "字符串比较 2 \> 10: $?"
字符串比较 2 \> 10: 0

译者注:变量不加引号时,若其值为空或含空白,会被拆分或直接「消失」,导致参数个数变化、结果错误甚至报错([: too many arguments)。习惯上应给变量加双引号。示例:

# 译者注示例:变量加引号与不加引号
$ x="a b"; [ $x = "a b" ] 2>&1; echo "未加引号: $?"
bash: [: too many arguments
未加引号: 2
$ x="a b"; [ "$x" = "a b" ] && echo "加引号: 相等"
加引号: 相等

times

times

打印 Shell 及其子进程的用户时间和系统时间。返回状态为零。

译者注:输出共两行:第一行是 Shell 自身的用户/系统时间,第二行是其所有子进程累计的用户/系统时间。这与 time 关键字(只统计单条命令)不同。示例:

# 译者注示例:times 输出(数值随机器与运行情况而异)
$ times
0m0.005s 0m0.004s
0m0.010s 0m0.025s

trap

trap [-lpP] [ACTION] [SIGSPEC ...]

ACTION 是一条命令,当 Shell 收到 SIGSPEC 中的任一信号时读取并执行它。若 ACTION 缺席(且只有一个 SIGSPEC)或等于 -,每个指定 SIGSPEC 的处置(disposition)被重置为 Shell 启动时的值。若 ACTION 是空字符串,则每个 SIGSPEC 指定的信号被 Shell 及其调用的命令忽略。

若未提供任何参数,trap 以一组 trap 命令的形式打印与每个被捕获信号关联的动作;这组命令可重新用作 Shell 输入,以恢复当前的信号处置。

若 ACTION 不存在且提供了 -ptrap 以一组可重新用作 Shell 输入、以恢复当前信号处置的 trap 命令形式,显示与每个 SIGSPEC 关联的陷阱命令;若未提供 SIGSPEC,则显示所有被捕获信号的陷阱命令。-P 选项行为类似,但只显示与每个 SIGSPEC 参数关联的动作。-P 至少需要一个 SIGSPEC 参数。-P-p 选项可以在子 Shell 环境(例如命令替换)中使用;只要它们用在 trap 改变信号处理方式之前,就会显示其父进程陷阱的状态。

-l 选项打印信号名称及其对应编号的列表。每个 SIGSPEC 是信号名称或信号编号。信号名称不区分大小写,SIG 前缀可省略。若 -l 不带 SIGSPEC 参数,它打印合法信号名称的列表。

若 SIGSPEC 是 0EXIT,ACTION 在 Shell 退出时执行。若 SIGSPEC 是 DEBUG,ACTION 在每条简单命令、for 命令、case 命令、select 命令、(( )) 算术命令、[[ ]] 条件命令、算术 for 命令执行之前,以及 Shell 函数中第一条命令执行之前执行。关于 extdebug Shell 选项(参见第 4.3.2 节(shopt 内建命令))对 DEBUG 陷阱影响的细节,参见其说明。若 SIGSPEC 是 RETURN,ACTION 在每次 Shell 函数或用 .source 内建命令执行的脚本结束时执行。

若 SIGSPEC 是 ERR,只要管道(可能由单个简单命令组成)、命令列表或复合命令返回非零退出状态,ACTION 就执行,但受下列条件约束。以下情况 ERR 陷阱不会执行:失败的命令是紧跟在 untilwhile 保留字之后的命令列表的一部分;是 ifelif 保留字后测试的一部分;是 &&|| 列表中执行的部分(最后那个 &&|| 之后的命令除外);是管道中除最后一条外的任何命令(视 pipefail Shell 选项的状态而定);或命令的返回状态正被 ! 取反。这些都是 errexit-e)选项遵循的相同条件。

当 Shell 非交互时,进入非交互式 Shell 时已被忽略的信号无法被捕获或重置。交互式 Shell 允许捕获进入时被忽略的信号。创建子 Shell 或子 Shell 环境时,未被忽略的被捕获信号会被重置为原始值。

返回状态为零,除非某个 SIGSPEC 未指定合法信号;否则非零。

译者注:常用信号速查(编号为 Linux 标准值):HUP(1) 挂断、INT(2) 中断(Ctrl-C)、QUIT(3) 退出并产生核心转储、KILL(9) 强制杀死、TERM(15) 终止、CONT(18) 继续、STOP(19) 暂停。其中 KILLSTOP 不可被捕获或忽略。信号名不区分大小写,SIG 前缀可省略;trap -l 可列出全部信号。trap 的三种常见形式:trap 'cmd' SIG 捕获、trap '' SIG 忽略、trap - SIG 恢复默认。

0/EXIT 外,还有几个伪信号:DEBUG(每条命令执行前触发,可用于调试)、ERR(命令失败时触发)、RETURN(函数或 source 的脚本返回时触发)。示例:

# 译者注示例:EXIT 陷阱与信号列表
$ bash -c 'trap "echo 退出时执行" EXIT; echo 脚本主体'
脚本主体
退出时执行
$ trap -l | head -n 1
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
# 译者注示例:DEBUG 陷阱借助 BASH_COMMAND 显示即将执行的命令
$ bash -c 'trap "echo 执行: \$BASH_COMMAND" DEBUG; ls /tmp >/dev/null; true'
执行: ls /tmp > /dev/null
执行: true

true

true

什么也不做,返回状态 0。

umask

umask [-p] [-S] [MODE]

把 Shell 进程的文件创建掩码设置为 MODE。若 MODE 以数字开头,它被解释为八进制数;否则被解释为类似于 chmod 命令所接受的符号模式掩码。若省略 MODE,umask 打印掩码的当前值。若提供了 -S 选项而没有 MODE 参数,umask 以符号格式打印掩码;默认输出是八进制数。若提供了 -p 选项且省略 MODE,输出形式可重新用作输入。模式成功更改或未提供 MODE 参数时返回状态为零,否则非零。

注意,当模式被解释为八进制数时,umask 的每一位数字都是从 7 中减去的。因此,umask 为 022 时得到的权限是 755

译者注:八进制 umask 表示「要从默认权限中减去的位」:新建文件默认权限为 666、新建目录默认权限为 777,各减去 umask 后得到实际权限。所以 umask 022 时文件权限为 644、目录权限为 755。符号形式(umask -S)可读性更好。示例:

# 译者注示例:umask 的显示与效果
$ umask 022
$ umask
0022
$ umask -S
u=rwx,g=rx,o=rx
$ touch /tmp/umask-test && ls -l /tmp/umask-test
-rw-r--r-- 1 jack jack 0 Aug  6 22:07 /tmp/umask-test
$ rm -f /tmp/umask-test

unset

unset [-fnv] [NAME]

删除每个变量或函数 NAME。若给了 -v 选项,每个 NAME 指一个 Shell 变量,该变量被删除。若给了 -f 选项,NAME 指 Shell 函数,函数定义被删除。若提供了 -n 选项,且 NAME 是带 nameref(名称引用)属性的变量,被删除的是 NAME 本身而不是它引用的变量。若提供了 -f 选项,-n 无效。若未提供任何选项,每个 NAME 指一个变量;若没有同名变量,则删除同名函数(若有)。只读变量和函数不能被删除。变量或函数被删除时,它们也会从传给后续命令的环境中移除。某些 Shell 变量不能被删除。某些 Shell 变量被删除后会失去其特殊行为;这类行为在各变量的描述中有说明。返回状态为零,除非某个 NAME 是只读的或不能被删除。

译者注:区分「未设置」与「设置为空」:unset x 之后,${x-unset} 输出默认值;x=(赋空值)之后变量已存在,${x-unset} 输出空字符串。开启 set -u(参见第 4.3.1 节(set 内建命令))后,引用未设置的变量会报错,而空变量不会。示例:

# 译者注示例:unset 与赋空值的区别
$ unset x; echo "A=${x-unset}"; x=; echo "B=${x-unset}"
A=unset
B=

4.2 Bash 内建命令

本节介绍 Bash 特有或经过 Bash 扩展的内建命令(builtin)。其中有些命令在 POSIX 标准中有规定。

alias

alias [-p] [NAME[=VALUE] ...]

无参数或使用 -p 选项时,alias 在标准输出上以可重新用作输入的形式打印别名列表。若提供了参数,则为每个给出了 VALUE 的 NAME 定义别名。若未给出 VALUE,则打印别名 NAME 的名称和值。VALUE 末尾的空格会导致命令解析期间展开该别名时,对下一个单词检查是否要做别名替换。除非给出的某个 NAME(没有对应的 =VALUE)未定义过别名,否则 alias 返回真。别名(alias)在第 6.6 节(别名)中描述。

译者注:别名必须在命令行被「读取」之前就已存在,这一点常常令人意外。利用 VALUE 末尾的空格可以把别名串起来:展开以空格结尾的别名后,紧跟着的下一个单词还会再被检查一次别名定义。下面的例子演示有无尾随空格的区别:

$ alias a='echo first'
$ alias b='echo second'
$ alias a2='a '      # 以空格结尾:展开 a2 后还会检查下一个单词 b
$ a2 b
first echo second
$ alias a2='a'       # 无尾随空格:b 不再被检查
$ a2 b
first b

译者注:别名在命令行被读取(解析)时展开,而不是在执行时。因此同一行里先 alias 后使用是不生效的;另外非交互式 Shell 默认不展开别名,需要先用 shopt -s expand_aliases 开启:

$ shopt -s expand_aliases; alias c='echo yes'; c
bash: 行 1: c: 未找到命令

bind

bind [-m KEYMAP] [-lsvSVX]
bind [-m KEYMAP] [-q FUNCTION] [-u FUNCTION] [-r KEYSEQ]
bind [-m KEYMAP] -f FILENAME
bind [-m KEYMAP] -x KEYSEQ[: ]SHELL-COMMAND
bind [-m KEYMAP] KEYSEQ:FUNCTION-NAME
bind [-m KEYMAP] KEYSEQ:READLINE-COMMAND
bind [-m KEYMAP] -p|-P [READLINE-COMMAND]
bind READLINE-COMMAND-LINE

显示当前的 Readline(参见第 8 章(命令行编辑))键位与函数绑定,把键序列绑定到 Readline 函数、宏或 Shell 命令,或设置 Readline 变量。每个非选项参数都是一条键绑定或命令,写法与在 Readline 初始化文件(参见第 8.3 节(Readline 初始化文件))中出现的相同,但每条绑定或命令都必须作为单独的参数传入;例如 "\C-x\C-r":re-read-init-file

在下列描述中,凡是以可被再次读取的形式显示输出的选项,都会把输出格式化成 Readline 初始化文件中会出现的命令,或可单独作为参数提供给 bind 命令的形式。

若提供了选项,其含义如下:

-m KEYMAP

把 KEYMAP 用作受后续绑定影响的键位表(keymap)。可接受的 KEYMAP 名称有 emacsemacs-standardemacs-metaemacs-ctlxvivi-movevi-commandvi-insertvi 等价于 vi-commandvi-move 也是同义词);emacs 等价于 emacs-standard

-l

列出所有 Readline 函数的名称。

-p

以可被用作后续 bind 命令参数或写入 Readline 初始化文件的形式,显示 Readline 函数名与绑定。若选项处理之后还有剩余参数,bind 把它们当作 readline 命令名,并把输出限制在这些名称上。

-P

列出当前的 Readline 函数名与绑定。若选项处理之后还有剩余参数,bind 把它们当作 readline 命令名,并把输出限制在这些名称上。

-s

以可被用作后续 bind 命令参数或写入 Readline 初始化文件的形式,显示绑定到宏的 Readline 键序列及它们输出的字符串。

-S

显示绑定到宏的 Readline 键序列及它们输出的字符串。

-v

以可被用作后续 bind 命令参数或写入 Readline 初始化文件的形式,显示 Readline 变量名与值。

-V

列出当前的 Readline 变量名与值。

-f FILENAME

从 FILENAME 读取键绑定。

-q FUNCTION

显示调用所命名 Readline FUNCTION 的键序列。

-u FUNCTION

解除绑定到所命名 Readline FUNCTION 的所有键序列。

-r KEYSEQ

移除 KEYSEQ 的任何当前绑定。

-x KEYSEQ:SHELL-COMMAND

使 SHELL-COMMAND 在每次输入 KEYSEQ 时被执行。KEYSEQ 与 SHELL-COMMAND 之间的分隔符是空白字符或冒号(冒号后可跟空白字符)。若分隔符是空白字符,SHELL-COMMAND 必须用双引号括起,Readline 会在保存它之前展开 SHELL-COMMAND 中任何特殊的反斜杠转义。若分隔符是冒号,外围的双引号可有可无,Readline 在保存前不展开命令字符串。由于整个键绑定表达式必须是单独一个参数,应当用单引号把它括起来。SHELL-COMMAND 执行时,Shell 把 READLINE_LINE 变量设为 Readline 行缓冲区的内容,把 READLINE_POINTREADLINE_MARK 变量分别设为插入点的当前位置与保存的插入点(MARK)。Shell 把用户提供的任何数字参数赋给 READLINE_ARGUMENT 变量;若没有参数,则不设置该变量。若被执行的命令改变了 READLINE_LINEREADLINE_POINTREADLINE_MARK 中任何一个的值,这些新值将反映到编辑状态中。

译者注bind -x 把键序列绑定到 Shell 命令而非 Readline 函数,绑定在启用行编辑的交互式 Shell 中生效。非交互式 Shell 中执行 bind 会提示「未启用行编辑」的警告,但绑定操作本身仍可注册成功,可用 bind -X 查看:

$ bind -x '"\C-x\C-l": echo "line: $READLINE_LINE"'
$ bind -X
"\C-x\C-l" "echo \"line: $READLINE_LINE\""

绑定后,在交互式终端里按 Ctrl-x 再按 Ctrl-l 就会执行 echo "line: $READLINE_LINE",此时 READLINE_LINE 是命令行当前的文本。典型用途是实现自定义快捷键(如快速把当前行写入文件)。

-X

以可被重新用作后续 bind 命令参数的形式,列出绑定到 Shell 命令的所有键序列及与之关联的命令。

除非提供了无效选项或发生错误,返回状态为零。

builtin

builtin [SHELL-BUILTIN [ARGS]]

执行指定的 Shell 内建命令 SHELL-BUILTIN,把 ARGS 传给它,并返回其退出状态。在定义与 Shell 内建命令同名的 Shell 函数时,这很有用——可以在函数内保留该内建命令的功能。若 SHELL-BUILTIN 不是 Shell 内建命令,返回状态非零。

译者注builtincommandenable 三者构成了处理「函数遮蔽内建命令」的完整方案:builtin 只执行内建命令本身(跳过同名函数);command 跳过同名函数,但仍会执行内建命令或搜索 PATHenable -n 则把内建命令彻底禁用,让同名的外部程序成为默认选择。典型用法是在与内建命令同名的函数内部调用 builtin

$ cd() { builtin cd "$@" && pwd; }
$ cd /tmp
/tmp

caller

caller [EXPR]

返回任何活动子例程调用(Shell 函数或用 .source 内建命令执行的脚本)的上下文。

不带 EXPR 时,caller 显示当前子例程调用的行号和源文件名。若提供了非负整数作为 EXPR,caller 显示当前执行调用栈中该位置对应的行号、子例程名和源文件。这些额外信息可用于例如打印栈回溯(stack trace)。当前帧是第 0 帧。

除非 Shell 没有在执行子例程调用,或 EXPR 与调用栈中的有效位置不对应,否则返回值为 0。

译者注caller 是调试函数的利器。下面的例子演示各帧的含义:caller(无参数)报告当前子例程调用(c 被调用处)的行号与源文件;caller 0 还带出调用者 b 的函数名;caller 1 报告 b 被调用处的信息:

$ cat > /tmp/frames.sh <<'EOF'
a() { b; }
b() { c; }
c() {
    caller
    caller 0
    caller 1
}
a
EOF
$ bash /tmp/frames.sh
2 /tmp/frames.sh
2 b /tmp/frames.sh
1 a /tmp/frames.sh

command

command [-pVv] COMMAND [ARGUMENTS ...]

command 内建命令用 ARGUMENTS 运行 COMMAND,忽略任何名为 COMMAND 的 Shell 函数。只执行 Shell 内建命令或通过搜索 PATH 找到的命令。若存在名为 ls 的 Shell 函数,在函数内运行 command ls 将执行外部命令 ls,而不是递归调用该函数。-p 选项表示使用一个保证能找到全部标准工具的默认 PATH 值。此时若找不到 COMMAND 或发生错误,返回状态为 127;否则为 COMMAND 的退出状态。

若提供了 -V-v 选项,command 打印对 COMMAND 的描述。-v 选项显示一个单词,指出用于调用 COMMAND 的命令名或文件名;-V 选项产生更详细的描述。此时,找到 COMMAND 则返回状态为零,否则非零。

译者注command -vtype -t 类似,都是判断命令类型的手段;区别是 command -v 只输出一个单词(内建命令名或可执行文件的路径),且不查别名表:

$ command -v cd
cd
$ command -v ls
/usr/bin/ls

在脚本中常用 command -v somecmd 来探测某个外部命令是否存在。

declare

declare [-aAfFgiIlnrtux] [-p] [NAME[=VALUE] ...]

声明变量并给它们赋予属性。若未给出 NAME,则改为显示变量或 Shell 函数的值。

-p 选项显示每个 NAME 的属性与值。当 -p 与 NAME 参数一起使用时,除 -f-F 之外的其它选项都被忽略。

当带 -p 但不提供 NAME 参数时,declare 显示具有附加选项所指定属性的所有变量的属性与值。若 -p 未带其它选项,declare 显示所有 Shell 变量的属性与值。-f 选项把显示范围限制为 Shell 函数。

-F 选项抑制函数定义的显示,只打印函数名和属性。若用 shopt(参见第 4.3.2 节(shopt 内建命令))启用了 extdebug Shell 选项,还会显示定义每个 NAME 的源文件名和行号。-F 隐含 -f

-g 选项强制在全局作用域创建或修改变量,即使 declare 是在 Shell 函数中执行的。当 declare 不在 Shell 函数中执行时,该选项被忽略。

-I 选项使局部变量继承周围作用域中任何同 NAME 的现有变量的属性(nameref 属性除外)和值。若不存在现有变量,则局部变量初始为未设置(unset)。

以下选项可用于把输出限制在具有指定属性的变量上,或给变量赋予属性:

-a

每个 NAME 都是索引数组(indexed array)变量(参见第 6.7 节(数组))。

-A

每个 NAME 都是关联数组(associative array)变量(参见第 6.7 节(数组))。

-f

每个 NAME 都指代一个 Shell 函数。

-i

该变量按整数处理;给变量赋值时执行算术求值(参见第 6.5 节(Shell 算术))。

-l

给变量赋值时,把所有大写字符转换为小写。大写属性被禁用。

-n

给每个 NAME 赋予 nameref 属性,使它成为另一个变量的名称引用。那个被引用的变量由 NAME 的值定义。对 NAME 的所有引用、赋值和属性修改,除使用或改变 -n 属性本身的那些之外,都在 NAME 的值所引用的变量上执行。nameref 属性不能应用于数组变量。

-r

使 NAME 变为只读。这些名称之后不能再被后续赋值语句赋值,也不能被 unset。

-t

给每个 NAME 赋予 trace 属性。被追踪(trace)的函数从调用它的 Shell 继承 DEBUGRETURN 陷阱。trace 属性对变量没有特殊含义。

-u

给变量赋值时,把所有小写字符转换为大写。小写属性被禁用。

-x

把每个 NAME 标记为通过环境导出给后续命令。

+ 代替 - 则改为关闭指定属性,例外是 +a+A 不能用来销毁数组变量,+r 不会移除只读属性。

在函数中使用时,declare 使每个 NAME 成为局部变量,与 local 命令相同,除非提供了 -g 选项。若变量名后跟 =VALUE,则把变量值设为 VALUE。

使用 -a-A 以及复合赋值语法创建数组变量时,附加属性要到后续赋值时才生效。

除非遇到无效选项、试图用 -f foo=bar 定义函数、试图给只读变量赋值、试图不用复合赋值语法(参见第 6.7 节(数组))给数组变量赋值、某个 NAME 不是合法的 Shell 变量名、试图关闭只读变量的只读状态、试图关闭数组变量的数组状态,或试图用 -f 显示不存在的函数,否则返回状态为零。

译者注declare -i 声明的变量在赋值时自动做算术求值,可以省去 $((...)) 的展开写法,也可以做表达式运算:

$ declare -i x; x=5+3; echo "$x"
8

译者注-a 声明索引数组(下标为整数,从 0 开始),-A 声明关联数组(下标为任意字符串)。关联数组是 Bash 4.0 起才有的特性,复合赋值语法 (元素 ...)([键]=值 ...) 的细节参见第 6.7 节(数组):

$ declare -a arr=(10 20 30); echo "${arr[1]}"
20
$ declare -A assoc=([name]=jack [age]=42); echo "${assoc[name]}"
jack

译者注-l-u 在赋值时强制转换大小写,适合规范化用户输入(如用户名):

$ declare -l s="Hello WORLD"; echo "$s"
hello world
$ declare -u t="hello world"; echo "$t"
HELLO WORLD

+l+u 可关闭对应属性。

译者注-n(nameref)相当于给变量起「别名」,对 ref 的一切读写都会落到 real 上,常用于把变量名传给函数、实现引用传参:

$ declare -n ref=real; real=42; echo "$ref"
42
$ ref=100; echo "$real"
100

译者注declare -p 输出的声明可以直接粘贴回去重新执行(-p 的输出形式与 aliasbind 一样都是「可重用为输入」的格式),也常用于调试变量类型:

$ x=5; declare -p x
declare -- x="5"

译者注:函数内的 declare 默认创建局部变量(同 local),需要修改全局变量时必须显式加 -g

$ f() { declare -g gx=99; }; f; echo "$gx"
99

echo

echo [-neE] [ARG ...]

输出 ARG,以空格分隔,以换行符结尾。除非发生写错误,返回状态为 0。若指定了 -n,则不打印结尾的换行符。

若给出 -e 选项,echo 解释下列反斜杠转义字符。-E 选项禁用对这些转义字符的解释,即使在默认解释它们的系统上也是如此。xpg_echo Shell 选项决定 echo 是否解释任何选项并展开这些转义字符。echo 不把 -- 解释为选项的结束。

echo 解释以下转义序列:

序列 含义
\a 响铃(报警)
\b 退格
\c 抑制后续输出
\e\E 转义字符(ESC)
\f 换页
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\\ 反斜杠
\0NNN 值为八进制数 NNN(零到三个八进制数字)的八位字符
\xHH 值为十六进制数 HH(一个或两个十六进制数字)的八位字符
\uHHHH 值为十六进制数 HHHH(一到四个十六进制数字)的 Unicode(ISO/IEC 10646)字符
\UHHHHHHHH 值为十六进制数 HHHHHHHH(一到八个十六进制数字)的 Unicode(ISO/IEC 10646)字符

echo 原样写出任何无法识别的反斜杠转义字符。

译者注\0NNN 是八进制、\xHH 是十六进制,\u/\U 是 Unicode 码点,适合输出不可见字符或非 ASCII 字符。注意八进制写法必须以 0 开头(\101 会被原样输出):

$ echo -e '\0101 \x42'    # 八进制 0101 = 65 = 'A',十六进制 42 = 66 = 'B'
A B
$ echo -e '中文'
中文

enable

enable [-a] [-dnps] [-f FILENAME] [NAME ...]

启用和禁用 Shell 内建命令。禁用某个内建命令后,与 Shell 内建命令同名的可执行文件就可以在不指定完整路径名的情况下被执行,尽管 Shell 通常先于文件搜索内建命令。

若提供 -n,则禁用 NAME。否则启用 NAME。例如,要使用通过 $PATH 找到的 test 二进制文件而不是 Shell 内建版本,输入 enable -n test

若提供 -p 选项,或未提供 NAME 参数,则打印 Shell 内建命令列表。没有其它参数时,列表由所有已启用的 Shell 内建命令组成。-n 选项表示只打印已禁用的内建命令。-a 选项表示列出每个内建命令,并标明其是否已启用。-s 选项表示把 enable 限制在 POSIX 特殊内建命令上。

-f 选项表示在支持动态加载的系统上,从共享对象 FILENAME 加载新的内建命令 NAME。若 FILENAME 不含斜杠,Bash 将使用 BASH_LOADABLES_PATH 变量的值作为冒号分隔的目录列表,在其中搜索 FILENAME。BASH_LOADABLES_PATH 的默认值取决于系统,可能包含 "." 以强制搜索当前目录。-d 选项删除用 -f 加载的内建命令。若 -s-f 一起使用,新内建命令将成为 POSIX 特殊内建命令(参见第 4.4 节(特殊内建命令))。

若未提供选项且某个 NAME 不是 Shell 内建命令,enable 将尝试从名为 NAME 的共享对象加载 NAME,如同执行了 enable -f NAME NAME 一样。

除非某个 NAME 不是 Shell 内建命令,或从共享对象加载新内建命令时出错,否则返回状态为零。

译者注:默认情况下 Shell 总是先执行内建命令,即使 $PATH 里有同名可执行文件。enable -n NAME 禁用内建命令后,外部程序胜出;enable NAME 重新启用。例如调试 test 二进制时:

$ enable -n test
$ command -v test
/usr/bin/test
$ enable test
$ command -v test
test

注意 enable -n 禁用 . 时,其同义词 source 也一并失效(二者是同一个内建命令的两个名称),操作前请确认没有脚本依赖它。

译者注enable -f 是 Bash 的动态加载(loadable)机制入口,Bash 源码树的 examples/loadables 目录里提供了可编译加载的内建命令示例(如 sleeprealpath 等);加载搜索路径由 BASH_LOADABLES_PATH 指定。该特性只在编译时启用了动态加载支持的系统上可用,enable -d 用于卸载。

help

help [-dms] [PATTERN]

显示关于内建命令的有用信息。若指定了 PATTERN,help 按下文所述为所有匹配 PATTERN 的命令给出详细帮助;否则显示所有内建命令和 Shell 复合命令的列表。

若提供了选项,其含义如下:

选项 含义
-d 显示每个 PATTERN 的简短描述
-m 以类似手册页的格式显示每个 PATTERN 的描述
-s 只显示每个 PATTERN 的简短用法概要

若 PATTERN 包含模式匹配字符(参见第 3.5.8.1 节(模式匹配)),它被当作 Shell 模式处理,help 打印每个匹配 PATTERN 的帮助主题的描述。

否则,若 PATTERN 与某帮助主题的名称完全匹配,help 打印与该主题关联的描述。再否则,help 执行前缀匹配,打印所有匹配帮助主题的描述。

除非没有命令匹配 PATTERN,否则返回状态为零。

译者注help 只覆盖内建命令与复合命令,外部命令的用法要查手册页。PATTERN 支持通配符与前缀匹配,例如 help re 会同时匹配 readreadarrayreadonly

$ help -d cd
cd - Change the shell working directory.
$ help -s read
read: read [-Eers] [-a 数组] [-d 分隔符] [-i 文本] [-n 字符数] [-N 字符数] [-p 提示符] [-t 超时] [-u fd] [名称 ...]

let

let EXPRESSION [EXPRESSION ...]

let 内建命令允许对 Shell 变量执行算术运算。每个 EXPRESSION 都按照下文第 6.5 节(Shell 算术)中给出的规则作为算术表达式求值。若最后一个 EXPRESSION 求值为 0,let 返回 1;否则 let 返回 0。

译者注let 与复合命令 (( ... )) 等价,都按 C 语言规则求值:/ 是整除、** 是乘方、支持 ++/--/%= 等赋值运算符;表达式结果为 0 时返回状态为 1(失败),非 0 时返回 0。表达式里含空格或 * 等元字符时必须加引号:

$ let x=2**10; echo "$x"
1024
$ let "y = 10 / 3"; echo "$y"
3
$ let z=1-1; echo $?
1

local

local [OPTION] NAME[=VALUE] ...

对每个参数,创建名为 NAME 的局部变量并给它赋值为 VALUE。OPTION 可以是 declare 接受的任何选项。local 只能用在函数内;它使变量 NAME 的可见作用域限制在该函数及其子级。在函数之外使用 local 是错误。

若 NAME 是 -,则它把 Shell 选项的集合局部化到调用 local 的那个函数:函数内在调用 local 之后用 set 内建命令改变的任何 Shell 选项,都会在函数返回时恢复为原值。恢复过程如同执行了一系列 set 命令来恢复函数之前的设置。

无操作数时,local 向标准输出写出局部变量列表。

除非在函数外使用 local、提供了无效的 NAME,或 NAME 是只读变量,否则返回状态为零。

译者注local - 是 Bash 4.2 起支持的特殊形式,把 set 的选项(而非变量)局部化:函数内对选项的任何修改在返回时自动还原,无需手动保存和恢复:

$ f() { local -; set -o nounset; echo "inside: $-"; }
$ f
inside: huBc
$ echo "outside: $-"
outside: hBc

注意 local - 要写在函数开头、其它 set 修改之前,函数返回时才能完整还原。

logout

logout [N]

退出登录 Shell,向 Shell 的父进程返回状态 N。

译者注logout 只对登录 Shell 有意义;在非登录 Shell 中直接 exit 即可。在交互式登录 Shell 中,若当前有停止的作业,logout 会先报错拒绝退出,需要再执行一次(与 exit 行为类似)。

mapfile

mapfile [-d DELIM] [-n COUNT] [-O ORIGIN] [-s COUNT]
    [-t] [-u FD] [-C CALLBACK] [-c QUANTUM] [ARRAY]

从标准输入(若提供了 -u 选项则为文件描述符 FD)读取行,存入索引数组变量 ARRAY。变量 MAPFILE 是默认的 ARRAY。若提供了选项,其含义如下:

-d

用 DELIM 的第一个字符结束每行输入,而不是换行符。若 DELIM 是空字符串,mapfile 在读到一个 NUL 字符时结束一行。

-n

最多复制 COUNT 行。若 COUNT 为 0,复制所有行。

-O

从索引 ORIGIN 开始向 ARRAY 赋值。默认索引为 0。

-s

丢弃读取的前 COUNT 行。

-t

从读取的每行中移除结尾的 DELIM(默认是换行符)。

-u

从文件描述符 FD 而不是标准输入读取行。

-C

每读取 QUANTUM 行就求值一次 CALLBACK。-c 选项指定 QUANTUM。

-c

指定每次调用 CALLBACK 之间读取的行数。

若指定了 -C 而未指定 -c,默认的 quantum(间隔行数)为 5000。求值 CALLBACK 时,会把下一个要赋值的数组元素的索引以及要赋给该元素的行作为附加参数提供给它。CALLBACK 在读入该行之后、给数组元素赋值之前求值。

若未提供显式的起点,mapfile 会在赋值前清空 ARRAY。

除非提供了无效的选项或选项参数、ARRAY 无效或不可赋值,或 ARRAY 不是索引数组,否则 mapfile 返回零。

译者注mapfile(别名 readarray)是「把文件逐行读进数组」的惯用工具,常与 -t(去掉行尾换行)配合,比 while read 管道(子 Shell 中丢失变量)更高效直接:

$ printf 'a\nb\nc\n' > /tmp/mf.txt
$ mapfile -t arr < /tmp/mf.txt
$ echo "${arr[1]} ${#arr[@]}"
b 3

-s 跳过前 N 行、-n 限制行数、-O 指定起始下标(指定 -O 时不先清空数组):

$ mapfile -s 1 -n 1 -O 5 brr < /tmp/mf.txt
$ declare -p brr
declare -a brr=([5]=$'b\n')

不带数组名时写入默认变量 MAPFILE

printf

printf [-v VAR] FORMAT [ARGUMENTS]

在 FORMAT 的控制下把格式化后的 ARGUMENTS 写到标准输出。-v 选项把输出赋给变量 VAR,而不是打印到标准输出。

FORMAT 是包含三种对象的字符串:普通字符(直接复制到标准输出)、字符转义序列(转换后复制到标准输出)和格式说明符(每个都会打印下一个连续的 ARGUMENT)。除了标准的 printf(3) 格式字符 cCsSndiouxXeEfFgGaA 之外,printf 还解释下列附加格式说明符:

%b

使 printfecho -e(参见第 4.2 节(Bash 内建命令))那样展开对应 ARGUMENT 中的反斜杠转义序列。

%q

使 printf 以可重新用作 Shell 输入的形式输出对应 ARGUMENT。若参数字符串中有任何字符需要,%q%Q 使用 ANSI-C 引号风格(参见第 3.1.2.4 节(ANSI-C 引号)),否则使用反斜杠引号。若格式字符串使用了 printf替代形式(alternate form),这两种格式会用单引号给参数字符串加引号。

%Q

%q 类似,但在加引号之前先把任何提供的精度应用于 ARGUMENT。

%(DATEFMT)T

使 printf 输出以 DATEFMT 作为 strftime(3) 格式字符串所得到的日期时间字符串。对应 ARGUMENT 是表示自 epoch 以来秒数的整数。该格式说明符识别两个特殊的参数值:-1 表示当前时间,-2 表示 Shell 被调用时的时间。若未指定参数,转换行为如同提供了 -1。这是对通常 printf 行为的一个例外。

%b%q%T 格式说明符都使用格式说明中的字段宽度与精度参数,从展开后的参数(它通常比原始参数包含更多字符)中写出那么多字节(或为它使用那么宽的字段)。

%n 格式说明符接受一个被当作 Shell 变量名处理的对应参数。

%s%c 格式说明符接受 l(long)修饰符,该修饰符强制把参数字符串转换为宽字符字符串,并按字符(而非字节)应用任何提供的字段宽度和精度。%S%C 格式说明符分别等价于 %ls%lc

非字符串格式说明符的参数按 C 语言常量处理,但允许前导的正号或负号;若首字符是单引号或双引号,则该值是(按当前区域设置(locale)计算的)后续字符的数值。

FORMAT 在必要时被重复使用,以消费掉所有 ARGUMENTS。若 FORMAT 需要的 ARGUMENTS 多于实际提供的数量,多余的格式说明符行为如同提供了零值或空字符串(视情况而定)。成功时返回值为零;若提供了无效选项或发生写错误或赋值错误,则返回非零。

译者注%q 的输出可直接作为 Shell 输入复用(例如在脚本中生成另一个脚本),是「安全地给任意字符串加引号」的推荐手段:

$ printf '%q\n' 'hello world'
hello\ world
$ printf '%q\n' "a'b"
a\'b
$ printf -v q '%q' 'hello world'; eval "v=$q"; echo "$v"
hello world

译者注%becho -e 的转义规则一致,但嵌在格式串中,可精确控制位置与宽度,也没有 echo 是否解释选项的歧义(echo 的行为还受 xpg_echo 选项影响):

$ printf '%b\n' 'a\tb'
a	b

译者注%(格式)T 是 Bash 4.2 起提供的日期格式化捷径,格式串语法与 strftime(3) 一致(%Y 年份、%m 月份、%d 日等)。参数 -1 表示当前时间(缺省值),-2 表示 Shell 启动时刻:

$ printf '%(%Y-%m-%d)T\n' -1
2026-08-06

译者注-v 把结果写入变量而不打印,使 printf 成为「按格式造字符串」的工具。%n 则把已输出的字符数写入对应变量:

$ printf -v msg 'Hello %s' world; echo "$msg"
Hello world
$ printf 'abcdef%n\n' n; echo "n=$n"
abcdef
n=6

译者注:非字符串格式(如 %d)的参数按 C 常量解析:允许 +/- 前缀、支持 0x 十六进制,且可用引号引字符取其字符码:

$ printf '%d\n' "'A" 0x10 +7
65
16
7

read

read [-Eers] [-a ANAME] [-d DELIM] [-i TEXT] [-n NCHARS]
    [-N NCHARS] [-p PROMPT] [-t TIMEOUT] [-u FD] [NAME ...]

从标准输入(或 -u 选项参数提供的文件描述符 FD)读取一行,按照上文第 3.5.7 节(单词拆分)所述拆分为单词,并把第一个单词赋给第一个 NAME,第二个单词赋给第二个 NAME,依此类推。若单词多于名称,剩余单词及它们之间的分隔符赋给最后一个 NAME。若从输入流读到的单词少于名称,剩余名称被赋空值。IFS 变量的值中的字符用于把行拆分为单词,所用规则与 Shell 展开时相同(上文第 3.5.7 节(单词拆分)已述)。反斜杠字符 \ 移除下一个读取字符的任何特殊含义,并用于行续行。

若提供了选项,其含义如下:

-a ANAME

单词赋给数组变量 ANAME 从 0 开始的连续下标。赋值前移除 ANAME 的所有元素。其它 NAME 参数被忽略。

-d DELIM

DELIM 的第一个字符结束输入行,而不是换行符。若 DELIM 是空字符串,read 在读到一个 NUL 字符时结束一行。

-e

若标准输入来自终端,read 使用 Readline(参见第 8 章(命令行编辑))获取该行。Readline 使用当前的(若行编辑之前未激活则为默认的)编辑设置,但使用 Readline 的默认文件名补全。

-E

若标准输入来自终端,read 使用 Readline(参见第 8 章(命令行编辑))获取该行。Readline 使用当前的(若行编辑之前未激活则为默认的)编辑设置,但使用 Bash 的默认补全,包括可编程补全。

-i TEXT

若正在用 Readline 读取该行,read 在编辑开始前把 TEXT 放入编辑缓冲区。

-n NCHARS

read 在读入 NCHARS 个字符后返回,而不是等待一整行输入,除非遇到 EOF 或 read 超时;但若在到达分隔符之前读到的字符不足 NCHARS 个,则遵从分隔符。

-N NCHARS

read 在读入恰好 NCHARS 个字符后返回,而不是等待一整行输入,除非遇到 EOF 或 read 超时。输入中的分隔符字符不被特殊对待,不会在读到 NCHARS 个字符之前让 read 返回。结果不按 IFS 的字符拆分;其意图是把读到的确切字符赋给变量(反斜杠除外;参见下面的 -r 选项)。

-p PROMPT

在尝试读取任何输入之前显示 PROMPT(不带结尾换行符),但仅当输入来自终端时。

-r

若给出此选项,反斜杠不作为转义字符,而是被视为行的一部分。特别地,反斜杠-换行符对随后不能用作行续行。

-s

静默模式。若输入来自终端,字符不回显。

-t TIMEOUT

使 read 在 TIMEOUT 秒内没有读入一整行输入(或指定数量的字符)时超时并返回失败。TIMEOUT 可以是带小数部分的十进制数。此选项仅在 read 从终端、管道或其它特殊文件读取输入时有效;从普通文件读取时无效。若 read 超时,它把已读到的任何部分输入保存到指定变量 NAME,并返回大于 128 的状态。若 TIMEOUT 为 0,read 立即返回,不尝试读取任何数据。此时若指定文件描述符上有输入可用,或读取将返回 EOF,退出状态为 0;否则非零。

-u FD

从文件描述符 FD 而不是标准输入读取输入。

除 DELIM 为空字符串的情况外,read 忽略输入中的任何 NUL 字符。

若未提供 NAME,read 把读到的行(去掉结尾分隔符,其余不变)赋给变量 REPLY

除非遇到文件结束、read 超时(此时状态大于 128)、发生变量赋值错误(例如给只读变量赋值),或给 -u 提供了无效的文件描述符作为参数,否则退出状态为零。

译者注readIFS 的值拆分单词。默认 IFS 为空格/制表符/换行(连续的空白合并处理);改成其它字符(如 :)即可解析 one:two:three 这类格式。单词多于名称时,剩余部分(含分隔符)全部归最后一个名称:

$ IFS=: read a b c <<< "one:two:three"
$ echo "$b"
two
$ read x <<< "a b c d"; echo "$x"
a b c d

译者注-a 把拆分出的单词直接写入数组(下标从 0 开始),比列出固定数量的名称更适应元素个数不定的输入:

$ read -a nums <<< "10 20 30"
$ echo "${nums[1]}"
20

译者注-d 自定义行结束符;-d ''(空字符串)表示以 NUL 字符结束一行,常用于解析以 NUL 分隔的字符串流。注意除 -d '' 外,read 会忽略输入中的其它 NUL 字符:

$ read -d '' v < <(printf "ab\0cd")
$ echo "$v"
ab

译者注-n-N 的关键区别:-n 在遇到分隔符(换行)时即使没读够 NCHARS 个字符也会提前返回;-N 必须读够 NCHARS 个字符才返回,分隔符原样保留在结果中(含换行),且结果不再按 IFS 拆分:

$ read -n 3 v < <(printf "abcdef"); echo "n=$v"
n=abc
$ read -n 5 v < <(printf "ab\ncd"); echo "d=$v"
d=ab
$ read -N 3 w < <(printf "a\nbc"); printf 'N=<%s>\n' "$w"
N=<a
b>

译者注-t 用于带超时的输入等待(如菜单倒计时)。超时时状态码大于 128(本机实测为 142),且已读到的部分输入会保存在变量里。注意它只对终端、管道等特殊文件有效,读普通文件时不会超时:

$ read -t 1 v < <(sleep 2); echo "status=$? v=<$v>"
status=142 v=<>

译者注:不带名称时,read 把整行(不含结尾分隔符)存入 REPLY,是「把一整行读进变量」的最简写法:

$ read <<< "hello"
$ echo "$REPLY"
hello

译者注-e-E 都只在输入来自终端时启用 Readline 行编辑,区别仅在补全行为:-e 用 Readline 自带的文件名补全,-E 用 Bash 的补全(含第 8.6 节(可编程补全)自定义的补全规则)。-s 不回显输入,适合读取密码;-i 常与 -e 配合提供预填文本。

readarray

readarray [-d DELIM] [-n COUNT] [-O ORIGIN] [-s COUNT]
    [-t] [-u FD] [-C CALLBACK] [-c QUANTUM] [ARRAY]

从标准输入(若提供了 -u 选项则为文件描述符 FD)读取行,存入索引数组变量 ARRAY。

mapfile 的同义词。

译者注readarraymapfile 完全等价,二者可任意混用;具体用法参见上文 mapfile 的译者注与选项说明。

source

source [-p PATH] FILENAME [ARGUMENTS]

. 的同义词(参见第 4.1 节(Bourne Shell 内建命令))。

译者注source/. 在当前 Shell 中执行文件,文件里定义的变量、函数会留在当前环境,这与执行子 Shell 脚本完全不同;ARGUMENTS 会成为文件脚本的位置参数($1$2……)。-p PATH 选项指定搜索 FILENAME 的路径。与进程替换配合还可以「执行一段动态生成的内容」:

$ printf 'echo "arg1=$1"' > /tmp/src.sh
$ source /tmp/src.sh AA
arg1=AA
$ source <(echo "x=42"); echo "$x"
42

type

type [-afptP] [NAME ...]

指出如果把每个 NAME 用作命令名,它将被如何解释。

若使用 -t 选项,当 NAME 分别是别名、Shell 保留字、Shell 函数、Shell 内建命令或可执行文件时,type 打印一个单词:分别为 aliaskeywordfunctionbuiltinfile。若找不到 NAME,type 什么都不打印并返回失败状态。

若使用 -p 选项,type 要么返回通过搜索 $PATHname 找到的可执行文件的名称,要么(若 -t 不会返回 file)什么都不返回。

-P 选项强制对每个 NAME 执行路径搜索,即使 -t 不会返回 file

若 NAME 存在于哈希命令表中,-p-P 选项打印哈希值,该值不一定是 $PATH 中最先出现的文件。

若使用 -a 选项,type 返回所有包含名为 NAME 的命令的位置。这包括别名、保留字、函数和内建命令,但可以加上路径搜索选项(-p-P)把输出限制为可执行文件。若 -a-p 一起提供,type 不查找哈希命令表,只对 NAME 执行 PATH 搜索。

若使用 -f 选项,type 不尝试查找 Shell 函数,与 command 内建命令一样。

若所有 NAME 都被找到,返回状态为零;若有任何一个未找到,则非零。

译者注type -t 输出的五个单词正好对应命令的五种来源:alias(别名)、keyword(保留字)、function(Shell 函数)、builtin(内建命令)、file(外部可执行文件);type -p 只报告 file 类的路径:

$ type -t echo; type -t if; type -t ls
builtin
keyword
file
$ type -p ls
/usr/bin/ls

typeset

typeset [-afFgrxilnrtux] [-p] [NAME[=VALUE] ...]

提供 typeset 命令是为了与 Korn shell 兼容。它是 declare 内建命令的同义词。

译者注typesetdeclare 功能完全一致,仅选项串的写法略有差异(本版手册中二者选项列表不同,实际行为相同),Korn shell 用户迁移到 Bash 时习惯用它。

ulimit

ulimit [-HS] -a
ulimit [-HS] [-bcdefiklmnpqrstuvxPRT] [LIMIT]

在允许此类控制的系统上,ulimit 提供对 Shell 及其启动的进程可用资源的控制。若给出了选项,其解释如下:

选项 含义
-S 改变并报告与资源关联的软限制(soft limit)。
-H 改变并报告与资源关联的硬限制(hard limit)。
-a 报告所有当前限制;不设置任何限制。
-b 套接字缓冲区的最大大小。
-c 创建的 core 文件的最大大小。
-d 进程数据段的最大大小。
-e 最大调度优先级("nice")。
-f Shell 及其子进程写入的文件的最大大小。
-i 挂起信号的最大数量。
-k 可分配的 kqueue 的最大数量。
-l 可锁定到内存中的最大大小。
-m 最大常驻集大小(许多系统不遵守此限制)。
-n 打开的文件描述符的最大数量(大多数系统不允许设置此值)。
-p 管道缓冲区大小。
-q POSIX 消息队列中的最大字节数。
-r 最大实时调度优先级。
-s 最大栈大小。
-t 以秒为单位的最大 CPU 时间量。
-u 单个用户可用的最大进程数。
-v Shell(在某些系统上还包括其子进程)可用的最大虚拟内存量。
-x 文件锁的最大数量。
-P 伪终端(pseudoterminal)的最大数量。
-R 实时进程在阻塞前可运行的最大时间,以微秒为单位。
-T 线程的最大数量。

若提供了 LIMIT 且未使用 -a 选项,LIMIT 是指定资源的新值。特殊的 LIMIT 值 hardsoftunlimited 分别表示当前硬限制、当前软限制和无限制。硬限制一旦被非 root 用户设置就不能再提高;软限制可以提高到硬限制的值为止。否则,ulimit 打印指定资源的当前软限制值,除非提供了 -H 选项。当指定了多个资源时,会在值之前打印限制名称和单位(如适用)。设置新限制时,若既未提供 -H 也未提供 -Sulimit 同时设置硬限制和软限制。若未提供任何选项,则假定为 -f

数值以 1024 字节为增量,例外如下:-t 以秒为单位;-R 以微秒为单位;-p 以 512 字节块为单位;-P-T-b-k-n-u 是不缩放的原始数值;在 POSIX 模式(参见第 6.11.2 节(Bash 的 POSIX 模式))下,-c-f 以 512 字节为增量。

除非提供了无效的选项或参数,或设置新限制时出错,否则返回状态为零。

译者注:软限制是当前生效的值,进程可自行把它提高到硬限制以内;硬限制一旦由非 root 用户设定就不可再提高(只能 root 降低)。不带资源选项时默认是 -f(文件大小,单位 1024 字节块)。查询示例(数值随系统而异):

$ ulimit -n
524288
$ ulimit -f
unlimited

设置示例:ulimit -t 60 把 CPU 时间软限制设为 60 秒;ulimit -t unlimited 恢复。注意在容器或沙箱环境中,设置某些限制可能被内核拒绝。

unalias

unalias [-a] [NAME ... ]

从别名列表中移除每个 NAME。若提供了 -a,移除所有别名。除非提供的某个 NAME 不是已定义的别名,否则返回值为真。别名在第 6.6 节(别名)中描述。

译者注unalias -a 清空全部别名,常用于清理启动文件或交互式配置中定义过的别名。与别名展开时机一致(见上文 alias 的译者注),unalias 所在行本身不受影响,从下一行起才生效:

$ alias x='echo hi'
$ x
hi
$ unalias x
$ type -t x; echo $?
1

4.3 修改 Shell 行为

4.3.1 set 内建命令

这个内建命令非常复杂,值得单独用一节来介绍。set 允许你更改 Shell 选项的值、设置位置参数,或者显示 Shell 变量的名称和值。

set [-abefhkmnptuvxBCEHPT] [-o OPTION-NAME] [--] [-] [ARGUMENT ...]
set [+abefhkmnptuvxBCEHPT] [+o OPTION-NAME] [--] [-] [ARGUMENT ...]
set -o
set +o

如果不提供任何选项或参数,set 会按当前区域设置(locale)的排序顺序,显示所有 Shell 变量和函数的名称与值,其格式可重新用作输入,用来设置或重置当前已设置的变量。只读变量无法被重置。在 POSIX 模式下,只列出 Shell 变量。

译者注set 不带参数时的输出非常庞大(包含全部 Shell 变量、数组与函数定义,还可能包含 SHELLOPTSBASHOPTS 等只读变量),所以「重新用作输入」通常指把输出存盘、之后再载入来恢复变量——重新给只读变量赋值时会报错,但不会中断载入过程。实测:

# 译者注示例:set 的输出可保存下来,稍后重新载入以恢复变量
$ bash -c 'x=1; set > /tmp/jc-state; grep "^x=" /tmp/jc-state; unset x; . /tmp/jc-state 2>/dev/null; echo "x=$x"'
x=1
x=1

当提供选项时,它们用于设置或取消设置 Shell 属性。选项处理之后剩余的任何参数都会替换位置参数。

选项(如果指定)的含义如下:

-a

每个被创建或修改的变量或函数都会被赋予导出(export)属性,并标记为导出到后续命令的环境中。

译者注-a(allexport)相当于给此后每个变量赋值都自动加上 export。它只影响赋值发生之后创建的变量,不会回头导出现有的变量;脚本开头 set -a、结尾 set +a 的写法可让中途所有新变量都进入环境。实测:

# 译者注示例:set -a 使之后赋值的变量自动具有导出属性
$ set -a; x=1; env | grep '^x='
x=1

-b

使已终止的后台作业的状态被立即报告,而不是等到打印下一个主提示符之前(或在某些情况下,等到前台命令退出时)才报告。此选项仅在启用作业控制时才有效。

-e

如果某个管道(参见第 3.2.3 节(管道))返回非零状态,则立即退出;该管道可能由单个简单命令(参见第 3.2.2 节(简单命令))、一个命令列表(参见第 3.2.4 节(命令列表))或一个复合命令(参见第 3.2.5 节(复合命令))构成。如果失败的命令属于下列情形,Shell 不会退出:它是紧跟在 whileuntil 保留字之后的命令列表的一部分;它是 if 语句中测试的一部分;它是 &&|| 列表中除紧跟最后的 &&|| 之后的命令之外的任何命令;它是管道中除最后一个命令之外的任何命令(是否属于此情形取决于 pipefail Shell 选项的状态);或者该命令的返回状态正被 ! 取反。如果某个复合命令(子 Shell 除外)由于某条命令在 -e 被忽略期间失败而返回非零状态,Shell 也不会退出。如果设置了 ERR 陷阱(trap),它会在 Shell 退出之前执行。

译者注-e(errexit)的「豁免上下文」是最容易踩坑的地方,务必记住:失败发生在 while/until 的条件里、if 的测试里、&&|| 列表里(紧跟最后的 &&/|| 之后的命令除外)、管道中除最后一个以外的命令里,或者被 ! 取反时,Shell 都不会退出。实测:

# 译者注示例:-e 的豁免上下文(后续命令都执行了,说明没有退出)
$ bash -c 'set -e; false; echo 未执行到'
$ echo $?
1
$ bash -c 'set -e; while false; do echo 循环体; done; echo 循环之后'
循环之后
$ bash -c 'set -e; if false; then :; fi; echo if之后'
if之后
$ bash -c 'set -e; false || echo 被救回; ! false; echo 全部完成'
被救回
全部完成

译者注-epipefail 的相互作用值得单独演示:默认 pipefail 关闭时,管道退出状态取最后一个命令的,false | true 的状态是 0,-e 不会触发;开启 pipefail 后管道状态变为「最后一个以非零状态退出的命令」的状态,-e 就会因此退出——即使失败的命令位于管道前面。实测:

# 译者注示例:pipefail 对 -e 的影响(set -o pipefail 后 false | true 也会触发退出)
$ bash -c 'set -o pipefail; false | true; echo "管道状态=$?"'
管道状态=1
$ bash -c 'set -e; false | true; echo 未执行到'
未执行到
$ bash -c 'set -e -o pipefail; false | true; echo 未执行到'
$ echo $?
1

此选项分别作用于 Shell 环境和每个子 Shell 环境(参见第 3.7.3 节(命令执行环境)),并可能导致子 Shell 在尚未执行完子 Shell 内的所有命令时便提前退出。

译者注:子 Shell 里的失败会让子 Shell 提前退出,进而使整个命令返回非零。注意这与命令替换 $(...) 不同:命令替换默认在子 Shell 环境中取消 -e(见下文的 inherit_errexit 选项)。实测:

# 译者注示例:-e 使子 Shell 提前退出
$ bash -c 'set -e; (echo 第一条; false; echo 子Shell里第三条); echo 父Shell继续'
第一条
$ echo $?
1

如果某个复合命令或 Shell 函数在 -e 被忽略的上下文中执行,那么无论是否设置了 -e、也无论其中是否有命令返回失败状态,复合命令或函数体内执行的任何命令都不会受 -e 设置的影响。如果某个复合命令或 Shell 函数在 -e 被忽略的上下文中执行时设置了 -e,该设置要等到这个复合命令、或包含该函数调用的命令执行完毕之后才会生效。

-f

禁用文件名展开(通配符展开)。

译者注-f(noglob)关闭文件名展开后,*.txt 之类的模式会按字面传给命令;用 set +f(或 set +o noglob)恢复。通配符偶尔会意外匹配到不想匹配的文件,在脚本开头 set -f 是常见的防御性写法。实测:

# 译者注示例:set -f 使通配符失效,模式按字面输出
$ cd /tmp/jc-glob
$ echo *.txt
root.txt top.txt
$ set -f
$ echo *.txt
*.txt

-h

在查找命令执行时即定位并记住(哈希)命令。此选项默认启用。

-k

所有赋值语句形式的参数都会放入命令的环境中,而不仅仅是位于命令名之前的那些。

译者注:默认只有命令名之前name=value 才算赋值语句;-k(keyword)让命令名之后的也算。注意这些赋值只进入该条命令的环境,不会改变当前 Shell 的变量——因此要用外部命令(如 sh)才能观察到。实测:

# 译者注示例:set -k 把命令名之后的赋值也放入该命令的环境
$ set -k
$ sh -c 'echo "x=$x"' hello x=1
x=1
$ set +k
$ sh -c 'echo "x=$x"' hello x=1
x=

-m

启用作业控制(参见第 7 章(作业控制))。所有进程都在独立的进程组中运行。后台作业完成时,Shell 会打印一行包含其退出状态的信息。

-n

读取命令但不执行。可用于检查脚本的语法错误。交互式 Shell 会忽略此选项。

译者注:给脚本做语法检查的常用命令是 bash -n script.sh;注意 -n 只检查语法,不检查运行时问题(如命令不存在、变量未定义)。实测:

# 译者注示例:bash -n 只做语法检查,不执行任何命令
$ printf 'echo 会执行吗\necho )\n' > /tmp/jc-syn.sh
$ LC_ALL=C bash -n /tmp/jc-syn.sh
/tmp/jc-syn.sh: line 2: syntax error near unexpected token `)'
/tmp/jc-syn.sh: line 2: `echo )'
$ echo $?
2

-o OPTION-NAME

设置与 OPTION-NAME 对应的选项。如果 -o 不带 OPTION-NAME,set 会打印当前的 Shell 选项设置;如果 +o 不带 OPTION-NAME,set 会在标准输出上打印一系列 set 命令,用于重建当前的选项设置。合法的选项名称如下:

译者注set -o(不带参数)打印两列选项清单;set +o(不带参数)打印一组可以直接重新执行的 set 命令,常用于把当前选项状态存盘、之后在子 Shell 或脚本里恢复。实测:

# 译者注示例:set -o 显示当前设置;set +o 输出可重新执行的命令
$ set -o | grep pipefail
pipefail        off
$ set +o | grep pipefail
set +o pipefail
  • allexport:与 -a 相同。
  • braceexpand:与 -B 相同。
  • emacs:使用 emacs 风格的行编辑界面(参见第 8 章(命令行编辑))。这也会影响 read -e 所使用的编辑界面。
  • errexit:与 -e 相同。
  • errtrace:与 -E 相同。
  • functrace:与 -T 相同。
  • hashall:与 -h 相同。
  • histexpand:与 -H 相同。
  • history:启用命令历史记录,如第 9.1 节(Bash 的历史记录设施)所述。在交互式 Shell 中此选项默认开启。
  • ignoreeof:交互式 Shell 在读到 EOF 时不会退出。
  • keyword:与 -k 相同。
  • monitor:与 -m 相同。
  • noclobber:与 -C 相同。
  • noexec:与 -n 相同。
  • noglob:与 -f 相同。
  • nolog:目前被忽略(不产生任何效果)。
  • notify:与 -b 相同。
  • nounset:与 -u 相同。
  • onecmd:与 -t 相同。
  • physical:与 -P 相同。
  • pipefail:如果设置,管道的返回值为最后一个(最右边的)以非零状态退出的命令的值;如果管道中的所有命令都成功退出,则为零。此选项默认关闭。
  • posix:启用 POSIX 模式;当 Bash 的默认行为与 POSIX 标准不一致时,改变其行为以符合标准(参见第 6.11.2 节(Bash 的 POSIX 模式))。这是为了让 Bash 表现为该标准的严格超集。
  • privileged:与 -p 相同。
  • verbose:与 -v 相同。
  • vi:使用 vi 风格的行编辑界面。这也会影响 read -e 所使用的编辑界面。
  • xtrace:与 -x 相同。

译者注set -o posixbash --posix 等价。开启 POSIX 模式会改变许多行为(差异清单见第 6.11.2 节(Bash 的 POSIX 模式)),还会自动启用 inherit_errexit,并让第 4.4 节(特殊内建命令)所述的三个差异生效。

-p

开启特权模式。在此模式下,不处理 $BASH_ENV$ENV 文件,Shell 函数不从环境中继承,并且环境中出现的 SHELLOPTSBASHOPTSCDPATHGLOBIGNORE 变量会被忽略。如果 Shell 以有效用户(组)ID 不等于真实用户(组)ID 的方式启动,且未提供 -p 选项,则会采取上述行动,并将有效用户 ID 设置为真实用户 ID。如果在启动时提供了 -p 选项,有效用户 ID 不会被重置。关闭此选项会使有效用户 ID 和组 ID 被设置为真实用户 ID 和组 ID。

译者注-p(privileged)与 setuid/setgid 环境的安全有关:以有效 UID 不等于真实 UID 的方式启动(如经 setuid 二进制调用)时,Bash 默认拒绝处理 $BASH_ENV/$ENV 启动文件、不继承环境中的 Shell 函数、忽略环境中的 SHELLOPTS/BASHOPTS/CDPATH/GLOBIGNORE,并把有效 UID 重置为真实 UID,防止环境变量劫持;指定 -p 则保留有效 UID。

-r

启用受限 Shell 模式(参见第 6.10 节(受限 Shell))。此选项一旦设置便无法取消。

-t

读取并执行一条命令后退出。

-u

在执行参数展开时,将未设置的变量和参数(特殊参数 @*、以及用 @* 作下标的数组变量除外)视为错误。错误消息会写入标准错误,非交互式 Shell 会退出。

译者注-u(nounset)在脚本里引用未定义变量时直接报错退出,能尽早暴露变量名的拼写错误。注意 ${VAR:-默认值} 这类「带默认值」的写法不受影响,$@$*${arr[@]} 即使为空也不会报错。实测(本机 Bash 5.3 的退出码为 127):

# 译者注示例:set -u 下引用未定义变量会报错并退出
$ LC_ALL=C bash -c 'set -u; echo "$UNDEFINED"'
bash: UNDEFINED: unbound variable
$ echo $?
127
$ bash -c 'set -u; echo "${UNDEFINED:-默认值}"'
默认值

-v

在读取 Shell 输入行时将其打印到标准错误。

-x

在简单命令、for 命令、case 命令、select 命令和算术 for 命令及其参数或相关单词列表完成展开之后、执行之前,将其打印到标准错误。Shell 会在命令及其展开后的参数之前打印 PS4 变量的展开值。

译者注-x(xtrace)是调试脚本的第一利器:它在每条命令展开后、执行前,把命令及其参数打印到标准错误,前缀为 PS4 的展开值(默认 + )。把 PS4 改成带文件与行号的形式,可以精确定位出错位置。实测:

# 译者注示例:set -x 打印展开后的命令(前缀为 PS4 的展开值)
$ bash -c 'set -x; x=42; echo "$x"'
+ x=42
+ echo 42
42
# 译者注示例:用 PS4 显示执行到哪个文件的哪一行
$ printf 'echo hi\necho bye\n' > /tmp/jc-trace.sh
$ PS4='+ ${BASH_SOURCE}:${LINENO}: ' bash -x /tmp/jc-trace.sh
+ /tmp/jc-trace.sh:1: echo hi
hi
+ /tmp/jc-trace.sh:2: echo bye
bye

-B

Shell 会执行花括号展开(参见第 3.5.1 节(花括号展开))。此选项默认开启。

-C

阻止使用 >>&<> 的输出重定向覆盖已存在的文件。使用重定向操作符 >| 代替 > 可以覆盖此限制,强制创建输出文件。

译者注-C(noclobber)开启后,覆盖失败返回非零状态,原文件保持不变;>| 可强制覆盖,>> 追加不受影响。用 set +C(或 set +o noclobber)恢复。实测:

# 译者注示例:set -C 阻止覆盖重定向;>| 强制覆盖;>> 不受影响
$ LC_ALL=C bash -c 'echo 旧内容 > /tmp/jc-nc; set -C; echo 新内容 > /tmp/jc-nc'
bash: line 1: /tmp/jc-nc: cannot overwrite existing file
$ LC_ALL=C bash -c 'echo 旧内容 > /tmp/jc-nc; set -C; echo 新内容 >| /tmp/jc-nc; cat /tmp/jc-nc'
新内容
$ LC_ALL=C bash -c 'echo 旧内容 > /tmp/jc-nc; set -C; echo 新内容 >> /tmp/jc-nc; cat /tmp/jc-nc'
旧内容
新内容

-E

如果设置,任何 ERR 陷阱都会被 Shell 函数、命令替换以及在子 Shell 环境中执行的命令继承。通常情况下,ERR 陷阱不会在这些情况下被继承。

-H

启用 ! 风格的历史替换(参见第 9.3 节(历史展开))。此选项在交互式 Shell 中默认开启。

-P

如果设置,Bash 在执行 cd 等更改当前目录的命令时,不解析符号链接,而是使用物理目录结构。默认情况下,Bash 在执行更改当前目录的命令时遵循逻辑目录链。

例如,如果 /usr/sys 是指向 /usr/local/sys 的符号链接,那么:

$ cd /usr/sys; echo $PWD
/usr/sys
$ cd ..; pwd
/usr

如果开启了 set -P,那么:

$ cd /usr/sys; echo $PWD
/usr/local/sys
$ cd ..; pwd
/usr/local

译者注:原文的例子假设系统上存在 /usr/sys 这样的符号链接;同样的效果可以在 /tmp 下复现(本机实测输出)。注意 PWD 变量显示逻辑路径还是物理路径,由 -L/-P 决定,与 pwd -L/pwd -P 一致。

# 译者注示例:set -P 让 cd 使用物理目录而不是逻辑(符号链接)路径
$ mkdir -p /tmp/jc-phys/local/sys
$ ln -sfn /tmp/jc-phys/local/sys /tmp/jc-phys/sys
$ cd /tmp/jc-phys/sys; echo "$PWD"; cd ..; pwd
/tmp/jc-phys/sys
/tmp/jc-phys
$ set -P
$ cd /tmp/jc-phys/sys; echo "$PWD"; cd ..; pwd
/tmp/jc-phys/local/sys
/tmp/jc-phys/local

-T

如果设置,任何 DEBUGRETURN 陷阱都会被 Shell 函数、命令替换以及在子 Shell 环境中执行的命令继承。通常情况下,DEBUGRETURN 陷阱不会在这些情况下被继承。

--

如果此选项后没有参数,则取消设置位置参数。否则,位置参数被设置为 ARGUMENTS,即使其中一些以 - 开头。

译者注:脚本里常用 set -- 清空位置参数、或 set -- "$@" 额外参数 给位置参数追加内容;-- 之后的内容即使以 - 开头也会被当作位置参数。-(单独一个减号)与 -- 不同:- 表示选项结束,把剩余参数赋给位置参数,并关闭 -x-v 选项;若没有参数,位置参数保持不变。实测:

# 译者注示例:set -- 重新设置位置参数,$# 随之更新
$ set -- a b c
$ echo "$1/$2/$3(共 $# 个)"
a/b/c(共 3 个)

-

表示选项的结束,并将所有剩余的 ARGUMENTS 赋给位置参数。-x-v 选项被关闭。如果没有参数,位置参数保持不变。

使用 + 而不是 - 会关闭这些选项。这些选项也可以在调用 Shell 时使用。当前启用的选项集可以在 $- 中找到。

译者注$-(特殊参数)保存当前生效的选项字母,脚本里常用来按需检查选项状态(如 case $- in *f*) ...;; esac)。实测脚本中默认只有 hB

# 译者注示例:$- 显示当前启用的选项字母
$ printf 'echo "$-"\nset -f\necho "$-"\n' > /tmp/jc-opts.sh
$ bash /tmp/jc-opts.sh
hB
fhB

剩余的 N 个 ARGUMENTS 是位置参数,按顺序赋给 $1$2、……、$N。特殊参数 # 被设置为 N。

除非提供了无效的选项,否则返回状态始终为零。

4.3.2 shopt 内建命令

这个内建命令允许你更改附加的可选 Shell 行为。

shopt [-pqsu] [-o] [OPTNAME ...]

切换控制可选 Shell 行为的各项设置的取值。这些设置可以是下面列出的选项,也可以(在使用 -o 选项时)是 set 内建命令的 -o 选项所提供的那些选项(参见第 4.3.1 节(set 内建命令))。

不带任何选项,或者使用 -p 选项时,显示所有可设置选项的列表,并标明每个选项是否已设置;如果提供了任何 OPTNAME,则输出仅限于这些选项。-p 选项的输出形式可以重新用作输入。

其他选项的含义如下:

-s

启用(设置)每个 OPTNAME。

-u

禁用(取消设置)每个 OPTNAME。

-q

抑制正常输出;返回状态指示 OPTNAME 是已设置还是未设置。如果 -q 带有多个 OPTNAME 参数,则当所有 OPTNAME 都已启用时返回状态为零,否则为非零。

-o

将 OPTNAME 的值限制为 set 内建命令的 -o 选项所定义的那些选项(参见第 4.3.1 节(set 内建命令))。

如果 -s-u 不带任何 OPTNAME 参数,shopt 分别只显示已设置或未设置的那些选项。

除非另有说明,shopt 选项默认是禁用(关闭)的。

列出选项时,如果所有 OPTNAME 都已启用,返回状态为零,否则为非零。设置或取消设置选项时,除非某个 OPTNAME 不是合法的 Shell 选项,否则返回状态为零。

译者注shopt -p 的输出可直接作为 shopt 的输入(例如 shopt -p > /tmp/opts,之后用 . /tmp/opts 恢复);-q 不输出任何内容,只以返回状态表示开关,适合写进条件判断。另外,shopt -oset -o/set +o 控制的是同一组选项,可以互换使用。实测:

# 译者注示例:shopt -q 配合条件判断;shopt -p 输出可重新执行的命令
$ shopt -q globstar && echo 已开启 || echo 未开启
未开启
$ shopt -s globstar
$ shopt -q globstar && echo 已开启 || echo 未开启
已开启
$ shopt -p globstar
shopt -s globstar

shopt 选项列表如下:

array_expand_once

如果设置,Shell 在算术表达式求值期间、执行可进行变量赋值的内建命令期间、以及执行进行数组解引用(dereferencing)的内建命令期间,会抑制对关联数组和索引数组下标的多次求值。

assoc_expand_once

已弃用;是 array_expand_once 的同义词。

autocd

如果设置,作为命令名出现的目录名会像作为 cd 命令的参数一样被处理。此选项仅由交互式 Shell 使用。

译者注autocd 只在交互式 Shell 中生效,是 zsh 老用户的习惯:直接输入目录名就相当于 cd,在脚本里无效。实测(bash -ic 模拟交互式 Shell):

# 译者注示例:autocd:直接输入目录名即切换目录(仅交互式 Shell)
$ bash -ic 'shopt -s autocd; cd /tmp/jc-glob; /tmp/jc-glob; pwd' 2>/dev/null
/tmp/jc-glob

bash_source_fullpath

如果设置,加入 BASH_SOURCE 数组变量的文件名会被转换为完整路径名(参见第 5.2 节(Bash 变量))。

cdable_vars

如果设置,cd 内建命令的一个不是目录的参数会被当作变量的名称,该变量的值就是要切换到的目录。

译者注cdable_vars 允许 cd 变量名 进入「变量的值」所指向的目录,可以当快捷方式用。实测(本机 Bash 5.3 中,经此方式 cd 成功时会把实际进入的目录打印到标准输出,因此示例中出现了两行相同的输出):

# 译者注示例:cdable_vars:cd 的参数不是目录时,把它当作值为目录路径的变量名
$ bash -c 'shopt -s cdable_vars; mydir=/tmp/jc-glob; cd mydir; pwd'
/tmp/jc-glob
/tmp/jc-glob

cdspell

如果设置,cd 命令会尝试纠正目录名组成成分中轻微的拼写错误。轻微错误包括字符颠倒、缺少一个字符和多余一个字符。如果 cd 纠正了目录名,它会打印纠正后的文件名,然后命令继续执行。此选项仅由交互式 Shell 使用。

译者注cdspell 纠正后打印的纠正结果(示例中的 /tmp/jc-glob)就是上面所说的「打印纠正后的文件名」。实测:

# 译者注示例:cdspell 自动纠正目录名的轻微拼写错误(仅交互式 Shell)
$ bash -ic 'shopt -s cdspell; cd /tmp/jc-globl' 2>/dev/null
/tmp/jc-glob

checkhash

如果设置,Bash 在尝试执行哈希表中找到的命令之前,会先检查该命令是否仍然存在。如果哈希过的命令已不存在,Bash 会进行常规的路径搜索。

译者注checkhash 的差别体现在失败信息上:未开启时,Bash 仍会尝试执行哈希表中已失效的路径(报 No such file or directory);开启后重新做 PATH 搜索(报 command not found)。实测:

# 译者注示例:checkhash:被哈希的命令删除后,重新走 PATH 搜索
$ mkdir -p /tmp/jc-ch
$ printf '#!/bin/sh\necho ran\n' > /tmp/jc-ch/chcmd; chmod +x /tmp/jc-ch/chcmd
$ LC_ALL=C bash -c 'PATH=/tmp/jc-ch:$PATH; chcmd; rm /tmp/jc-ch/chcmd; chcmd' 2>&1
ran
bash: line 1: /tmp/jc-ch/chcmd: No such file or directory
$ printf '#!/bin/sh\necho ran\n' > /tmp/jc-ch/chcmd; chmod +x /tmp/jc-ch/chcmd
$ LC_ALL=C bash -c 'shopt -s checkhash; PATH=/tmp/jc-ch:$PATH; chcmd; rm /tmp/jc-ch/chcmd; chcmd' 2>&1
ran
bash: line 1: chcmd: command not found

checkjobs

如果设置,Bash 在退出交互式 Shell 之前,会列出任何已停止和正在运行的作业的状态。如果有任何作业正在运行,Bash 会推迟退出,直到在没有执行任何中间命令的情况下再次尝试退出(参见第 7 章(作业控制))。如果有任何作业处于停止状态,Shell 总是推迟退出。

checkwinsize

如果设置,Bash 会在每个外部(非内建)命令执行后检查窗口大小,并在必要时更新 LINESCOLUMNS 的值,使用的是与标准错误相关联的文件描述符(如果它是一个终端)。此选项默认启用。

cmdhist

如果设置,Bash 会尝试把多行命令的所有行保存在同一条历史记录条目中。这样便于重新编辑多行命令。此选项默认启用,但只有在命令历史记录已启用时才会起作用(参见第 9.1 节(Bash 的历史记录设施))。

compat31
compat32
compat40
compat41
compat42
compat43
compat44

这些选项控制 Shell 兼容模式(参见第 6.12 节(Shell 兼容模式))的某些方面。

complete_fullquote

如果设置,Bash 在执行补全时,会对文件名和目录名中的所有 Shell 元字符加引号。如果未设置,当这些元字符出现在待补全单词中的 Shell 变量引用里时,Bash 会把美元符号等元字符从「补全文件名时需要加引号的字符」集合中移除。这意味着,展开为目录的变量名中的美元符号不会被加引号;但文件名中出现的任何美元符号也不会被加引号。仅当 Bash 使用反斜杠为补全的文件名加引号时,此选项才起作用。此变量默认设置,这也是 4.2 及之前版本中 Bash 的默认行为。

direxpand

如果设置,Bash 在执行文件名补全时,会用单词展开的结果替换目录名。这会改变 Readline 编辑缓冲区的内容。如果未设置,Bash 会尽量保留用户输入的内容。

dirspell

如果设置,Bash 在单词补全期间,如果最初提供的目录名不存在,会尝试对目录名进行拼写纠正。

dotglob

如果设置,Bash 会在文件名展开的结果中包含以 . 开头的文件名。即使设置了 dotglob,文件名 ... 也必须始终显式匹配。

译者注dotglob 只影响模式匹配:开启后 * 能匹配 .hidden 之类的文件;但 echo * 永远不会包含 ... 这两个目录项,它们只能被显式写出来匹配。实测:

# 译者注示例:dotglob 让 * 也能匹配以 . 开头的文件
$ cd /tmp/jc-glob; touch .hidden
$ echo *
a root.txt top.txt
$ shopt -s dotglob
$ echo *
a .hidden root.txt top.txt

execfail

如果设置,非交互式 Shell 在无法执行作为 exec 内建命令参数给出的文件时不会退出。交互式 Shell 在 exec 失败时不会退出。

译者注:默认(execfail 关闭)时,非交互式 Shell 执行 exec 失败会直接退出,后面的命令不会执行;开启后继续执行。交互式 Shell 无论开关都会继续。实测:

# 译者注示例:execfail 让非交互式 Shell 在 exec 失败后继续执行
$ LC_ALL=C bash -c 'exec /no/such/file; echo 未执行到'
bash: line 1: /no/such/file: No such file or directory
$ LC_ALL=C bash -c 'shopt -s execfail; exec /no/such/file; echo survived'
bash: line 1: /no/such/file: No such file or directory
survived

expand_aliases

如果设置,别名会按下文第 6.6 节(别名)所述进行展开。此选项在交互式 Shell 中默认启用。

译者注:交互式 Shell 默认开启别名展开,但非交互式(脚本)默认关闭——脚本里想用别名,必须先 shopt -s expand_aliases。还有一个易踩的坑:别名在解析期展开,bash -c '…' 会把整串命令先解析完再执行,所以同一字符串里「先定义别名再用」不生效;写成脚本文件(逐行解析)则可以。实测:

# 译者注示例:脚本中开启 expand_aliases 后别名可用
$ printf 'shopt -s expand_aliases\nalias greet="echo hello"\ngreet\n' > /tmp/jc-al.sh
$ bash /tmp/jc-al.sh
hello
# 译者注示例:bash -c 整串先解析后执行,同串内定义后再用别名不生效
$ LC_ALL=C bash -c 'shopt -s expand_aliases; alias g="echo hi"; g'
bash: g: command not found

extdebug

如果在调用 Shell 时或在一个 Shell 启动文件中设置,则安排在 Shell 启动之前执行调试器配置文件,与 --debugger 选项相同。如果在调用之后设置,则启用为调试器使用而设计的行为:

  1. declare 内建命令的 -F 选项(参见第 4.2 节(Bash 内建命令))会显示与每个作为参数给出的函数名相对应的源文件名和行号。
  2. 如果 DEBUG 陷阱运行的命令返回非零值,则跳过下一条命令,不予执行。
  3. 如果 DEBUG 陷阱运行的命令返回 2,并且 Shell 正在子例程(Shell 函数或由 .source 内建命令执行的 Shell 脚本)中执行,则 Shell 模拟一次对 return 的调用。
  4. BASH_ARGCBASH_ARGV 会按照它们各自描述的方式更新(参见第 5.2 节(Bash 变量))。
  5. 启用函数跟踪:命令替换、Shell 函数以及用 ( COMMAND ) 调用的子 Shell 会继承 DEBUGRETURN 陷阱。
  6. 启用错误跟踪:命令替换、Shell 函数以及用 ( COMMAND ) 调用的子 Shell 会继承 ERR 陷阱。

extglob

如果设置,启用上文第 3.5.8.1 节(模式匹配)中描述的扩展模式匹配功能。

译者注extglob 提供 ?(模式)*(模式)+(模式)@(模式)!(模式) 五种扩展模式。注意解析器始终认得 !(…) 这种写法:extglob 未开启时,它会被当作语法错误而不是按字面处理。实测:

# 译者注示例:extglob 未开启时 !(…) 是语法错误;开启后表示「不匹配」
$ LC_ALL=C bash -c 'cd /tmp/jc-glob; echo !(r*).txt'
bash: -c: line 1: syntax error near unexpected token `('
bash: -c: line 1: `cd /tmp/jc-glob; echo !(r*).txt'
$ printf 'shopt -s extglob\necho !(r*).txt\n' > /tmp/jc-extglob.sh
$ (cd /tmp/jc-glob && bash /tmp/jc-extglob.sh)
top.txt

extquote

如果设置,在双引号括起的 ${PARAMETER} 展开内部执行 $'STRING'$"STRING" 引号处理。此选项默认启用。

failglob

如果设置,在文件名展开期间与文件名不匹配的模式会产生展开错误。

force_fignore

如果设置,FIGNORE Shell 变量指定的后缀会使单词在单词补全时被忽略,即使这些被忽略的单词是唯一可能的补全。FIGNORE 的描述参见第 5.2 节(Bash 变量)。此选项默认启用。

globasciiranges

如果设置,模式匹配的方括号表达式中使用的范围表达式(参见第 3.5.8.1 节(模式匹配))在进行比较时,表现得如同处于传统的 C 区域设置(locale)中。也就是说,模式匹配不考虑当前区域设置的排序(collating)顺序,因此 b 不会排在 AB 之间,而大写与小写 ASCII 字符会排列在一起。

译者注:该选项只影响方括号表达式中的范围(如 [a-z]):开启后按 C 区域设置的 ASCII 码顺序比较,[a-z] 不包含大写字母;关闭(默认)时按当前区域设置的排序序列比较,在不少区域设置下 [a-z] 会把大写字母也包含进去。对单字符形式的模式(非范围形式)没有影响。

globskipdots

如果设置,文件名展开永远不会匹配文件名 ...,即使模式以 . 开头。此选项默认启用。

globstar

如果设置,在文件名展开上下文中使用的模式 ** 会匹配所有文件和零个或多个目录及子目录。如果该模式后跟 /,则只匹配目录和子目录。

译者注globstar** 是递归通配:**/*.txt 匹配任意层级目录下的 *.txt** 后跟 / 时只匹配目录(含子目录)。注意 ** 仍受 dotglobglobskipdots 等选项约束,不会匹配 ...。实测:

# 译者注示例:globstar 让 ** 递归匹配任意层级的文件与目录
$ bash -c 'cd /tmp/jc-glob; shopt -s globstar; echo **/*.txt'
a/b/f.txt a/f2.txt root.txt top.txt
$ bash -c 'cd /tmp/jc-glob; shopt -s globstar; echo **/'
a/ a/b/

gnu_errfmt

如果设置,Shell 错误消息按照标准的 GNU 错误消息格式书写。

histappend

如果设置,Shell 退出时,历史列表会追加到 HISTFILE 变量值所指定的文件中,而不是覆盖该文件。

译者注:默认行为是 Shell 退出时把历史列表覆盖写回 HISTFILE,同时打开多个会话时会互相覆盖;histappend 改为追加(不少发行版在启动文件中默认开启它)。HISTFILEHISTSIZEHISTFILESIZE 等变量参见第 5.2 节(Bash 变量)。

histreedit

如果设置,并且正在使用 Readline,用户在历史替换失败后会有机会重新编辑。

histverify

如果设置,并且正在使用 Readline,历史替换的结果不会立即传给 Shell 解析器,而是将结果行载入 Readline 编辑缓冲区,允许进一步修改。

hostcomplete

如果设置,并且正在使用 Readline,Bash 在补全包含 @ 的单词时会尝试进行主机名补全(参见第 8.4.6 节(让 Readline 替你输入))。此选项默认启用。

huponexit

如果设置,交互式登录 Shell 退出时,Bash 会向所有作业发送 SIGHUP(参见第 3.7.6 节(信号))。

译者注huponexit 只在交互式登录 Shell 退出时向作业发送 SIGHUP;配合 disownnohup 可以防止后台作业随会话结束被挂断。

inherit_errexit

如果设置,命令替换会继承 errexit 选项的值,而不是在子 Shell 环境中取消设置它。启用 POSIX 模式时此选项会被启用。

译者注:命令替换 $(…) 默认在子 Shell 环境中取消 errexit,因此 set -e$(false; echo x) 里的 echo 仍会执行、外层也照常继续;inherit_errexit 让命令替换继承 -e,失败会使替换立即以非零状态结束。开启 POSIX 模式时此选项自动启用。实测:

# 译者注示例:inherit_errexit 使命令替换继承 errexit(默认在命令替换中失效)
$ bash -c 'set -e; echo "$(false; echo 子命令执行了)"'
子命令执行了
$ bash -c 'set -e; shopt -s inherit_errexit; echo "$(false; echo 子命令执行了)"'
$ echo $?
0

interactive_comments

在交互式 Shell 中,以 # 开头的单词会使该单词及该行上其余的所有字符被忽略,如同在非交互式 Shell 中一样。此选项默认启用。

lastpipe

如果设置,并且作业控制未激活,Shell 会在当前 Shell 环境中运行管道中不在后台执行的最后一个命令。

译者注:默认管道中的命令在子 Shell 中执行,所以 printf x | read v 之后 $v 是空的;lastpipe 让最后一个命令在当前 Shell 中执行,变量得以保留。注意它仅在作业控制未激活时有效,交互式 Shell 默认不生效。实测:

# 译者注示例:lastpipe 使管道最后一条命令在当前 Shell 中执行
$ bash -c 'shopt -s lastpipe; x=之前; printf 之后 | read x; echo "x=$x"'
x=之后

lithist

如果启用,并且 cmdhist 选项已启用,多行命令在保存到历史中时,会在可能的情况下使用内嵌换行符,而不是使用分号作为分隔符。

localvar_inherit

如果设置,局部变量在赋予任何新值之前,会继承先前作用域中存在的同名变量的值和属性。nameref 属性不会被继承。

译者注:默认 local v 会以空值重新声明局部变量;localvar_inherit 让它在赋值前先继承外层同名变量的当前值与属性(nameref 属性除外)。实测:

# 译者注示例:localvar_inherit 使局部变量继承外层同名变量的值
$ bash -c 'f() { local v; echo "v=[$v]"; }; g() { local v=outer; f; }; g'
v=[]
$ bash -c 'f() { local v; echo "v=[$v]"; }; g() { local v=outer; f; }; shopt -s localvar_inherit; g'
v=[outer]

localvar_unset

如果设置,对先前函数作用域中的局部变量调用 unset 会将其标记为已取消设置,使得后续查找在该函数返回之前都认为它们未设置。这与在当前函数作用域中取消设置局部变量的行为相同。

login_shell

如果 Shell 作为登录 Shell 启动(参见第 6.1 节(调用 Bash)),Shell 会设置此选项。该值不可更改。

mailwarn

如果设置,并且 Bash 正在检查邮件的文件自上次检查以来被访问过,Bash 会显示消息 "The mail in MAILFILE has been read"(MAILFILE 中的邮件已被阅读)。

no_empty_cmd_completion

如果设置,并且正在使用 Readline,Bash 在空行上尝试补全时,不会在 PATH 中搜索可能的补全。

nocaseglob

如果设置,Bash 在执行文件名展开时,会以不区分大小写的方式匹配文件名。

nocasematch

如果设置,Bash 在执行 case[[ 条件命令(参见第 3.2.5.2 节(条件结构))的匹配时、执行模式替换单词展开时、或者作为可编程补全的一部分过滤可能的补全时,会以不区分大小写的方式匹配模式。

译者注nocasematch 只影响 case[[ ]] 的模式匹配,不影响文件名展开(文件名展开要区分大小写时用 nocaseglob)。实测:

# 译者注示例:nocasematch 使 [[ ]] 与 case 的模式匹配不区分大小写
$ [[ a == A ]] && echo 匹配 || echo 不匹配
不匹配
$ shopt -s nocasematch
$ [[ a == A ]] && echo 匹配 || echo 不匹配
匹配

noexpand_translation

如果设置,Bash 会用单引号而不是双引号括起 $"..." 引号处理的翻译结果。如果字符串未被翻译,此选项没有效果。

nullglob

如果设置,与任何文件都不匹配的文件名展开模式(参见第 3.5.8 节(文件名展开))会展开为空并被移除,而不是展开为它们自身。

译者注:默认情况下,没有匹配的模式会原样输出;nullglob 让这种模式展开成空(模式被移除,echo 输出为空行);failglob 则直接报错并返回非零。区别在于:nullglob 静默删除无匹配的模式,适合「有则用之、无则跳过」;failglob 强制暴露无匹配,适合要求模式必须匹配的场合。实测:

# 译者注示例:默认 / nullglob / failglob 三种行为的对比
$ cd /tmp/jc-glob
$ echo *.nomatch
*.nomatch
$ shopt -s nullglob
$ echo 匹配结果: *.nomatch
匹配结果:
$ shopt -u nullglob; shopt -s failglob
$ LC_ALL=C bash -c 'cd /tmp/jc-glob; shopt -s failglob; echo *.nomatch'
bash: line 1: no match: *.nomatch

patsub_replacement

如果设置,Bash 会把模式替换的替换字符串中出现的 & 展开为模式匹配到的文本,如上文第 3.5.3 节(Shell 参数展开)所述。此选项默认启用。

译者注:参数展开的模式替换 ${v//模式/替换串} 中,开启本选项后替换串里的 & 表示「被模式匹配到的文本」,连续写 && 可以实现重复。实测:

# 译者注示例:patsub_replacement 让替换串中的 & 代表被匹配的文本
$ v=hello
$ echo "${v//e/&&}"
h&&llo
$ shopt -s patsub_replacement
$ echo "${v//e/&&}"
heello

progcomp

如果设置,启用可编程补全设施(参见第 8.6 节(可编程补全))。此选项默认启用。

progcomp_alias

如果设置,并且可编程补全已启用,Bash 会把没有任何补全的命令名当作可能的别名,并尝试进行别名展开。如果它有别名,Bash 会使用展开别名后得到的命令词尝试可编程补全。

promptvars

如果设置,提示符字符串在按下文第 6.9 节(控制提示符)所述展开之后,还要进行参数展开、命令替换、算术展开和引号去除。此选项默认启用。

restricted_shell

如果 Shell 以受限模式启动(参见第 6.10 节(受限 Shell)),Shell 会设置此选项。该值不可更改。此选项在启动文件执行时不会被重置,这使得启动文件可以发现 Shell 是否受限。

shift_verbose

如果设置,shift 内建命令在移位次数超过位置参数的个数时打印错误消息。

译者注:默认 shift 越界时静默返回非零状态;开启后打印错误消息,便于脚本排错。实测:

# 译者注示例:shift_verbose 使 shift 越界时打印错误消息
$ bash -c 'set -- a; shift 5; echo "rc=$?"'
rc=1
$ LC_ALL=C bash -c 'set -- a; shopt -s shift_verbose; shift 5'
bash: line 1: shift: 5: shift count out of range

sourcepath

如果设置,.source)内建命令在未提供 -p 选项时,使用 PATH 的值来查找包含作为参数给出的文件的目录。此选项默认启用。

varredir_close

如果设置,对于使用 {varname} 重定向语法(参见第 3.6 节(重定向))分配的文件描述符,Shell 会在命令完成时自动关闭它们,而不是让它们保持打开。

译者注{varname} 重定向默认在命令结束后保持打开,便于后续命令继续使用;varredir_close 让 Shell 在命令完成时自动关闭它。实测(用 /proc/$$/fd/$fd 是否存在来判断描述符是否仍打开):

# 译者注示例:varredir_close 使 {varname} 重定向的 fd 在命令结束后自动关闭
$ bash -c 'echo hi {fd}>/dev/null; [ -e /proc/$$/fd/$fd ] && echo 仍打开 || echo 已关闭'
hi
仍打开
$ bash -c 'shopt -s varredir_close; echo hi {fd}>/dev/null; [ -e /proc/$$/fd/$fd ] && echo 仍打开 || echo 已关闭'
hi
已关闭

xpg_echo

如果设置,echo 内建命令默认展开反斜杠转义序列。如果 posix Shell 选项(参见第 4.3.1 节(set 内建命令))也已启用,echo 不解释任何选项。

译者注:默认 echo 不解释参数中的反斜杠序列(echo 'a\nb' 原样输出);xpg_echo 开启后默认解释 \n\t 等转义(相当于自动带上 -e)。需要可移植的输出时,建议显式使用 printf。实测:

# 译者注示例:xpg_echo 使 echo 默认解释反斜杠转义序列
$ echo 'a\nb'
a\nb
$ shopt -s xpg_echo
$ echo 'a\nb'
a
b

4.4 特殊内建命令

由于历史原因,POSIX 标准将几个内建命令归类为「特殊」(special)。当 Bash 在 POSIX 模式下执行时,特殊内建命令与其他内建命令在三个方面有所不同:

  1. 在命令查找过程中,特殊内建命令先于 Shell 函数被找到。
  2. 如果特殊内建命令返回错误状态,非交互式 Shell 会退出。
  3. 位于命令之前的赋值语句在该命令执行完毕后,仍然在 Shell 环境中保持生效。

译者注:三个差异都容易用实例观察。实测:

# 译者注示例(第 1 点):非 POSIX 模式下可以定义与特殊内建命令同名的函数并调用;POSIX 模式下甚至不允许定义
$ bash -c 'unset() { echo 我的函数; }; unset x; echo 完成'
我的函数
完成
$ LC_ALL=C bash -c 'set -o posix; unset() { echo 我的函数; }'
bash: line 1: `unset': is a special builtin
# 译者注示例(第 2 点):特殊内建命令出错时,非交互式 Shell 直接退出,后续命令不再执行
$ LC_ALL=C bash -c 'readonly v; unset v; echo 执行到这里'
bash: line 1: unset: v: cannot unset: readonly variable
执行到这里
$ LC_ALL=C bash -c 'set -o posix; readonly v; unset v; echo 执行到这里'
bash: line 1: unset: v: cannot unset: readonly variable
# 译者注示例(第 3 点):POSIX 模式下,特殊内建命令前的赋值在命令结束后仍然生效
$ bash -c 'x=5 :; echo "x=[$x]"'
x=[]
$ bash -c 'set -o posix; x=5 :; echo "x=[$x]"'
x=[5]

当 Bash 不在 POSIX 模式下执行时,这些内建命令的行为与其余 Bash 内建命令没有区别。Bash 的 POSIX 模式在第 6.11.2 节(Bash 的 POSIX 模式)中描述。

译者注:注意上面的对比都与「模式」相关:非 POSIX 模式下 unset: 等都是普通内建命令,所以函数可以覆盖、赋值不保留、出错不退出;切到 POSIX 模式后,第 1 至 3 条差异全部生效。其中第 3 点还有个经典对照:: 属于特殊内建命令而 true 不属于,所以即使都在 POSIX 模式下,x=5 : 之后 $x 是 5,x=5 true 之后 $x 仍是空。

以下是 POSIX 特殊内建命令(完整语法与说明参见第 4.1 节(Bourne Shell 内建命令)与第 4.2 节(Bash 内建命令)):

  • break:从 forwhileuntilselect 循环中退出(参见第 4.1 节)。
  • :(冒号,空命令):展开参数并执行重定向,除此之外不做任何事情,返回状态为零(参见第 4.1 节)。
  • .(句点):在当前 Shell 上下文中读取并执行文件中的命令(参见第 4.1 节)。
  • source:与 . 等价(参见第 4.1 节)。
  • continue:跳到 forwhileuntilselect 循环的下一轮迭代(参见第 4.1 节)。
  • eval:将参数拼接成命令并执行(参见第 4.1 节)。
  • exec:用指定的命令替换当前进程,或仅修改 Shell 的文件描述符(参见第 4.1 节)。
  • exit:以指定状态退出 Shell(参见第 4.1 节)。
  • export:为变量赋予导出属性,使其进入环境(参见第 4.1 节)。
  • readonly:将变量或函数标记为只读,之后无法修改或取消设置(参见第 4.1 节)。
  • return:从 Shell 函数或由 ./source 载入的文件中返回(参见第 4.1 节)。
  • set:设置或取消设置 Shell 选项与位置参数(参见第 4.3.1 节(set 内建命令))。
  • shift:移动位置参数(参见第 4.1 节)。
  • times:打印 Shell 及其子进程累计的用户时间与系统时间(参见第 4.1 节)。
  • trap:为信号或 Shell 事件(如 ERRDEBUGRETURN)设置或移除处理动作(参见第 4.1 节)。
  • unset:删除变量或函数(参见第 4.1 节)。

译者注:清单里既有 break/continue/return 这类流程控制命令,也有 : 这种「什么都不做」的空命令,还有 export/readonly/set/unset 这类会修改 Shell 状态的命令——把它们列为特殊,正是为了让它们在脚本出错、或需要持久赋值时有一致的、可预期的行为。交互式 Shell 中第 2 条差异不适用(只有非交互式 Shell 才会因特殊内建命令出错而退出)。

posted @ 2026-08-07 04:39  立体风  阅读(15)  评论(0)    收藏  举报