unbash:纯 TypeScript 零依赖的 Bash 解析器,给 Agent 用的审计层
阅读时间: 大约 9 分钟
unbash:纯 TypeScript 零依赖的 Bash 解析器,给 Agent 用的审计层

unbash 是 webpro-nl 用 TypeScript 写的 Bash 解析器(当前 playground 显示版本 4.0.11,ISC 协议,npm install unbash),口号是”Fast 0-deps bash parser”。它做的事很明确:把一段 Shell 源码不执行地解析成一棵带类型、带源码位置的 AST。据 Koala 项目库点评,这类工具过去几乎被 tree-sitter 垄断,但 tree-sitter 要带原生插件或 WASM 运行时,在浏览器和 Serverless 里部署别扭;Agent 如今大量生成并执行 Shell,执行前做静态审计正需要这样一个轻量解析层。本文基于官方 README 与在线 playground 梳理。
一、它解决什么问题
官方列的使用场景很贴近现实:
- 为权限弹窗、allowlist、审计报告分类命令、重定向、命令替换与后台执行;
- 静态盘点 package scripts、CI 步骤、任务运行器配置、hooks 与源码内 Shell 调用里会调用哪些可执行文件、引用了哪些字面文件/配置/依赖;
- 单独提取
curl这类受支持命令,而不把相邻的管道、逻辑链、重定向、注释折叠进它的参数; - 给 Agent 生成或用户粘贴的 Bash 附诊断信息(带源码位置的错误与部分树);
- 对管道、条件、展开、嵌套命令做解释、可视化或结构化预览;
- 按源码范围查找/迁移命令与参数,且保留周围格式与注释不动。
一个重要边界:当 Bash 嵌在 JSON/YAML/源码里时,官方建议先用宿主语言解析器把 Shell 字符串抽出来——unbash 只负责这段字符串内部的结构与位置,命令级参数语义与安全策略由调用方负责。
二、AST 长什么样
官方示例 if [ -f "$1" ]; then cat "$1"; fi 解析后,顶层是 Script,里面是 Statement,command 类型为 If,再往下是 clause/then 各自的 CompoundList。上面第二张图正是 playground 对该语句的真实输出。

它支持的语法面相当广:命令、控制流、管道、重定向、赋值、复合语句、参数与单词展开、进程与命令替换、coproc、heredoc、herestring、嵌套与生成式语法等。嵌套命令会在参数操作数、数组下标、算术表达式、brace 展开、extglob、重定向目标与 heredoc 体内保持结构化。节点保留源码位置,单词同时保留原文(raw text)与去引号后的值(dequoted value)。
三、三个容易踩的实现细节
README 里有几处相当诚实的工程说明,值得专门指出:
parts是惰性 getter:一个 Word 把它的展开放在parts里,首次访问才计算,且不是可枚举自有属性——Object.keys(word)、对象展开、structuredClone都看不到它,却不会报错。README 特别警告:一个靠Object.keys遍历的通用 walker 会完全找不到命令替换,还静默地报告无异常;正确做法是直接读word.parts,或依赖JSON.stringify的toJSON。- 错误要在每一层嵌套脚本上查:惰性解析出来的脚本,其内部解析错误挂在那个脚本节点上,而不是根节点。只读根脚本的
errors数组,会漏掉某个$(...)体内解析失败的情况。 - 位置是 UTF-16 偏移:
[pos, end)是从零开始的半开区间,基于 UTF-16 code unit,由最近的 ParsedScript 持有源码——处理非 BMP 字符时要留意。
四、与同类工具的取舍(README 自述)
| 维度 | tree-sitter-bash | sh-syntax (mvdan/sh) | bash-parser | unbash |
|---|---|---|---|---|
| 增量解析 | ✅ | |||
| CST 保留全部标点 | ✅ | |||
| 细粒度 ERROR 节点 | ✅ | |||
| 多 Shell 方言 | ✅(Bash/POSIX/mksh/Bats/Zsh) | 仅 Bash(兼容多数 POSIX sh) | ||
| 成熟格式化/pretty-print | ✅ | 基础 printer(不留空白注释) | ||
| 零依赖、同步 TS | 部分 | ✅ | ||
| 无原生插件/WASM | WASM 包装 | ✅ | ||
| 容错部分 AST | ERROR 节点包裹 | POSIX-only 会拒 Bash 语法 | ✅ best-effort |
官方口径很克制:tree-sitter-bash 在增量解析、CST、细粒度错误恢复上”就是正确选择”;sh-syntax(对 Go 版 mvdan/sh 的 WASM 封装)在多方言与成熟格式化上”强烈推荐”;bash-parser 自 2017 年后未维护、社区 fork 已归档,且其 POSIX-only 模式会拒绝 Bash 专属语法。unbash 的差异化在于:纯 TypeScript、同步、零依赖、无需 WASM 加载、JSON 友好的小 AST,以及面向编辑器/用户输入的容错部分树。
五、口径与局限:它明确不做的事
这是评估它安全用途时最关键的一节,全部来自官方原文:
- 它不执行代码、不做 Shell 展开、不提供沙箱、也不判断命令是否安全。README 直说:安全敏感的调用方必须自己检查每个脚本里的单词片段、嵌套脚本与错误。
- 不是 PowerShell/cmd 解析器:只面向 Bash(多数 POSIX sh 语法也兼容)。
- printer 不保留空白与注释(除 shebang 外),需要原样重排格式的场景要小心。
- 容错有边界:README 承认对”深层嵌套的参数与算术展开、替换、子 shell、brace、条件、循环、select、case、
[[ ]]组”的恢复是有界的——不是任意乱码都能救回来。 - 解析
process.argv(string[])不该用它:官方明确建议那种场景用 Node 的parseArgs或 yargs/citty。
六、客观分析:优势与适合人群
优势: 在浏览器/Serverless 里拿一棵”能跑、零依赖、同步”的 Bash AST,这件事过去要扛原生模块或 WASM;惰性结构化 word parts + 递归解析替换,正好命中”审计 Agent 生成命令会调哪些可执行文件”的刚需;容错部分树让它能接编辑器和不完整输入。
适合: 在 Web 端或 Serverless 里做命令审计、权限提示、CI 静态盘点的工具链作者;需要按源码范围安全改写脚本的迁移工具。
不适合: 需要增量解析或完整 CST 的语法高亮/编辑器内核(用 tree-sitter);需要多方言格式化(用 sh-syntax);把它当沙箱或安全判决器——它只是解析层,安全判断仍在你自己手里。