<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>展开 · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/%E5%B1%95%E5%BC%80/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/%E5%B1%95%E5%BC%80/index.xml" rel="self" type="application/rss+xml"/><item><title>Shell 参数展开产生替代值</title><link>https://www.xbcatm.com/zh/posts/shell-parameter-expansion-rewrites/</link><pubDate>Tue, 16 Nov 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-parameter-expansion-rewrites/</guid><description>参数展开在命令执行前提供默认值、转换文本；引号、空值和赋值运算符共同决定最终结果。</description><content:encoded>&lt;p&gt;参数展开根据 Shell 参数产生一个值，随后命令才收到展开后的参数。它可以提供默认值、拒绝缺失配置，或者删除字符串的一部分，不必为这些操作启动额外进程。但“执行命令前展开”并不等于“初次解析脚本时就求值”：函数在被调用时读取变量当前的内容，函数定义之后修改变量，会影响后续调用。区分解析、展开和执行，才能解释为什么相同的表达式在不同时间产生不同结果。&lt;/p&gt;
&lt;h2 id="默认值要区分未设置与空字符串"&gt;默认值要区分未设置与空字符串&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;${setting:-default}&lt;/code&gt; 在变量未设置或者为空时使用默认值；&lt;code&gt;${setting-default}&lt;/code&gt; 只处理未设置的情况，保留调用者明确传入的空字符串。冒号改变的是配置语义，不能当成随意省略的标点。例如空字符串可能表示关闭某个前缀，擅自用默认值替换就会改变用户的选择。&lt;code&gt;${setting:+enabled}&lt;/code&gt; 只在变量已设置且非空时产生替代文本，去掉冒号后，已设置但为空也满足条件。&lt;/p&gt;
&lt;p&gt;普通默认值展开不会修改变量本身。&lt;code&gt;${setting:=default}&lt;/code&gt; 才会在选中默认分支时把结果存进变量，之后再读取就能看到新值。&lt;code&gt;${setting:?message}&lt;/code&gt; 则在变量未设置或为空时把诊断写到标准错误，并让非交互 Shell 以非零状态退出。去掉冒号后，这两个运算符也只检查是否设置。应当先确定调用约定：允许为空、允许省略，还是必须提供，然后选择相应形式，而不是把所有配置问题都掩盖成一个默认值。&lt;/p&gt;
&lt;p&gt;花括号还负责把变量名与相邻文本分开。&lt;code&gt;&amp;quot;${prefix}_suffix&amp;quot;&lt;/code&gt; 是给变量内容追加后缀，&lt;code&gt;&amp;quot;$prefix_suffix&amp;quot;&lt;/code&gt; 却读取另一个变量。用几个配置拼文件名时，看似缺值的问题可能只是变量名边界写错。应分别检查各个组成部分，再排查最终路径；一个语法完全合法的字符串，也可能混入来自错误变量的内容。&lt;/p&gt;
&lt;h2 id="模式处理字符串不访问文件"&gt;模式处理字符串，不访问文件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;${name#pattern}&lt;/code&gt; 与 &lt;code&gt;${name##pattern}&lt;/code&gt; 分别删除最短和最长匹配前缀；对应的后缀运算符是 &lt;code&gt;%&lt;/code&gt; 与 &lt;code&gt;%%&lt;/code&gt;。这里使用 Shell 模式，不是正则表达式。匹配不到时保留原字符串，匹配成功也不会自动回写原变量。斜杠在这里就是普通的文本内容，这些运算不会查询目录、确认文件存在或者解析符号链接，因此不能用一次字符串截取证明路径安全。&lt;/p&gt;
&lt;p&gt;如果结果必须保持一个参数，就给整个展开加双引号。&lt;code&gt;&amp;quot;${name##*/}&amp;quot;&lt;/code&gt; 仍然会应用花括号里的模式，删除直到最后一个斜杠的前缀；外层引号保护的是展开结果，不会让内部星号失去模式含义。这些形式属于可移植的 Shell 操作，而下标截取、全局替换等还可能依赖 Bash 扩展。脚本既然用 &lt;code&gt;sh&lt;/code&gt; 启动，就不能仅凭作者的交互环境支持某种写法而假定所有机器都支持。&lt;/p&gt;
&lt;p&gt;默认值的选择还涉及校验边界。配置中的零、文本 false，以及只包含空白的值，都是非空字符串，不会因为使用带冒号的默认展开就被拒绝。端口、路径或数字上限是否合法，仍需单独判断；选中了默认值或通过缺值检查，都不能替代业务内容校验。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;未加引号的展开结果可能继续经历分词与文件名展开。花括号只划定变量名，不保护参数边界；变量包含空格或星号时，命令可能收到额外参数，甚至收到当前目录里碰巧匹配的文件名。另外，默认表达式只有在对应分支被选中时才会展开。如果把命令替换放进默认值，那么缺值分支就可能启动进程，不能把这种表达式当成完全没有副作用的常量。&lt;/p&gt;
&lt;p&gt;参数展开也不是命令模板系统。从变量得到的分号、引号等文本，不会自动重新解析成 Shell 语法。需要动态参数列表时，在 Bash 中使用数组，或在可移植 Shell 中明确构造位置参数；不要把整条命令存成字符串，再用 &lt;code&gt;eval&lt;/code&gt; 解释。&lt;code&gt;set -u&lt;/code&gt; 能发现部分未设置变量，却不能判断业务取值是否合理；&lt;code&gt;pipefail&lt;/code&gt; 管理管道状态，也不会改变这里的字符串运算规则。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;用 Bash 运行下面的完整实验。所有变量都局限于子 Shell，输出同时显示替代值和变量实际保存的值；方括号使空字符串清晰可见。故意触发的缺值错误放在更内层的子 Shell 中，以便观察失败而不提前结束整组验证。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bash --noprofile --norc &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;BASH&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;unset demo_setting
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;printf &amp;#39;unset=[%s]\n&amp;#39; &amp;#34;${demo_setting-default}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_setting=
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;printf &amp;#39;empty=[%s] colon=[%s]\n&amp;#39; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;${demo_setting-default}&amp;#34; &amp;#34;${demo_setting:-default}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;printf &amp;#39;substitute=[%s] stored=[%s]\n&amp;#39; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;${demo_setting:-fallback}&amp;#34; &amp;#34;$demo_setting&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;: &amp;#34;${demo_setting:=saved}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;printf &amp;#39;assigned=[%s]\n&amp;#39; &amp;#34;$demo_setting&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_name=&amp;#39;reports/weekly report.tar.gz&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;printf &amp;#39;base=[%s] one=[%s] all=[%s]\n&amp;#39; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;${demo_name##*/}&amp;#34; &amp;#34;${demo_name%.*}&amp;#34; &amp;#34;${demo_name%%.*}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;printf &amp;#39;original=[%s]\n&amp;#39; &amp;#34;$demo_name&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;show_value() { printf &amp;#39;current=[%s]\n&amp;#39; &amp;#34;$demo_setting&amp;#34;; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;demo_setting=later
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;show_value
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if (unset demo_setting; : &amp;#34;${demo_setting:?required for demo}&amp;#34;) 2&amp;gt;/dev/null; then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; printf &amp;#39;unexpected success\n&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; printf &amp;#39;missing value rejected\n&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;BASH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第一行应为 &lt;code&gt;unset=[default]&lt;/code&gt;，第二行是 &lt;code&gt;empty=[] colon=[default]&lt;/code&gt;，直接展示冒号对空值的影响。普通替代之后仍然显示 &lt;code&gt;stored=[]&lt;/code&gt;，使用赋值运算符后才出现 &lt;code&gt;assigned=[saved]&lt;/code&gt;。取文件名得到 &lt;code&gt;weekly report.tar.gz&lt;/code&gt;；删除最短后缀得到 &lt;code&gt;reports/weekly report.tar&lt;/code&gt;，删除最长后缀得到 &lt;code&gt;reports/weekly report&lt;/code&gt;，但随后打印的原变量仍完整保留。&lt;/p&gt;
&lt;p&gt;函数最后输出 &lt;code&gt;later&lt;/code&gt;，证明函数定义没有把变量值冻结在解析时。末行应确认缺值被拒绝，而不出现意外成功。这组输出分别验证替代、赋值、模式删除与失败处理；单看命令返回成功，并不能证明调用者要求保留的空配置没有被错误替换。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html"&gt;Bash 手册参数展开&lt;/a&gt;，定义展开运算符，已核验 2026-06-10。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_06_02"&gt;POSIX Shell 参数展开&lt;/a&gt;，未设置与空值的区别及可移植的模式删除，已核验 2026-09-07。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>