unbash: Fast Bash Parser
unbash is a fast, zero-dependency library written in TypeScript for parsing Bash scripts into a structured Abstract Syntax Tree (AST). Currently stable at version 2.2.0, it differentiates itself by offering a pure JavaScript/TypeScript implementation without WASM or native bindings, providing a fully typed API, and delivering high performance, often outperforming alternatives by a significant margin. Its release cadence follows a typical Semantic Versioning approach, with updates for features and bug fixes. Key differentiators include its AST-centric output (unlike CST-focused parsers), tolerant parsing that collects errors rather than throwing exceptions, and built-in support for advanced Bash syntax such as process substitutions, coproc, `[[ ]]` test expressions, `(( ))` arithmetic evaluations, and extglob. It is designed for environments requiring a lightweight, synchronous parsing solution. While it excels at AST generation and speed, it does not offer incremental parsing, full token preservation for CSTs, or multi-shell dialect support (e.g., pure POSIX sh), features found in libraries like tree-sitter-bash or sh-syntax. It requires Node.js v14 or higher and maintains a small bundle size (13KB gzipped).
Common errors
-
Error [ERR_REQUIRE_ESM]: require() of ES Module ... from ... not supported. Instead, change the require of ... to a dynamic import() or top-level import statement.
cause Attempting to use `require('unbash')` in a CommonJS module, but unbash v2+ is an ES Module.fixUpdate your import statement to `import { parse } from 'unbash';` and ensure your Node.js project is configured for ESM (e.g., by adding `"type": "module"` to your `package.json`). -
SyntaxError: The requested module 'unbash' does not provide an export named 'parse'
cause Incorrectly trying to import `parse` as a default export or using a CommonJS-style named import when the package uses named ESM exports.fixUse a named import specifically: `import { parse } from 'unbash';` (and similarly for `print` from `unbash/print`). -
My parsed and then printed script looks different from the original input; comments and exact whitespace are gone.
cause The `print` function is an opinionated code formatter. It intentionally re-formats the AST into a standardized Bash script string and does not preserve original non-semantic elements.fixThis is expected behavior. The purpose of `print` is to produce a well-formatted, functional Bash script from the AST, not to recreate the input verbatim. If exact formatting preservation is critical, `unbash`'s `print` might not be suitable for that specific task. -
My program didn't report any errors even when I fed it a syntactically incorrect Bash script.
cause unbash implements tolerant parsing, meaning it collects syntax errors in the `errors` array within the returned AST object instead of throwing an exception.fixAfter every call to `parse()`, you must explicitly check the `ast.errors` property. For example: `const ast = parse(script); if (ast.errors && ast.errors.length > 0) { console.error('Parsing failed:', ast.errors); }`
Warnings
- breaking unbash is an ES Module (ESM) package since version 2. Attempting to `require()` it in a CommonJS environment will result in a runtime error.
- gotcha The `print` utility is an opinionated formatter. It does not preserve original whitespace, comments (except shebang), or exact input formatting. It generates a normalized Bash script from the AST.
- gotcha unbash uses tolerant parsing. This means syntax errors do not throw exceptions but are collected within the `errors` property of the returned AST object. Your application must explicitly check this property.
- gotcha unbash generates an Abstract Syntax Tree (AST) and does not support incremental parsing or Concrete Syntax Tree (CST) output. If you need full token preservation, granular error recovery with `ERROR` nodes, or incremental parsing for editor integration, alternatives like `tree-sitter-bash` might be more suitable.
- gotcha unbash is specifically designed for Bash syntax. It does not provide inherent support for other shell dialects such as pure POSIX sh, mksh, or others. For multi-dialect support, consider alternatives like `sh-syntax`.
Install
-
npm install unbash -
yarn add unbash -
pnpm add unbash
Imports
- parse
const { parse } = require('unbash')import { parse } from 'unbash' - print
import { print } from 'unbash'import { print } from 'unbash/print' - Script
import { type Script } from 'unbash'
Quickstart
import { parse, type Script } from "unbash";
import { print } from "unbash/print";
// Define a Bash script string to parse.
const bashScript = 'if [ -f "$1" ]; then\n echo "File exists: $1"\n cat "$1"\nelse\n echo "File not found: $1"\nfi';
// Parse the script into an AST (Abstract Syntax Tree).
// unbash performs tolerant parsing, meaning it attempts to create an AST even with syntax errors,
// collecting any issues in the 'errors' property of the returned object.
const ast: Script = parse(bashScript);
// Check if any parsing errors occurred.
if (ast.errors && ast.errors.length > 0) {
console.warn("Parsing errors detected:", ast.errors);
// Depending on your application, you might want to stop here or attempt to recover.
} else {
console.log("Successfully parsed script into AST (truncated):\n", JSON.stringify(ast, null, 2).substring(0, 500) + "...");
}
// Use the built-in printer to convert the AST back into a Bash script string.
// Note: The `print` function is opinionated and will reformat the script,
// not necessarily preserving original whitespace, comments, or exact layout.
const printedScript = print(ast);
console.log("\nRe-printed script from AST:\n", printedScript);
// Demonstrate parsing a script with an obvious error to show tolerant parsing
const erroneousScript = 'for i in 1 2 3; do echo $i fi;'; // Missing 'done'
const erroneousAst = parse(erroneousScript);
if (erroneousAst.errors && erroneousAst.errors.length > 0) {
console.error("\nDetected errors in erroneous script example:", erroneousAst.errors);
}