Skip to content

[Bug] scanCommands 误报:手册页/文档/同名目录被当成命令,缺失项被判成 external(实例 just → just.1) #31

Closed

Description

@youyv

现象

「终端工具」卡片的「完整路径 / 由谁提供」列会显示错误的路径。真实案例:v0.40.1 在本机把 Casey.Just 从 1.58.0 升到 1.59.0 后,用插件自己的 probeToolchain()(卡片渲染用的同一段代码)探测,just 行返回:

{
  "id": "just", "cmd": "just", "wingetId": "Casey.Just",
  "state": "current", "provider": "winget", "version": "1.59.0",
  "commandPath": "C:\\Users\\…\\WinGet\\Packages\\Casey.Just_Microsoft.Winget.Source_8wekyb3d8bbwe\\just.1"
}

commandPath 指向的是手册页 just.1——同一目录里明明有 just.exe,但 just.1 在 readdir 的字典序里排在前面(.1 < .e),而扫描是「首个命中者胜出」。

复现(纯函数级,与机器环境无关)

scanCommands 是导出的纯函数,可以直接喂假 io。构造目录:

条目 实际是什么
bat.exe 真可执行文件
fzf 无扩展名的真可执行文件(MSYS 风格,应识别)
make.1 手册页
tree.txt 文本文档
jq.md Markdown 文档
gdu.d/ 一个目录
const io = { pathDirs: () => [dir], readdir: (d) => readdirSync(d) }
scanCommands(io, TOOLCHAIN)

v0.40.1 实测(与 main 同):

bat   -> bat.exe       [应识别]
fzf   -> fzf           [应识别]
make  -> make.1        [不该识别]
tree  -> tree.txt      [不该识别]
jq    -> jq.md         [不该识别]
gdu   -> gdu.d         [不该识别]   ← 目录也被当成命令

根因

src/toolchain.js 的 scanCommands:

// "make.exe" / "make.cmd" / "make" all resolve the command `make`.
const wanted = new Map(...)
...
for (const entry of entries) {
  if (typeof entry !== 'string') continue
  const base = entry.replace(/\.[^./\\]+$/, '').toLowerCase()   // ← 剥掉任意扩展名

注释写明了意图是「.exe / .cmd / 无扩展名都算 make」,但实现是剥掉任意扩展名,并且:

  1. 不校验扩展名是否属于可执行集合(.exe/.cmd/.bat/.com,或按 PATHEXT);
  2. 不区分文件与目录——readdir 返回的目录名同样参与匹配。

影响

  • 显示层:「完整路径 / 由谁提供」列显示错路径,把手册页当成可执行文件。
  • 判定层(更要紧):classifyToolState 在 wingetOwned === false 时只看 hasCommand。实测:
classifyToolState({ hasCommand: true /* 实为 make.1 */, wingetOwned: false, upgradable: false })
  -> 'external'   (isActionable -> false)
应为 -> 'missing' (isActionable -> true)

即:一台确实没装该命令、但 PATH 目录里恰好有同名文档 / 手册页 / 目录的机器,该行会被判成 external(由他人工具链提供),isActionable 为 false →「一键安装」永远不会勾选它,用户也拿不到安装入口。本机 just 的状态之所以仍正确(current),只是因为 winget 清单已拥有它(wingetOwned 命中在前),把这个缺陷掩盖了。

建议修法

只认可执行:扩展名属于 {.exe,.cmd,.bat,.com} 或无扩展名,并且必须是文件而非目录。

const EXEC_EXTS = new Set(['.exe', '.cmd', '.bat', '.com'])
// …
const m = entry.match(/(\.[^./\\]+)$/)
const ext = m ? m[1].toLowerCase() : ''
if (ext !== '' && !EXEC_EXTS.has(ext)) continue
const base = ext === '' ? entry : entry.slice(0, -ext.length)

无扩展名必须保留——Git Bash 的 PATH 目录里有大量 MSYS 可执行文件是无扩展名的(例如 /usr/bin/make);. / .. 这类条目也应排除。目录项可以给 io 增加 stat/isFile 来排除,或先只加扩展名白名单、把目录这一路单独处理。

环境

  • dsh-gitbash-shell v0.40.1(npm);已对照 main 的 src/toolchain.js,代码相同
  • Windows 10.0.26200 · Node 24.19.0 · winget 1.29.380
  • 触发场景:设置 → 插件 → dsh-gitbash-shell →「终端工具」执行检查 / 更新

如需,我可以直接提 PR(含 scanCommands 的回归用例)。

Activity

  1. KannaKuron commented on Oct 16, 2026

    @KannaKuron
    Owner

    感谢这份报告——根因分析、纯函数级复现和影响分层(显示层 / 判定层)都非常到位,已对照 main 的 src/toolchain.js 确认属实:

    • scanCommands 的 entry.replace(/\.[^./\\]+$/, '') 确实会剥掉任意扩展名(make.1 / tree.txt / jq.md 全部误命中);
    • gdu.d 这类目录也参与匹配;
    • classifyToolState 在 wingetOwned === false 时只看 hasCommand(if (hasCommand) return 'external'),所以「同名文档挡住安装入口」的判定层影响是真实的。

    非常欢迎你直接提 PR(含回归用例)。评审时我们会关注这几点,供参考:

    1. 扩展名白名单 + 无扩展名保留——同意你的修法。白名单 {.exe,.cmd,.bat,.com} + 无扩展名继续命中,这一个改动就能覆盖你报告的全部真实案例(just.1/make.1/tree.txt/jq.md/gdu.d 的扩展名都不在白名单里,自动排除);
    2. 目录排除建议放第二步:当前 io.readdir 的契约是返回 string[](不带 isFile 信息),要区分「无扩展名的目录」和「无扩展名的 MSYS 可执行文件」(fzf 这种)需要给 io 加 stat/Dirent 能力并牵动调用方。建议本 PR 先只做扩展名白名单(纯函数改动、零 io 变更),目录区分留作后续——真正会被漏掉的只有「恰好与工具同名的无扩展名目录」这一种罕见形态;
    3. . / ..:Node 的 readdirSync 不返回这两项,防御性排除可以加但不强求;
    4. 回归用例建议照你的复现表锁三层:文档/手册页不命中、无扩展名真可执行(fzf)仍命中、字典序 just.1 < just.exe 时首个可执行命中者的路径正确。判定层建议补一条 classifyToolState({ hasCommand: false, wingetOwned: false }) → missing 的联动断言。

    另外提 PR 时请顺手在 CHANGELOG 加一条(仓库惯例:变更随版本提交)。我们会照常跑完整测试套件 + windows-latest CI 后合并发版。

  2. KannaKuron commented on Oct 17, 2026

    @KannaKuron
    Owner

    已随 PR #32 合并,并发布 v0.40.4(npmjs / npmmirror 双通道已同步)。

    修复要点:scanCommands 新增可执行扩展名白名单 {.exe,.cmd,.bat,.com}(无扩展名条目照旧命中,/usr/bin/make 一类不受影响),手册页 / 文档 / 同名目录不再以同名词根冒充命令,也不再抢走 first-hit;判定层联动——被同名文档挡路的缺失命令恢复 missing ⇒ 一键安装入口重新出现。回归锁三层(含你实测的 just.1 排在 just.exe 之前的字典序场景)+ classifyToolState 联动断言;CI ubuntu+windows × Node 22/24 四矩阵绿。

    再次感谢高质量的「报告+修复」一体交付。升级到 0.40.4 后如果 just 一行仍有异常,欢迎直接重开或追加 issue;无扩展名目录 vs MSYS 可执行文件的区分(Dirent)已记录为后续项。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions