Bash source 位置参数:明明单独传了参数,set -- 为什么仍会改变外层

前天 3阅读

公共脚本通过 source 载入,调用时还专门给它传了一组参数,外层原有参数却仍被改掉。先查看被载入文件是否执行了 set --。在本文实测的 Bash 顶层调用中,只读取临时参数与显式重设位置参数,会得到不同的返回后状态;不能把传参当成通用隔离措施。

以下代码在 Bash 5.2.37 默认模式执行。保存为 demo.sh,用 bash --noprofile --norc demo.sh 运行,不要把整个实验 source 到日常终端。它仅创建并清理自己的临时目录,不写启动文件,也不修改已有用户文件。每个结论都由返回后的断言验证。

先验证只读参数的基线

part.sh 总会打印收到的两个参数并改变一个普通变量,只有 DEMO_REWRITE 为 yes 时才执行 set --。第一轮把开关设为 no,并用 child、value 调用 source;文件内部看到这两个临时值,返回后外层仍是 parent、original。

Bash source 位置参数:明明单独传了参数,set -- 为什么仍会改变外层

AI概念示意图:同一工作空间中的参数和普通状态可能有不同恢复范围,调用前后的状态需要分别核对。图片不是终端截图。

set -euo pipefail
work=$(mktemp -d)
trap 'rm -rf -- "$work"' EXIT
cat > "$work/part.sh" <<'PART'
printf 'inside: %s,%s\n' "$1" "$2"
DEMO_MARK=changed
if [[ "$DEMO_REWRITE" == yes ]]; then
    set -- inside replacement
fi
PART

set -- parent original
DEMO_MARK=before
DEMO_REWRITE=no
source "$work/part.sh" child value
[[ "$#" == 2 && "$1" == parent && "$2" == original ]]
[[ "$DEMO_MARK" == changed ]]
printf 'restored: %s,%s; mark=%s\n' "$1" "$2" "$DEMO_MARK"

saved_arguments=("$@")
DEMO_REWRITE=yes
source "$work/part.sh" child value
[[ "$#" == 2 && "$1" == inside && "$2" == replacement ]]
printf 'reset retained: %s,%s\n' "$1" "$2"
set -- "${saved_arguments[@]}"
[[ "$#" == 2 && "$1" == parent && "$2" == original ]]
printf 'explicit restore: %s,%s\n' "$1" "$2"

第一次输出 inside: child,value,随后 restored: parent,original。这个正常样本说明我们确实为被载入文件传入了独立的一组参数,读到的并非外层原值。但 DEMO_MARK 已从 before 变为 changed,证明代码仍在同一个 shell 环境内执行。

显式重设是另一个必须检查的分支

第二轮先保存外层参数,把开关改为 yes,再用完全相同的 child、value 载入同一文件。这一次文件内部执行 set --,返回后实际保留下来的是 inside、replacement,输出 reset retained 对应这个状态。运行断言比根据函数调用的直觉猜测更可靠。

因此,检查 source 调用不能只看有没有附加参数,还要读被载入代码如何处理位置参数。set 是明确修改参数的内建命令,source 则在当前执行环境运行文件。这里特意限定顶层和实际版本;不要把结论未经验证地推广到函数内部、兼容模式或其他 shell。

本文没有用 shift 代替 set -- 做实验,两者也不能仅因都能改变参数就推定所有恢复细节相同。若自己的脚本库采用另一种消费方式,应把那个实际动作放进同样的进入前、内部、返回后三个检查点,保留可重复的最小样本。

需要保留时,明确保存与恢复

代码在第二次调用前用数组保存每个位置参数,返回后再用带引号的数组展开交给 set --。最后 explicit restore 再次得到 parent、original。这样恢复的是一份明确保存的参数清单,不依赖被载入文件恰好没有改动它们。

数组保留每个参数的边界,不能随手把所有参数拼成一串再拆开,否则包含空格或空字符串的输入会被改变。本例用简单单词隔离恢复问题,迁移时仍应加上空参数和含空格参数,验证保存与还原没有顺便改变调用接口。

这几行显式恢复只覆盖位置参数,并不回滚普通变量、目录、函数定义或 shell 选项。若 source 过程中直接退出或提前中断,也可能走不到恢复语句;需要可靠清理时,应把异常路径和调用结构另行设计,不能把正常路径的样本当成完整恢复框架。

让公共脚本说明影响范围

如果文件只是提供函数定义,尽量避免在顶层执行消耗参数的业务动作。需要立即初始化时,应声明接收哪些参数、返回什么状态以及会修改哪些名称。调用者才能选择显式保存、通过函数限定参数范围,或直接使用独立进程运行任务。

source 执行的是代码,不是单纯读取配置文本。被载入文件使用 exit 可能结束整个调用 shell;需要结束当前载入时应评估 return。文件头的解释器声明也不会让 source 自动另启一种语言,内容仍由当前 Bash 解析。

实验用带斜杠的路径指定自己创建的文件,避免搜索到另一份同名脚本。排查真实库时,先确认实际文件和 Bash 版本,再检查其顶层参数操作。不要看到第一轮只读测试正常,就跳过会改写参数的分支;两种状态需要各自留下验证证据。

参考资料

GNU Bash 维护者提供的参考手册:点号内建命令

GNU Bash 参考手册:位置参数与 set

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。