<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>路径 · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/%E8%B7%AF%E5%BE%84/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/%E8%B7%AF%E5%BE%84/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>