+3
-0
@@ -63,2 +63,3 @@ import type { DeferredCommandExpansion, ParseError, Word, WordPart } from "./types.ts"; | ||
| } | ||
| export declare function requiresFunctionKeyword(name: string): boolean; | ||
| export declare function hasEmbeddedWordStructure(source: string, start: number, end: number): boolean; | ||
@@ -150,2 +151,3 @@ export declare const LexContext: { | ||
| private _unbalanced; | ||
| private _notArithmetic; | ||
| private _dqText; | ||
@@ -169,2 +171,3 @@ private _dqHasExpansions; | ||
| private readArithmeticExpansion; | ||
| private buildArithmeticExpression; | ||
| private readArithmeticCommand; | ||
@@ -171,0 +174,0 @@ private readCommandSubstitution; |
+39
-12
@@ -449,11 +449,16 @@ import { hasEmbeddedWordStructure, LexContext, MAX_SYNTAX_NESTING, Token, Lexer, TokenValue } from "./lexer.js"; | ||
| } | ||
| const negated = this.tok.peek(LexContext.CommandStart).token === Token.Bang; | ||
| if (negated) { | ||
| if (!time) | ||
| // `pipeline_command: '!' pipeline_command` — each `!` toggles the invert flag | ||
| // instead of nesting, so an even number of them cancels out. | ||
| let bangs = 0; | ||
| let negated = false; | ||
| while (this.tok.peek(LexContext.CommandStart).token === Token.Bang) { | ||
| if (!time && bangs === 0) | ||
| pipelinePos = this.tok.peek(LexContext.CommandStart).pos; | ||
| this.tok.next(LexContext.CommandStart); | ||
| bangs++; | ||
| negated = !negated; | ||
| } | ||
| const first = this.command(); | ||
| if (!first) { | ||
| if (time || negated) { | ||
| if (time || bangs > 0) { | ||
| const pipeline = { | ||
@@ -472,3 +477,3 @@ type: "Pipeline", | ||
| } | ||
| if (!time && !negated) | ||
| if (!time && bangs === 0) | ||
| pipelinePos = first.pos; | ||
@@ -496,3 +501,3 @@ const commands = [first]; | ||
| } | ||
| if (commands.length === 1 && !negated && !time) { | ||
| if (commands.length === 1 && bangs === 0 && !time) { | ||
| // Pass redirects up for list() to consume | ||
@@ -628,3 +633,6 @@ this._redirects = firstRedirects; | ||
| subshell() { | ||
| const pos = this.tok.next(LexContext.CommandStart).pos; | ||
| return this.subshellBody(this.tok.next(LexContext.CommandStart).pos); | ||
| } | ||
| // Continues a subshell whose '(' the caller already consumed. | ||
| subshellBody(pos) { | ||
| if (this.syntaxDepth === MAX_SYNTAX_NESTING) { | ||
@@ -768,2 +776,6 @@ this.error("maximum subshell nesting depth exceeded", pos); | ||
| this.skipNewlines(LexContext.CommandStart); | ||
| if (this.tok.peek(LexContext.CommandStart).token === Token.LBrace) { | ||
| const bg = this.braceGroup(); | ||
| return { type: "For", pos, end: bg.end, name, wordlist, body: bg.body }; | ||
| } | ||
| if (!this.accept(Token.Do, LexContext.CommandStart)) | ||
@@ -938,2 +950,6 @@ this.error("expected 'do'", this.tok.getPos()); | ||
| this.skipNewlines(LexContext.CommandStart); | ||
| if (this.tok.peek(LexContext.CommandStart).token === Token.LBrace) { | ||
| const bg = this.braceGroup(); | ||
| return { type: "Select", pos, end: bg.end, name, wordlist, body: bg.body }; | ||
| } | ||
| if (!this.accept(Token.Do, LexContext.CommandStart)) | ||
@@ -1111,9 +1127,20 @@ this.error("expected 'do'", this.tok.getPos()); | ||
| const name = this.readWord(LexContext.Normal); | ||
| let body; | ||
| if (this.tok.peek(LexContext.CommandStart).token === Token.LParen) { | ||
| this.tok.next(LexContext.CommandStart); | ||
| if (!this.accept(Token.RParen, LexContext.CommandStart)) | ||
| this.error("expected ')' after '('", this.tok.getPos()); | ||
| const openPos = this.tok.next(LexContext.CommandStart).pos; | ||
| // `(` is the optional empty parameter list only when `)` follows immediately; | ||
| // anything else opens a subshell body, as in `f() ( ... )`. | ||
| if (this.tok.peek(LexContext.CommandStart).token === Token.RParen) { | ||
| this.tok.next(LexContext.CommandStart); | ||
| this.skipNewlines(LexContext.CommandStart); | ||
| body = this.commandAsBody(); | ||
| } | ||
| else { | ||
| body = this.subshellBody(openPos); | ||
| } | ||
| } | ||
| this.skipNewlines(LexContext.CommandStart); | ||
| const body = this.commandAsBody(); | ||
| else { | ||
| this.skipNewlines(LexContext.CommandStart); | ||
| body = this.commandAsBody(); | ||
| } | ||
| const redirects = this._redirects; | ||
@@ -1120,0 +1147,0 @@ this._redirects = EMPTY_REDIRECTS; |
+3
-1
@@ -0,1 +1,2 @@ | ||
| import { requiresFunctionKeyword } from "./lexer.js"; | ||
| import { WordImpl } from "./word.js"; | ||
@@ -234,3 +235,4 @@ export function print(script) { | ||
| const pad = " ".repeat(indent); | ||
| let out = wd(n.name) + "() {\n"; | ||
| const name = wd(n.name); | ||
| let out = (requiresFunctionKeyword(name) ? "function " + name + " {" : name + "() {") + "\n"; | ||
| if (n.body.type === "BraceGroup") { | ||
@@ -237,0 +239,0 @@ out += stmts(n.body.body.commands, indent + 1) + "\n"; |
+6
-6
| { | ||
| "name": "unbash", | ||
| "version": "4.0.7", | ||
| "version": "4.0.8", | ||
| "description": "Fast 0-deps bash parser written in TypeScript", | ||
@@ -58,10 +58,10 @@ "keywords": [ | ||
| "mitata": "^1.0.34", | ||
| "oxfmt": "^0.58.0", | ||
| "oxlint": "^1.73.0", | ||
| "release-it": "^21.0.0", | ||
| "oxfmt": "^0.62.0", | ||
| "oxlint": "^1.77.0", | ||
| "release-it": "^21.0.1", | ||
| "remark": "^15.0.1", | ||
| "remark-preset-webpro": "^2.2.0", | ||
| "rolldown": "1.1.5", | ||
| "rolldown": "1.2.3", | ||
| "sh-syntax": "^0.6.0", | ||
| "tree-sitter": "^0.25.0", | ||
| "tree-sitter": "^0.25.1", | ||
| "tree-sitter-bash": "^0.25.1", | ||
@@ -68,0 +68,0 @@ "typescript": "^7.0.2", |
+13
-14
@@ -127,5 +127,3 @@ # unbash | ||
| ```js | ||
| const nested = word.parts.find( | ||
| (part) => part.type === "CommandExpansion", | ||
| ).script; | ||
| const nested = word.parts.find((part) => part.type === "CommandExpansion").script; | ||
| const command = nested.commands[0].command; | ||
@@ -224,14 +222,15 @@ | ||
| Median relative performance across three runs on Apple M1 Pro/32GB using Node.js | ||
| 22.23.2. unbash is x times faster: | ||
| 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. | ||
| | 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 | | ||
| | 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 | | ||
| Run the benchmarks using Node.js v22: | ||
| Run the benchmarks using Node.js v22, (native tree-sitter binding does not build on newer releases): | ||
@@ -245,3 +244,3 @@ ```sh | ||
| The parser bundle is 77KB minified and 18KB gzipped. | ||
| The parser bundle is 80KB minified and 19KB gzipped. | ||
@@ -248,0 +247,0 @@ ## Playgrounds |
Sorry, the diff of this file is too big to display
270075
1.79%6884
1.21%269
-0.37%