
Company News
Free Business Plan Upgrades for Open Source Maintainers
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.
Fast 0-deps bash parser written in TypeScript
npm install unbash
Use unbash when your input is Bash syntax. A shell command or a complete script, and you need to inspect its structure without executing it. It returns a typed, source-positioned AST.
Example use cases:
curl from pasted shell input while keeping
neighboring pipelines, logical chains, redirects, and comments separateunbash supports commands, control flows, pipelines, redirects, assignments, compound statements, parameter and word expansions, process and nested substitutions, coproc, heredocs, herestrings, nested and generated syntax, etc.
Nested commands remain structured inside parameter operands and array indexes, arithmetic expressions, brace expansions, extglobs, redirect targets, and heredoc bodies. Nodes retain source positions and words retain both raw text and dequoted values.
Malformed and incomplete input returns a best-effort partial AST with detected,
source-positioned errors. Recovery is bounded for deeply nested parameter and
arithmetic expansions, substitutions, subshells, braces, conditionals, loops,
select, case, and [[ ]] groups.
unbash does not execute code, perform shell expansion, provide a sandbox, or
decide whether a command is safe. Security-sensitive consumers must inspect word
parts, nested scripts, and errors on each parsed script. unbash is a tolerant
parser: for malformed or incomplete input, it recovers where possible and
returns a best-effort partial AST with source-positioned errors. It does not
target PowerShell, cmd.exe, or other shell languages. Much POSIX sh syntax
is also valid Bash.
To parse process.argv (string[]), use Node.js parseArgs or a CLI
library such as yargs or citty.
import { 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" */ ] }
}
}]
}
See the full AST at unbash.statichost.page#input=if [ -f...
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); // same
Read 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)
Word-like fields that can execute nested shell syntax expose the same structure.
BraceExpansion, ExtendedGlob, and ArithmeticWord use parts; parameter
and assignment array indexes use indexParts.
Positions index the source owned by the nearest ParsedScript. Root scripts and
verbatim nested substitutions share the caller's source, so their pos/end
slice that source directly:
const nested = word.parts.find(
(part) => part.type === "CommandExpansion",
).script;
const command = nested.commands[0].command;
source.slice(command.pos, command.end); // exact nested command source
A legacy backtick script whose body contains backslash escapes owns its decoded
string as a non-enumerable source property. Ordinary scripts nested inside it
index that decoded source. Object spread and structuredClone omit source
because it is non-enumerable.
Parse errors inside a lazily parsed script surface on that script, not on the
root: check errors on every nested script while traversing. A consumer that
only reads the root errors array cannot tell that a substitution body failed
to parse.
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"
fi
tree-sitter-bash is the right choice when you need:
ERROR nodes and continues
parsingunbash provides:
sh-syntax is a WASM wrapper around the robust mvdan/sh Go parser. It is highly recommended if you need:
unbash provides:
bash-parser (last publish: 2017) and its fork @ericcornelissen/bash-parser (community dependency maintenance fork ❤️ now archived) provide:
unbash provides:
[[ ]] test expressions; bash-parser treats [[ ]] as ordinary commands and
(( )) as nested subshellsfor, select, process substitution, coproc, array
assignments, extglob, ;&/;;& case fallthrough, Bash 5.3 command
substitutions, and {variable} file-descriptor redirectsMedian relative performance across three runs on Apple M1 Pro/32GB using Node.js 22.23.2. unbash is x times faster:
| Parser | short | advanced | medium | large |
|---|---|---|---|---|
| tree-sitter-bash (native) | 17x | 10x | 7x | 10x |
| tree-sitter-bash (WASM) | 20x | 14x | 15x | 17x |
| sh-syntax | 3560x | 2370x | 15x | 9x |
| bash-parser | 317x | N/A | N/A | N/A |
| @ericcornelissen/bash-parser | 335x | N/A | N/A | N/A |
Run the benchmarks using Node.js v22:
pnpm install
node bench/all.ts
The parser bundle is 77KB minified and 18KB gzipped.
ISC
FAQs
Fast 0-deps bash parser written in TypeScript
The npm package unbash receives a total of 7,755,952 weekly downloads. As such, unbash popularity was classified as popular.
We found that unbash demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.

Security News
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.

Security News
During a UK cyber test, a Mythos 5 agent used sockpuppets, social engineering, and prompt injection to try to get a maintainer to merge malware.