🎩 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
4.0.6
to
4.0.7
+3
-0
dist/lexer.d.ts

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

raw: boolean;
keywordEligible: boolean;
constructor(owner?: Lexer | null);

@@ -122,2 +123,3 @@ get value(): string;

peek(ctx?: LexContext): TokenValue;
peekFollow(closers: Uint8Array): TokenValue;
next(ctx?: LexContext): TokenValue;

@@ -140,2 +142,3 @@ unshift(tok: TokenValue): void;

private _wordHasExpansions;
private _wordKeywordEligible;
private _wordIsAssignment;

@@ -142,0 +145,0 @@ private _wordAssignmentOperatorPos;

@@ -167,2 +167,16 @@ import { hasEmbeddedWordStructure, LexContext, MAX_SYNTAX_NESTING, Token, Lexer, TokenValue } from "./lexer.js";

listTerminators[Token.DoubleSemiAmp] = 1;
// After one of these Bash is at a command-start position, the only place a reserved-word
// terminator may follow with no separator.
const compoundClosers = new Uint8Array(37);
compoundClosers[Token.RParen] = 1;
compoundClosers[Token.RBrace] = 1;
compoundClosers[Token.DblRBracket] = 1;
compoundClosers[Token.Fi] = 1;
compoundClosers[Token.Done] = 1;
compoundClosers[Token.Esac] = 1;
compoundClosers[Token.ArithCmd] = 1;
// Inside `[[ ]]` only an unquoted `!` negates; `'!'` and `\!` are ordinary operands.
function isTestNegation(t) {
return t.token === Token.Word && t.keywordEligible && t.value === "!";
}
const commandStarts = new Uint8Array(37);

@@ -272,2 +286,23 @@ commandStarts[Token.Word] = 1;

const commands = this.list();
for (;;) {
const unexpected = this.tok.peek(LexContext.CommandStart);
if (unexpected.token === Token.EOF)
break;
this.error(`unexpected token '${unexpected.value}'`, unexpected.pos);
// `In` cannot join `listTerminators`: `list()` shares it, and `in` must not terminate a
// list inside `for`/`case`.
if (!listTerminators[unexpected.token] && unexpected.token !== Token.In)
break;
this.tok.next(LexContext.CommandStart);
let separator = this.tok.peek(LexContext.CommandStart).token;
if (separator !== Token.Semi && separator !== Token.Newline && separator !== Token.Amp)
break;
while (separator === Token.Semi || separator === Token.Newline || separator === Token.Amp) {
this.tok.next(LexContext.CommandStart);
separator = this.tok.peek(LexContext.CommandStart).token;
}
const recovered = this.list();
for (let i = 0; i < recovered.length; i++)
commands.push(recovered[i]);
}
const lexerErrors = this.tok._errors;

@@ -279,2 +314,4 @@ if (lexerErrors !== null && lexerErrors.length > 0) {

}
if (this.errors !== null && this.errors.length > 1)
this.errors.sort((a, b) => a.pos - b.pos);
const result = {

@@ -336,3 +373,3 @@ type: "Script",

for (;;) {
t = this.tok.peek(LexContext.Normal).token;
t = this.tok.peekFollow(compoundClosers).token;
if (t !== Token.Semi && t !== Token.Newline && t !== Token.Amp)

@@ -408,8 +445,8 @@ break;

let pipelinePos = 0;
if (this.tok.peek(LexContext.CommandStart).token === Token.Word &&
this.tok.peek(LexContext.CommandStart).value === "time") {
const firstToken = this.tok.peek(LexContext.CommandStart);
if (firstToken.token === Token.Word && firstToken.keywordEligible && firstToken.value === "time") {
time = true;
pipelinePos = this.tok.next(LexContext.CommandStart).pos;
if (this.tok.peek(LexContext.CommandStart).token === Token.Word &&
this.tok.peek(LexContext.CommandStart).value === "-p")
const flag = this.tok.peek(LexContext.CommandStart);
if (flag.token === Token.Word && flag.keywordEligible && flag.value === "-p")
this.tok.next(LexContext.CommandStart);

@@ -519,3 +556,3 @@ }

let redirects = EMPTY_REDIRECTS;
while (this.tok.peek(LexContext.Normal).token === Token.Redirect) {
while (this.tok.peekFollow(compoundClosers).token === Token.Redirect) {
redirects = this.collectRedirect(redirects, LexContext.Normal);

@@ -972,7 +1009,7 @@ }

let t = this.tok.peek(LexContext.TestMode);
if (t.token !== Token.Word || t.value !== "!")
if (!isTestNegation(t))
return this.parseTestPrimary();
const firstPos = this.tok.next(LexContext.TestMode).pos;
t = this.tok.peek(LexContext.TestMode);
if (t.token !== Token.Word || t.value !== "!") {
if (!isTestNegation(t)) {
const operand = this.parseTestPrimary();

@@ -982,3 +1019,3 @@ return { type: "TestNot", pos: firstPos, end: operand.end, operand };

const positions = [firstPos];
while (t.token === Token.Word && t.value === "!") {
while (isTestNegation(t)) {
positions.push(this.tok.next(LexContext.TestMode).pos);

@@ -985,0 +1022,0 @@ t = this.tok.peek(LexContext.TestMode);

+51
-21
import { WordImpl } from "./word.js";
export function print(script) {
let out = "";
if (script.shebang)
out += script.shebang + "\n\n";
out += stmts(script.commands, 0);
return out;
// Saved and restored, not cleared: a lazy getter can re-enter print(), and clearing would
// drop the outer call's pending heredocs.
const outerQueued = heredocQueue.length;
try {
let out = "";
if (script.shebang)
out += script.shebang + "\n\n";
out += stmts(script.commands, 0);
return out;
}
finally {
heredocQueue.length = outerQueued;
}
}

@@ -23,2 +31,3 @@ function isFunc(s) {

const pad = " ".repeat(indent);
const queued = heredocQueue.length;
let out = pad + printNode(s.command, indent);

@@ -29,22 +38,39 @@ for (const r of s.redirects)

out += " &";
out += heredocBodies(s);
return out;
return heredocQueue.length === queued ? out : flushHeredocs(out, queued);
}
function heredocBodies(s) {
// Bash reads a heredoc body from the line after its `<<`. `redir` marks where it printed and
// this drains each mark at the end of its own line, so reflowing cannot misplace a body.
const HEREDOC_MARK = "\u0000";
const heredocQueue = [];
function flushHeredocs(text, queued) {
if (heredocQueue.length === queued)
return text;
const pending = heredocQueue.splice(queued);
if (pending.length === 1 && text.endsWith(HEREDOC_MARK)) {
return text.slice(0, -1) + "\n" + pending[0].content + delimName(pending[0]);
}
let out = "";
if (s.command.type === "Command") {
for (const r of s.command.redirects)
out += heredocBody(r);
let copied = 0;
let next = 0;
let mark = text.indexOf(HEREDOC_MARK);
while (mark !== -1) {
let lineEnd = text.indexOf("\n", mark);
if (lineEnd === -1)
lineEnd = text.length;
let marks = 0;
while (mark !== -1 && mark < lineEnd) {
out += text.slice(copied, mark);
copied = mark + 1;
marks++;
mark = text.indexOf(HEREDOC_MARK, copied);
}
out += text.slice(copied, lineEnd);
copied = lineEnd;
// `content` already ends in a newline, so this lands the delimiter on its own line.
for (; marks > 0 && next < pending.length; marks--, next++) {
out += "\n" + pending[next].content + delimName(pending[next]);
}
}
for (const r of s.redirects)
out += heredocBody(r);
return out;
return out + text.slice(copied);
}
function heredocBody(r) {
if (r.operator !== "<<" && r.operator !== "<<-")
return "";
if (r.content == null)
return "";
return "\n" + r.content + delimName(r);
}
function delimName(r) {

@@ -431,2 +457,6 @@ return r.target?.value ?? "";

}
if (r.content != null && (r.operator === "<<" || r.operator === "<<-")) {
heredocQueue.push(r);
out += HEREDOC_MARK;
}
return out;

@@ -433,0 +463,0 @@ }

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

@@ -5,0 +5,0 @@ "keywords": [

+33
-23

@@ -5,2 +5,4 @@ # unbash

[![NPM Version][2]][1] [![NPM Downloads][3]][1]
## Install

@@ -52,4 +54,4 @@

To parse `process.argv` (`string[]`), use Node.js [`parseArgs`][1] or a CLI
library such as [yargs][2] or [citty][3].
To parse `process.argv` (`string[]`), use Node.js [`parseArgs`][4] or a CLI
library such as [yargs][5] or [citty][6].

@@ -80,2 +82,4 @@ ## Usage

See the full AST at [unbash.statichost.page#input=if \[ -f...][7]
### Word parts

@@ -126,3 +130,5 @@

```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;

@@ -166,3 +172,3 @@

[tree-sitter-bash][4] is the right choice when you need:
[tree-sitter-bash][8] is the right choice when you need:

@@ -182,7 +188,7 @@ - Incremental parsing

- Best-effort error recovery that preserves a partial AST and collects errors
- Also see [tree-sitter-bash gaps covered by unbash][5]
- Also see [tree-sitter-bash gaps covered by unbash][9]
## unbash vs sh-syntax
[sh-syntax][6] is a WASM wrapper around the robust [mvdan/sh][7] Go parser. It
[sh-syntax][10] is a WASM wrapper around the robust [mvdan/sh][11] Go parser. It
is highly recommended if you need:

@@ -199,8 +205,8 @@

- Best-effort partial ASTs for malformed or incomplete editor and user input
- Also see [sh-syntax gaps covered by unbash][8]
- Also see [sh-syntax gaps covered by unbash][12]
## unbash vs bash-parser
[bash-parser][9] (last publish: 2017) and its fork
[@ericcornelissen/bash-parser][10] (community dependency maintenance fork ❤️ now
[bash-parser][13] (last publish: 2017) and its fork
[@ericcornelissen/bash-parser][14] (community dependency maintenance fork ❤️ now
archived) provide:

@@ -248,4 +254,4 @@

- [unbash.statichost.page][11]
- [ast-explorer.dev][12]
- [unbash.statichost.page][15]
- [ast-explorer.dev][16]

@@ -256,13 +262,17 @@ ## License

[1]: https://nodejs.org/api/util.html#utilparseargsconfig
[2]: https://yargs.js.org/
[3]: https://www.npmjs.com/package/citty
[4]: https://github.com/tree-sitter/tree-sitter-bash
[5]: https://github.com/webpro-nl/unbash/issues/6
[6]: https://github.com/un-ts/sh-syntax
[7]: https://github.com/mvdan/sh
[8]: https://github.com/webpro-nl/unbash/issues/7
[9]: https://github.com/vorpaljs/bash-parser
[10]: https://github.com/ericcornelissen/bash-parser
[11]: https://unbash.statichost.page
[12]: https://ast-explorer.dev/#eNoVjDsKwzAQRK8yDK5CyAGS2nVAId02jixZAbFr/Kls393rbh7zeBsrn5xLqpV3jr5X/XVzcYgOKRaD8LoNzffTBqFotgkZf8XtUW14oTdRIHaLq00WYscwpRFtCO8g2psm75n3toPHCdz+Ivg=
[1]: https://www.npmx.dev/package/unbash
[2]: https://img.shields.io/npm/v/unbash?color=f56e0f
[3]: https://img.shields.io/npm/dm/unbash?color=f56e0f
[4]: https://nodejs.org/api/util.html#utilparseargsconfig
[5]: https://yargs.js.org/
[6]: https://www.npmjs.com/package/citty
[7]: https://unbash.statichost.page/#input=if%20%5B%20-f%20%22%241%22%20%5D%3B%20then%20cat%20%22%241%22%3B%20fi
[8]: https://github.com/tree-sitter/tree-sitter-bash
[9]: https://github.com/webpro-nl/unbash/issues/6
[10]: https://github.com/un-ts/sh-syntax
[11]: https://github.com/mvdan/sh
[12]: https://github.com/webpro-nl/unbash/issues/7
[13]: https://github.com/vorpaljs/bash-parser
[14]: https://github.com/ericcornelissen/bash-parser
[15]: https://unbash.statichost.page
[16]: https://ast-explorer.dev/#eNoVjDsKwzAQRK8yDK5CyAGS2nVAId02jixZAbFr/Kls393rbh7zeBsrn5xLqpV3jr5X/XVzcYgOKRaD8LoNzffTBqFotgkZf8XtUW14oTdRIHaLq00WYscwpRFtCO8g2psm75n3toPHCdz+Ivg=

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