🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

unbash

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

unbash - npm Package Compare versions

Comparing version
3.0.0
to
4.0.0
+3
-1
dist/lexer.d.ts

@@ -63,2 +63,3 @@ import type { DeferredCommandExpansion, ParseError, Word, WordPart } from "./types.ts";

private src;
private srcEnd;
private pos;

@@ -72,3 +73,4 @@ private current;

_buildParts: boolean;
constructor(src: string);
constructor(src: string, start?: number, end?: number);
getSource(): string;
get errors(): ParseError[];

@@ -75,0 +77,0 @@ getCollectedExpansions(): DeferredCommandExpansion[];

@@ -6,1 +6,4 @@ export type * from "./types.ts";

};
export declare function parseRegion(source: string, start: number, end: number): Script & {
errors?: ParseError[];
};

@@ -240,17 +240,26 @@ import { LexContext, Token, Lexer, TokenValue } from "./lexer.js";

export function parse(source) {
const parser = new Parser(source);
return parser.parse(source.length);
return new Parser(source, 0, source.length).run();
}
// Parse a [start, end) window of `source` in place, so the resulting nodes index the original
// source directly. Used to resolve substitution scripts with absolute offsets; not public API.
export function parseRegion(source, start, end) {
return new Parser(source, start, end).run();
}
class Parser {
tok;
source;
start;
end;
errors = [];
_redirects = [];
constructor(source) {
this.tok = new Lexer(source);
constructor(source, start, end) {
this.tok = new Lexer(source, start, end);
this.source = source;
this.start = start;
this.end = end;
}
parse(sourceLen) {
run() {
const start = this.start;
let shebang;
if (this.source.charCodeAt(0) === 35 && this.source.charCodeAt(1) === 33) {
if (start === 0 && this.source.charCodeAt(0) === 35 && this.source.charCodeAt(1) === 33) {
const nl = this.source.indexOf("\n");

@@ -267,4 +276,4 @@ shebang = nl === -1 ? this.source : this.source.slice(0, nl);

type: "Script",
pos: 0,
end: sourceLen,
pos: start,
end: this.end,
shebang,

@@ -271,0 +280,0 @@ commands,

import { Lexer } from "./lexer.js";
import { parse } from "./parser.js";
import { parse, parseRegion } from "./parser.js";
/**

@@ -10,3 +10,7 @@ * Compute the structural parts of a word by re-scanning the source.

export function computeWordParts(source, word) {
const lexer = new Lexer(source);
// Bound the re-lex to the word's span. A word inside a substitution script carries the
// whole original as its source, so an unbounded scan would overrun the word into an
// adjacent delimiter (e.g. a backtick or `)` immediately after it). For top-level words
// word.end is the natural boundary, so the bound is a no-op.
const lexer = new Lexer(source, word.pos, word.end);
const parts = lexer.buildWordParts(word.pos);

@@ -24,3 +28,3 @@ if (!parts)

export function computeHereDocBodyParts(source, word) {
const lexer = new Lexer(source);
const lexer = new Lexer(source, word.pos, word.end);
const parts = lexer.buildHereDocParts(word.pos, word.end);

@@ -32,9 +36,22 @@ if (!parts)

}
/**
* Resolve each collected substitution's inner script. The script is parsed *in place*
* against the original source over the window [innerStart, innerStart + inner.length),
* so every node is born with absolute pos/end — no slicing, no re-basing — and nested
* substitutions compose because deeper words re-lex the original on demand.
*
* Escaped backticks rebuild `inner` with the escapes removed, so it is no longer a verbatim
* substring of the source and carries no innerStart; those parse the rebuilt slice and stay
* relative to it — the single documented exception to absolute offsets.
*/
function resolveCollected(lexer) {
const source = lexer.getSource();
for (const e of lexer.getCollectedExpansions()) {
if (e.inner !== undefined) {
e.script = parse(e.inner);
e.script =
e.innerStart !== undefined ? parseRegion(source, e.innerStart, e.innerStart + e.inner.length) : parse(e.inner);
e.inner = undefined;
e.innerStart = undefined;
}
}
}

@@ -60,2 +60,4 @@ export interface Word {

inner: string | undefined;
/** Internal: absolute offset of `inner` in the original source; cleared after resolution. */
innerStart?: number;
}

@@ -73,2 +75,4 @@ export interface ArithmeticExpansionPart {

inner: string | undefined;
/** Internal: absolute offset of `inner` in the original source; cleared after resolution. */
innerStart?: number;
}

@@ -130,2 +134,3 @@ export type ExtGlobOperator = "?" | "*" | "+" | "@" | "!";

script: Script | undefined;
innerStart?: number;
}

@@ -132,0 +137,0 @@ export type DoubleQuotedChild = LiteralPart | SimpleExpansionPart | ParameterExpansionPart | CommandExpansionPart | ArithmeticExpansionPart;

{
"name": "unbash",
"version": "3.0.0",
"version": "4.0.0",
"description": "Fast 0-deps bash parser written in TypeScript",

@@ -48,15 +48,15 @@ "keywords": [

"@ericcornelissen/bash-parser": "^0.5.3",
"@types/node": "^24.10.13",
"@types/node": "^24.13.1",
"bash-parser": "^0.5.0",
"mitata": "^1.0.34",
"oxfmt": "^0.35.0",
"oxlint": "^1.50.0",
"oxfmt": "^0.53.0",
"oxlint": "^1.68.0",
"remark": "^15.0.1",
"remark-preset-webpro": "^2.1.1",
"rolldown": "1.0.0-rc.6",
"remark-preset-webpro": "^2.2.0",
"rolldown": "1.1.0",
"sh-syntax": "^0.5.8",
"tree-sitter": "^0.25.0",
"tree-sitter-bash": "^0.25.1",
"typescript": "^5.8.3",
"web-tree-sitter": "^0.26.5"
"typescript": "^6.0.3",
"web-tree-sitter": "^0.26.9"
},

@@ -71,3 +71,3 @@ "remarkConfig": {

},
"packageManager": "pnpm@10.29.2"
"packageManager": "pnpm@11.5.2"
}

@@ -32,2 +32,17 @@ # unbash

## Source offsets
Every node carries `pos`/`end` as absolute offsets into the original source, so `source.slice(node.pos, node.end)` yields that node's text at any nesting depth — including inside command, process, arithmetic, and `${ }` substitutions, and parameter-expansion sub-fields. No need to track inner offsets while traversing.
```ts
const source = 'echo "$(date -u) $(whoami)"';
const ast = parse(source);
const sub = ast.commands[0].command.suffix[0].parts[0].parts[0]; // the $(date -u)
source.slice(sub.script.pos, sub.script.end); // → "date -u"
```
`parts` is computed lazily — accessing it resolves a word's substitutions in place. Every word's `text` equals its source span (`source.slice(pos, end)`); `value` carries the interpreted form (quotes resolved).
The one exception to absolute offsets: legacy escaped backticks (`` `… \`…\` …` ``) rebuild their inner with the escapes removed, so it is no longer a verbatim substring of the source — the nested script's offsets stay relative to that rebuilt inner.
## Print

@@ -34,0 +49,0 @@

Sorry, the diff of this file is too big to display