+6
-0
@@ -69,2 +69,3 @@ import type { DeferredCommandExpansion, ParseError, Word, WordPart } from "./types.ts"; | ||
| readonly TestMode: 2; | ||
| readonly CommandPrefix: 3; | ||
| }; | ||
@@ -96,2 +97,4 @@ export type LexContext = (typeof LexContext)[keyof typeof LexContext]; | ||
| findClosingBracket(start: number, end?: number): number; | ||
| /** Find the closing bracket of `$[ … ]`, which unlike a subscript does not recurse into `${ }`. */ | ||
| private findClosingArithmeticBracket; | ||
| /** Find the closing brace for a parameter expansion, ignoring braces inside nested shell syntax. */ | ||
@@ -137,2 +140,4 @@ findClosingBrace(start: number, end?: number): number; | ||
| private readHereDocBody; | ||
| private matchHereDocDelimiter; | ||
| private logicalLineEnd; | ||
| private skipHereDocBody; | ||
@@ -161,2 +166,3 @@ private parseHereDocBody; | ||
| private readWord; | ||
| private classifyWord; | ||
| private readWordText; | ||
@@ -163,0 +169,0 @@ private readInnerWordText; |
+31
-15
@@ -243,2 +243,7 @@ import { hasEmbeddedWordStructure, LexContext, MAX_SYNTAX_NESTING, Token, Lexer, TokenValue } from "./lexer.js"; | ||
| }; | ||
| // A heredoc delimiter is quote-removed but never expanded, so it has no expandable structure. | ||
| const heredocDelimiterParts = (source, word) => { | ||
| const raw = source.slice(word.pos, word.end); | ||
| return raw === word.text ? undefined : [{ type: "Literal", value: word.text, text: raw }]; | ||
| }; | ||
| const EMPTY_PREFIX = []; | ||
@@ -1078,4 +1083,4 @@ const EMPTY_SUFFIX = []; | ||
| const firstEnd = first.end; | ||
| // Unary test: -op word | ||
| if (UNARY_TEST_OPS[val] === 1) { | ||
| // Unary test: -op word, recognized only as written. | ||
| if (first.keywordEligible && UNARY_TEST_OPS[val] === 1) { | ||
| const nt = this.tok.peek(LexContext.TestMode).token; | ||
@@ -1095,3 +1100,3 @@ if (nt === Token.Word) { | ||
| const nt = this.tok.peek(LexContext.TestMode); | ||
| if (nt.token === Token.Word && BINARY_TEST_OPS[nt.value] === 1) { | ||
| if (nt.token === Token.Word && nt.keywordEligible && BINARY_TEST_OPS[nt.value] === 1) { | ||
| const op = this.tok.next(LexContext.TestMode).value; | ||
@@ -1153,14 +1158,23 @@ let right; | ||
| let lastEnd = cmdPos; | ||
| while (this.tok.peek(LexContext.CommandStart).token === Token.Assignment) { | ||
| const t = this.tok.next(LexContext.CommandStart); | ||
| lastEnd = t.end; | ||
| if (prefix === EMPTY_PREFIX) | ||
| prefix = []; | ||
| prefix.push(this.parseAssignment(t)); | ||
| // Assignments and redirects interleave freely; after the first element CommandPrefix | ||
| // keeps the following command name from being read as a reserved word. | ||
| let ctx = LexContext.CommandStart; | ||
| for (;;) { | ||
| const t = this.tok.peek(ctx).token; | ||
| if (t === Token.Assignment) { | ||
| const assignment = this.tok.next(ctx); | ||
| lastEnd = assignment.end; | ||
| if (prefix === EMPTY_PREFIX) | ||
| prefix = []; | ||
| prefix.push(this.parseAssignment(assignment)); | ||
| } | ||
| else if (t === Token.Redirect) { | ||
| redirects = this.collectRedirect(redirects, ctx); | ||
| lastEnd = redirects[redirects.length - 1].end; | ||
| } | ||
| else { | ||
| break; | ||
| } | ||
| ctx = LexContext.CommandPrefix; | ||
| } | ||
| // Consume prefix redirects (e.g. "2>/dev/null cmd") | ||
| while (this.tok.peek(LexContext.CommandStart).token === Token.Redirect) { | ||
| redirects = this.collectRedirect(redirects, LexContext.CommandStart); | ||
| lastEnd = redirects[redirects.length - 1].end; | ||
| } | ||
| if (this.tok.peek(LexContext.Normal).token !== Token.Word) { | ||
@@ -1231,3 +1245,5 @@ return { | ||
| if (t.targetEnd > t.targetPos) { | ||
| r.target = new WordImpl(t.content ?? "", t.targetPos, t.targetEnd, this.source, undefined, this.depth); | ||
| const heredoc = t.value === "<<" || t.value === "<<-"; | ||
| const resolver = heredoc ? heredocDelimiterParts : undefined; | ||
| r.target = new WordImpl(t.content ?? "", t.targetPos, t.targetEnd, this.source, resolver, this.depth); | ||
| } | ||
@@ -1234,0 +1250,0 @@ else { |
+1
-1
| { | ||
| "name": "unbash", | ||
| "version": "4.0.8", | ||
| "version": "4.0.9", | ||
| "description": "Fast 0-deps bash parser written in TypeScript", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
+8
-8
@@ -222,14 +222,14 @@ # unbash | ||
| Parse throughput in MB/s, higher is better, with unbash's relative speed in | ||
| parentheses. Median of five runs on Apple M1 Pro/32GB using Node.js 22.23.2. | ||
| parentheses. Median of eleven runs on Apple M1 Pro/32GB using Node.js 24.19.0. | ||
| | Parser | short (1.2KB) | advanced (0.9KB) | medium (150KB) | large (965KB) | | ||
| | ---------------------------- | ------------: | ---------------: | -------------: | ------------: | | ||
| | **unbash** | **76.7** | **69.0** | **99.6** | **111.4** | | ||
| | tree-sitter-bash (native) | 4.60 (17x) | 6.61 (10x) | 14.97 (7x) | 11.72 (10x) | | ||
| | tree-sitter-bash (WASM) | 3.81 (20x) | 4.79 (14x) | 6.62 (15x) | 6.31 (18x) | | ||
| | sh-syntax | 0.02 (3361x) | 0.03 (2299x) | 7.13 (14x) | 12.66 (9x) | | ||
| | bash-parser | 0.24 (317x) | n/a | n/a | n/a | | ||
| | @ericcornelissen/bash-parser | 0.23 (336x) | n/a | n/a | n/a | | ||
| | **unbash** | **75.7** | **69.4** | **97.1** | **115.7** | | ||
| | tree-sitter-bash (native) | 4.59 (16x) | 6.52 (11x) | 14.90 (7x) | 11.78 (10x) | | ||
| | tree-sitter-bash (WASM) | 4.56 (17x) | 5.57 (12x) | 8.55 (11x) | 7.55 (15x) | | ||
| | sh-syntax | 0.03 (2870x) | 0.04 (1947x) | 7.59 (13x) | 13.58 (9x) | | ||
| | bash-parser | 0.28 (275x) | n/a | n/a | n/a | | ||
| | @ericcornelissen/bash-parser | 0.26 (291x) | n/a | n/a | n/a | | ||
| Run the benchmarks using Node.js v22, (native tree-sitter binding does not build on newer releases): | ||
| Run the benchmarks using Node.js v22+: | ||
@@ -236,0 +236,0 @@ ```sh |
Sorry, the diff of this file is too big to display
277997
2.93%7052
2.44%