命令替换会收集一个命令的标准输出,再把得到的文本放入外层表达式。常见的 $(command) 适合获取时间戳、计算出的标签或其他较小的文本值,但它并不是任意字节的透明传输通道,也不是可靠的文件名列表容器。使用前需要理解三个边界:末尾换行如何处理、内部命令在哪个执行环境运行,以及失败状态如何传递。只看到变量里有内容,不能证明整个操作成功。
先收集文本,再决定怎样使用
本文讨论的标准命令替换形式会让 Bash 在子 shell 环境中运行内部命令,收集标准输出,并删除末尾所有换行符。位于文本中间的换行仍然保留。双引号能阻止后续分词和文件名展开,却不能恢复已经删除的尾部换行。如果需要精确保留最后几个字节,应该使用文件或数据流,而不是依靠 shell 变量原样往返。
赋值语句 result=$(producer) 会把处理后的文本存进变量,不在此处拆成多个参数。后续使用时仍应写成 "$result",保留它的参数边界。类似地,consumer "$(producer)" 会传递一个参数,即使结果为空。去掉引号可能得到多个参数,也可能完全没有参数;输出中的星号还可能匹配当前目录的文件,让同样的生产者输出在不同目录产生不同调用结果。
标准错误和标准输出彼此独立,除非主动重定向,诊断信息仍可能显示在终端,而变量只收集结果正文。将两路输出合并后,警告文本可能污染本应供程序读取的值。因此,应先确定生产者的输出契约,再决定是否合并;直接吞掉所有诊断信息也可能让空结果失去可解释的原因。输出内容、诊断信息和退出状态应分别检查。
不要把这里的作用域结论推广到较新 Bash 的每一种捕获语法。本文使用的美元符号加圆括号形式具有实验验证的子 Shell 行为;较新的当前环境捕获形式会保留不同的副作用。说明一个捕获值时,应明确语法与解释器版本,而不是把所有能够取得文本的写法都视为等价。
坏掉的样子
子 shell 内修改变量或执行 cd,不会改变调用者对应的变量和工作目录。但文件系统或网络副作用并不会因此消失:在命令替换里创建的文件是真实文件,发出的请求也是真实请求。执行环境隔离不能被误解为事务回滚,也不能保证内部操作没有外部影响。
有输出也不代表生产者成功。仅包含赋值的命令会使用最后一个命令替换的退出状态,因此 if result=$(producer); then 可以检查生产者结果。相反,printf '%s' "$(producer)" 的状态通常来自 printf,而不是内部命令。生产者可以先输出部分内容,再失败退出;应立即保存退出码,并明确失败时是否允许使用这些不完整内容。后面的打印日志命令会覆盖 $? 所指向的最近状态。
多条命令组成的替换尤其需要明确失败传播:前一步失败后,最后一步仍可能成功,从而使整个替换返回成功。重要操作应在生产者内部显式判断,并返回预期状态。函数中如果需要检查捕获赋值的状态,也应把 local 声明和赋值分成两步,避免声明内建命令掩盖生产者退出码。不能仅凭脚本开启了某个全局选项,就假设每种上下文都会按同样方式中止。
命令替换还会在外层命令运行前收集输出,因此不适合无限输出或需要逐步消费的交互数据。不要通过未引用的 find 输出构造任意文件名列表,空白和通配字符会破坏原有路径边界。可以改用 find 的 exec 接口直接传递路径,或者选择明确定义边界的记录协议。是否需要一个文本值,还是一组独立记录,应在选择语法前确定。
快速验证
下面代码块只使用 Bash 内建命令,不需要准备文件或服务。它把输出内容和退出码分别断言,并主动构造退出码为七的失败生产者。这个失败是实验的一部分,整个测试仍必须运行到最后的通过提示,不能把任意非零输出都当成环境故障。
bash --noprofile --norc <<'BASH'
set -e
outer=parent
captured=$(outer=child; printf 'first\nsecond\n\n')
[[ $outer == parent ]]
[[ $captured == $'first\nsecond' ]]
printf 'captured <%q>\n' "$captured"
if partial=$(printf 'incomplete'; exit 7); then
printf 'unexpected producer success\n' >&2
exit 1
else
status=$?
fi
[[ $status -eq 7 && $partial == incomplete ]]
printf 'producer status %s; partial <%s>\n' "$status" "$partial"
printf '%s' "$(exit 9)"
[[ $? -eq 0 ]]
IFS=$' \t\n'
set -- "$(printf 'two words\n')"
[[ $# -eq 1 && $1 == 'two words' ]]
set -- "$(printf '\n\n')"
[[ $# -eq 1 && $1 == '' ]]
printf 'PASS: capture, subshell scope, failure status, empty result\n'
BASH
第一行以转义形式显示捕获值,应该只包含正文中间的一个换行,没有尾部换行。下一行同时显示退出码七和部分输出,证明失败状态与非空正文可以同时存在。之后外层 printf 成功的断言说明,只检查消费者状态会漏掉生产者失败。最后还检查了空结果在引号内形成一个空参数。这些实验验证的是文中几个小型同步生产者的行为;真实命令若存在耗时、输出格式或错误语义要求,仍应分别验证这些契约。
来源
- GNU Bash:命令替换,标准替换形式的执行环境和换行处理,已核验 2026-09-06。
- GNU Bash:分词,未引用捕获结果的处理,已核验 2026-09-06。
- GNU Bash:特殊参数,问号参数提供的最近退出状态,已核验 2026-09-06。