编程 Bash 5.3:不再 fork 的命令替换 ${ cmd; } 与 ${|cmd;}

2026-10-28 21:04:52

Bash 5.3:不再 fork 的命令替换 ${ cmd; } 与 ${|cmd;}

Bash 5.3 已发布,发布说明见 LWN 的报道。这一版的多数改动集中在交互体验和 Readline 一侧,但对写脚本的人来说,最值得关注的是新增的两种命令替换形式:它们在当前 shell 中执行,不 fork 子进程、不走管道。

${ cmd; } 与 ${|cmd;}

out=${ command; }
out=${|command;}

两者都在当前 shell 进程内执行命令,因此命令产生的副作用——变量赋值、cd、数组和函数的修改、trap 中累积的状态——都留在当前 shell,而不是丢在子 shell 里。

区别在结果怎么来:

  • ${ cmd; } 捕获 command 的 stdout 作为替换结果,语义上接近 $(...),只是执行环境不同。
  • ${|cmd;} 不捕获 stdout(命令想输出什么会直接打到终端),命令结束时替换结果取 REPLY 变量的值,因此约定是在命令里设置 REPLY。

和 $(...) 的差别可以直接看出来:

x=old
setx() { x=new; printf 'captured'; }

a=$(setx)          # a=captured,x 仍是 old(子 shell)
b=${ setx; }       # b=captured,x 变成 new(当前 shell)
echo "$a $b x=$x"  # captured captured x=new

需要在当前 shell 里累积状态的场合,这个差别就是实际收益:解析配置文件、维护计数器、往数组里塞元素,都不必再用 nameref 或全局变量回传。

${|command;} 适合“结果不是一个输出流”的情况,比如命令本身还要打印进度,或者要返回多个值:

#!/usr/bin/env bash
# 需要 bash 5.3+

count_lines() {
  local n=0 line
  while IFS= read -r line; do n=$((n + 1)); done < "$1"
  REPLY=$n
}

n=${|count_lines access.log; }
printf 'lines: %d\n' "$n"

代价也要说清楚:这两条语法只有 bash 5.3+ 认识,老版本 bash 会直接报语法错误,dash/ksh/zsh 更不认。#!/usr/bin/env bash 并不保证版本,公共脚本里最好显式检查:

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

另外,花括号形式第一眼很像参数扩展,可读性上会让不熟悉的人愣一下,写进共享库之前在注释里标好版本要求。

GLOBSORT

新增变量,用来指定路径名扩展结果的排序方式,可选值包括 name、size、blocks、mtime、atime、ctime、numeric、none,加 - 前缀表示降序:

GLOBSORT=-mtime
for f in *.log; do
  printf '%s\n' "$f"   # 最新的排在最前
done

none 跳过额外排序,目录项很多时能省掉一部分开销。

其他对脚本有用的改动

  • compgen -V varname:把生成的补全结果存入 varname 而不是打到 stdout,脚本里可以直接用,不用再解析输出。
  • read -E:用 readline 读取,并启用 bash 的默认补全(含 programmable completion)。和只用 readline、不做补全的 -e 区分开。
  • source -p PATH:在指定 PATH 里查找脚本。注意老版本 bash 会把 -p 当成文件名,迁移时要留意。
  • BASH_MONOSECONDS:动态变量,返回系统单调时钟,不受系统时间调整影响。
  • BASH_TRAPSIG:trap 执行期间被设为当前信号的编号,一个 handler 挂多个信号时可以分辨来源。
  • TIMEFORMAT:精度上限从 3 位小数提高到 6 位,%6R 可以拿到微秒级结果。
  • trap -P:打印每个信号当前关联的 trap 动作,不执行也不修改。
  • printf 的 %q / %Q 支持备用形式,强制单引号输出。
  • umask 为完整的 POSIX 合规增加了特性。
  • MULTIPLE_COPROCS 默认开启,不用再手动打开就能同时使用多个 coproc。
  • 交互式 shell 在 source 脚本期间不再打印作业完成通知,脚本里跑后台任务不会再插播 [1]+ Done。
  • test 内建在参数多于 4 个且带括号子表达式时解析行为有变化:向前扫描右括号,括号内不超过 4 个参数就走 posixtest(),以对齐 coreutils 的 test。这类表达式本身有歧义,不要依赖跨 shell 的一致结果。
  • 源码按 C23 更新,不再支持 K&R 编译器;可以链接系统已安装的 Readline,但需要 8.1 以上。

计时示例

start=$BASH_MONOSECONDS
sleep 2
elapsed=$(( BASH_MONOSECONDS - start ))
printf 'elapsed: %d s\n' "$elapsed"

单调时钟的好处是中途改系统时间、NTP 校时都不会让差值变成负数或跳变,比拿 date +%s 相减稳。

一条命令搞定副作用与结果

把上面几种拼起来,一个“读文件、统计、结果放变量”的脚本可以不产生任何子 shell:

#!/usr/bin/env bash
# 需要 bash 5.3+

total_size() {
  local f sum=0
  for f; do
    sum=$(( sum + $(stat -c %s -- "$f") ))
  done
  REPLY=$sum
}

s=${|total_size *.c; }
echo "total: $s bytes"

stat 本身仍是外部命令,但函数体、循环和累加都在当前 shell 里完成,${|...;} 只是把最终值从 REPLY 取出来。

升级前值得先确认的两点:CI 和部署机上的 bash 版本,以及脚本是否会被 /bin/sh 调用。前者决定能不能用新语法,后者决定这些花括号写法会不会在别的 shell 上直接语法报错。

复制全文 生成海报 Bash Shell 脚本 Linux

推荐文章

程序员茄子在线接单