编程 Bash 5.3 实用改动:命令替换不再 fork、GLOBSORT 排序,还有两处会咬到旧脚本

2026-11-10 21:09:37

Bash 5.3 对脚本作者有用的改动:不 fork 的命令替换、GLOBSORT,以及两处会咬到旧脚本的兼容性变化

Bash 5.3 的变更清单很长,但大部分是补全、readline、POSIX 细节。下面只挑写脚本时会真正碰到的部分:两类可以减少 fork 的新语法、GLOBSORT,以及几处会改变现有脚本行为的兼容性调整。

先确认目标机器上跑的是 5.3

常见发行版仓库里的 bash 还停在 5.1/5.2,容器基础镜像更旧。新语法在旧 bash 上解析阶段就会失败(${ cmd; } 报 bad substitution),靠 if 分支绕不过去,所以先判断版本:

if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3) )); then
echo "bash 5.3+"
fi

自己编译的话有两个前提:

  • 链接已安装的 Readline 也可以,但只有 readline-8.1 及以后才提供 bash 5.3 需要的全部符号,更早的版本无法正确工作。
  • 源码已按 C23 更新,K&R 风格的编译器不再能编译。install-strip 和 strip 这两个 Makefile 目标现在能处理交叉编译。

${ command; } 和 ${|command;}:命令替换不再 fork

新的命令替换形式在当前 shell 的执行上下文里跑命令,不开子进程、不走管道:

  • ${ command; } 把命令的 stdout 作为替换结果;
  • ${|command;} 不抓 stdout,而是取命令执行完后 REPLY 变量的值。

5.2 的写法:

# 每次调用 fork 一个子 shell
count=$( for f in *.log; do (( n++ )); done; echo "$n" )

5.3:

count_logs() {
local n=0 f
for f in *.log; do (( n++ )); done
REPLY=$n
}

${|count_logs; }
printf 'logs: %s\n' "$REPLY"

要 stdout 结果就用另一种形式:

sum() { local s=0 x; for x; do (( s += x )); done; REPLY=$s; }

${|sum 1 2 3; }
echo "$(( REPLY * 2 ))"        # 12

# 直接拿 stdout 作为替换值
p=$(${ printf '%s' "$base/./x"; })

因为是当前环境,函数里改的变量、cd、shopt、set 的改动都会留在当前 shell 里——这既是省掉 fork 的原因,也是要小心的地方。别在这种替换里写 exit,也别指望失败只影响子 shell。

BASH_MONOSECONDS:测耗时不用再 fork date

BASH_MONOSECONDS 每次引用都返回系统单调时钟的值(秒,整数),不受系统时间调整影响,适合算超时和耗时段落。

5.2:

start=$(date +%s%N)
do_work
end=$(date +%s%N)
printf '%d ms\n' "$(( (end - start) / 1000000 ))"

5.3:

start=$BASH_MONOSECONDS
do_work
end=$BASH_MONOSECONDS
printf '%d s\n' "$(( end - start ))"

放在循环里差别更明显:每轮读一次变量,比每轮 fork 一个 date 便宜一个数量级。精度是秒,需要毫秒级时还得用 EPOCHREALTIME(注意它不是单调时钟)或外部命令。

GLOBSORT:让路径名展开自己排好序

GLOBSORT 指定路径名展开结果的排序方式,键有 name、size、blocks、mtime、atime、ctime、numeric、none,键前加 - 表示降序(+ 显式写升序,也是默认)。

# 5.2:只能借外部命令排序
ls -lS /var/log/*.log | head -3

# 5.3:展开时就是有序的
GLOBSORT=size
printf '%s\n' /var/log/*.log | head -3

GLOBSORT=-mtime                 # 最新的排前面
printf '%s\n' /var/log/*.log | head -3

GLOBSORT=none                   # 按目录顺序,不排
printf '%s\n' /var/log/*.log > /dev/null

它只影响 shell 自己的路径名展开,ls、find 的输出不受影响;nullglob、failglob、dotglob 的行为照旧。

变量、内建命令与补全的零碎改动

  • source / . 新增 -p PATH,用给定的 PATH 而不是 $PATH 找文件:
. -p "$HOME/lib:/usr/local/lib" common.sh
  • compgen -V varname:把生成的结果存进变量,不再打到 stdout。
compgen -V git_cmds -c -- 'git '
printf '%s\n' "${git_cmds[@]}"
  • read -E:用 readline 读入,但 Tab 触发的是 bash 默认补全(含可编程补全),不是文件名补全。
read -E -p 'log file> ' f
  • trap -P SIG:打印某个信号当前绑定的动作。BASH_TRAPSIG 在 trap 执行期间给出正在处理的信号编号。
trap 'printf "caught %s\n" "$BASH_TRAPSIG"' INT TERM
trap -P INT
  • 声明类内建前面的 command 不再破坏特殊赋值解析(新的 POSIX 要求):
command declare -A m=([k]=v)     # 仍按 declare 的语法解析
  • printf:%q 和 %Q 的 alternate form 强制用单引号;多字节 locale 下 %ls(%S)、%lc(%C)分别按宽字符串和宽字符处理。
printf '%q\n'  "a b"      # a\ b
printf '%#q\n' "a b"      # 'a b'

LC_ALL=C.UTF-8 printf '%ls\n' "你好"
  • TIMEFORMAT 的精度上限从 3 位小数提到 6 位:
TIMEFORMAT='%6R s'
time sleep 0.5
  • MULTIPLE_COPROCS 默认开启,多个 coproc 可以并存;注意默认名都叫 COPROC,并存时自己命名。
  • 新增 bash_source_fullpath shopt,BASH_SOURCE 里放全路径,configure 时可以设默认值。
shopt -s bash_source_fullpath
echo "${BASH_SOURCE[0]}"
  • wait -n 现在会返回已终止的进程替换、以及用户已经被通知过的作业(与不带选项的 wait 一致);POSIX 模式下会把作业从作业表/已终止子进程列表里移除;与进程替换相关的行为按文档对齐。
  • $PATH 为 NULL 时视同 .。清空过 PATH 的服务脚本、包装脚本要留意这个语义。
  • 窗口大小检查的范围扩大:trap、bind -x、可编程补全执行期间都会检查;checkwinsize 在交互 shell 启动的子 shell 里也能用。
  • 新增可加载内建 fltexpr,做浮点运算,用法类似 let:
enable -f /usr/lib/bash/fltexpr fltexpr
fltexpr 'x = 1/3.0'
echo "$x"

二进制通常打包在 bash 的 loadable builtins 包里,发行版可能单独拆包。

其余值得一提的:type -a -P 同时报告哈希路径和 $PATH 搜索结果;jobs 按 POSIX 要求把作业从作业表移除;未闭合复合命令(如 if 少了 fi)的报错会带起始行号;[[ ]] 里的正则编译失败会报错;bind -x 支持用空白分隔键序列和命令(命令必须双引号),bind -x -p 会用这种新语法打印绑定;可编程补全函数返回 124 后加载的新 compspec 可用场景变多;shell 名为 su/-su 时用登录 shell 作为 $BASH;POSIX 模式不再要求函数名是合法 shell 标识符;trap 里执行不带参数的 exit,只在 trap 顶层且会导致 trap 结束时才沿用 trap 前的 $?;交互 shell 执行 trap 时即使当下不是交互状态也会打印作业通知;umask 增加若干特性以完全符合 POSIX;解析器在命令未完成就遇到 EOF 时输出更多信息;文档大幅更新。Readline 侧新增 search-ignore-case、rl_full_quoting_desired、rl_reparse_colors、rl_completion_rewrite_hook,新增可绑定命令 execute-named-command(emacs 模式默认绑 M-x)和 export-completions,增量/非增量搜索里可以用 ^V/^Q 对搜索串里的字符加引号,complete/compopt 新增 fullquote。

两处会改变现有脚本行为的兼容性变化

一、test 在参数超过 4 个且含括号子表达式时的解析行为变了。 5.3 会向前扫描找右括号,括号内参数不超过 4 个时调用 posixtest(),与 coreutils 的 test 对齐;具体行为还取决于 shell 兼容级别,而且这类表达式本身仍然是歧义的。

# 5.2 和 5.3 可能给出不同结果
test \( -n "$a" -o -n "$b" \) -a $# -gt 2

改写掉最稳妥:

if [[ -n $a || -n $b ]] && (( $# > 2 )); then
:
fi

二、交互 shell 在 source 脚本期间不再报告后台作业完成,通知推迟到脚本执行完。依赖 source 过程中输出顺序的脚本会看到变化。

来源

  • LWN 对 bash 5.3 变更的整理:
  • 变更条目本身来自 bash 源码发布的 CHANGES / NEWS 文件。
复制全文 生成海报 Shell Bash 脚本 运维

推荐文章

程序员茄子在线接单