参数展开根据 Shell 参数产生一个值,随后命令才收到展开后的参数。它可以提供默认值、拒绝缺失配置,或者删除字符串的一部分,不必为这些操作启动额外进程。但“执行命令前展开”并不等于“初次解析脚本时就求值”:函数在被调用时读取变量当前的内容,函数定义之后修改变量,会影响后续调用。区分解析、展开和执行,才能解释为什么相同的表达式在不同时间产生不同结果。
默认值要区分未设置与空字符串
${setting:-default} 在变量未设置或者为空时使用默认值;${setting-default} 只处理未设置的情况,保留调用者明确传入的空字符串。冒号改变的是配置语义,不能当成随意省略的标点。例如空字符串可能表示关闭某个前缀,擅自用默认值替换就会改变用户的选择。${setting:+enabled} 只在变量已设置且非空时产生替代文本,去掉冒号后,已设置但为空也满足条件。
普通默认值展开不会修改变量本身。${setting:=default} 才会在选中默认分支时把结果存进变量,之后再读取就能看到新值。${setting:?message} 则在变量未设置或为空时把诊断写到标准错误,并让非交互 Shell 以非零状态退出。去掉冒号后,这两个运算符也只检查是否设置。应当先确定调用约定:允许为空、允许省略,还是必须提供,然后选择相应形式,而不是把所有配置问题都掩盖成一个默认值。
花括号还负责把变量名与相邻文本分开。"${prefix}_suffix" 是给变量内容追加后缀,"$prefix_suffix" 却读取另一个变量。用几个配置拼文件名时,看似缺值的问题可能只是变量名边界写错。应分别检查各个组成部分,再排查最终路径;一个语法完全合法的字符串,也可能混入来自错误变量的内容。
模式处理字符串,不访问文件
${name#pattern} 与 ${name##pattern} 分别删除最短和最长匹配前缀;对应的后缀运算符是 % 与 %%。这里使用 Shell 模式,不是正则表达式。匹配不到时保留原字符串,匹配成功也不会自动回写原变量。斜杠在这里就是普通的文本内容,这些运算不会查询目录、确认文件存在或者解析符号链接,因此不能用一次字符串截取证明路径安全。
如果结果必须保持一个参数,就给整个展开加双引号。"${name##*/}" 仍然会应用花括号里的模式,删除直到最后一个斜杠的前缀;外层引号保护的是展开结果,不会让内部星号失去模式含义。这些形式属于可移植的 Shell 操作,而下标截取、全局替换等还可能依赖 Bash 扩展。脚本既然用 sh 启动,就不能仅凭作者的交互环境支持某种写法而假定所有机器都支持。
默认值的选择还涉及校验边界。配置中的零、文本 false,以及只包含空白的值,都是非空字符串,不会因为使用带冒号的默认展开就被拒绝。端口、路径或数字上限是否合法,仍需单独判断;选中了默认值或通过缺值检查,都不能替代业务内容校验。
坏掉的样子
未加引号的展开结果可能继续经历分词与文件名展开。花括号只划定变量名,不保护参数边界;变量包含空格或星号时,命令可能收到额外参数,甚至收到当前目录里碰巧匹配的文件名。另外,默认表达式只有在对应分支被选中时才会展开。如果把命令替换放进默认值,那么缺值分支就可能启动进程,不能把这种表达式当成完全没有副作用的常量。
参数展开也不是命令模板系统。从变量得到的分号、引号等文本,不会自动重新解析成 Shell 语法。需要动态参数列表时,在 Bash 中使用数组,或在可移植 Shell 中明确构造位置参数;不要把整条命令存成字符串,再用 eval 解释。set -u 能发现部分未设置变量,却不能判断业务取值是否合理;pipefail 管理管道状态,也不会改变这里的字符串运算规则。
快速验证
用 Bash 运行下面的完整实验。所有变量都局限于子 Shell,输出同时显示替代值和变量实际保存的值;方括号使空字符串清晰可见。故意触发的缺值错误放在更内层的子 Shell 中,以便观察失败而不提前结束整组验证。
bash --noprofile --norc <<'BASH'
unset demo_setting
printf 'unset=[%s]\n' "${demo_setting-default}"
demo_setting=
printf 'empty=[%s] colon=[%s]\n' \
"${demo_setting-default}" "${demo_setting:-default}"
printf 'substitute=[%s] stored=[%s]\n' \
"${demo_setting:-fallback}" "$demo_setting"
: "${demo_setting:=saved}"
printf 'assigned=[%s]\n' "$demo_setting"
demo_name='reports/weekly report.tar.gz'
printf 'base=[%s] one=[%s] all=[%s]\n' \
"${demo_name##*/}" "${demo_name%.*}" "${demo_name%%.*}"
printf 'original=[%s]\n' "$demo_name"
show_value() { printf 'current=[%s]\n' "$demo_setting"; }
demo_setting=later
show_value
if (unset demo_setting; : "${demo_setting:?required for demo}") 2>/dev/null; then
printf 'unexpected success\n'
else
printf 'missing value rejected\n'
fi
BASH
第一行应为 unset=[default],第二行是 empty=[] colon=[default],直接展示冒号对空值的影响。普通替代之后仍然显示 stored=[],使用赋值运算符后才出现 assigned=[saved]。取文件名得到 weekly report.tar.gz;删除最短后缀得到 reports/weekly report.tar,删除最长后缀得到 reports/weekly report,但随后打印的原变量仍完整保留。
函数最后输出 later,证明函数定义没有把变量值冻结在解析时。末行应确认缺值被拒绝,而不出现意外成功。这组输出分别验证替代、赋值、模式删除与失败处理;单看命令返回成功,并不能证明调用者要求保留的空配置没有被错误替换。
来源
- Bash 手册参数展开,定义展开运算符,已核验 2026-06-10。
- POSIX Shell 参数展开,未设置与空值的区别及可移植的模式删除,已核验 2026-09-07。