<?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>Bash · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/bash/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/bash/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>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></channel></rss>