Skip to content

webpro-nl/unbash

Repository files navigation

unbash

Fast 0-deps bash parser written in TypeScript

Install

npm install unbash

Usage

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" */ ] }
    }
  }]
}

Word parts

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)

Print

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

unbash vs tree-sitter-bash

tree-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 ERROR nodes 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

unbash vs sh-syntax

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

unbash vs bash-parser

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.)

Benchmarks

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.ts

Size

unbash is 53K minified, 13KB gzipped.

Playgrounds

License

ISC

About

Fast 0-deps bash parser written in TypeScript

Topics

Resources

Stars

Watchers

Forks

Releases

Contributors

Languages