<?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>Shell · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/shell/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/shell/index.xml" rel="self" type="application/rss+xml"/><item><title>bash history 是可以搜索的命令日志</title><link>https://www.xbcatm.com/zh/posts/linux-bash-history-session/</link><pubDate>Mon, 28 Feb 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/linux-bash-history-session/</guid><description>Bash 历史便于复用命令，但会话内存、磁盘保存、过滤规则和显式导入共同决定哪些内容还能被搜索。</description><content:encoded>&lt;p&gt;命令历史能减少重复输入，却不是所有已执行操作的完整记录。Bash 通常在交互 Shell 中启用历史功能，配置还可以过滤条目或限制保留数量。运行中 Shell 的可搜索列表，与磁盘上的历史文件，是两个独立状态。理解这个区别，才能解释为什么某条命令在一个终端能找到，在另一个终端却没有，以及为什么意外结束会话可能丢掉尚未保存的记录。&lt;/p&gt;
&lt;h2 id="会话内存与历史文件不同"&gt;会话内存与历史文件不同&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HISTSIZE&lt;/code&gt; 限制内存中的历史条目数量，&lt;code&gt;HISTFILESIZE&lt;/code&gt; 限制保存文件的行数。多行命令与时间戳记录会让行数不等于命令数，不能用简单的文件行数判断究竟保留了多少次操作。&lt;code&gt;HISTFILE&lt;/code&gt; 决定保存位置，通常指向用户的 Bash 历史文件。变量未设置或为空时不会正常保存；目标不可写时，当前会话仍可能查到记录，却没有对应的持久化副本。&lt;/p&gt;
&lt;p&gt;Bash 在参数展开之前、历史展开之后记录命令文本，并受到 &lt;code&gt;HISTCONTROL&lt;/code&gt; 与 &lt;code&gt;HISTIGNORE&lt;/code&gt; 的影响。因此，保存下来的变量引用并不一定显示原来执行时的变量值。当前目录、别名、函数、环境及外部文件，也都可能在下次复用命令之前发生变化。历史保存的是有用的输入，不是完整的可复现运行环境，更不是可以直接信任的操作审计证据。&lt;/p&gt;
&lt;h2 id="追加保存不会自动刷新其他终端"&gt;追加保存不会自动刷新其他终端&lt;/h2&gt;
&lt;p&gt;Bash 正常退出时保存历史，&lt;code&gt;histappend&lt;/code&gt; 决定追加到文件还是覆盖文件。如果此前没有主动保存，异常终止可能丢掉全部未写入的条目，不只是最后一条。&lt;code&gt;history -a&lt;/code&gt; 追加当前会话新输入、且尚未追加过的历史行。提前保存可以缩小丢失窗口，但不等于承诺任何系统故障之后都能完整恢复，更不能把一次文件写入等同于业务日志的可靠持久化。&lt;/p&gt;
&lt;p&gt;另一个仍在运行的 Shell 持有自己的列表，需要通过 &lt;code&gt;history -n&lt;/code&gt; 等操作读取新追加的行。仅仅启用追加选项，不会让另一个终端的搜索结果实时更新。&lt;code&gt;history -r&lt;/code&gt; 把整个文件内容追加到现有列表，重复运行可能制造重复条目；&lt;code&gt;history -w&lt;/code&gt; 则用当前列表替换文件，可能丢掉其他会话留下的记录。这些命令的方向和覆盖语义不同，不能当作几种等价的同步写法。&lt;/p&gt;
&lt;p&gt;如果把追加和导入接入 &lt;code&gt;PROMPT_COMMAND&lt;/code&gt;，还要保留已有提示符钩子，以及它们对上一条命令退出状态的使用。直接覆盖已有钩子可能破坏其他 Shell 行为。多个会话共享一个文件，也不会因此形成可信的全局执行顺序。需要顺序、退出码或责任归属时，应使用独立的操作日志，而不是从交错的历史文本中反推完整事实。&lt;/p&gt;
&lt;h2 id="搜索后先检查再执行"&gt;搜索后先检查，再执行&lt;/h2&gt;
&lt;p&gt;在常见的 Emacs 风格 Readline 绑定中，Ctrl-R 向后增量搜索，再按一次查找更早的匹配。应使用编辑按键退出搜索，先检查所选命令，再决定是否执行；Ctrl-G 可以取消搜索。回车会接受命令，Ctrl-J 也是换行，不能把它说成不会执行的预览键。其他编辑模式或自定义终端的绑定可能不同，应以当前配置为准。&lt;/p&gt;
&lt;p&gt;复用旧命令时，检查主机名、目录、选项和目标。上个月正确的命令，今天可能已经指向另一套环境。用辨识度高的片段搜索可以减少翻找，但找到之后仍应查看整行，不能把第一条匹配结果直接当成当前想做的操作。历史搜索解决的是找回输入的问题，不负责重新判断操作意图。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;命令消失可能来自过滤规则、保留上限、不同的历史文件、保存失败，或者当前会话没有导入新记录。应分别核对这些原因。&lt;code&gt;ignoredups&lt;/code&gt; 只排除连续重复，不会对整个历史全局去重；&lt;code&gt;ignoreboth&lt;/code&gt; 只是把它与忽略行首空格的规则组合，也不会得到一份全局唯一的命令档案。判断条目是否丢失之前，先明确当前启用了什么过滤条件。&lt;/p&gt;
&lt;p&gt;行首空格只有在相应配置生效时才会排除条目。它不会移除终端录制、进程参数或其他位置的日志，因此不能把凭据直接放进命令行，再依赖历史过滤保证保密。诊断示例也应使用合成条目和临时历史文件，避免为了证明历史功能可用，就把真实用户最近执行过的命令打印出来。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;使用 Python 3、Bash 和常见 Unix 工具运行下面的完整示例。环境在 Bash 启动之前就把历史文件指向临时目录，整个过程只查看合成文本。花括号分组让控制命令先被整体读取，避免启用历史之后又把实验自身的控制步骤逐行写进列表。退出前解除自动保存目标，随后由临时目录管理器清理测试文件。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tempfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&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="s1"&gt;set -eu
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;set -o history
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -c
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -s &amp;#39;demo original&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -w &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;printf &amp;#39;first-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="s1"&gt;cat &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -c
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -s &amp;#39;demo replacement&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -w &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;printf &amp;#39;replacement-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="s1"&gt;cat &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;history -r &amp;#34;$HISTFILE&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;printf &amp;#39;memory-after-reread:\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="s1"&gt;history | sed &amp;#39;s/^[[:space:]]*[0-9][0-9]*[[:space:]]*//&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;unset HISTFILE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;}&amp;#39;&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bash-history-demo-&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;history_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;history&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BASH_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HISTFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;HISTSIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;100&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;HISTFILESIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;100&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HISTCONTROL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HISTIGNORE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;HISTTIMEFORMAT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--noprofile&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--norc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第一次写入后，文件只包含 &lt;code&gt;demo original&lt;/code&gt;；替换后只包含 &lt;code&gt;demo replacement&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#Bash-History-Facilities"&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#Bash-History-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#Commands-For-History"&gt;Readline 历史命令&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-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>umask 过滤新建权限，默认 ACL 会改变规则</title><link>https://www.xbcatm.com/zh/posts/umask-sets-the-default-file-mode/</link><pubDate>Sat, 09 Oct 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/umask-sets-the-default-file-mode/</guid><description>在支持 ACL 的文件系统中验证位掩码、既有文件、后续 chmod 与默认 ACL 继承。</description><content:encoded>&lt;p&gt;进程的 umask 在普通模式位创建规则下，为新文件和目录移除权限位。应用先提供请求模式，掩码再限制这份请求。它既不是完整访问策略，也不会重写所有既有文件。如果父目录具有默认访问控制列表，创建计算方式还会改变，因此只检查进程掩码，可能无法准确预测最后得到的权限。排查时需要同时看到应用请求和目录环境。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;没有默认 ACL 时，普通权限结果是请求模式与 umask 按位取反后的值进行按位与。例如请求 0666、掩码 0022，得到 0644。这个计算不是算术减法：相同掩码下请求 0600，仍然得到 0600。应用没有请求执行权限时，宽松掩码也不会主动添加执行位；掩码负责移除，而不是凭空授予应用未请求的普通权限。&lt;/p&gt;
&lt;p&gt;限制严格的 umask 不会修复已经存在的全员可写文件。打开并截断该文件，会改变内容，却保留既有模式。后续 chmod 也可以不受当前掩码限制地扩大权限。因此，把 umask 当作权限永远不能超过的上界，会遗漏既有文件与之后权限修改两类情况。必须区分文件首次创建，以及此后围绕同一 inode 发生的操作。&lt;/p&gt;
&lt;p&gt;父目录具有默认 ACL 时，Linux 从它派生新对象的访问 ACL，再按应用请求模式限制权限，不再按普通方式应用进程 umask。因此，即使创建者掩码是 0077，共享目录里仍可能生成组可访问文件。遇到与预期不同的模式时，应检查继承的 ACL，而不能只看到严格掩码就断言内核没有遵守权限设置。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;把脚本保存为 &lt;code&gt;mask.py&lt;/code&gt;，在安装 Python 3 和 setfacl 的 Linux 环境中运行 &lt;code&gt;python3 mask.py&lt;/code&gt;。临时目录所在文件系统必须支持默认 ACL。实测使用 Python 3.11.2、acl 2.3.1，以及通过 TMPDIR 选择的独立 ext4 卷。最初 tmpfs 环境拒绝设置默认 ACL，因此完整实验随后在 ext4 上重新执行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;tempfile&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;stat&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_WRONLY&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_CREAT&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_EXCL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S_IMODE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;umask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mo"&gt;0o022&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;xbcatm-umask-&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;data&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o666&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o644&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;executable&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o777&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o755&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;private&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;folder&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mo"&gt;0o777&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o755&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mo"&gt;0o600&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;~&lt;/span&gt;&lt;span class="mo"&gt;0o022&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="mo"&gt;0o600&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mo"&gt;0o022&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;without default ACL: requested mode AND NOT umask; 0666 becomes 0644, 0777 becomes 0755&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;umask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mo"&gt;0o077&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;strict&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o666&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;existing&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o666&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mo"&gt;0o666&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_WRONLY&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_TRUNC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o666&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;restrictive mask leaves an existing 0666 file unchanged; chmod can widen permissions later&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;acl_folder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;acl&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;acl_folder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;setfacl&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-d&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-m&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;u::rwx,g::rwx,o::---&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acl_folder&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;inherited&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;acl_folder&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;inherited&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inherited&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o666&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;restricted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;acl_folder&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;restricted&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;restricted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inherited&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o660&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;restricted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;with default ACL and umask 0077: request 0666 yields 0660; request 0600 still yields 0600&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/proc/self/status&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;:&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Umask&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mo"&gt;0o077&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/proc/self/status observes this process mask without temporarily changing it&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;umask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;掩码为 0022、没有继承默认 ACL 时，请求 0666 得到 0644 文件；请求 0777，普通文件和目录均得到 0755；请求 0600 则仍为 0600。脚本还检查算术减法会对最后一种请求产生不同结果，直接暴露常见计算错误。这些结果说明，需要理解位操作，而不是把八进制权限数字当作普通额度相减。&lt;/p&gt;
&lt;p&gt;改为掩码 0077 后，新文件请求 0666，最终得到 0600。另一个文件被明确 chmod 为 0666，随后以截断方式打开，但没有创建操作，其模式保持 0666。这个对照区分首次创建与既有 inode 上的后续操作，而不是假定每次 open 都重新套用创建策略。严格掩码无法替代对存量权限的检查。&lt;/p&gt;
&lt;p&gt;接着，脚本给父目录设置默认 ACL：所属用户和组拥有完整权限，其他用户没有权限。进程掩码仍为 0077，新文件请求 0666，却得到 0660。继承 ACL 在严格掩码下保留组读写权限，同时应用请求排除了执行权限。结果来自实际文件状态检查，不是只看 setfacl 命令执行成功就推测新文件行为。&lt;/p&gt;
&lt;p&gt;同一目录中另一个文件请求 0600，结果仍为 0600。默认规则不会添加请求模式排除的权限。这两种请求一起验证了默认 ACL 对普通掩码规则的例外，也验证应用显式请求仍然参与限制。只观察较宽的一种结果，容易误以为 ACL 可以忽略应用的所有权限请求，因此必须保留这个对照。&lt;/p&gt;
&lt;p&gt;脚本从 &lt;code&gt;/proc/self/status&lt;/code&gt; 读取 Umask，确认进程掩码仍为 0077，在 finally 中恢复原值并清理临时目录。如果文件系统无法设置 ACL，命令会失败，不会跳过核心例外后声称完整通过。前一次环境失败与之后完整执行的结果分别保留，避免把环境能力缺失误写成契约已经验证。&lt;/p&gt;
&lt;h2 id="在正确层级应用创建策略"&gt;在正确层级应用创建策略&lt;/h2&gt;
&lt;p&gt;umask 会随正常进程创建继承，并在执行另一个程序后保留。服务或命令应通过真实启动路径设置它。无关终端的掩码不会追溯改变已经运行的进程，shell 配置文件也只有被启动链读取时才生效。需要验证的是最终创建文件的进程状态，而不是另一个方便观察的控制终端。&lt;/p&gt;
&lt;p&gt;对于共享相关文件系统状态的线程，umask 是进程范围的设置。为了读取旧值而临时改动它，可能干扰同时发生的文件创建。Linux 上读取 proc 状态字段可以避免这种修改。应用如果必须创建私有文件，应请求足够严格的权限，并核对真实存储环境，不能依赖一次短暂改掩码覆盖所有并发创建行为。&lt;/p&gt;
&lt;p&gt;还应一起检查父目录访问权限、所有权、ACL 和最终文件权限。0660 文件位于不可访问父目录中时，组用户并不会自动获得完整访问路径；模式位也不能单独描述所有命名 ACL 条目。本实验验证创建结果，端到端访问则需要让预期身份沿完整路径实际尝试。权限数字正确，只是整条访问条件中的一部分。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/umask.2.html"&gt;umask(2)&lt;/a&gt;，创建掩码、继承、默认 ACL 交互与 proc 检查，已核验 2026-09-12。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/acl.5.html"&gt;acl(5)&lt;/a&gt;，默认 ACL 继承与请求模式对权限的约束，已核验 2026-09-12。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/open.2.html"&gt;open(2)&lt;/a&gt;，创建模式与打开或截断既有文件，已核验 2026-09-12。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>别名免除重复敲击</title><link>https://www.xbcatm.com/zh/posts/shell-aliases-shrink-repeated-typing/</link><pubDate>Mon, 04 Oct 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-aliases-shrink-repeated-typing/</guid><description>别名把一个短名字映射成长命令，常用的一串操作因此可以一缩即成。它主要为交互而设，脚本里一般不会用。</description><content:encoded>&lt;p&gt;别名在 Bash 读取命令时，把命令位置的一个词替换成一段 Shell 文本。&lt;code&gt;alias ll='ls -l'&lt;/code&gt; 这种个人快捷方式很方便，但别名并不是一个可执行文件，也没有独立的位置参数作用域。非交互 Bash 默认关闭别名展开，脚本却可以明确启用它；“默认不展开”与“脚本中绝对不能使用”是两回事。理解这个区别，才能解释为什么终端里好用的简称到了自动化环境就找不到。&lt;/p&gt;
&lt;h2 id="替换发生在读取输入时"&gt;替换发生在读取输入时&lt;/h2&gt;
&lt;p&gt;命令位置上没有被引用的词，才可能被当成别名展开。定义 &lt;code&gt;ll&lt;/code&gt; 后，&lt;code&gt;ll 'two words'&lt;/code&gt; 仍会把带空格的文本作为一个参数传给命令，不能笼统地说别名必然破坏参数引用。真正的限制是：别名没有属于自己的 &lt;code&gt;$1&lt;/code&gt;，也不能像函数那样拥有并重新排列一组独立的参数。调用者后面追加的参数，只是继续参与替换后的命令解析。&lt;/p&gt;
&lt;p&gt;替换文本甚至可以包含管道或多条命令，因此别名在语法上并不局限于单一程序。代价是读者更难直接看出简称背后的行为。Bash 会继续检查替换文本的第一个词，但不会再次展开当前正在展开的同名别名，所以 &lt;code&gt;alias ls='ls -F'&lt;/code&gt; 不会无限递归。还有一个容易忽略的规则：别名值末尾如果留有空白，紧随其后的命令词也会接受别名检查，不应把尾部空格当成无意义的排版。&lt;/p&gt;
&lt;p&gt;定义本身的引号还决定另一层求值时机。单引号让美元符号保留到替换文本以后被解释；双引号却可能在定义别名时就展开变量。原本想跟随目录变量变化的快捷方式，于是可能固定住旧值。遇到这种差异时，应检查实际保存的别名定义，而不只是回看自己最初输入的那一行。&lt;/p&gt;
&lt;h2 id="作用域与生效时机分开看"&gt;作用域与生效时机分开看&lt;/h2&gt;
&lt;p&gt;别名属于当前 Shell，不是可以导出给新 Bash 进程的环境变量。圆括号创建的子 Shell 会继承当前 Shell 状态，这与重新启动一个 Bash 程序不同。新进程读取启动文件后出现同名别名，表示配置又执行了一遍，并不证明别名通过环境自动传递。因此排查问题时，应确认命令究竟由当前交互 Shell、圆括号子 Shell，还是另一个程序启动。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;shopt -s expand_aliases&lt;/code&gt; 能在非交互 Bash 中启用展开，但定义仍须在使用它的输入被读取之前生效。Bash 会先读完一整行或完整复合命令，再执行其中的内容。同一行先定义别名、后调用新名字，并不能赶在解析后半行之前建立定义；把定义塞进正在读取的函数体，也存在同样的时机问题。把定义单独放一行，比依赖分号后的即时生效可靠。&lt;/p&gt;
&lt;p&gt;做可复现实验时，还要注意：禁用登录和交互启动文件，并没有禁用非交互的 BASH_ENV 钩子。除了明确选项，还应使用受控环境，否则继承的启动文件可以在第一条测试命令之前定义同名函数、安装别名或改变选项，让观察结果掺入会话配置。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;函数定义被读取时，函数体中的别名可能已经展开。后来修改别名表，并不会重写那份已经解析好的函数体，所以函数仍执行旧内容，而直接调用别名却执行新内容。需要行为清晰、参数可控的复用逻辑时，应直接定义函数，并使用带双引号的 &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; 转发参数，保留空字符串以及带空格的参数边界。&lt;/p&gt;
&lt;p&gt;另一个常见误判，是把交互快捷方式当成其他程序也能启动的命令。进程启动器不会读取你的交互别名表。可以在发生问题的 Shell 中使用 &lt;code&gt;type name&lt;/code&gt; 查看解析结果，用 &lt;code&gt;alias name&lt;/code&gt; 检查定义，用 &lt;code&gt;unalias name&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;&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;shopt -u expand_aliases
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;alias demo_alias=&amp;#39;printf &amp;#34;alias=&amp;lt;%s&amp;gt;\n&amp;#34;&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_alias hidden 2&amp;gt;/dev/null
&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;disabled=%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;shopt -s expand_aliases
&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_alias &amp;#39;two words&amp;#39; &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;alias demo_late=&amp;#39;printf &amp;#34;late\n&amp;#34;&amp;#39;; demo_late 2&amp;gt;/dev/null
&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;same-line=%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;demo_late
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;alias demo_version=&amp;#39;printf &amp;#34;old\n&amp;#34;&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_function() { demo_version; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;alias demo_version=&amp;#39;printf &amp;#34;new\n&amp;#34;&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_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;demo_version
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;unalias demo_alias
&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 command -v demo_alias &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; 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 command\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;alias removed\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;127&lt;/code&gt;。打开后依次输出 &lt;code&gt;alias=&amp;lt;two words&amp;gt;&lt;/code&gt; 与 &lt;code&gt;alias=&amp;lt;&amp;gt;&lt;/code&gt;，表明带空格的参数和空参数均被保留。同一行的首次调用仍返回 &lt;code&gt;127&lt;/code&gt;，下一行才打印 &lt;code&gt;late&lt;/code&gt;。随后函数打印 &lt;code&gt;old&lt;/code&gt;，直接调用打印 &lt;code&gt;new&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://www.xbcatm.com/zh/posts/shell-functions-borrow-positional-parameters/"&gt;Shell 函数借用位置参数&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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#Aliases"&gt;Bash 参考手册：别名&lt;/a&gt;，维护者托管的手册对展开时机和非交互行为的说明，已核验 2026-09-07。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_03_01"&gt;POSIX 别名替换&lt;/a&gt;，命令词替换及引用规则，已核验 2026-09-07。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html"&gt;GNU Bash：启动文件&lt;/a&gt;，非交互 BASH_ENV 行为（官方索引文本），已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>重定向重新安排流向</title><link>https://www.xbcatm.com/zh/posts/redirection-reorders-the-streams/</link><pubDate>Wed, 29 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/redirection-reorders-the-streams/</guid><description>重定向在命令启动前把标准流指向文件等处；顺序对同时改 stdout 与 stderr 方向尤其重要。</description><content:encoded>&lt;p&gt;重定向在命令执行前改变它的文件描述符。Shell 从左到右处理每一项，遇到打开文件或复制描述符的操作，就在那个位置完成相应变更。描述符零、一、二通常分别表示标准输入、标准输出和标准错误。把命令行读成对这些句柄的顺序操作，就能解释诊断为什么去了错误的位置，也能解释文件为什么还没有被程序读取就已经变空。&lt;/p&gt;
&lt;h2 id="打开文件本身就可能改变数据"&gt;打开文件本身就可能改变数据&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;gt;output&lt;/code&gt; 打开输出文件，并且通常把已存在的普通文件截断为零长度；&lt;code&gt;&amp;gt;&amp;gt;output&lt;/code&gt; 使用追加方式，&lt;code&gt;&amp;lt;input&lt;/code&gt; 则打开文件供读取。这些动作由 Shell 完成，不是命令所指程序的内部行为。如果必须打开的目标失败，命令不会执行，但此前已经成功的重定向仍可能创建或截断其他文件。不能把“程序没有启动”理解成“整条命令完全没有副作用”。&lt;/p&gt;
&lt;p&gt;因此，&lt;code&gt;sed expression input &amp;gt;input&lt;/code&gt; 会在转换程序读取数据之前破坏原文件。即便先写 &lt;code&gt;&amp;lt;input&lt;/code&gt; 打开输入，后续重定向截断同一个底层文件，也不能保住那些内容。应写入独立临时文件，检查转换状态与结果，再根据文件归属、权限和所在文件系统，明确执行替换步骤。输入输出恰好同名时，不能指望 Shell 自动替你实现安全的原地编辑。&lt;/p&gt;
&lt;h2 id="复制记住的是当下的目标"&gt;复制记住的是当下的目标&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;gt;log 2&amp;gt;&amp;amp;1&lt;/code&gt; 先让描述符一指向日志，再让描述符二引用同一个已打开目标。&lt;code&gt;2&amp;gt;&amp;amp;1 &amp;gt;log&lt;/code&gt; 则先复制原来的标准输出，之后只改变描述符一，因此标准错误仍留在原目标，例如终端或负责捕获输出的管道。这里复制的是描述符引用，不是文件内容，也不是给两个数字建立一个会随之后续变化的动态连接。&lt;/p&gt;
&lt;p&gt;Bash 的 &lt;code&gt;&amp;amp;&amp;gt;log&lt;/code&gt; 可以简写同时重定向两条流，而 &lt;code&gt;&amp;gt;log 2&amp;gt;&amp;amp;1&lt;/code&gt; 也适用于可移植 Shell。不要把它写成 &lt;code&gt;&amp;gt;log 2&amp;gt;log&lt;/code&gt;：后者分别打开同一个文件，两份打开状态拥有独立偏移量，输出可能相互覆盖。即使描述符共享一个目标，应用程序自身的缓冲仍会影响消息真正写出的时间，所以合并日志也不自动保证业务事件严格按发生顺序排列。&lt;/p&gt;
&lt;p&gt;管道会先把标准输出连接到下一阶段，然后才应用命令自己的重定向。因此 &lt;code&gt;producer &amp;gt;file | consumer&lt;/code&gt; 的生产者把输出交给了文件，消费者反而读不到那些数据。相反，&lt;code&gt;producer 2&amp;gt;&amp;amp;1 | consumer&lt;/code&gt; 会把诊断也送进管道。如果下游要求机器可解析的记录，把面向人的错误文本混进去，就可能破坏原本合法的输入格式。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;文件名来自变量时应加引号，避免空格让目标展开成多个词。描述符编号必须紧贴操作符：&lt;code&gt;2&amp;gt;errors&lt;/code&gt; 重定向标准错误，&lt;code&gt;2 &amp;gt;errors&lt;/code&gt; 却把数字二作为普通参数传给命令，并重定向标准输出。一个空格就可能同时改变程序收到的输入参数和输出位置，排查时不能只看目标文件名有没有拼对。&lt;/p&gt;
&lt;p&gt;Bash 的 &lt;code&gt;set -C&lt;/code&gt; 启用禁止覆盖模式，通常会拒绝用普通输出重定向覆盖已存在的普通文件。这有助于防止误操作，但不是备份机制，也不是对所有文件类型的统一保护；&lt;code&gt;&amp;gt;|&lt;/code&gt; 会明确绕过这个选项。还要区分单条命令的临时重定向与 &lt;code&gt;exec &amp;gt;log&lt;/code&gt;：后者修改当前 Shell 的描述符，可能把后续所有命令的输出一起转走，不能在交互验证时无意间让整个会话失声。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面的完整实验使用 Bash 和常见 Unix 工具，只会截断、删除刚创建的专用临时文件。它比较两种重定向顺序，演示输入输出同文件时的数据丢失，并验证目标无法打开时函数体确实没有运行。清理步骤逐个列出实验文件，不使用递归删除，也不会触碰原有文件。&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 -eu
&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_dir=$(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 -f &amp;#34;$demo_dir/both&amp;#34; &amp;#34;$demo_dir/out&amp;#34; &amp;#34;$demo_dir/input&amp;#34;; rmdir &amp;#34;$demo_dir&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;emit() { printf &amp;#39;out\n&amp;#39;; printf &amp;#39;err\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;emit &amp;gt; &amp;#34;$demo_dir/both&amp;#34; 2&amp;gt;&amp;amp;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;demo_captured=$(emit 2&amp;gt;&amp;amp;1 &amp;gt; &amp;#34;$demo_dir/out&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;$(cat &amp;#34;$demo_dir/both&amp;#34;)&amp;#34; = &amp;#34;$(printf &amp;#39;out\nerr&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;[ &amp;#34;$(cat &amp;#34;$demo_dir/out&amp;#34;)&amp;#34; = out ]
&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_captured&amp;#34; = err ]
&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;both:\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;cat &amp;#34;$demo_dir/both&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;out-file=[%s] captured=[%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;$(cat &amp;#34;$demo_dir/out&amp;#34;)&amp;#34; &amp;#34;$demo_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;printf &amp;#39;seed\n&amp;#39; &amp;gt; &amp;#34;$demo_dir/input&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;sed &amp;#39;s/seed/replaced/&amp;#39; &amp;#34;$demo_dir/input&amp;#34; &amp;gt; &amp;#34;$demo_dir/input&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;[ ! -s &amp;#34;$demo_dir/input&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;input truncated\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;if (set -C; emit &amp;gt; &amp;#34;$demo_dir/both&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 overwrite\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; printf &amp;#39;noclobber rejected overwrite\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;demo_ran=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;mark_run() { demo_ran=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;if mark_run 2&amp;gt;/dev/null &amp;gt; &amp;#34;$demo_dir/missing/output&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;unexpected redirection\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; [ &amp;#34;$demo_ran&amp;#34; = 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; printf &amp;#39;function-ran=%s\n&amp;#39; &amp;#34;$demo_ran&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;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;out&lt;/code&gt; 和 &lt;code&gt;err&lt;/code&gt;。交换顺序后，输出文件只含 &lt;code&gt;out&lt;/code&gt;，变量单独捕获到 &lt;code&gt;err&lt;/code&gt;。转换实验报告输入已经变空，因为截断先于程序读取。最后两行分别证明禁止覆盖选项拒绝了替换，以及目标打开失败时仍为 &lt;code&gt;function-ran=no&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://www.xbcatm.com/zh/notes/redirects-and-error-codes-should-preserve-the-story/"&gt;重定向与错误码应保留故事的完整性&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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#Redirections"&gt;Bash 参考手册：重定向&lt;/a&gt;，从左到右的描述符变更及禁止覆盖行为，已核验 2026-09-07。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_07"&gt;POSIX Shell 重定向&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-pipes-stream-between-processes/</link><pubDate>Tue, 28 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-pipes-stream-between-processes/</guid><description>管道把前一个命令的标准输出接到下一个的标准输入，数据随到随走，长生产者因此能与消费者并行推进。</description><content:encoded>&lt;p&gt;shell 管道把一个命令的标准输出连接到下一个命令的标准输入。各段命令可以并发运行，生产者不必全部完成后消费者才开始处理。这种连接支持增量处理，却不保证任一程序真的采用增量算法。用户态缓冲、提前结束以及各阶段的退出状态，都会影响整项操作是否达到预期。理解管道时，需要同时观察数据流、文件描述符生命周期和进程结果，不能只看最后一行输出。&lt;/p&gt;
&lt;h2 id="有限容量的字节流"&gt;有限容量的字节流&lt;/h2&gt;
&lt;p&gt;管道传输的是字节，不是自带边界的记录。读者不能假设每次读取恰好得到一行，也不能假设一次读取对应生产者的一次写入。需要消息边界的程序必须自己定义和解析。标准错误默认独立于管道连接，因此，上游诊断可能显示在终端，却不会进入下游过滤器。是否合并诊断和数据应当明确决定，避免错误文本污染本来供机器读取的内容。&lt;/p&gt;
&lt;p&gt;内核缓冲区容量有限。对于阻塞操作，管道满时写者等待；管道空但仍有写端打开时，读者等待。这种反压限制生产者在这个管道上领先消费者的距离。容量取决于操作系统和配置，并不是普遍固定为六十四千字节。程序自身仍可能维护不断增长的数据结构，因此，使用管道不等于整个处理链的内存已经有界，必须另外检查各段算法。&lt;/p&gt;
&lt;p&gt;用户态缓冲还可能引入额外等待。某个程序向终端逐行刷新，改成管道输出后却可能先积累较大的块。此时消费者看起来没有动静，并不代表内核丢了数据；数据可能尚未离开生产者的用户态缓冲。应先检查程序提供的流式处理和刷新控制，再考虑调整内核限制。完整排序等转换本身也需要先看到更多输入，添加随意的睡眠无法改变这种依赖。&lt;/p&gt;
&lt;p&gt;原子写入与缓冲容量是不同属性。Linux 对不超过 PIPE_BUF 的适当写入提供不与其他写者交错的保证，并有阻塞模式条件；这仍然不会为读者建立消息边界。几次短写入可能被一次读取取得，一次写入也可能分几次才读完，不能把某次观察到的块大小当作解析协议。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;只有所有指向写端的文件描述符都关闭后，读者才会看到文件结束。如果某个子进程继承了不再使用的写端，即使看起来生产者已经退出，消费者也可能一直等不到结束。反方向上，所有读端关闭后继续写入会产生 SIGPIPE；忽略这个信号时，写操作会以 EPIPE 错误失败。因此，消费者主动提前结束，也可能让原本正常的生产者以非零状态退出。&lt;/p&gt;
&lt;p&gt;Bash 默认返回最后一段命令的状态。上游失败、下游 cat 成功读完部分数据时，整体可能仍然成功。开启 pipefail 后，整体返回从左到右最靠右的非零状态，并不是时间上最先失败的进程，也不是最左侧失败命令。PIPESTATUS 数组短暂保留每段结果，应立即保存，因为后面一次赋值或打印就可能替换这份观察记录。&lt;/p&gt;
&lt;p&gt;Bash 管道中的阶段通常运行在子 shell 环境。在管道右侧循环里给变量赋值，通常不会修改调用者的变量。lastpipe 选项在关闭作业控制时可以改变最后一段的执行位置，因此依赖状态保留的代码必须明确 shell 配置。需要保留循环状态时，也可以让循环直接通过重定向读取输入，避免把它放进管道带来的作用域变化。&lt;/p&gt;
&lt;p&gt;前台 Bash 管道会等待各阶段结束，并不是消费者读到第一行就立即返回。如果生产者捕获断管错误后继续执行其他工作，整体完成时间仍会被拖延。因此，管道应被理解为多个具有独立结果、通过描述符连接的进程，不能假设它们总会像一个不可分割操作一样共同成功或共同停止。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面实验需要 Bash、cat、yes 和 head。它先用主动返回七的生产者比较默认状态和 pipefail，然后构造两段都失败的管道，最后检查提前关闭的消费者。仅在收集预期非零结果时关闭 errexit，取得结果后立即重新开启，再执行断言。&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 +o pipefail
&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;(printf &amp;#39;partial\n&amp;#39;; exit 7) | cat &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;plain=(&amp;#34;$?&amp;#34; &amp;#34;${PIPESTATUS[@]}&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 -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;[[ ${plain[*]} == &amp;#39;0 7 0&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 -o pipefail
&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;(printf &amp;#39;partial\n&amp;#39;; exit 7) | cat &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;strict=(&amp;#34;$?&amp;#34; &amp;#34;${PIPESTATUS[@]}&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 -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;[[ ${strict[*]} == &amp;#39;7 7 0&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;(exit 7) | (exit 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;multiple=(&amp;#34;$?&amp;#34; &amp;#34;${PIPESTATUS[@]}&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 -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;[[ ${multiple[*]} == &amp;#39;4 7 4&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;yes | head -n 1 &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;early=(&amp;#34;$?&amp;#34; &amp;#34;${PIPESTATUS[@]}&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 -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;[[ ${early[0]} -ne 0 &amp;amp;&amp;amp; ${early[1]} -ne 0 &amp;amp;&amp;amp; ${early[2]} -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;printf &amp;#39;default: %s\npipefail: %s\nrightmost: %s\nearly close: %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;${plain[*]}&amp;#34; &amp;#34;${strict[*]}&amp;#34; &amp;#34;${multiple[*]}&amp;#34; &amp;#34;${early[*]}&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;PASS: aggregate and individual stage outcomes\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;每个打印数组的第一项是管道整体状态，后面按源码顺序列出各段状态。前三组预期分别为零七零、七七零、四七四。最后一组中，head 成功，而 yes 返回非零；断言没有硬编码具体信号编号，避免把平台表示方式混入行为要求。生产环境应区分这种主动提前停止与必需输出丢失，不能看到 pipefail 非零就无条件重试全部操作。吞吐量、缓冲延迟和内存峰值仍需独立测量，这些退出状态断言并没有验证性能。&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/Pipelines.html"&gt;GNU Bash：管道&lt;/a&gt;，进程执行和整体状态规则，已核验 2026-09-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/pipe.7.html"&gt;Linux man-pages：pipe(7)&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>作业控制在进程组之间交接终端前台</title><link>https://www.xbcatm.com/zh/posts/job-control-splits-the-terminal/</link><pubDate>Mon, 27 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/job-control-splits-the-terminal/</guid><description>通过真实伪终端暂停和恢复管道、中断作业，并观察后台读取因终端输入规则而停止。</description><content:encoded>&lt;p&gt;交互式作业控制让 shell 管理共享控制终端的多个进程组。终端同一时刻只有一个前台组，其他作业可以在后台运行，也可以保持停止状态。前台归属、是否运行和 shell 的作业表，是三种不同状态。后台作业不等于已经暂停，停止作业也不等于已经终止；判断当前交互为什么没有进展，需要先分清这些状态。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;启用终端信号处理时，输入已配置的中断或暂停字符，会由终端驱动向前台组发送信号，不是只发送给最后打印文字的那个进程。控制字符重新映射、终端模式变化以及应用信号处理函数，都可能改变结果。因此只说按某个键，并不能保证在每个程序和每套终端配置中得到相同反应，还要核对实际处理条件。&lt;/p&gt;
&lt;p&gt;普通后台作业尝试读取自己的控制终端时，会被 SIGTTIN 停止。把它放到后台，并不会给它单独分配一份键盘输入流。输出又有不同规则：后台写入通常允许，还会受 TOSTOP 等终端设置影响。因此后台执行不意味着彻底脱离终端，输入、输出和前台信号各有自己的约束，需要分别观察。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;%1&lt;/code&gt; 这类作业标识属于某个 shell 的作业表，不是全局进程编号。换一个终端或进入非交互 shell 后，不能假定相同号码仍然管理相同任务。管道和后台命令如何分组，也取决于真实 shell 调用是否启用了作业控制。交互终端中看见的行为，不能不加检查地套到所有脚本启动路径上。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;把脚本保存为 &lt;code&gt;jobcontrol.py&lt;/code&gt;，在装有 Bash 和 Python 的 Linux 环境中运行 &lt;code&gt;python3 jobcontrol.py&lt;/code&gt;。本轮使用 Linux 6.18.44、Bash 5.2.37 和 Python 3.12.14 原样重跑了两种语言的样本。脚本创建自己的伪终端与交互 Bash，不会把控制字符发给你用于启动它的终端：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;pty&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;re&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;select&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pty&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;=~&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ECHO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;|=&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ISIG&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ICANON&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VINTR&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\x03&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VSUSP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\x1a&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VEOF&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\x04&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcsetattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;termios&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TCSANOW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/bin/bash&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;bash&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--noprofile&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--norc&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-i&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PATH&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/usr/bin:/bin&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;TERM&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dumb&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;LC_ALL&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;C&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PS1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;XB-PROMPT&amp;gt; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;HISTFILE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/dev/null&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;known_groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;until_prompt&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;XB-PROMPT&amp;gt; &amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;],[],[],&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PTY closed before prompt&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;XB-PROMPT&amp;gt; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;replace&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;until_prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;terminal state did not change&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/proc&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;stat&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;) &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;until_prompt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sleep 60 | cat&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;known_groups&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pipeline_started&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/proc&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iterdir&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isdecimal&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;stat&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;) &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;comm&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ne"&gt;ProcessLookupError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cat&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sleep&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipeline_started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\x1a&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;until_prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Stopped&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Stopped&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;jobs -l&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Ctrl-Z stops the foreground pipeline and returns the terminal to the interactive shell&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;bg %1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;fg %1&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\x03&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;until_prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;RESULT:130&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;printf &amp;#39;RESULT:&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n&amp;#39; &lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s2"&gt;$?&lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;bg resumes without foreground ownership; fg restores ownership; Ctrl-C gives this pipeline status 130&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cat &amp;amp; printf &amp;#39;BG:&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n&amp;#39; &lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s2"&gt;$!&lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;BG:(\d+)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;&lt;span class="n"&gt;known_groups&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;T&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Stopped (tty input)&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;jobs -l&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a background reader is stopped by terminal input rules, rather than receiving the shell keyboard stream&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;fg %+&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcgetpgrp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;hello&lt;/span&gt;&lt;span class="se"&gt;\n\x04&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;until_prompt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;hello&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;RESULT:0&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;printf &amp;#39;RESULT:&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n&amp;#39; &lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s2"&gt;$?&lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;foregrounding that reader lets it read; EOF ends it normally; all jobs are reaped&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;jobs -p&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;exit&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;waited&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;waitpid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;waited&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WIFEXITED&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;known_groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;killpg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGKILL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ProcessLookupError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;waitpid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;样本启用终端信号处理和规范输入模式，明确配置中断、暂停与 EOF 字符，并关闭输入回显。Bash 启动时不加载用户配置文件，使用可识别提示符。这些条件使实验不依赖用户别名、提示符命令或自定义快捷键；观察到的行为来自样本明确建立的终端状态，而不是碰巧继承的交互环境。&lt;/p&gt;
&lt;p&gt;Bash 首先在前台运行 sleep 与 cat 管道，控制进程观察到终端前台 PGID 从 Bash 变成管道。本轮有一次在只确认前台 PGID 变化后发送暂停字符，最终等待超时；没有追踪到内核层面的确切原因。因此现在先从 Linux 的进程信息中确认本实验的 sleep 与 cat 都已启动且位于预期进程组，再发送暂停字符。这个检查只用于私有、受控且尚未退出的两个子进程，不是通用的进程发现方法。发送暂停字符后，作业停止，输出 Stopped，终端前台回到 Bash。&lt;code&gt;jobs -l&lt;/code&gt; 确认 shell 仍然保留该作业记录。停止并没有销毁任务，因此后续仍可恢复，而不是必须重新启动一份新的工作。&lt;/p&gt;
&lt;p&gt;执行 &lt;code&gt;bg %1&lt;/code&gt; 后，管道恢复，Bash 仍保留终端前台；&lt;code&gt;fg %1&lt;/code&gt; 再把前台归属交回管道。发送中断字符后，采用默认行为的这条管道结束，Bash 报告状态一百三十。这是此次命令的实测结果，不代表每个应用都必须响应 INT 退出，也不代表所有运行时都使用相同数字表达信号终止。&lt;/p&gt;
&lt;p&gt;脚本接着启动后台 cat，记录其 PID，并等待进程进入停止状态。Bash 报告 &lt;code&gt;Stopped (tty input)&lt;/code&gt;，说明它遇到终端输入限制，没有完成，也没有读走 shell 的下一条命令。后台读取期间，shell 始终是终端前台组。这样可以把等待交互资格，与程序崩溃、正常结束或主动睡眠分开解释。&lt;/p&gt;
&lt;p&gt;将该读取者调到前台后，它能收到 hello；在规范输入的空行位置发送 EOF 字符，使它正常结束。Bash 随后报告零状态，作业表为空。控制进程退出 Bash 并回收它；断言失败时，清理也只针对实验创建的隔离进程组。整个流程验证停止、恢复、输入和退出，而不是只检查命令是否被 shell 接受。&lt;/p&gt;
&lt;h2 id="明确工作寿命和输入来源"&gt;明确工作寿命和输入来源&lt;/h2&gt;
&lt;p&gt;停止作业不需要交互输入就能继续时，可以使用 bg；再次需要终端时使用 fg。把输入重定向到合适文件或管道，可以让后台工作不依赖键盘读取，但不会自动让它脱离会话，也不会定义登录结束时的行为。因此“在后台运行”只回答当前调度与交互关系的一部分，不能替代完整生命周期设计。&lt;/p&gt;
&lt;p&gt;需要跨交互会话持续运行，并明确重启、日志和资源策略的工作，应交给服务管理器。shell 作业表适合管理当前交互任务，却不是持久服务状态。把任务移出作业表，或者调整挂断信号处理，也只解决更窄的问题，不能直接等同于已经建立可靠的后台服务监督。&lt;/p&gt;
&lt;p&gt;当作业看似冻结，应先检查它是停止、等待数据，还是仍在计算，再决定是否发送终止信号。需要把作业标识、PID、PGID 与终端前台组对应起来，才能避免把正常的终端输入停止误判为崩溃。正确识别状态，有助于恢复原本需要的交互，同时保留仍然有效的工作进度。&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/Job-Control-Basics.html"&gt;Bash job-control basics&lt;/a&gt;，前台进程组、终端输入、暂停与作业状态，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Job-Control-Builtins.html"&gt;Bash job-control builtins&lt;/a&gt;，jobs、bg、fg 与 shell 局部作业标识，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man3/termios.3.html"&gt;termios(3)&lt;/a&gt;，终端控制字符与信号生成，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man3/tcgetpgrp.3.html"&gt;tcgetpgrp(3)&lt;/a&gt;，读取与选择终端前台进程组，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/proc_pid_stat.5.html"&gt;proc_pid_stat(5)&lt;/a&gt;，用于受控子进程就绪检查的父进程与进程组标识，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/proc_pid_comm.5.html"&gt;proc_pid_comm(5)&lt;/a&gt;，仅用于识别本实验受控子进程的命令名称，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>环境变量属于各自的进程</title><link>https://www.xbcatm.com/zh/posts/environment-variables-are-scoped-to-process/</link><pubDate>Sat, 25 Sep 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/environment-variables-are-scoped-to-process/</guid><description>导出的配置以环境字符串跨越程序边界；子 Shell、点命令和启动器显式传入的环境解释了常见差异。</description><content:encoded>&lt;p&gt;环境是程序启动时收到的一组名称与字符串值。子进程通常接收父进程选定的环境，修改自己的副本不会回写父进程或兄弟进程。这是一条有用的配置边界，却不是命令没有副作用的保证：程序仍可能根据配置修改共享文件、数据库或其他资源。理解变量如何传递，与判断程序实际会做什么，是两个需要分别核对的问题。&lt;/p&gt;
&lt;h2 id="导出决定另一个程序能收到什么"&gt;导出决定另一个程序能收到什么&lt;/h2&gt;
&lt;p&gt;Shell 变量与环境条目有关联，但并不是同一概念。&lt;code&gt;DEMO_SETTING=local&lt;/code&gt; 创建或更新 Shell 参数，&lt;code&gt;export DEMO_SETTING&lt;/code&gt; 才把它标记为启动外部命令时应包含的环境项。父 Shell 能打印变量，不代表新启动的 Bash 就能看到未导出的值。把赋值写进某个配置文件，也不会让每个 Shell 自动读取它；还必须确认文件是否在对应启动路径上被加载。&lt;/p&gt;
&lt;p&gt;Bash 从环境继承的变量已经带有导出属性，之后重新赋值会保留该属性。因此子进程修改值后，再启动孙进程时可以直接传递新值，不要求每一代重复写一次导出命令。&lt;code&gt;unset&lt;/code&gt; 删除变量，Bash 的 &lt;code&gt;export -n&lt;/code&gt; 则取消导出属性但保留当前 Shell 中的值。真正需要确认的是直接启动者传入了什么，而不是机械地搜索祖先脚本里出现了多少次导出语句。&lt;/p&gt;
&lt;h2 id="子-shell-不等于重新执行一个-shell-程序"&gt;子 Shell 不等于重新执行一个 Shell 程序&lt;/h2&gt;
&lt;p&gt;圆括号命令组和命令替换使用从当前 Shell 复制出来的子 Shell 环境，所以能够看到从未导出的普通 Shell 变量。它们的修改仍不会回到外层。这与重新执行一个 Bash 程序不同：后者收到的是导出的环境条目，不是父 Shell 整张参数表。若把两种情况都简单称为“子进程只认识已导出变量”，就会错误解释圆括号内的实际行为。&lt;/p&gt;
&lt;p&gt;使用点命令加载文件时，文件中的命令在当前 Shell 执行，赋值因而能直接改变调用者。这并没有打破进程边界，因为根本不是另一个进程把修改传回来。被加载的文件是可执行 Shell 代码，不是被动的数据文件；只有当你确实打算在当前上下文执行那些内容时，才应加载它。文件扩展名叫配置，并不会改变这一点。&lt;/p&gt;
&lt;h2 id="单次调用配置有自己的生命周期"&gt;单次调用配置有自己的生命周期&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;DEMO_SETTING=once external-command&lt;/code&gt; 为一次外部命令提供值，不会永久改写外层 Shell 的对应变量。这条结论针对外部命令；特殊内建命令、函数等结构的赋值行为，还受到具体 Shell 规则影响。因此不能进一步推导出“所有看起来像命令的写法都会启动隔离进程”，更不能用这种推断判断函数或者点命令是否会改动当前状态。&lt;/p&gt;
&lt;p&gt;在操作系统接口上，&lt;code&gt;execve&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 示例只打印专用演示变量，不读取外部配置文件，也不改变外层终端环境。传给点命令的短文档已经把测试所用配置完整写在示例中，便于检查将执行的内容。各行输出分别对应不同的变量传递边界，而不是靠查看某个配置声明就推断它已经生效。&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;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;DEMO_SETTING=local
&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;shell=%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;bash --noprofile --norc -c &amp;#39;printf &amp;#34;unexported-child=%s\n&amp;#34; &amp;#34;${DEMO_SETTING-unset}&amp;#34;&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;subshell=%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;export 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;bash --noprofile --norc &amp;lt;&amp;lt;&amp;#39;CHILD&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;exported-child=%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=child
&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 --noprofile --norc -c &amp;#39;printf &amp;#34;grandchild=%s\n&amp;#34; &amp;#34;$DEMO_SETTING&amp;#34;&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;CHILD
&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;parent-after-child=%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=once bash --noprofile --norc -c &amp;#39;printf &amp;#34;one-command=%s\n&amp;#34; &amp;#34;$DEMO_SETTING&amp;#34;&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;parent-after-command=%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;. /dev/stdin &amp;lt;&amp;lt;&amp;#39;CONFIG&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_SETTING=sourced
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;CONFIG
&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;parent-after-source=%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;BASH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;未导出变量时，新 Bash 打印 &lt;code&gt;unset&lt;/code&gt;，圆括号子 Shell 却能看到 &lt;code&gt;local&lt;/code&gt;。导出之后，新子进程收到 &lt;code&gt;local&lt;/code&gt;，修改自己的副本，再把 &lt;code&gt;child&lt;/code&gt; 传给孙进程；外层 Shell 仍是 &lt;code&gt;local&lt;/code&gt;。单次覆盖打印 &lt;code&gt;once&lt;/code&gt;，紧接着检查父级仍然得到 &lt;code&gt;local&lt;/code&gt;，说明覆盖没有反向传播。&lt;/p&gt;
&lt;p&gt;最后，点命令把当前这层演示 Shell 的值改成 &lt;code&gt;sourced&lt;/code&gt;。这些结果共同区分普通参数、导出条目、子 Shell 副本、单次调用覆盖，以及在当前 Shell 中执行代码。实验确认的是变量作用域，并没有宣称应用不会产生其他副作用，也没有代替对真实服务部署配置的检查。&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#Command-Execution-Environment"&gt;Bash 执行环境&lt;/a&gt;，导出条目与子 Shell 复制状态的区别，已核验 2026-09-07。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Environment"&gt;Bash 环境&lt;/a&gt;，导出属性与单次命令赋值，已核验 2026-09-07。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/functions/exec.html"&gt;POSIX exec 函数&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>shell 找不到那条裸命令时</title><link>https://www.xbcatm.com/zh/posts/shell-command-not-found-path-search/</link><pubDate>Thu, 20 May 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-command-not-found-path-search/</guid><description>命令查找受函数、内建命令、路径缓存和 PATH 顺序影响。斜杠绕过搜索，找到文件之后的执行失败则需要另行诊断。</description><content:encoded>&lt;p&gt;“找不到命令”说明一次查找失败，却不代表 Shell 每次都遍历了 &lt;code&gt;PATH&lt;/code&gt; 的全部目录。在普通 Bash 中，不带斜杠的命令词可能对应函数、内建命令、缓存的可执行路径，或者刚刚搜索到的程序。更早的输入读取阶段，别名还可能已经改写命令。面对报错，应先检查真正发生故障的那个 Shell，而不是立即重装一个其实已经存在的工具。&lt;/p&gt;
&lt;h2 id="先确认是哪一层查找给出结果"&gt;先确认是哪一层查找给出结果&lt;/h2&gt;
&lt;p&gt;展开之后，Bash 先检查函数，再检查内建命令，之后才查找外部程序。&lt;code&gt;type name&lt;/code&gt; 描述当前解析结果，&lt;code&gt;type -a name&lt;/code&gt; 可以看到其他定义或候选项。&lt;code&gt;command -v name&lt;/code&gt; 适合检查该 Shell 中是否存在某个命令，但结果不保证是文件路径，也可能说明它是函数或内建命令。因此不能无条件把输出交给复制文件或读取文件的工具。&lt;/p&gt;
&lt;p&gt;查找外部程序时，&lt;code&gt;PATH&lt;/code&gt; 中的目录按顺序参与搜索，靠前的旧版本可以遮住后面的新版本。Bash 还会缓存成功找到的路径：&lt;code&gt;hash -t name&lt;/code&gt; 可以检查缓存项，&lt;code&gt;hash -r&lt;/code&gt; 清空缓存表。原路径上的文件发生变化，与缓存仍选择旧位置，是两个不同的问题。另开一个终端可能重置这些状态，让故障暂时消失，却没有解释原会话到底发生了什么。&lt;/p&gt;
&lt;h2 id="斜杠直接选择路径"&gt;斜杠直接选择路径&lt;/h2&gt;
&lt;p&gt;命令词中的任何斜杠都会绕过外部程序的 &lt;code&gt;PATH&lt;/code&gt; 搜索。&lt;code&gt;./tool&lt;/code&gt; 指向当前目录的文件，&lt;code&gt;/opt/example/bin/tool&lt;/code&gt; 则指定绝对位置。单独输入 &lt;code&gt;tool&lt;/code&gt; 并不自动包含当前目录；只有空目录项、相对目录项等配置才可能让当前工作目录介入搜索。这意味着相同的命令名，在不同目录下也可能选中不同的可执行文件。&lt;/p&gt;
&lt;p&gt;给服务配置环境时，应明确列出可信目录。交互终端能运行，不证明定时任务、服务管理器或另一个用户拥有相同的环境和启动文件。排查时需要比较实际环境与工作目录。修改登录配置文件也不会自动更新已经运行的进程；应该把预期环境应用到真正负责启动命令的进程，再验证原来的调用链。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;Bash 通常在找不到命令时返回 &lt;code&gt;127&lt;/code&gt;，在找到却无法执行时返回 &lt;code&gt;126&lt;/code&gt;。这两个值可以引导调查，却不能取代诊断文本：脚本文件明明存在，但首行指定的解释器不存在，同样可能执行失败。应同时检查选中的文件、执行权限与解释器，而不是反复向搜索路径追加目录，最后把问题掩盖成一次偶然成功。&lt;/p&gt;
&lt;p&gt;也不要为了让名称被找到，就把可写的项目目录插到可信工具之前，或者不加区分地提高权限。先确认函数、别名或缓存是否参与了解析。Bash 的 &lt;code&gt;command name&lt;/code&gt; 可以绕过同名函数，明确的可执行路径可以绕过目录选择；这些做法不会自动让程序变得可信，也不能补齐程序自身缺失的依赖。找到命令与命令正确执行，是需要分别验证的两件事。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;下面的 Bash 实验创建两个同名的临时程序。只有这两个专用临时目录被加入搜索路径，全部环境变化都局限于子 Shell。退出时逐个删除实验创建的文件和目录，不修改任何已安装程序。故意执行失败的分支会立即记录状态，避免被随后的输出操作覆盖。&lt;/p&gt;
&lt;p&gt;准备阶段启用遇错退出，让临时文件创建失败时及时停止；开始故意制造执行失败之前，再关闭该选项。本轮在 Bash 5.2.37 上观察到文档规定的一百二十六与一百二十七。对于裸名称搜索失败，命令未找到处理函数可以提供自己的退出状态；下面故意缺失的路径含有斜杠，不会测试这一钩子。因此仍应把状态与实际执行路径、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;set -eu
&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_dir=$(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 -f &amp;#34;$demo_dir/one/demo-tool&amp;#34; &amp;#34;$demo_dir/two/demo-tool&amp;#34; &amp;#34;$demo_dir/blocked&amp;#34;; rmdir &amp;#34;$demo_dir/one&amp;#34; &amp;#34;$demo_dir/two&amp;#34; &amp;#34;$demo_dir&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;mkdir &amp;#34;$demo_dir/one&amp;#34; &amp;#34;$demo_dir/two&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;#!/bin/sh\nprintf &amp;#34;one\\n&amp;#34;\n&amp;#39; &amp;gt; &amp;#34;$demo_dir/one/demo-tool&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;#!/bin/sh\nprintf &amp;#34;two\\n&amp;#34;\n&amp;#39; &amp;gt; &amp;#34;$demo_dir/two/demo-tool&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;chmod +x &amp;#34;$demo_dir/one/demo-tool&amp;#34; &amp;#34;$demo_dir/two/demo-tool&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;PATH=&amp;#34;$demo_dir/one:$demo_dir/two:$PATH&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-tool
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;hash -p &amp;#34;$demo_dir/two/demo-tool&amp;#34; demo-tool
&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-tool
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;hash -r
&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-tool
&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-tool() { printf &amp;#39;function\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;demo-tool
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;command demo-tool
&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 -f demo-tool
&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_dir/two/demo-tool&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;gt; &amp;#34;$demo_dir/blocked&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;chmod 600 &amp;#34;$demo_dir/blocked&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 +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;if &amp;#34;$demo_dir/blocked&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 execution\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;not-executable=%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;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 &amp;#34;$demo_dir/missing&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 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;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=%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;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;one&lt;/code&gt;、&lt;code&gt;two&lt;/code&gt;、&lt;code&gt;one&lt;/code&gt;，分别证明普通搜索、显式设置缓存项，以及清缓存后的重新搜索。随后是 &lt;code&gt;function&lt;/code&gt;、&lt;code&gt;one&lt;/code&gt;、&lt;code&gt;two&lt;/code&gt;，区分函数调用、绕过函数和指定绝对路径。最后，不可执行的文件应返回 &lt;code&gt;126&lt;/code&gt;，不存在的路径应返回 &lt;code&gt;127&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://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html"&gt;POSIX shell 命令查找&lt;/a&gt;，定义了 PATH 查找规则与找不到命令时的退出状态，已核验 2026-06-01。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tiswww.case.edu/php/chet/bash/bashref.html#Command-Search-and-Execution"&gt;Bash 参考手册：命令查找与执行&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-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><item><title>Shell 的 trap 把信号转成一次清理动作</title><link>https://www.xbcatm.com/zh/posts/shell-signal-traps-run-on-the-way-out/</link><pubDate>Tue, 12 Jan 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/shell-signal-traps-run-on-the-way-out/</guid><description>Bash 的 trap 可以让已处理信号进入统一清理，但退出状态、子进程和无法捕获的终止仍需明确设计。</description><content:encoded>&lt;p&gt;trap 让 Shell 为某个信号或自身事件安排动作，适合删除临时文件、协调退出，但不会让中断变成原子操作，也无法保证任何终止方式都能完成清理。在 Bash 中，&lt;code&gt;trap&lt;/code&gt; 是内建命令，&lt;code&gt;EXIT&lt;/code&gt; 是 Shell 事件，而不是操作系统信号。可靠的设计需要明确处理哪些出口，并保留执行为什么结束的证据，不能仅凭注册了一次处理器就断言资源一定会被回收。&lt;/p&gt;
&lt;h2 id="信号处理与退出清理分开"&gt;信号处理与退出清理分开&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;trap 'cleanup' INT TERM&lt;/code&gt; 会替换这些信号的默认动作。如果处理器只是返回，Shell 可能继续执行；安装处理器并不天然等于启动关停。一种明确的 Bash 写法是在信号处理器中主动退出，再让统一的退出处理器负责清理。正常执行到末尾、显式调用退出命令，也会经过这个出口，从而减少各业务分支之间重复的资源回收逻辑。&lt;/p&gt;
&lt;p&gt;从函数或被点命令加载的文件返回，不等于结束整个 Shell。普通执行脚本里的顶层 &lt;code&gt;return&lt;/code&gt; 也不是通用退出方式。此外，不能跨 Shell 假定未处理的致命信号一定会触发退出清理。应明确处理准备支持的终止信号，并使用服务真正采用的解释器测试，而不是在交互终端里试过一次便推广到所有启动环境。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;trap - INT&lt;/code&gt; 恢复默认动作，&lt;code&gt;trap '' INT&lt;/code&gt; 则忽略信号，两者策略不同。非交互 Shell 还可能从启动者继承被忽略的信号，而且无法重新捕获或重置，所以启动路径也是行为的一部分。需要确认实际安装的动作时，可以检查 &lt;code&gt;trap -p&lt;/code&gt; 输出；配置中写过处理器，与当前 Shell 最终持有什么处理器，仍然需要区分。&lt;/p&gt;
&lt;h2 id="保留最初的退出原因"&gt;保留最初的退出原因&lt;/h2&gt;
&lt;p&gt;进入退出处理器后，应先保存 &lt;code&gt;$?&lt;/code&gt;，避免后面的命令覆盖它。清理末尾固定执行 &lt;code&gt;exit 1&lt;/code&gt;，会把成功改成失败，也会抹掉原来不同的错误码。使用保存的状态退出，才能保留触发收尾的原始结果。清理本身失败时仍应输出可见诊断，并制定明确策略，尤其要说明主工作成功而清理失败时最终如何报告，不能把这两种结果混成一句“脚本结束”。&lt;/p&gt;
&lt;p&gt;示例为已处理的中断选择一百三十，为终止请求选择一百四十三，沿用 Bash 常见的信号状态约定。但这是处理器主动执行的普通退出，不证明进程真的被操作系统信号直接杀死；使用进程接口的监督程序能够区分两者。短暂清理期间关闭退出陷阱、忽略中断和终止请求，是为了避免这些处理器重入清理逻辑，并不是无限期屏蔽关停的建议。&lt;/p&gt;
&lt;p&gt;这段清理代码有意优先保留最初的退出状态。删除命令即使报告错误，也不会改变已保存的结果，因此零退出码不是清理完成的独立证明。若残留资源会产生后果，需要另外测试清理失败，并决定是否报告不同的失败、进行安全重试，或保留恢复所需证据。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;强制杀死信号无法捕获，停止信号也不能捕获，更不能把停止视作清理机会。断电、崩溃和强制终止都可能留下资源。因此，陷阱不能替代崩溃安全写入、事务更新或单独定义的恢复流程。删除目标必须限制在脚本拥有的路径内；一个尚未初始化完成的变量，绝不能意外变成宽泛删除范围。清理动作的权限边界与正常业务操作同样需要检查。&lt;/p&gt;
&lt;p&gt;执行时机也有差别。Bash 等待同步前台命令结束时，可以推迟执行已经捕获的信号处理器；通过内建等待命令等待异步任务时，被捕获的信号则会先打断等待，随后进入处理器。因此，注册陷阱并不意味着任意阻塞操作都能立即响应关停，更不能据此承诺固定的退出时限。长时间外部工作需要结合真实等待方式验证。&lt;/p&gt;
&lt;p&gt;终端中的 Ctrl-C 会把中断信号发给前台进程组，而向单个进程编号发送信号只针对那个进程。于是，同一个子进程可能在一种测试中收到信号，在另一种测试中没有收到。管理工作进程的脚本，应分别定义信号转发、等待结束和资源归属；不能杀掉不相关进程，也不能把 Shell 已经退出当作所有后代进程都已停止的证据。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;使用 Python 3、Bash 及常见 Unix 工具运行下面的示例。每个分支启动一个独立 Bash，并在私有临时目录中创建一个文件；信号只发给这层新 Shell 自身。外层临时目录由 Python 管理，强制终止分支故意留下的文件，最后也由这个外层管理器删除，不会把残留留在用户工作区。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;signal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tempfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&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="s1"&gt;demo_file=$1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;cleanup() {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&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="s1"&gt; trap - EXIT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; trap &amp;#39;&amp;#39; INT TERM
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; rm -f -- &amp;#34;$demo_file&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; printf &amp;#39;cleanup=&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;\n&amp;#39; &amp;#34;$demo_status&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; exit &amp;#34;$demo_status&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;trap cleanup EXIT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;trap &amp;#39;exit 130&amp;#39; INT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;trap &amp;#39;exit 143&amp;#39; TERM
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;: &amp;gt; &amp;#34;$demo_file&amp;#34; || exit 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt;case &amp;#34;$2&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="s1"&gt; normal) exit 7 ;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; *) kill &amp;#34;-$2&amp;#34; &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="s1"&gt;esac
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&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="s1"&gt;&amp;#39;&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bash-trap-demo-&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;normal&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;TERM&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;143&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;INT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;KILL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGKILL&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--noprofile&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--norc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;trap-demo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BASH_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cleanup=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;remains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remains&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;KILL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;KILL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: rc=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; cleaned=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; remains=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;remains&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;前三种情况应分别返回七、一百四十三和一百三十，观察到清理输出，且文件已经不存在。强制杀死分支应得到 Python 表示信号终止的负数返回码，通常为负九，没有清理输出，同时文件仍存在；若由 Shell 调用，通常会把同一终止表示为一百三十七。这种差异来自结果表示方式，不能误判成实验不一致。&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#Signals"&gt;Bash 信号&lt;/a&gt;，等待期间的信号处理及前台进程组，已核验 2026-09-07。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#trap"&gt;POSIX trap&lt;/a&gt;，退出事件、继承的信号处置及陷阱动作，已核验 2026-09-07。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>