@aliou/sh

中文文档
0.4.0发布于 2026/11/05MIT AND BSD-3-ClauseTypeScriptES Module零依赖构建来源已验证近 30 天下载 6899

Shell parser/AST for POSIX/Bash/mksh/zsh (WIP)

原始使用文档
文档目录10 节
  1. Install
  2. Quick start
  3. Example: flag dangerous commands
  4. Options
  5. Lazy parsing
  6. Other exports
  7. Supported syntax
  8. Status
  9. Development
  10. License

@aliou/sh

Shell parser and typed AST for TypeScript. Handles POSIX, Bash, mksh, and zsh syntax. Zero runtime dependencies, ~66 KB bundled.

Install

npm install @aliou/sh

Requires Node.js 22+.

Quick start

import { parse } from "@aliou/sh";

const { ast } = parse('echo "hello $USER" | grep hello');
// ast.type === "Program"
// ast.body[0].command.type === "Pipeline"

parse returns a Program whose body holds Statement nodes. Every statement wraps a Command:

CommandShell syntax
SimpleCommandwords, assignments, redirects
Pipelinea | b
Logicala && b, a || b
IfClause, WhileClause, ForClause, SelectClause, CaseClausecontrol flow
FunctionDeclfoo() {}, function foo {}
Subshell, Block( ... ), { ... }
TestClause, ArithCmd[[ ... ]], (( ... ))
CoprocClause, TimeClausecoproc, time
DeclClausedeclare, local, export, readonly, typeset, nameref
LetClauselet i++ j=2
CStyleLoopfor (( ; ; ))

Words hold typed parts: Literal, SglQuoted, DblQuoted, ParamExp, CmdSubst, ArithExp, ProcSubst, BraceExp, ExtGlob. Every node carries pos and end (offset, line, col) pointing into the source.

Example: flag dangerous commands

Each node's type field narrows the union, so a generic walker can catch specific commands wherever they appear — pipelines, && chains, subshells, command substitutions:

import { parse, type SimpleCommand } from "@aliou/sh";

function findRmRf(node: unknown): string[] {
  if (!node || typeof node !== "object") return [];

  const cmd = node as SimpleCommand;
  if (cmd.type === "SimpleCommand") {
    const [name, ...args] = (cmd.words ?? [])
      .map((w) => (w.parts[0].type === "Literal" ? w.parts[0].value : ""))
      .filter(Boolean);
    if (name === "rm" && args.some((a) => a.startsWith("-") && a.includes("r") && a.includes("f"))) {
      return [`rm ${args.join(" ")}`];
    }
  }

  return Object.values(cmd).flatMap((v) =>
    Array.isArray(v) ? v.flatMap(findRmRf) : findRmRf(v),
  );
}

const { ast } = parse("deploy.sh && rm -rf ./scratch; cp -r dist/ b/ || (rm -rf b/ && exit 1)");
findRmRf(ast); // ["rm -rf ./scratch", "rm -rf b/"]

This kind of check is the project's main use case: inspect or rewrite commands before handing a script to something that executes it.

Options

interface ParseOptions {
  dialect?: "posix" | "bash" | "mksh" | "zsh"; // default: "bash"
  keepComments?: boolean; // default: false
  recoverErrors?: boolean; // default: false
}

Set recoverErrors: true to get a partial AST plus an errors array instead of a thrown exception. Useful for analyzing incomplete or broken scripts (editors, linters).

Lazy parsing

For large scripts, stream top-level nodes without building the whole AST:

import { parseStmtsSeq, parseWordsSeq } from "@aliou/sh";

for (const stmt of parseStmtsSeq(source)) { /* one statement at a time */ }
for (const word of parseWordsSeq(source)) { /* one word at a time */ }

Other exports

  • splitBraces(word) — expand {a,b} / {1..5} brace expansion into a word. The parser does not emit brace expansions by default.
  • NO_POS — sentinel position for nodes you build by hand.
  • Full TypeScript types for every AST node.

Supported syntax

  • Simple commands, pipelines, &&, ||, background (&), semicolons
  • Quoting: '...', "...", backslash continuations
  • Parameter expansion: $var, ${var:-default}, ${var/pat/repl}, slices, indirection
  • Command substitution: $(cmd), `cmd`
  • Arithmetic: $((expr)), ((expr)), C-style for (( ; ; ))
  • Process substitution: <(cmd), >(cmd)
  • Heredocs (<<, <<-) and herestrings (<<<)
  • Redirects: >, >>, <, >&, <&, <>, >|, &>, &>>, plus {varname} fd redirects (Bash/zsh)
  • Assignments and appends: FOO=bar, FOO+=bar, command-scoped env
  • Arrays: arr=(a b c), arr=([0]=x [1]=y)
  • Control flow: if/elif/else/fi, while/until, for/in, select/in, case/esac
  • Functions, subshells, blocks, [[ ]], (( )), coproc, time, !
  • Extended globs @(foo), *(bar) (Bash/mksh)
  • Comments (with keepComments: true)

Status

Work in progress. Covers the Bash subset needed for AST-based command analysis (classification, variable-mutation tracking, guardrail enforcement). It is not a complete POSIX/Bash parser; edge cases in less common dialect features may be missing.

Development

Requires Nix for the dev shell:

nix develop
pnpm install
pnpm test        # vitest
pnpm typecheck   # tsc --noEmit
pnpm lint        # biome
pnpm build       # rolldown + declarations

License

MIT — see LICENSE. This project is a TypeScript port of mvdan/sh (BSD 3-Clause, © Daniel Martí); the original license is included in LICENSE.

版本与发布记录

@aliou/sh 共发布 11 个版本,首次发布于 2026/02/05;过去 12 个月发布了 11 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.4.0latest2026/11/05今天
0.3.42026/10/0630 天前
0.3.32026/09/201 个月前
0.3.22026/09/131 个月前
0.3.12026/09/052 个月前
0.3.02026/08/242 个月前

关于 @aliou/sh 的常见问题

@aliou/sh 是什么?

Shell parser/AST for POSIX/Bash/mksh/zsh (WIP)

如何安装 @aliou/sh?

在项目目录运行 npm install @aliou/sh,或使用 pnpm add / yarn add / bun add。

@aliou/sh 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@aliou/sh 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@aliou/sh 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@aliou/sh 可以商用吗?

@aliou/sh 使用 MIT AND BSD-3-Clause 许可。声明的许可为 MIT AND BSD-3-Clause,请阅读许可原文确认使用条件。以上为概括,不构成法律意见,请以许可原文为准。

@aliou/sh 的下载量有多少?

根据 npm 官方下载统计,@aliou/sh 在 2026/10/06 – 2026/11/04 这 30 天内被下载 6,899 次,日均约 230 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@aliou/sh 还在维护吗?

最新版本 0.4.0 发布于 2026/11/05(今天);至今共发布 11 个版本,其中过去 12 个月发布了 11 个;首次发布于 2026/02/05。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。