包装脚本应当把收到的参数按原有边界继续传递。在 Bash 的命令参数位置,"$@" 会让每个位置参数分别成为一个带引号的词,因此能保留空值、空格、换行和字面通配符。只用两个简单文件名测试成功,并不能证明包装脚本正确;一旦它把参数列表拼成字符串再拆开,真实调用者传入的边界就可能丢失。转发是否可靠,应当用能暴露这种差异的输入来验证。
参数列表与拼接文本不同
位置参数属于当前脚本或正在执行的函数。$# 返回参数数量,内容为空字符串的参数也计算在内。"$1" 读取第一个参数,"${10}" 读取第十个;后者需要花括号,以区别于第一个参数后面接字符零的写法。$0 表示启动的 shell 或脚本名称,不会作为额外元素出现在 "$@" 中,不能把它误算进业务参数列表。
在普通命令边界上,"$@" 为每个参数保留一个独立词;完全没有位置参数时,它不产生任何参数。相比之下,"$*" 始终形成一个字符串,用 IFS 的第一个字符连接各个值。IFS 未设置时使用空格,设为空字符串时则不插入分隔符。它并不会删除参数内部原有的空格,但各参数原来的边界通常已经无法恢复,因此不适合转发任意列表。
省略引号的 $@ 不是等价简写,展开后的内容仍可能被分词和文件名展开处理。调用者传入的字面星号,到了包装脚本所在目录可能突然匹配出一组文件。往拼接文本里补引号字符也不能恢复结构。需要把列表保存到之后使用时,在 Bash 中写成 args=("$@"),并通过 "${args[@]}" 转发,即使随后调整位置参数,保存的列表仍保留各个元素。
坏掉的样子
函数执行期间拥有自己的位置参数列表。调用时传入的参数暂时替换当前可见列表,返回时恢复调用者的列表。函数内部使用 set 或 shift 修改的是这次函数调用的参数,但这不代表其他所有 shell 状态都自动局部化。例如,没有局部声明就修改 IFS,可能影响调用者之后的命令。参数作用域和普通变量作用域应分别理解,不能互相推导。
Shift 会删除前面的若干参数,并对剩余参数重新编号。解析选项时,必须先确认剩余参数足够,再消费选项及其值;要求删除的数量超过现有参数时会失败。仅检查当前数量大于零,还不足以处理需要额外值的选项。如果之后的错误提示或转发还需要被消费的原始值,应在开始解析前保存数组,避免试图从拼接日志中重新恢复。
引号也不能阻止接收程序把完整参数解释为选项。例如,文件名恰好为 --help 时,"$@" 会完整保留它,但目标工具仍可能显示帮助而不读取文件。传入操作数时,应使用工具文档规定的选项终止符,或适当添加路径前缀。包装脚本必须明确自己的选项在哪里结束,以及哪些剩余参数仍是下游命令的选项,不能不加区分地自动插入分隔符。
诊断时应显示参数数量,并逐项打印。用 "$*" 拼成一行,容易让零参数、一个空参数和包含分隔符的参数看起来相同。Bash printf 的 %q 格式可以帮助观察不明显的字符和边界,但记录日志与转发参数承担不同职责。真正执行时应继续使用保存的列表,不应把日志行重新解释为命令,否则又引入一次解析并失去原有保障。
快速验证
下面的完整代码块构造四项参数,分别为空字符串、带空格的文件名、字面星号模式和看起来像选项的值。随后检查函数参数作用域、零参数时两种引用展开的差异,以及越界 shift 的返回结果。实验只需要 Bash,不需要预先准备文件或启动外部服务。
bash --noprofile --norc <<'BASH'
set -e
IFS=$' \t\n'
set -- '' 'report final.md' '*.log' '--help'
saved=("$@")
verify() {
[[ $# -eq 4 && $1 == '' && $2 == 'report final.md' ]]
[[ $3 == '*.log' && $4 == '--help' ]]
printf 'arg <%q>\n' "$@"
}
verify "${saved[@]}"
consume() {
[[ $# -eq 2 ]]
shift
[[ $# -eq 1 && $1 == child-two ]]
set -- replaced
[[ $1 == replaced ]]
}
consume child-one child-two
[[ $# -eq 4 && $2 == 'report final.md' ]]
joined=$(IFS=,; printf '%s' "$*")
[[ $joined == ',report final.md,*.log,--help' ]]
count() { printf '%s' "$#"; }
set --
[[ $(count "$@") == 0 ]]
[[ $(count "$*") == 1 ]]
set -- sole
if shift 2; then
printf 'unexpected shift success\n' >&2
exit 1
fi
[[ $# -eq 1 && $1 == sole ]]
printf 'PASS: forwarding, function scope, empty list, shift limit\n'
BASH
预期先输出四行转义后的参数,最后输出通过信息。第一项应明确为空,星号保持字面意义。consume 函数返回后,外层仍应拥有原来的四个参数,而不是函数内部修改后的列表。对仅剩一个参数的列表执行删除两项应失败,并保留原参数。这些断言验证的是参数传输机制;真实包装脚本仍需针对自己的选项语法、必填值和下游调用方式进行验证,不能把机制测试当作所有命令行协议都正确的证明。
来源
- GNU Bash:特殊参数,位置参数展开、拼接和计数,已核验 2026-09-06。
- GNU Bash:数组,保存和转发独立元素,已核验 2026-09-06。
- GNU Bash:分词,未引用展开与空参数,已核验 2026-09-06。