Bash AST parser for JavaScript. Zero dependencies, complete syntax coverage, plain dict output.
Two core files (lexer.mjs + parser.mjs) parse any bash source into a JSON-serializable AST. No runtime validation overhead — factory functions produce plain objects directly.
npm install bashjsastimport { parse, print, query } from 'bashjsast'
// Parse → plain dict AST
const ast = parse('echo hello | grep h > out.txt')
console.log(JSON.stringify(ast, null, 2))
// Print → canonical bash text
print(ast) // "echo hello | grep h > out.txt"
// Query → structured extraction
const q = query('FOO=bar echo hello | grep -i h > out.txt')
q.commands() // ['echo', 'grep']
q.flags() // ['-i']
q.writes() // ['out.txt']
q.assignments() // [{ name: 'FOO', value: 'bar', append: false }]
q.pipeNames() // [['echo', 'grep']]
q.analyze() // all of the above in one callReturns a plain dict AST. Node types:
| Node | Description |
|---|---|
Script |
Root node, commands[] |
SimpleCommand |
name, args[], assignments[], redirects[] |
Pipeline |
commands[], negated |
List |
op (&&/` |
If |
test, body, alternate |
While / Until |
test, body |
For |
name, items[], body (word list and arithmetic) |
Case |
word, clauses[] (each: patterns[], body, terminator) |
Select |
name, items[], body |
Group |
body (braces { ... }) |
Subshell |
body (parens ( ... )) |
Function |
name, body, hasKeyword |
Arithmetic |
expression ((( ... ))) |
Condition |
condType, op, left, right ([[ ... ]]) |
Coproc |
name, body |
Converts AST back to canonical bash text. Round-trip idempotent: print(parse(print(parse(src)))) === print(parse(src)).
Structured extraction without manual AST traversal:
.commands()— all command names.flags()— all flags (e.g.-i,--verbose).args()— all non-flag arguments.pipes()— pipe chains as[{name, args}].pipeNames()— pipe chains as[name].writes()/.reads()— redirect targets.assignments()— variable assignments.functions()— function definitions.subcommands()—$()and` `subcommands.analyze()— all of the above in one call
Direct access to the tokenizer:
import { Lexer, T } from 'bashjsast'
const tokens = new Lexer('echo hello').tokenize()
// [{ type: 'WORD', value: 'echo' }, { type: 'WORD', value: 'hello' }, { type: 'EOF' }]Drop-in replacement for unbash in hook systems:
import { parse } from 'bashjsast/compat'
const result = parse('FOO=bar echo hello > out.txt')
// { type: 'simple', commands: [{ type: 'Command', name: {text: 'echo'}, prefix: [{text: 'FOO=bar'}], suffix: [{text: 'hello'}], redirects: [{operator: '>', content: 'out.txt'}] }], raw: '...' }Complete bash syntax support including:
- Simple commands, pipelines, lists (
&&,||) - All compound commands:
if/elif/else,while,until,for(word + arithmetic),case(;;/;&/;;&),select - Groups
{ }, subshells( ) - Functions (keyword and POSIX style)
- Arithmetic
(( ))and conditional[[ ]] - Coproc
- Redirects:
>,>>,<,<<,<<<,>&,<&,>|,&>,&>>,<> - Heredocs and here-strings
- Process substitution
<(),>() - Array assignments
=() - Assignments (including
+=) - Quoting: single, double,
$'',$"" - Command substitution
$()and` ` - Line continuation
\
| bashjsast | bashlex | |
|---|---|---|
| Language | JavaScript (ESM) | Python |
| Dependencies | 0 | 0 |
| Core size | 2 files, 1305 lines | ~10 files |
| Output | plain dict (JSON-safe) | custom AST node classes |
(( )) arithmetic |
yes | NotImplementedError |
[[ ]] conditional |
yes | no |
coproc |
yes | NotImplementedError |
select |
yes | no |
| AST → text printer | yes (round-trip idempotent) | no |
| Structured query API | yes (query().analyze()) |
no |
| bashlex test suite | 72/72 (100%) | — |
| Performance | ~0.05ms per parse | ~ms (Python) |
bashjsast passes every valid test case from bashlex's test suite and handles 5 syntax constructs bashlex explicitly does not support.
src/
lexer.mjs — tokenizer (context-dependent reserved words)
parser.mjs — recursive descent → plain dict AST
printer.mjs — AST → canonical bash text
query.mjs — structured extraction layer
compat.mjs — unbash-compatible output
index.mjs — public exports
schema/ — 34 JSON Schema Draft-07 files (AST contract documentation)
test/ — 129+ tests (node:test)
Core is two files: lexer.mjs (441 lines) + parser.mjs (864 lines). Zero dependencies.
MIT