Fast 0-deps bash parser written in TypeScript
npm install unbashimport { parse } from "unbash";
const ast = parse('if [ -f "$1" ]; then cat "$1"; fi');Result:
{
type: "Script",
commands: [{
type: "Statement",
command: {
type: "If",
clause: { type: "CompoundList", commands: [ /* [ -f "$1" ] */ ] },
then: { type: "CompoundList", commands: [ /* cat "$1" */ ] }
}
}]
}A Word holds its expansions in parts. This is a lazy getter, computed on
first access (not an own enumerable property):
const word = parse("echo a$(id)b").commands[0].command.suffix[0];
word.parts; // [Literal, CommandExpansion, Literal]
Object.keys(word); // ["text", "pos", "end"] — no `parts`
({ ...word }); // same
structuredClone(word); // sameRead parts directly, or serialize with JSON.stringify, which includes it
through toJSON. A generic walker driven by Object.keys finds no expansions
at all, and reports no error while doing so:
import { parse } from "unbash";
const script = parse('echo "$HOME" $(mktemp)');
for (const statement of script.commands) {
const command = statement.command;
if (command.type !== "Command") continue;
for (const word of [command.name, ...command.suffix]) {
for (const part of word?.parts ?? []) {
if (part.type === "CommandExpansion") console.log(part.text);
}
}
}
// $(mktemp)Basic opinionated printer, does not preserve whitespace or comments (except shebang):
import { parse } from "unbash";
import { print } from "unbash/printer";
const ast = parse('if [ -f "$1" ]; then cat "$1"; fi');
const script = print(ast);Result:
if [ -f "$1" ]; then
cat "$1"
fitree-sitter-bash is an excellent choice if you need:
- Incremental parsing
- CST output preserving all tokens and punctuation
- Granular error recovery that wraps errors in
ERRORnodes and continues parsing
unbash might be a good fit if you prefer:
- AST output
- A zero-dependency package that runs in any JS environment
- A typed TypeScript API
- Built-in parsing for command/process substitutions, coproc, Bash 5.3
${ cmd; },[[ ]],(( )), and extglob - Tolerant parsing that never throws and collects parse errors
sh-syntax is a WASM wrapper around the robust mvdan/sh Go parser. It is highly recommended if you need:
- Support for multiple shell dialects (bash, POSIX sh, mksh, Bats)
- Built-in formatting and pretty-printing (
print)
unbash might be a good fit if you prefer:
- A zero-dependency, synchronous API
- A detailed AST with structured word parts, parameter expansions, arithmetic expressions, and test expressions
bash-parser (last publish: 2017) and its fork @ericcornelissen/bash-parser (community dependency maintenance fork ❤️ now archived) might be interesting if you need:
- A POSIX-only mode that rejects bash-specific syntax
unbash might be a good fit if you prefer:
- A zero-dependency architecture
- A typed TypeScript API (ESM-only)
- Tolerant parsing that never throws and collects parse errors
- Structured AST nodes for parameter expansions, arithmetic expressions, and
[[ ]]test expressions - Support for many additional syntax features (like herestrings, C-style for
loops,
select, process substitution, etc. etc.)
Relative performance comparison (on Apple M1 Pro/32GB), unbash is x times faster:
| Parser | short | advanced | medium | large |
|---|---|---|---|---|
| tree-sitter-bash (native) | 13x | 9x | 4x | 5x |
| tree-sitter-bash (WASM) | 16x | 12x | 8x | 8x |
| sh-syntax | 2136x | 1537x | 8x | 4x |
| bash-parser | 256x | N/A | N/A | N/A |
| @ericcornelissen/bash-parser | 267x | N/A | N/A | N/A |
Run the benchmarks using Node.js v22:
pnpm install
node bench/all.tsunbash is 53K minified, 13KB gzipped.
ISC