<?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/%E8%84%9A%E6%9C%AC/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/%E8%84%9A%E6%9C%AC/index.xml" rel="self" type="application/rss+xml"/><item><title>Shell 的字段拆分把未加引号的变量拆成多个参数</title><link>https://www.xbcatm.com/zh/posts/shell-word-splitting-inserts-separators/</link><pubDate>Fri, 31 Dec 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-word-splitting-inserts-separators/</guid><description>当命令行上出现未加引号的变量时，shell 会按 IFS 把展开结果重新切分，于是一个变量可能在你不经意间变成好几个参数。</description><content:encoded>&lt;p&gt;变量保存的是一个字符串，但没有引号的展开可能把它变成多个命令参数。Bash 的分词阶段使用 IFS 指定的分隔字符检查展开结果，因此，手动引用后能正常使用的路径，放入变量再传递时却可能突然找不到文件。排查时首先应观察接收命令实际拿到的参数数量和每个参数的内容，也要区分空字符串与完全没有参数，不能仅凭终端上看起来相同的一行文本判断输入一致。&lt;/p&gt;
&lt;h2 id="是否分词取决于上下文"&gt;是否分词取决于上下文&lt;/h2&gt;
&lt;p&gt;默认 IFS 包含空格、制表符和换行符，它们的连续组合会分隔字段，开头和结尾的这类空白也会被忽略。未引用变量里有两个单词时，命令通常收到两个参数；变量为空时，则通常连一个参数都没有。使用 &lt;code&gt;&amp;quot;$value&amp;quot;&lt;/code&gt; 会保留一个参数，即使值是空字符串。对于命令行接口，显式传入空值和省略选项值可能触发完全不同的行为。&lt;/p&gt;
&lt;p&gt;分词针对适用上下文中的未引用参数展开、命令替换和算术展开结果，并不是重新解析整条命令。普通赋值语句 &lt;code&gt;copy=$value&lt;/code&gt; 会保留原字符串，不执行分词和文件名展开。但随后把 &lt;code&gt;$copy&lt;/code&gt; 不加引号地作为命令参数使用，仍会重新引入两个问题。Bash 双方括号条件表达式也具有不同于普通命令参数的展开规则，所以赋值或条件判断成功，不能证明后面的外部程序调用也安全。&lt;/p&gt;
&lt;p&gt;IFS 是一组分隔字符，不是多字符分隔串，也不是正则表达式。非空白分隔符和空白的处理并不相同，例如连续两个冒号可以产生一个空字段，而连续默认空白通常只是一个字段边界。把 IFS 设为空字符串会关闭分词，但不会把未引用的空展开变成显式空参数。若数据格式有引号、转义或嵌套结构，应使用理解该格式的解析器，不能把调整 IFS 当作通用解析方法。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;关闭分词并没有关闭随后的文件名展开。即使 IFS 为空，变量里的星号仍可能匹配当前目录下的文件；目录里刚增加一个文件，同一条命令的参数数量就会改变。引用变量能够同时阻止这两种转换，而修改 IFS 只处理了其中一个阶段。控制通配符的 shell 选项也独立存在，不能把它们和分词配置混为一谈。&lt;/p&gt;
&lt;p&gt;另一个常见问题是用一个字符串保存整组参数，然后往字符串内部塞引号。Bash 不会在变量展开后把这些字符重新当作 shell 源码里的引号，因此它们不能恢复原有参数边界。不要用 eval 补救：它会增加一次 shell 解析，让原本的数据获得执行命令的机会。需要在 Bash 中逐步组装命令时，使用数组保存各项，并通过 &lt;code&gt;&amp;quot;${args[@]}&amp;quot;&lt;/code&gt; 传递，每项内容和空值都能独立保留。&lt;/p&gt;
&lt;p&gt;有些协议确实规定了分隔符，此时主动分词可以合理使用，但必须先明确格式、空字段含义和有效字符范围，并把 IFS 的修改限制在所需操作范围。任意文件名和带引号的逗号分隔记录都不适合直接按空白切开。本文实验明确使用 Bash；其他 shell 的默认行为可能不同，例如通常配置下的 zsh 不会以相同方式拆分普通标量展开，不能把一个终端的观察直接推广到所有 shell。&lt;/p&gt;
&lt;p&gt;例如，变量展开产生的分号不会自动升级为 shell 的命令分隔符。它仍然属于参数数据，尽管周围文本仍可能被分词破坏。参数构造错误与主动把字符串重新解释成代码是两个不同问题，修复时应定位实际发生转换的阶段。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面的完整 Bash 实验直接断言参数数量，不使用会掩盖边界差异的 echo。它自行创建两个匹配文件，检查带空格的值、空值、空 IFS 下的通配符，以及连续冒号生成的空字段。测试目录退出时自动删除，不需要读者准备目录内容。&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;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set +f
&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 GLOBIGNORE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;shopt -u nullglob failglob dotglob
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;work=$(mktemp -d)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;trap &amp;#39;rm -rf -- &amp;#34;$work&amp;#34;&amp;#39; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;cd &amp;#34;$work&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;IFS=$&amp;#39; \t\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;value=&amp;#39;report final.md&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;set -- $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;[[ $# -eq 2 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -- &amp;#34;$value&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;[[ $# -eq 1 &amp;amp;&amp;amp; $1 == &amp;#39;report final.md&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;value=&amp;#39;&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;set -- $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;[[ $# -eq 0 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -- &amp;#34;$value&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;[[ $# -eq 1 &amp;amp;&amp;amp; $1 == &amp;#39;&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;touch a.txt &amp;#39;two words.txt&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;IFS=&amp;#39;&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;pattern=&amp;#39;*.txt&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;set -- $pattern
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $# -eq 2 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -- &amp;#34;$pattern&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;[[ $# -eq 1 &amp;amp;&amp;amp; $1 == &amp;#39;*.txt&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;IFS=:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;record=&amp;#39;red::blue&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;set -- $record
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $# -eq 3 &amp;amp;&amp;amp; $2 == &amp;#39;&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;colon field &amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;$@&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;args=(&amp;#39;report final.md&amp;#39; &amp;#39;&amp;#39; &amp;#39;*.txt&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;set -- &amp;#34;${args[@]}&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;[[ $# -eq 3 &amp;amp;&amp;amp; $2 == &amp;#39;&amp;#39; &amp;amp;&amp;amp; $3 == &amp;#39;*.txt&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;PASS: boundaries, empty values, globbing, delimiters\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;BASH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;预期输出三行冒号字段，中间一行的尖括号内为空，随后输出通过信息。前面的断言还验证了零参数和一个空参数确实不同，以及 IFS 为空时通配仍然执行。如果真实脚本仅在特定文件名下失败，可以在函数或外部命令的调用边界临时加入参数计数，并使用 Bash 的 printf &lt;code&gt;%q&lt;/code&gt; 格式逐项显示。这个输出有助于观察换行、空格和字面星号，但调试日志不是可随意重新执行的命令格式，不应再交给 eval 处理。&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/Word-Splitting.html"&gt;GNU Bash：分词&lt;/a&gt;，IFS 和空字段规则，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Expansions.html"&gt;GNU Bash：Shell 展开&lt;/a&gt;，分词先于文件名展开的顺序，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Arrays.html"&gt;GNU Bash：数组&lt;/a&gt;，保留参数列表边界，已核验 2026-09-06。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Shell 位置参数是一份有编号的清单</title><link>https://www.xbcatm.com/zh/posts/shell-positional-arguments-slot-the-cli/</link><pubDate>Mon, 29 Nov 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-positional-arguments-slot-the-cli/</guid><description>在脚本里,传入的参数会以 $1、$2……依次暴露,再有 $@ 和 $# 表示整份清单,因此顺序与个数都是一目了然的。</description><content:encoded>&lt;p&gt;Shell 脚本接收的是有顺序的参数列表，不是需要重新按空格拆分的一整条命令字符串。位置参数通过 &lt;code&gt;$1&lt;/code&gt;、&lt;code&gt;$2&lt;/code&gt; 等编号暴露这份列表，&lt;code&gt;$#&lt;/code&gt; 表示数量。固定且简单的接口可以直接使用位置，带可选设置或开关的接口则适合明确解析。无论采用哪种形式，都需要规定允许几个参数、是否允许空值，以及输入错误时怎样失败。&lt;/p&gt;
&lt;h2 id="解释含义之前先保住列表边界"&gt;解释含义之前，先保住列表边界&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;$0&lt;/code&gt; 表示脚本名称等特殊身份，不属于位置参数，也不计入 &lt;code&gt;$#&lt;/code&gt;。使用 &lt;code&gt;bash -c&lt;/code&gt; 时，命令字符串之后的第一个参数会成为 &lt;code&gt;$0&lt;/code&gt;，包装脚本若误以为它是 &lt;code&gt;$1&lt;/code&gt;，就会看起来少收到一个参数。超过第九个位置必须使用花括号：&lt;code&gt;${10}&lt;/code&gt; 是第十个参数，&lt;code&gt;$10&lt;/code&gt; 却是第一个参数再接一个字面数字零，两种写法不能互换。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; 会为每个传入参数保留一个词，包括空字符串和包含空格的值。未引用的展开可能再次分词并展开通配符，&lt;code&gt;&amp;quot;$*&amp;quot;&lt;/code&gt; 则会把列表连成一个字符串。转发或遍历时应使用带引号的完整列表。&lt;code&gt;set --&lt;/code&gt; 替换当前的位置参数，&lt;code&gt;shift&lt;/code&gt; 删除前面的若干项；这些操作在对应脚本或函数作用域中生效，不会反向改写外部调用者原来的参数列表。&lt;/p&gt;
&lt;p&gt;空参数仍然占据一个位置。检查 &lt;code&gt;$#&lt;/code&gt; 可以区分完全没有提供参数，与明确传入 &lt;code&gt;''&lt;/code&gt; 两种情况。&lt;code&gt;${1:?message}&lt;/code&gt; 同时拒绝缺失和空值，只有接口要求非空时才合适。应先验证数量，再读取必需的位置，最后判断内容是否合法，避免在参数本来不存在时就触发与接口说明无关的展开错误。&lt;/p&gt;
&lt;h2 id="getopts-检查语法业务规则检查含义"&gt;getopts 检查语法，业务规则检查含义&lt;/h2&gt;
&lt;p&gt;可移植的 &lt;code&gt;getopts&lt;/code&gt; 处理短选项。选项字符后面的冒号表示该选项需要一个值，但不会自动提供 &lt;code&gt;--user&lt;/code&gt; 这类长选项支持。选项说明开头的冒号表示采用安静错误报告：未知选项产生问号，缺少值产生冒号，相关选项字符放在 &lt;code&gt;OPTARG&lt;/code&gt; 中。脚本必须明确处理这些分支，不能因为解析循环继续运行就误以为输入合法。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OPTIND&lt;/code&gt; 记录解析进度。在同一 Shell 中开始解析另一份参数之前，应把它重置为一；解析完成后，使用 &lt;code&gt;shift &amp;quot;$((OPTIND - 1))&amp;quot;&lt;/code&gt; 让剩余操作数成为当前列表。&lt;code&gt;--&lt;/code&gt; 结束选项处理，因此以连字符开头的文件名也能被当作操作数。普通的非选项同样会结束扫描，不能假定写在文件名后面的选项会被自动搬到前面。&lt;/p&gt;
&lt;p&gt;解析器拿到一个值，不等于它的业务含义有效。调用者显式提供空字符串，可以满足“这里有一个选项参数”的语法要求，却不满足“名称必须非空”的接口规则。默认值也只应用于省略的可选设置，不应掩盖错误输入。诊断应写到标准错误，并在执行实际工作前返回约定的非零状态，让调用方能够可靠识别参数错误。&lt;/p&gt;
&lt;p&gt;可复用的 Bash 解析函数应当同时局部化 OPTARG 与 OPTIND。getopts 在处理选项时会更新或取消 OPTARG；即使每次正确重置索引，未局部化的 OPTARG 仍可能破坏外层解析器状态。下面让两者都局部化，并确认调用者预先设置的哨兵值在全部调用之后仍然保留。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;常见问题之一是正常输入能跑，多余的操作数却被悄悄丢弃。另一种是忘记重置解析索引，导致函数第一次调用正确，后续调用跳过部分选项。未引用转发也可能先改变参数数量，产生类似症状。应分别检查接收到的列表与解析状态，而不是不断补几个移位操作，直到某一次特定调用碰巧成功。&lt;/p&gt;
&lt;p&gt;下面的示例明确规定一个小接口：可选的名称选项、可选的详细输出开关，以及恰好一个输入操作数。名称必须非空。函数使用 Bash 的局部变量声明，因此完整示例依赖 Bash，虽然其中的选项解析器本身是可移植的。面向其他 Shell 的脚本，应采用目标 Shell 支持的作用域规则，而不能把局部声明也当成统一标准。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;在 Bash 中运行完整代码块。开头检查第十个参数的引用方式，以及空参数是否被保留。随后测试一次合法调用，再分别测试缺值、未知选项、空名称和多余操作数。演示函数只打印解析结果，不修改文件，也不启动外部程序，所以可以直接观察参数边界，而不把其他业务副作用混进验证。&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;&lt;span class="nv"&gt;BASH_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&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;set -- one 2 3 4 5 6 7 8 9 ten
&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;without-braces=&amp;lt;%s&amp;gt; tenth=&amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;$10&amp;#34; &amp;#34;${10}&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;set -- &amp;#39;&amp;#39; &amp;#39;two words&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;count=%s\n&amp;#39; &amp;#34;$#&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;for demo_arg in &amp;#34;$@&amp;#34;; do printf &amp;#39;argument=&amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;$demo_arg&amp;#34;; done
&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_cli() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; local OPTIND=1 OPTARG demo_option demo_name=guest demo_verbose=no
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; while getopts &amp;#39;:n:v&amp;#39; demo_option; do
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; case &amp;#34;$demo_option&amp;#34; in
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; n) demo_name=$OPTARG ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; v) demo_verbose=yes ;;
&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 for -%s\n&amp;#39; &amp;#34;$OPTARG&amp;#34; &amp;gt;&amp;amp;2; return 2 ;;
&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;unknown option -%s\n&amp;#39; &amp;#34;$OPTARG&amp;#34; &amp;gt;&amp;amp;2; return 2 ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; esac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; done
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; shift &amp;#34;$((OPTIND - 1))&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; if [ &amp;#34;$#&amp;#34; -ne 1 ] || [ -z &amp;#34;$demo_name&amp;#34; ]; 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;need a nonempty name and exactly one input\n&amp;#39; &amp;gt;&amp;amp;2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; return 2
&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; printf &amp;#39;name=&amp;lt;%s&amp;gt; verbose=%s input=&amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;$demo_name&amp;#34; &amp;#34;$demo_verbose&amp;#34; &amp;#34;$1&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;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;OPTARG=caller-sentinel
&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_cli -v -n &amp;#39;Jane Doe&amp;#39; -- &amp;#39;-input file&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;if demo_cli -n 2&amp;gt;/dev/null; then exit 1; else printf &amp;#39;missing=%s\n&amp;#39; &amp;#34;$?&amp;#34;; 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;if demo_cli -x 2&amp;gt;/dev/null; then exit 1; else printf &amp;#39;unknown=%s\n&amp;#39; &amp;#34;$?&amp;#34;; 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;if demo_cli -n &amp;#39;&amp;#39; input 2&amp;gt;/dev/null; then exit 1; else printf &amp;#39;empty=%s\n&amp;#39; &amp;#34;$?&amp;#34;; 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;if demo_cli one two 2&amp;gt;/dev/null; then exit 1; else printf &amp;#39;extra=%s\n&amp;#39; &amp;#34;$?&amp;#34;; 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;demo_cli second
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $OPTARG == caller-sentinel ]] || exit 1
&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;caller OPTARG preserved\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;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;one0&lt;/code&gt; 与 &lt;code&gt;ten&lt;/code&gt;，之后参数数量为二，打印出空字符串和带空格的第二项。合法调用保留 &lt;code&gt;Jane Doe&lt;/code&gt; 和 &lt;code&gt;-input file&lt;/code&gt; 的完整内容。四类错误输入都应返回状态二。最后一次调用重新使用默认名称、关闭详细开关，并接收 &lt;code&gt;second&lt;/code&gt;，证明重复解析从新的状态开始，而不是沿用前一次的索引或选项值。&lt;/p&gt;
&lt;p&gt;这些观察验证了接口边界，不只是证明存在一段用法提示。扩展真实命令时，应补充新选项的内容规则，同时维持已有的引用、空值和结束选项标记语义。参数能够被读到，是解析的开始；只有合法输入被接受、非法输入被明确拒绝，接口约定才真正成立。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Positional-Parameters"&gt;Bash 位置参数&lt;/a&gt;，编号参数及函数作用域规则，已核验 2026-09-07。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/getopts.html"&gt;POSIX getopts&lt;/a&gt;，选项语法错误与解析状态重置，已核验 2026-09-07。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><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><item><title>Bash 的通配符在命令执行前就展开</title><link>https://www.xbcatm.com/zh/posts/bash-globbing-expands-before-the-command-runs/</link><pubDate>Wed, 08 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/bash-globbing-expands-before-the-command-runs/</guid><description>星号和方括号初看像命令的一部分，但 shell 会先把它们展开成文件名，之后才真正执行。搞错这一顺序，脚本就会出问题。</description><content:encoded>&lt;p&gt;Bash 命令中的通配符通常会在目标程序启动前变成一组路径。它让批量处理很方便，也让同一行命令受到当前目录和 shell 选项的影响。程序拿到的是已经形成的参数，不会收到标记来告诉它哪些参数来自星号。因此，写命令时应先确定：希望传入实际匹配的文件，还是希望把模式本身交给另一个程序解释。两者需要不同的引号写法，不能依靠目标程序猜测意图。&lt;/p&gt;
&lt;h2 id="先匹配再保留参数边界"&gt;先匹配，再保留参数边界&lt;/h2&gt;
&lt;p&gt;Bash 先进行分词，再进行文件名展开。没有引用的星号、问号和方括号模式可能因此生成多个参数。但匹配到的文件名即使包含空格，也仍然是一个参数，因为文件名展开之后不会再执行一次分词。常见问题发生在下一步：循环拿到了完整路径，却用没有引号的变量把它传给另一个命令。循环体中的 &lt;code&gt;&amp;quot;$file&amp;quot;&lt;/code&gt; 仍然必要，不能因为前一步通配成功就省掉。&lt;/p&gt;
&lt;p&gt;如果接收方需要模式，就应引用模式。例如 &lt;code&gt;find . -name '*.txt'&lt;/code&gt; 将匹配规则交给 find；去掉引号后，Bash 会先在当前目录展开它，find 得到的可能是多个文件名，已经不是原来要表达的条件。变量里包含星号时也一样：引用变量会传递字面星号，不引用则可能触发文件名展开。引号只控制当前 shell 如何形成参数，不会禁止接收方按自己的语法解释收到的文本。&lt;/p&gt;
&lt;p&gt;通配符匹配目录项，并不保证结果都是普通文件。名为 &lt;code&gt;archive.txt&lt;/code&gt; 的目录也能匹配 &lt;code&gt;*.txt&lt;/code&gt;，只想处理普通文件时还需要判断类型。普通星号不会越过斜杠，默认也不匹配名字开头的点；需要隐藏文件时，应明确使用相应模式或开启 dotglob。遍历整棵目录树是另一个需求，不能把当前目录的一次展开误认为递归搜索。&lt;/p&gt;
&lt;p&gt;环境变量也能改变结果集合。Bash 中非空的 GLOBIGNORE 会过滤匹配结果，并且产生开启 dotglob 的副作用。因此，从交互会话继承状态的脚本可能意外纳入隐藏文件。下面的实验会清除 GLOBIGNORE 及较新版本的排序覆盖变量 GLOBSORT，并明确指定相关选项，让断言对应一套可重复的环境，而不是依赖读者终端的偶然配置。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;默认情况下，没有匹配结果的模式会保留为原文。因此，遍历 &lt;code&gt;*.png&lt;/code&gt; 的循环可能仍然运行一次，变量内容就是一个并不存在的星号文件名。开启 nullglob 后，未匹配的模式会直接消失，但这也可能让接收方变成零文件参数调用。某些程序此时等待标准输入，另一些程序则使用默认目标。未定义变量检查和管道失败检查都不能证明通配符至少找到了一个文件。&lt;/p&gt;
&lt;p&gt;对于允许为空的输入，可以在开启 nullglob 后先收集到 Bash 数组，检查元素数量，再决定是否调用消费者。对于必须存在的输入，failglob 会在未匹配时报告展开错误，并阻止该命令执行。如果只想临时改变行为，把选项修改放在子 shell 中，避免影响调用者的后续命令。交互终端的启动文件可能已经修改选项，所以终端上一次成功不能替代脚本自己的前置条件。&lt;/p&gt;
&lt;p&gt;以连字符开头的文件名还有另一类风险：接收程序可能将它当作选项。使用 &lt;code&gt;./*.txt&lt;/code&gt; 给结果添加目录前缀，能够避免这种名字直接位于参数开头；程序支持时，还应使用其文档规定的 &lt;code&gt;--&lt;/code&gt; 终止选项解析。引号保留参数边界，却不会关闭目标程序的选项解析。枚举和使用文件也不是原子操作，匹配之后文件仍可能被删除或替换，后续读取必须按照实际结果处理失败。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面的完整代码块需要 Bash 和常见临时目录工具。它自行创建测试目录，分别验证空格文件名、字面模式、未匹配策略和隐藏文件，退出时只删除自己的测试目录。不需要提前准备文件，也不会对当前目录已有文本文件执行操作。&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;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;export LC_ALL=C
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set +f
&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 GLOBIGNORE GLOBSORT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;shopt -u nullglob failglob dotglob
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;work=$(mktemp -d)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;trap &amp;#39;rm -rf -- &amp;#34;$work&amp;#34;&amp;#39; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;cd &amp;#34;$work&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;touch -- a.txt &amp;#39;two words.txt&amp;#39; .hidden.txt -option.txt
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;files=(./*.txt)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${#files[@]} -eq 3 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${files[2]} == &amp;#39;./two words.txt&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;match &amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;${files[@]}&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;literal=(&amp;#39;*.txt&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;[[ ${literal[0]} == &amp;#39;*.txt&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;missing=(./*.missing)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${missing[0]} == &amp;#39;./*.missing&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;shopt -s nullglob
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;missing=(./*.missing)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${#missing[@]} -eq 0 ]]
&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 (shopt -s failglob; printf &amp;#39;%s\n&amp;#39; ./*.missing) 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 match\n&amp;#39; &amp;gt;&amp;amp;2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; exit 1
&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;shopt -s dotglob
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;files=(./*.txt)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ ${#files[@]} -eq 4 ]]
&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;PASS: spaces, literal, no-match policies, hidden file\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;BASH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;预期先输出三行匹配结果，其中含空格的文件名完整地处于一行，最后输出通过信息。最初数组没有隐藏文件，开启 dotglob 后才变成四项。触发 failglob 的子 shell 失败属于预期结果，不能执行到打印文件名的命令；如果它反而成功，外层会明确报错退出。这些断言检查的是 Bash 展开和参数保留行为，不能代替接收程序对零参数、目录或选项的语义验证。把示例中的无害打印替换成真实操作前，还应检查该程序的输入契约。&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/Filename-Expansion.html"&gt;GNU Bash：文件名展开&lt;/a&gt;，匹配规则和未匹配选项，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Expansions.html"&gt;GNU Bash：Shell 展开&lt;/a&gt;，展开顺序与参数数量变化，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Arrays.html"&gt;GNU Bash：数组&lt;/a&gt;，收集匹配结果与保留元素边界，已核验 2026-09-06。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>$@ 等特殊变量给出函数的完整入参清单</title><link>https://www.xbcatm.com/zh/posts/shell-special-parameters-dollar-at/</link><pubDate>Fri, 16 Apr 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-special-parameters-dollar-at/</guid><description>$@、$#、$* 这类特殊变量，把函数或脚本收到的整份参数、数量与范围一次性交出来，无需逐个拿 $1。</description><content:encoded>&lt;p&gt;包装脚本应当把收到的参数按原有边界继续传递。在 Bash 的命令参数位置，&lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; 会让每个位置参数分别成为一个带引号的词，因此能保留空值、空格、换行和字面通配符。只用两个简单文件名测试成功，并不能证明包装脚本正确；一旦它把参数列表拼成字符串再拆开，真实调用者传入的边界就可能丢失。转发是否可靠，应当用能暴露这种差异的输入来验证。&lt;/p&gt;
&lt;h2 id="参数列表与拼接文本不同"&gt;参数列表与拼接文本不同&lt;/h2&gt;
&lt;p&gt;位置参数属于当前脚本或正在执行的函数。&lt;code&gt;$#&lt;/code&gt; 返回参数数量，内容为空字符串的参数也计算在内。&lt;code&gt;&amp;quot;$1&amp;quot;&lt;/code&gt; 读取第一个参数，&lt;code&gt;&amp;quot;${10}&amp;quot;&lt;/code&gt; 读取第十个；后者需要花括号，以区别于第一个参数后面接字符零的写法。&lt;code&gt;$0&lt;/code&gt; 表示启动的 shell 或脚本名称，不会作为额外元素出现在 &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; 中，不能把它误算进业务参数列表。&lt;/p&gt;
&lt;p&gt;在普通命令边界上，&lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; 为每个参数保留一个独立词；完全没有位置参数时，它不产生任何参数。相比之下，&lt;code&gt;&amp;quot;$*&amp;quot;&lt;/code&gt; 始终形成一个字符串，用 IFS 的第一个字符连接各个值。IFS 未设置时使用空格，设为空字符串时则不插入分隔符。它并不会删除参数内部原有的空格，但各参数原来的边界通常已经无法恢复，因此不适合转发任意列表。&lt;/p&gt;
&lt;p&gt;省略引号的 &lt;code&gt;$@&lt;/code&gt; 不是等价简写，展开后的内容仍可能被分词和文件名展开处理。调用者传入的字面星号，到了包装脚本所在目录可能突然匹配出一组文件。往拼接文本里补引号字符也不能恢复结构。需要把列表保存到之后使用时，在 Bash 中写成 &lt;code&gt;args=(&amp;quot;$@&amp;quot;)&lt;/code&gt;，并通过 &lt;code&gt;&amp;quot;${args[@]}&amp;quot;&lt;/code&gt; 转发，即使随后调整位置参数，保存的列表仍保留各个元素。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;函数执行期间拥有自己的位置参数列表。调用时传入的参数暂时替换当前可见列表，返回时恢复调用者的列表。函数内部使用 set 或 shift 修改的是这次函数调用的参数，但这不代表其他所有 shell 状态都自动局部化。例如，没有局部声明就修改 IFS，可能影响调用者之后的命令。参数作用域和普通变量作用域应分别理解，不能互相推导。&lt;/p&gt;
&lt;p&gt;Shift 会删除前面的若干参数，并对剩余参数重新编号。解析选项时，必须先确认剩余参数足够，再消费选项及其值；要求删除的数量超过现有参数时会失败。仅检查当前数量大于零，还不足以处理需要额外值的选项。如果之后的错误提示或转发还需要被消费的原始值，应在开始解析前保存数组，避免试图从拼接日志中重新恢复。&lt;/p&gt;
&lt;p&gt;引号也不能阻止接收程序把完整参数解释为选项。例如，文件名恰好为 &lt;code&gt;--help&lt;/code&gt; 时，&lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; 会完整保留它，但目标工具仍可能显示帮助而不读取文件。传入操作数时，应使用工具文档规定的选项终止符，或适当添加路径前缀。包装脚本必须明确自己的选项在哪里结束，以及哪些剩余参数仍是下游命令的选项，不能不加区分地自动插入分隔符。&lt;/p&gt;
&lt;p&gt;诊断时应显示参数数量，并逐项打印。用 &lt;code&gt;&amp;quot;$*&amp;quot;&lt;/code&gt; 拼成一行，容易让零参数、一个空参数和包含分隔符的参数看起来相同。Bash printf 的 &lt;code&gt;%q&lt;/code&gt; 格式可以帮助观察不明显的字符和边界，但记录日志与转发参数承担不同职责。真正执行时应继续使用保存的列表，不应把日志行重新解释为命令，否则又引入一次解析并失去原有保障。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面的完整代码块构造四项参数，分别为空字符串、带空格的文件名、字面星号模式和看起来像选项的值。随后检查函数参数作用域、零参数时两种引用展开的差异，以及越界 shift 的返回结果。实验只需要 Bash，不需要预先准备文件或启动外部服务。&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;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;IFS=$&amp;#39; \t\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;set -- &amp;#39;&amp;#39; &amp;#39;report final.md&amp;#39; &amp;#39;*.log&amp;#39; &amp;#39;--help&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;saved=(&amp;#34;$@&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;verify() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; [[ $# -eq 4 &amp;amp;&amp;amp; $1 == &amp;#39;&amp;#39; &amp;amp;&amp;amp; $2 == &amp;#39;report final.md&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; [[ $3 == &amp;#39;*.log&amp;#39; &amp;amp;&amp;amp; $4 == &amp;#39;--help&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;arg &amp;lt;%q&amp;gt;\n&amp;#39; &amp;#34;$@&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;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;verify &amp;#34;${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;consume() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; [[ $# -eq 2 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; shift
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; [[ $# -eq 1 &amp;amp;&amp;amp; $1 == child-two ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; set -- replaced
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; [[ $1 == replaced ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;consume child-one child-two
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $# -eq 4 &amp;amp;&amp;amp; $2 == &amp;#39;report final.md&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;joined=$(IFS=,; printf &amp;#39;%s&amp;#39; &amp;#34;$*&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;[[ $joined == &amp;#39;,report final.md,*.log,--help&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;count() { printf &amp;#39;%s&amp;#39; &amp;#34;$#&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;set --
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(count &amp;#34;$@&amp;#34;) == 0 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(count &amp;#34;$*&amp;#34;) == 1 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -- sole
&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 shift 2; 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 shift success\n&amp;#39; &amp;gt;&amp;amp;2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; exit 1
&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;[[ $# -eq 1 &amp;amp;&amp;amp; $1 == sole ]]
&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;PASS: forwarding, function scope, empty list, shift limit\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;BASH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;预期先输出四行转义后的参数，最后输出通过信息。第一项应明确为空，星号保持字面意义。consume 函数返回后，外层仍应拥有原来的四个参数，而不是函数内部修改后的列表。对仅剩一个参数的列表执行删除两项应失败，并保留原参数。这些断言验证的是参数传输机制；真实包装脚本仍需针对自己的选项语法、必填值和下游调用方式进行验证，不能把机制测试当作所有命令行协议都正确的证明。&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/Special-Parameters.html"&gt;GNU Bash：特殊参数&lt;/a&gt;，位置参数展开、拼接和计数，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Arrays.html"&gt;GNU Bash：数组&lt;/a&gt;，保存和转发独立元素，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Word-Splitting.html"&gt;GNU Bash：分词&lt;/a&gt;，未引用展开与空参数，已核验 2026-09-06。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>退出码是用一个数字汇报成败</title><link>https://www.xbcatm.com/zh/posts/shell-exit-status-reports-blur/</link><pubDate>Sat, 27 Mar 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-exit-status-reports-blur/</guid><description>及时保存退出状态，并按目标命令的契约解释；Shell 的分支判断不等同于业务故障分类。</description><content:encoded>&lt;p&gt;退出状态是 shell 用来决定下一步执行什么的小整数。对 shell 控制流来说，零表示成功，非零进入失败分支，但这并不意味着每个数字都有统一的业务含义。搜索没有匹配、输入格式错误和服务器不可用，都可能返回非零，却需要不同处理。决定重试、忽略或终止之前，应先阅读目标命令的契约。不能只因为状态不为零，就把所有结果归为同一种事故。&lt;/p&gt;
&lt;h2 id="让状态始终对应原来的命令"&gt;让状态始终对应原来的命令&lt;/h2&gt;
&lt;p&gt;Bash 通过 &lt;code&gt;$?&lt;/code&gt; 提供最近前台命令的状态。需要保留时，必须在记录日志、执行另一个条件判断或清理操作之前保存；这些操作也有自己的状态，会覆盖原本想观察的证据。函数通常使用最后执行的命令状态作为返回值，因此，末尾一句成功的日志输出就可能把前面的失败包装成整个函数成功，调用者看到的结果与实际工作脱节。&lt;/p&gt;
&lt;p&gt;当命令成功与否决定后续动作时，可以使用 &lt;code&gt;if command; then ... else ... fi&lt;/code&gt;。如果需要精确退出码，在 else 分支第一步保存 &lt;code&gt;$?&lt;/code&gt;，输出诊断或清理后再通过 return 或 exit 显式传播。不要写成 &lt;code&gt;if ! command&lt;/code&gt;，却期待进入成功分支后的 &lt;code&gt;$?&lt;/code&gt; 仍然是原始失败码：逻辑取反已经把非零结果转换成成功，数值含义在这里发生了变化。&lt;/p&gt;
&lt;p&gt;Bash 在找不到命令时使用一百二十七，找到却无法执行时使用一百二十六。因致命信号终止时，它使用一百二十八加信号编号的约定。这些规则有助于理解 shell 的报告，但应用也可以主动返回相同数字。因此，只有一个状态值还不足以证明内部具体发生了哪件事；应同时保留失败操作的身份和标准错误信息，避免根据数字臆造故障原因。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;管道默认使用最后一段的退出状态。上游生成器失败时，下游仍可能成功读取一个空流或不完整数据，从而让整体看起来成功。开启 pipefail 后，Bash 返回从左到右最靠右的非零状态；全部成功才为零。PIPESTATUS 能提供每一段的结果，但后续命令同样会覆盖这个数组，所以必须先完整保存观察结果，再进行打印或判断。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;set -e&lt;/code&gt; 开启的 errexit 具有由语法和调用上下文决定的例外。作为 if 条件的失败、与或列表中的部分命令，以及其他文档规定的场景，都不会自动结束脚本。函数被用作条件时，函数体中的失败命令之后也可能继续执行。ERR trap 也有类似排除规则，并不是每次失败都会产生的通知机制。两者都不能证明整个脚本已经可靠处理所有错误。&lt;/p&gt;
&lt;p&gt;大范围追加 &lt;code&gt;|| true&lt;/code&gt; 会丢掉有用区别。只有该操作所有失败都可接受时，这种写法才表达了真实需求；否则，它可能一起吞掉权限错误或命令不存在等意外问题。应单独处理文档规定的预期结果，并继续传播其他失败。另外，&lt;code&gt;first &amp;amp;&amp;amp; second || recover&lt;/code&gt; 会在 second 失败时执行 recover，它并不等同于只判断 first 的 if 语句，清理逻辑不能建立在这个错误等价关系上。&lt;/p&gt;
&lt;p&gt;后台任务还存在启动与完成之间的边界。成功启动并不表示任务最终成功，应立即保存 &lt;code&gt;$!&lt;/code&gt; 中的任务标识，并检查等待该任务时 wait 返回的状态。如果启动多项必需工作，就应分别保留标识并收集结果，否则最后一条成功的前台命令可能掩盖已经在后台失败的工作。返回给调用者的状态应对应业务所需的全部完成条件。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面完整的 Bash 实验主动构造失败，并断言状态如何传播。它检查即时保存、逻辑取反、被当作条件调用的函数，以及启动后失败的后台任务。不需要准备文件，也不会访问外部服务；其中的非零结果是有意安排的测试输入。&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;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;fail_seven() { return 7; }
&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 fail_seven; 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; exit 1
&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; saved=$?
&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;printf &amp;#39;saved status %s\n&amp;#39; &amp;#34;$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;[[ $saved -eq 7 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $? -eq 0 ]]
&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 ! fail_seven; 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; negated=$?
&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;[[ $negated -eq 0 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;continued=no
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;conditional_function() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; false
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; continued=yes
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&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 conditional_function; 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; [[ $continued == yes ]]
&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; exit 1
&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;(exit 6) &amp;amp;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;job_pid=$!
&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 wait &amp;#34;$job_pid&amp;#34;; 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; exit 1
&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; job_status=$?
&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;[[ $job_status -eq 6 ]]
&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;PASS: saved failure, negation, conditional context, wait\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;BASH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;预期首先打印保存的退出码七，然后出现通过提示。取反后的状态应为零；即使开启 errexit，条件函数仍然执行到修改变量的语句；wait 则应报告子任务的退出码六。这些断言揭示控制流的边界，不会替任意工具定义状态码含义。实际采用时，还应分别测试预期的否定结果和意外运行错误，并确认包装脚本最终向调用者返回了真正需要的结果，而不是最后一次打印或清理的偶然状态。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/s/bash/manual/html_node/Exit-Status.html"&gt;GNU Bash：退出状态&lt;/a&gt;，shell 状态约定，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/s/bash/manual/html_node/The-Set-Builtin.html"&gt;GNU Bash：Set 内建命令&lt;/a&gt;，errexit 的排除场景，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Pipelines.html"&gt;GNU Bash：管道&lt;/a&gt;，管道整体状态规则，已核验 2026-09-06。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Shell 函数像脚本一样读取自己的参数</title><link>https://www.xbcatm.com/zh/posts/shell-functions-borrow-positional-parameters/</link><pubDate>Tue, 16 Mar 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-functions-borrow-positional-parameters/</guid><description>函数内部，$1、$2 等引用的是本函数的参数而非整个脚本的，让逻辑可复用而不依赖全局。</description><content:encoded>&lt;p&gt;Shell 函数把一组命令组织到同一个名称之下，并接收本次调用自己的位置参数。普通调用时，函数体在当前 Shell 中执行，不会自动启动独立进程。这方便复用操作，也意味着赋值、切换目录及其他 Shell 状态变化可能影响调用方。函数接口除了规定输入，还应明确输出、退出状态和有意产生的副作用，不能只因为把几行命令包进了函数，就认为它们已经与外层隔离。&lt;/p&gt;
&lt;h2 id="每次调用都有自己的参数列表"&gt;每次调用都有自己的参数列表&lt;/h2&gt;
&lt;p&gt;调用 &lt;code&gt;demo_func first second&lt;/code&gt; 后，这次函数内部的前两个位置分别是传入的两个值，参数数量为二。再次调用其他函数，会暂时安装下一层自己的参数列表；返回后，调用者的列表重新可见。因此，在被调用函数中移位，不会消耗调用者的位置参数。Bash 的 &lt;code&gt;$0&lt;/code&gt; 也不会因此变成当前函数名称，不能用它判断自己正在哪个函数里面。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; 转发整份列表，才能保留空参数、空格以及通配符字符各自的边界。应先检查数量，再读取必需的位置，同时区分省略参数与明确传入空字符串。Shell 函数没有自动执行这些规则的参数声明，调用时语法上能写出一串值，并不意味着这些值已经满足函数的业务约定。&lt;/p&gt;
&lt;h2 id="局部变量使用动态作用域"&gt;局部变量使用动态作用域&lt;/h2&gt;
&lt;p&gt;Bash 的局部声明会创建一个变量，遮住外层的同名绑定；函数返回之后，原来的绑定再次可见。若修改调用者变量并不是接口的一部分，应为临时值、循环计数器等声明局部变量。面向多个解释器的可移植脚本，不能假设每个 Shell 都实现了 Bash 的这套局部变量设施，解释器要求必须与使用的语法保持一致。&lt;/p&gt;
&lt;p&gt;但 Bash 采用动态作用域：被另一个函数调用的函数，可以看到调用者已经声明的局部变量。如果它没有声明自己的局部变量，就直接给同名变量赋值，修改的可能正是调用者的局部绑定。最外层的全局值依然受保护，但中间那层函数的值已经改变。因此，局部变量不是其他函数完全不能接触的词法私有对象，沿真实调用链测试才看得见这种影响。&lt;/p&gt;
&lt;p&gt;普通的局部变量声明不会自动恢复工作目录、Shell 选项、陷阱，或函数体已经改写的文件。Bash 另外提供 &lt;code&gt;local -&lt;/code&gt;，可以在函数返回时恢复通过 set 改动的选项；这是一项明确的选项作用域机制，与单纯声明临时变量不同。这些效果都需要各自的约定。如果把函数放进普通命令替换，它会在子 Shell 环境中运行，变量和目录变化不会回到外层，但文件写入仍可能保留。捕获输出改变的是执行边界，不是为任意操作提供一套安全沙箱。&lt;/p&gt;
&lt;h2 id="状态与数据走不同通道"&gt;状态与数据走不同通道&lt;/h2&gt;
&lt;p&gt;没有显式返回时，函数通常采用最后执行命令的状态。标准输出应承载约定的数据，诊断则写到标准错误。&lt;code&gt;return 7&lt;/code&gt; 把一个状态交给调用者，不是返回文本数字作为数据；&lt;code&gt;exit&lt;/code&gt; 会结束正在执行的 Shell，而不只是离开当前函数。设计辅助函数时，需要让调用者知道究竟应读取哪个通道，以及失败后还能不能使用已经打印出的部分结果。&lt;/p&gt;
&lt;p&gt;当生产者退出状态重要时，把局部声明与命令替换赋值分开。写成 &lt;code&gt;local result=$(producer)&lt;/code&gt; 时，观察到的是声明内建命令的状态，它可能掩盖生产者失败；先声明，再单独赋值，赋值语句才能反映命令替换的状态。之后仍需在日志或清理覆盖它之前保存。把两句合成一行虽然简短，却可能改变错误传播，不能当作纯粹的排版差异。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;没有局部声明的计数器可能破坏调用者的循环，即便调用者已经把自己的计数器声明为局部也一样。测试辅助函数时，应既从顶层调用，也从另一层函数调用；只检查全局值是否恢复，会漏掉动态作用域影响。同样，标准输出上意外出现的进度消息，会污染原本只捕获一个路径或值的调用方，测试应检查实际捕获结果而不只是看屏幕。&lt;/p&gt;
&lt;p&gt;不要依赖错误即退出选项，认为函数内每次失败都会可预测地停下。它受调用上下文影响：被用作条件的函数，体内某条命令失败之后仍可能继续执行。对于失败后必须停止后续工作的操作，应明确检查结果，并在诊断之后传播约定的状态。函数的返回值要代表其契约要求的工作结果，而不是最后一条成功打印语句的偶然状态。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;在 Bash 中运行下面完整代码块。它只在新 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;&lt;span class="nv"&gt;BASH_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&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;demo_value=global
&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_worker() { demo_value=changed; }
&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_scope() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; local demo_value=inner
&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;local-before=%s\n&amp;#39; &amp;#34;$demo_value&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_worker
&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;local-after=%s\n&amp;#39; &amp;#34;$demo_value&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;}
&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_scope
&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;global-after=%s\n&amp;#39; &amp;#34;$demo_value&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_args() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; local demo_arg
&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;function-count=%s\n&amp;#39; &amp;#34;$#&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; for demo_arg in &amp;#34;$@&amp;#34;; do printf &amp;#39;argument=&amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;$demo_arg&amp;#34;; done
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; shift
&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;after-shift=%s\n&amp;#39; &amp;#34;$#&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; return 7
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;set -- outer-one outer-two
&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 demo_args &amp;#39;&amp;#39; &amp;#39;two words&amp;#39;; 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; exit 99
&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; demo_status=$?
&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;printf &amp;#39;function-status=%s outer-count=%s outer-first=%s\n&amp;#39; &amp;#34;$demo_status&amp;#34; &amp;#34;$#&amp;#34; &amp;#34;$1&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_masked() { local demo_result=$(return 9); }
&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_checked() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; local demo_result
&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_result=$(return 9)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&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_masked
&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;combined-declaration=%s\n&amp;#39; &amp;#34;$?&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;if demo_checked; then exit 99; else printf &amp;#39;separate-assignment=%s\n&amp;#39; &amp;#34;$?&amp;#34;; 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;inner&lt;/code&gt; 变成 &lt;code&gt;changed&lt;/code&gt;，但全局值仍是 &lt;code&gt;global&lt;/code&gt;。两个传入参数应分别打印，其中一个为空；函数移位后返回七，外层依然持有两个参数，第一个仍是 &lt;code&gt;outer-one&lt;/code&gt;。最后，合并声明报告零，独立赋值报告九。这些结果验证真实调用边界，也揭示局部声明能够保护什么、不能保护什么，避免把“变量叫局部”误当成完全隔离的证据。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Shell-Functions"&gt;Bash Shell 函数&lt;/a&gt;，函数参数列表及动态变量作用域，已核验 2026-09-07。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Bash-Builtins"&gt;Bash 专用内建命令&lt;/a&gt;，局部声明行为及其返回状态，已核验 2026-09-07。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#The-Set-Builtin"&gt;Bash set 内建命令&lt;/a&gt;，函数作为条件时的错误即退出语义，已核验 2026-09-07。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>命令替换把一条命令的输出嵌进另一行</title><link>https://www.xbcatm.com/zh/posts/shell-command-substitution-embeds-output/</link><pubDate>Wed, 03 Mar 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-command-substitution-embeds-output/</guid><description>把命令包进 $(...) 后，它的标准输出会替换到这里成为参数或单词，于是脚本可在运行期用一条命令的结果实时拼装另一行。</description><content:encoded>&lt;p&gt;命令替换会收集一个命令的标准输出，再把得到的文本放入外层表达式。常见的 &lt;code&gt;$(command)&lt;/code&gt; 适合获取时间戳、计算出的标签或其他较小的文本值，但它并不是任意字节的透明传输通道，也不是可靠的文件名列表容器。使用前需要理解三个边界：末尾换行如何处理、内部命令在哪个执行环境运行，以及失败状态如何传递。只看到变量里有内容，不能证明整个操作成功。&lt;/p&gt;
&lt;h2 id="先收集文本再决定怎样使用"&gt;先收集文本，再决定怎样使用&lt;/h2&gt;
&lt;p&gt;本文讨论的标准命令替换形式会让 Bash 在子 shell 环境中运行内部命令，收集标准输出，并删除末尾所有换行符。位于文本中间的换行仍然保留。双引号能阻止后续分词和文件名展开，却不能恢复已经删除的尾部换行。如果需要精确保留最后几个字节，应该使用文件或数据流，而不是依靠 shell 变量原样往返。&lt;/p&gt;
&lt;p&gt;赋值语句 &lt;code&gt;result=$(producer)&lt;/code&gt; 会把处理后的文本存进变量，不在此处拆成多个参数。后续使用时仍应写成 &lt;code&gt;&amp;quot;$result&amp;quot;&lt;/code&gt;，保留它的参数边界。类似地，&lt;code&gt;consumer &amp;quot;$(producer)&amp;quot;&lt;/code&gt; 会传递一个参数，即使结果为空。去掉引号可能得到多个参数，也可能完全没有参数；输出中的星号还可能匹配当前目录的文件，让同样的生产者输出在不同目录产生不同调用结果。&lt;/p&gt;
&lt;p&gt;标准错误和标准输出彼此独立，除非主动重定向，诊断信息仍可能显示在终端，而变量只收集结果正文。将两路输出合并后，警告文本可能污染本应供程序读取的值。因此，应先确定生产者的输出契约，再决定是否合并；直接吞掉所有诊断信息也可能让空结果失去可解释的原因。输出内容、诊断信息和退出状态应分别检查。&lt;/p&gt;
&lt;p&gt;不要把这里的作用域结论推广到较新 Bash 的每一种捕获语法。本文使用的美元符号加圆括号形式具有实验验证的子 Shell 行为；较新的当前环境捕获形式会保留不同的副作用。说明一个捕获值时，应明确语法与解释器版本，而不是把所有能够取得文本的写法都视为等价。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;子 shell 内修改变量或执行 cd，不会改变调用者对应的变量和工作目录。但文件系统或网络副作用并不会因此消失：在命令替换里创建的文件是真实文件，发出的请求也是真实请求。执行环境隔离不能被误解为事务回滚，也不能保证内部操作没有外部影响。&lt;/p&gt;
&lt;p&gt;有输出也不代表生产者成功。仅包含赋值的命令会使用最后一个命令替换的退出状态，因此 &lt;code&gt;if result=$(producer); then&lt;/code&gt; 可以检查生产者结果。相反，&lt;code&gt;printf '%s' &amp;quot;$(producer)&amp;quot;&lt;/code&gt; 的状态通常来自 printf，而不是内部命令。生产者可以先输出部分内容，再失败退出；应立即保存退出码，并明确失败时是否允许使用这些不完整内容。后面的打印日志命令会覆盖 &lt;code&gt;$?&lt;/code&gt; 所指向的最近状态。&lt;/p&gt;
&lt;p&gt;多条命令组成的替换尤其需要明确失败传播：前一步失败后，最后一步仍可能成功，从而使整个替换返回成功。重要操作应在生产者内部显式判断，并返回预期状态。函数中如果需要检查捕获赋值的状态，也应把 local 声明和赋值分成两步，避免声明内建命令掩盖生产者退出码。不能仅凭脚本开启了某个全局选项，就假设每种上下文都会按同样方式中止。&lt;/p&gt;
&lt;p&gt;命令替换还会在外层命令运行前收集输出，因此不适合无限输出或需要逐步消费的交互数据。不要通过未引用的 find 输出构造任意文件名列表，空白和通配字符会破坏原有路径边界。可以改用 find 的 exec 接口直接传递路径，或者选择明确定义边界的记录协议。是否需要一个文本值，还是一组独立记录，应在选择语法前确定。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面代码块只使用 Bash 内建命令，不需要准备文件或服务。它把输出内容和退出码分别断言，并主动构造退出码为七的失败生产者。这个失败是实验的一部分，整个测试仍必须运行到最后的通过提示，不能把任意非零输出都当成环境故障。&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;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;outer=parent
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;captured=$(outer=child; printf &amp;#39;first\nsecond\n\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;[[ $outer == parent ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $captured == $&amp;#39;first\nsecond&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;captured &amp;lt;%q&amp;gt;\n&amp;#39; &amp;#34;$captured&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;if partial=$(printf &amp;#39;incomplete&amp;#39;; exit 7); 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 producer success\n&amp;#39; &amp;gt;&amp;amp;2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; exit 1
&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; status=$?
&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;[[ $status -eq 7 &amp;amp;&amp;amp; $partial == incomplete ]]
&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;producer status %s; partial &amp;lt;%s&amp;gt;\n&amp;#39; &amp;#34;$status&amp;#34; &amp;#34;$partial&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;%s&amp;#39; &amp;#34;$(exit 9)&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;[[ $? -eq 0 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;IFS=$&amp;#39; \t\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;set -- &amp;#34;$(printf &amp;#39;two words\n&amp;#39;)&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;[[ $# -eq 1 &amp;amp;&amp;amp; $1 == &amp;#39;two words&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;set -- &amp;#34;$(printf &amp;#39;\n\n&amp;#39;)&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;[[ $# -eq 1 &amp;amp;&amp;amp; $1 == &amp;#39;&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;PASS: capture, subshell scope, failure status, empty result\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;BASH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第一行以转义形式显示捕获值，应该只包含正文中间的一个换行，没有尾部换行。下一行同时显示退出码七和部分输出，证明失败状态与非空正文可以同时存在。之后外层 printf 成功的断言说明，只检查消费者状态会漏掉生产者失败。最后还检查了空结果在引号内形成一个空参数。这些实验验证的是文中几个小型同步生产者的行为；真实命令若存在耗时、输出格式或错误语义要求，仍应分别验证这些契约。&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/Command-Substitution.html"&gt;GNU Bash：命令替换&lt;/a&gt;，标准替换形式的执行环境和换行处理，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Word-Splitting.html"&gt;GNU Bash：分词&lt;/a&gt;，未引用捕获结果的处理，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html"&gt;GNU Bash：特殊参数&lt;/a&gt;，问号参数提供的最近退出状态，已核验 2026-09-06。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Here-string 提供文本并追加换行</title><link>https://www.xbcatm.com/zh/posts/shell-here-string-feeds-stdin/</link><pubDate>Mon, 15 Feb 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-here-string-feeds-stdin/</guid><description>Bash 的 here-string 把展开后的字符串加上换行交给标准输入；仍需验证接收方的字节与解析规则。</description><content:encoded>&lt;p&gt;Bash 的 here-string 会把一个词展开后的值送给命令的标准输入，并在末尾追加一个换行符。当变量里已经有少量文本，而目标程序从标准输入读取时，这种写法很方便。它省掉了额外的打印命令，但并不保证输入与变量逐字节相同，也不承诺内部一定使用内存、管道或文件。判断是否适用，应看接收方需要什么输入，而不是只比较命令行长度。&lt;/p&gt;
&lt;h2 id="接收命令读取之前先完成展开"&gt;接收命令读取之前先完成展开&lt;/h2&gt;
&lt;p&gt;三个小于号之后的词可以包含变量展开、算术展开和命令替换，因此不一定是字面常量。单引号保留源码中的字面文本，双引号则允许通常的变量和命令展开。通过变量展开得到的字符串，即使包含美元符号和括号，也不会仅因出现在 here-string 中就被再次作为 shell 命令执行。源码中明确写出的替换语法与变量保存的数据必须区分。&lt;/p&gt;
&lt;p&gt;当前 Bash 不会对 here-string 的词执行分词或文件名展开，而是把结果作为一个字符串交付。这里引用变量仍能清楚表达意图，但不能把这种重定向上下文和普通命令参数位置混为一谈。后者使用未引用变量时仍可能按空白拆分，或把星号变成文件名。因此，某个值通过 here-string 正常到达，并不能证明在别处省略引号也安全。&lt;/p&gt;
&lt;p&gt;末尾追加换行是无条件发生的。原值没有换行时会多一个，原值已经有尾部换行时还会再多一个。空变量也因此产生包含一个换行字节的输入，而不是立即结束的零字节输入。对于按行处理的工具，这常常符合需求；对于严格长度、签名字节或特定结束字符的协议，则可能直接改变意义，不能把它当作原始字节透传方式。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;Shell 形成输入后，read 还会按自己的规则解释。默认分词可能移除首尾 IFS 空白，反斜杠也可能作为转义符处理。读取需要保留空格和反斜杠的一行时，应使用 &lt;code&gt;IFS= read -r name&lt;/code&gt;。它保护的是这一行的内容；单次 read 仍然在第一个换行处结束，并不会自动把任意多行字符串全部装进变量。输入构造规则与消费者解析规则是两个阶段。&lt;/p&gt;
&lt;p&gt;带重定向的内建命令或循环可以在当前 shell 中运行，因此其中的赋值能够在后续代码中继续使用。相比把循环放进管道，这有助于避免 Bash 通常为管道阶段创建子 shell 导致的状态丢失。重定向本身不会包上一层子 shell，但如果表达式的其他地方明确使用子 shell 或命令替换，它们仍然具有通常的作用域与副作用，不能因为用了 here-string 就忽略。&lt;/p&gt;
&lt;p&gt;源码没有显式文件名，并不能证明 Bash 内部没有使用文件或其他存储方式。程序不应依赖标准输入可寻址、具有固定缓冲容量，或绝不产生临时后端存储。对于已经存在的大文件，直接文件重定向能避免先把全部内容收集进变量。如果需要不追加换行的精确文本，可以使用格式明确的 printf 或文件输入，仍应通过接收方实际读到的字节验证结果。&lt;/p&gt;
&lt;p&gt;Here-string 还是特定 shell 的语法功能，并非所有名为 sh 的解释器都支持。使用它的脚本应选择支持该功能的解释器，测试时也应明确调用同一个解释器。把 echo 管道机械替换为 here-string，可能同时改变换行和变量作用域，不能只当作外观不同的等价写法。应该比较输入字节、记录数量和状态变化，确认真实调用链仍符合需求。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面的完整实验需要 Bash 和 Python 三。Python 将标准输入显示成十六进制，使额外的换行字节清晰可见；随后捕获的是十六进制文本，所以命令替换删除其末尾换行并不会丢失被检查的原始输入信息。其他断言使用 read 和带重定向的循环验证空白、字面文本与多行读取，不需要创建测试文件。&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;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;hex_input() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; python3 -c &amp;#39;import sys; print(sys.stdin.buffer.read().hex())&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;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(hex_input &amp;lt;&amp;lt;&amp;lt; X) == 580a ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(hex_input &amp;lt;&amp;lt;&amp;lt; &amp;#39;&amp;#39;) == 0a ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(hex_input &amp;lt;&amp;lt;&amp;lt; $&amp;#39;X\n&amp;#39;) == 580a0a ]]
&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;%s&amp;#39; X | hex_input) == 58 ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;value=&amp;#39; a\b *.txt &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;IFS= read -r received &amp;lt;&amp;lt;&amp;lt; &amp;#34;$value&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;[[ $received == &amp;#34;$value&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;literal=&amp;#39;$(printf injected)&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;IFS= read -r received &amp;lt;&amp;lt;&amp;lt; &amp;#34;$literal&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;[[ $received == &amp;#39;$(printf injected)&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;value=$&amp;#39;one\ntwo&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;IFS= read -r first &amp;lt;&amp;lt;&amp;lt; &amp;#34;$value&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;[[ $first == one ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;lines=0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;while IFS= read -r line; do
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; lines=$((lines + 1))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;done &amp;lt;&amp;lt;&amp;lt; &amp;#34;$value&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;[[ $lines -eq 2 ]]
&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;PASS: newline bytes, preserved text, single read, loop scope\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;BASH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;预期只输出最后的通过信息。十六进制断言明确区分一个换行和两个换行，并与不添加换行的 printf 比较。看起来像命令的字面字符串必须保持原文，循环计数也应保留在外层 shell 中。这些结果验证文档规定的输入行为，没有检查内部存储机制。改用其他消费者之前，还应确认它自身如何处理空行、多条记录和尾部空白，不能把 read 的观察直接推广到所有程序。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/s/bash/manual/html_node/Redirections.html"&gt;GNU Bash：重定向&lt;/a&gt;，here-string 展开与追加换行规则，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/s/bash/manual/html_node/Bash-Builtins.html"&gt;GNU Bash：Bash 内建命令&lt;/a&gt;，read 分隔和原始输入模式，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html"&gt;GNU Bash：命令替换&lt;/a&gt;，捕获输出与尾部换行删除，已核验 2026-09-06。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>case 只执行与模式匹配的那一支</title><link>https://www.xbcatm.com/zh/posts/shell-case-branches-match-patterns/</link><pubDate>Sat, 30 Jan 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-case-branches-match-patterns/</guid><description>case 语句把某个词依次与若干模式作比较，命中第一种才执行那一支——这是按值分派的一个整洁写法。</description><content:encoded>&lt;p&gt;Case 语句把一个值依次与一组 shell 模式比较，适合命令分发、有限答案选择或文件名后缀分类。每个分支明确自己处理哪些输入，多个模式也可以共享同一个动作。分支顺序和默认行为都是接口的一部分：输入同时属于多个类别时谁先处理，完全不属于已知类别时是否报错，都需要明确规定。不能只看到成功输入能跑通，就认为整个分发器已经可靠。&lt;/p&gt;
&lt;h2 id="模式匹配整个值"&gt;模式匹配整个值&lt;/h2&gt;
&lt;p&gt;Case 使用的是 shell 模式，不是正则表达式。星号匹配任意长度字符串，问号匹配一个字符，方括号描述字符集合或范围。因此，&lt;code&gt;[0-9]*&lt;/code&gt; 表示一个数字后面可以跟任意内容，并不表示整串都是数字。若要验证非空十进制文本，可以先拒绝空字符串，以及包含任意非数字字符的字符串，而不必把输入放进算术求值环境。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;start|up|run)&lt;/code&gt; 这样的多个模式可以共用动作。&lt;code&gt;*.log)&lt;/code&gt; 等后缀规则则只是在分类字符串，并不会打开文件或搜索目录。因此，匹配成功不能证明对应文件存在。与普通文件名展开不同，字符串匹配中的斜杠和开头的点也没有相同的特殊排除作用。分类、文件存在性和文件类型是不同判断，真正操作文件前仍应根据需求单独验证。&lt;/p&gt;
&lt;p&gt;Bash 会对被测试的词执行文档规定的展开，但不会像普通命令参数那样对它分词或执行文件名展开。因此，这个位置的未引用空变量不会删掉语法上的词，进而把 case 变成语法错误。引用被测试值仍然是清楚的书写习惯。若开启 nounset，则要另外处理未提供的位置参数，或先验证参数数量；引号本身不能让未设置参数变得可用。&lt;/p&gt;
&lt;p&gt;实际执行某个分支后，case 返回该分支最后执行命令的状态。因此，匹配成功与动作成功是两件事。下面的实验也让一个已经选中的分支返回六，并检查失败继续向外传播，避免把“找到了正确分支”误报成“分支里的操作已经成功”。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;使用通常的双分号结束符时，首个匹配分支执行后，case 就结束。精确值应放在宽泛类别前，兜底星号通常放在最后，否则通用选项分支会吞掉后面的精确状态命令。语法检查只能证明结构可解析，不能证明顺序符合业务优先级。应该构造同时匹配两个规则的输入，并断言实际执行哪个动作，才能发现被遮蔽的分支。&lt;/p&gt;
&lt;p&gt;如果没有任何模式匹配，case 不执行动作，却仍然返回零。因此，没有默认分支不等于会自动报错。命令分发器若要求拒绝未知命令，就必须在默认分支明确报告并返回失败；可选分类逻辑则可能有意允许不匹配。这个策略来自调用契约，不能假设 case 自带完整输入校验，更不能依赖自动退出选项补足业务含义。&lt;/p&gt;
&lt;p&gt;模式上的引号也会改变含义。&lt;code&gt;&amp;quot;*.log&amp;quot;&lt;/code&gt; 匹配字面字符，而不引用的写法匹配整个后缀类别。变量提供模式时同样需要区分：它究竟是受控的匹配表达式，还是应该精确比较的普通文本。不要让一个本来只是标识符的值意外获得通配能力，也不要为了“所有东西都加引号”而破坏本来需要的模式语义。&lt;/p&gt;
&lt;p&gt;选项和版本还会改变行为。Nocasematch 控制是否忽略大小写；扩展模式要求在相关代码被解析之前开启 extglob。较新 Bash 还提供继续执行或继续匹配的其他分支结束符，它们不同于双分号，也不一定能在旧 shell 上运行。下面实验刻意只使用普通模式和双分号，保持 Bash 三点二兼容语法，本轮实际使用 Bash 五点二运行，避免把可选语法混入基本规则。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面完整代码包含精确命令、别名组、通配类别、空输入错误和未知输入错误。第二个函数验证十进制文本，不会把数字开头但后面含字母的值放过。断言同时检查正常输出与错误码，而不是仅观察整个脚本最后有没有非零退出。&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;set -e
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;export LC_ALL=C
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;shopt -u nocasematch
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;route() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; case &amp;#34;${1-}&amp;#34; in
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; status|--status) printf &amp;#39;%s&amp;#39; status ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; start|up|run) printf &amp;#39;%s&amp;#39; start ;;
&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;#39;&amp;#39;) return 2 ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; --*) return 3 ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; *.log) printf &amp;#39;%s&amp;#39; log ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; *) return 4 ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; esac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(route --status) == status ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(route up) == start ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $(route &amp;#39;report final.log&amp;#39;) == log ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;expect_error() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; local expected=$1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; shift
&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 route &amp;#34;$@&amp;#34;; 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; return 1
&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; local actual=$?
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; [[ $actual -eq $expected ]]
&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;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;expect_error 2 &amp;#39;&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;expect_error 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;expect_error 3 --unknown
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;expect_error 4 STATUS
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;digits() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; case &amp;#34;${1-}&amp;#34; in
&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;#39;&amp;#39;|*[!0-9]*) return 1 ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; *) return 0 ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; esac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;digits 007
&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 digits 7abc; then exit 1; 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;if digits &amp;#39;&amp;#39;; then exit 1; 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;empty=&amp;#39;&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;matched=no
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;case $empty in &amp;#39;&amp;#39;) matched=yes ;; esac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $matched == yes ]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;case unmatched in known) exit 1 ;; esac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[[ $? -eq 0 ]]
&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 case match in match) (exit 6) ;; esac; 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; exit 1
&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; [[ $? -eq 6 ]]
&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;printf &amp;#39;PASS: precedence, aliases, empty input, validation, no match\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;BASH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;预期只输出最后的通过提示，没有其他正文。精确的状态选项必须优先于通用未知选项分支；由于实验关闭了忽略大小写，大写 STATUS 应当被拒绝。末尾的检查还证明，展开为空的被测试词语法有效，以及完全没有模式匹配时 case 会成功返回。这些断言只验证分发选择；把无害打印换成真实动作后，还需要独立检查参数、外部副作用和失败传播，不能把选中了正确分支等同于操作已经成功。&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/Conditional-Constructs.html"&gt;GNU Bash：条件结构&lt;/a&gt;，case 的顺序、展开和返回状态，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/s/bash/manual/html_node/Pattern-Matching.html"&gt;GNU Bash：模式匹配&lt;/a&gt;，通配符与扩展模式语义，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Word-Splitting.html"&gt;GNU Bash：分词&lt;/a&gt;，展开值发生分词的上下文，已核验 2026-09-06。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>