@ssweens/pi-leash 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1397 @@
1
+ //#region src/tokenizer/charsets.ts
2
+ const operatorChars = new Set([
3
+ ";",
4
+ "|",
5
+ "&"
6
+ ]);
7
+ const redirChars = new Set([">", "<"]);
8
+ const symbolChars = new Set([
9
+ "(",
10
+ ")",
11
+ "{",
12
+ "}"
13
+ ]);
14
+ const isDigit = (value) => value >= "0" && value <= "9";
15
+ const isNameChar = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9" || c === "_";
16
+ const isNameStart = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
17
+ const specialParams = new Set([
18
+ "@",
19
+ "*",
20
+ "#",
21
+ "?",
22
+ "-",
23
+ "$",
24
+ "!"
25
+ ]);
26
+
27
+ //#endregion
28
+ //#region src/tokenizer/scan-backtick.ts
29
+ function scanBacktick(source, pos) {
30
+ let j = pos + 1;
31
+ while (j < source.length && source.charAt(j) !== "`") {
32
+ if (source.charAt(j) === "\\") j++;
33
+ j++;
34
+ }
35
+ return {
36
+ part: {
37
+ type: "backtick",
38
+ raw: source.slice(pos + 1, j)
39
+ },
40
+ end: j + 1
41
+ };
42
+ }
43
+
44
+ //#endregion
45
+ //#region src/tokenizer/scan-expansion.ts
46
+ function scanExpansion(source, pos) {
47
+ if (source.charAt(pos) !== "$") return null;
48
+ const next = source.charAt(pos + 1);
49
+ if (next === "(" && source.charAt(pos + 2) === "(") {
50
+ let j = pos + 3;
51
+ let depth = 0;
52
+ while (j < source.length) {
53
+ if (source.charAt(j) === ")" && source.charAt(j + 1) === ")" && depth === 0) break;
54
+ if (source.charAt(j) === "(") depth++;
55
+ if (source.charAt(j) === ")") depth--;
56
+ j++;
57
+ }
58
+ return {
59
+ part: {
60
+ type: "arith-exp",
61
+ raw: source.slice(pos + 3, j).trim()
62
+ },
63
+ end: j + 2
64
+ };
65
+ }
66
+ if (next === "(") {
67
+ let j = pos + 2;
68
+ let depth = 1;
69
+ while (j < source.length && depth > 0) {
70
+ if (source.charAt(j) === "(") depth++;
71
+ if (source.charAt(j) === ")") depth--;
72
+ j++;
73
+ }
74
+ return {
75
+ part: {
76
+ type: "cmd-subst",
77
+ raw: source.slice(pos + 2, j - 1)
78
+ },
79
+ end: j
80
+ };
81
+ }
82
+ if (next === "{") {
83
+ let j = pos + 2;
84
+ let depth = 1;
85
+ while (j < source.length && depth > 0) {
86
+ if (source.charAt(j) === "{") depth++;
87
+ if (source.charAt(j) === "}") depth--;
88
+ j++;
89
+ }
90
+ const inner = source.slice(pos + 2, j - 1);
91
+ let nameEnd = 0;
92
+ let prefix = "";
93
+ if (inner.charAt(0) === "!" || inner.charAt(0) === "#") {
94
+ prefix = inner.charAt(0);
95
+ nameEnd = 1;
96
+ }
97
+ while (nameEnd < inner.length && isNameChar(inner.charAt(nameEnd))) nameEnd++;
98
+ const name = inner.slice(prefix ? 1 : 0, nameEnd);
99
+ const rest = inner.slice(nameEnd);
100
+ if (rest.length > 0) {
101
+ const opMatch = rest.match(/^(:-|:=|:\+|:\?|-|\+|=|\?|##|%%|#|%|\/\/|\/)/);
102
+ if (opMatch) {
103
+ const op = opMatch[0];
104
+ return {
105
+ part: {
106
+ type: "param",
107
+ name,
108
+ braced: true,
109
+ op,
110
+ value: rest.slice(op.length)
111
+ },
112
+ end: j
113
+ };
114
+ }
115
+ return {
116
+ part: {
117
+ type: "param",
118
+ name: inner,
119
+ braced: true
120
+ },
121
+ end: j
122
+ };
123
+ }
124
+ return {
125
+ part: {
126
+ type: "param",
127
+ name,
128
+ braced: true
129
+ },
130
+ end: j
131
+ };
132
+ }
133
+ if (isNameStart(next)) {
134
+ let j = pos + 2;
135
+ while (j < source.length && isNameChar(source.charAt(j))) j++;
136
+ return {
137
+ part: {
138
+ type: "param",
139
+ name: source.slice(pos + 1, j),
140
+ braced: false
141
+ },
142
+ end: j
143
+ };
144
+ }
145
+ if (isDigit(next)) return {
146
+ part: {
147
+ type: "param",
148
+ name: next,
149
+ braced: false
150
+ },
151
+ end: pos + 2
152
+ };
153
+ if (specialParams.has(next)) return {
154
+ part: {
155
+ type: "param",
156
+ name: next,
157
+ braced: false
158
+ },
159
+ end: pos + 2
160
+ };
161
+ return null;
162
+ }
163
+
164
+ //#endregion
165
+ //#region src/tokenizer/scan-redir.ts
166
+ function tryRedirOp(source, pos) {
167
+ if (source.startsWith("<<<", pos)) return {
168
+ op: "<<<",
169
+ len: 3
170
+ };
171
+ if (source.startsWith("&>>", pos)) return {
172
+ op: "&>>",
173
+ len: 3
174
+ };
175
+ if (source.startsWith("<<-", pos)) return {
176
+ op: "<<-",
177
+ len: 3
178
+ };
179
+ if (source.startsWith(">>", pos)) return {
180
+ op: ">>",
181
+ len: 2
182
+ };
183
+ if (source.startsWith(">&", pos)) return {
184
+ op: ">&",
185
+ len: 2
186
+ };
187
+ if (source.startsWith(">|", pos)) return {
188
+ op: ">|",
189
+ len: 2
190
+ };
191
+ if (source.startsWith("<>", pos)) return {
192
+ op: "<>",
193
+ len: 2
194
+ };
195
+ if (source.startsWith("<&", pos)) return {
196
+ op: "<&",
197
+ len: 2
198
+ };
199
+ if (source.startsWith("&>", pos)) return {
200
+ op: "&>",
201
+ len: 2
202
+ };
203
+ if (source.startsWith("<<", pos)) return {
204
+ op: "<<",
205
+ len: 2
206
+ };
207
+ if (source.charAt(pos) === ">") return {
208
+ op: ">",
209
+ len: 1
210
+ };
211
+ if (source.charAt(pos) === "<") return {
212
+ op: "<",
213
+ len: 1
214
+ };
215
+ return null;
216
+ }
217
+
218
+ //#endregion
219
+ //#region src/tokenizer/utils.ts
220
+ function tokenPartsText(parts) {
221
+ return parts.map((p) => {
222
+ if (p.type === "lit") return p.value;
223
+ if (p.type === "sgl") return p.value;
224
+ if (p.type === "dbl") return p.parts.map((dp) => dp.type === "lit" ? dp.value : "").join("");
225
+ return "";
226
+ }).join("");
227
+ }
228
+
229
+ //#endregion
230
+ //#region src/tokenizer/tokenize.ts
231
+ function tokenize(source, options = {}) {
232
+ const tokens = [];
233
+ let i = 0;
234
+ let atBoundary = true;
235
+ while (i < source.length) {
236
+ const ch = source.charAt(i);
237
+ if (ch === " " || ch === " " || ch === "\r") {
238
+ atBoundary = true;
239
+ i += 1;
240
+ continue;
241
+ }
242
+ if (ch === "\\" && source.charAt(i + 1) === "\n") {
243
+ atBoundary = true;
244
+ i += 2;
245
+ continue;
246
+ }
247
+ if (ch === "\\" && source.charAt(i + 1) === "\r") {
248
+ if (source.charAt(i + 2) === "\n") {
249
+ atBoundary = true;
250
+ i += 3;
251
+ continue;
252
+ }
253
+ }
254
+ if (ch === "\n") {
255
+ tokens.push({
256
+ type: "op",
257
+ value: ";"
258
+ });
259
+ atBoundary = true;
260
+ i += 1;
261
+ const pendingHeredocs = [];
262
+ for (let ti = 0; ti < tokens.length; ti++) {
263
+ const t = tokens[ti];
264
+ if (t && t.type === "redir" && (t.op === "<<" || t.op === "<<-") && !Object.hasOwn(t, "_collected")) {
265
+ const delimTok = tokens[ti + 1];
266
+ if (delimTok && delimTok.type === "word") {
267
+ pendingHeredocs.push({
268
+ strip: t.op === "<<-",
269
+ delimiter: tokenPartsText(delimTok.parts)
270
+ });
271
+ t._collected = true;
272
+ }
273
+ }
274
+ }
275
+ for (const hd of pendingHeredocs) {
276
+ let body = "";
277
+ while (i < source.length) {
278
+ let lineEnd = source.indexOf("\n", i);
279
+ if (lineEnd === -1) lineEnd = source.length;
280
+ const line = source.slice(i, lineEnd);
281
+ const checkLine = hd.strip ? line.replace(/^\t+/, "") : line;
282
+ i = lineEnd < source.length ? lineEnd + 1 : lineEnd;
283
+ if (checkLine === hd.delimiter) break;
284
+ const processedLine = hd.strip ? line.replace(/^\t+/, "") : line;
285
+ body += `${processedLine}\n`;
286
+ }
287
+ tokens.push({
288
+ type: "heredoc-body",
289
+ content: body
290
+ });
291
+ }
292
+ continue;
293
+ }
294
+ if (ch === "#" && atBoundary) {
295
+ const start = i + 1;
296
+ i += 1;
297
+ while (i < source.length && source.charAt(i) !== "\n") i += 1;
298
+ if (options.keepComments) tokens.push({
299
+ type: "comment",
300
+ text: source.slice(start, i)
301
+ });
302
+ continue;
303
+ }
304
+ if (ch === "!" && atBoundary) {
305
+ tokens.push({
306
+ type: "op",
307
+ value: "!"
308
+ });
309
+ atBoundary = true;
310
+ i += 1;
311
+ continue;
312
+ }
313
+ if (isDigit(ch)) {
314
+ let j = i;
315
+ while (j < source.length && isDigit(source.charAt(j))) j += 1;
316
+ const redir = tryRedirOp(source, j);
317
+ if (redir) {
318
+ tokens.push({
319
+ type: "redir",
320
+ op: redir.op,
321
+ fd: source.slice(i, j)
322
+ });
323
+ i = j + redir.len;
324
+ atBoundary = true;
325
+ continue;
326
+ }
327
+ }
328
+ if (ch === "(" && source.charAt(i + 1) === "(" && atBoundary) {
329
+ let j = i + 2;
330
+ let depth = 0;
331
+ while (j < source.length) {
332
+ const c = source.charAt(j);
333
+ if (c === ")" && source.charAt(j + 1) === ")" && depth === 0) break;
334
+ if (c === "(") depth++;
335
+ if (c === ")") depth--;
336
+ j++;
337
+ }
338
+ tokens.push({
339
+ type: "arith-cmd",
340
+ expr: source.slice(i + 2, j).trim()
341
+ });
342
+ i = j + 2;
343
+ atBoundary = true;
344
+ continue;
345
+ }
346
+ if ((ch === "<" || ch === ">") && source.charAt(i + 1) === "(" && atBoundary) {
347
+ const op = ch;
348
+ let j = i + 2;
349
+ let depth = 1;
350
+ while (j < source.length && depth > 0) {
351
+ if (source.charAt(j) === "(") depth++;
352
+ if (source.charAt(j) === ")") depth--;
353
+ j++;
354
+ }
355
+ const raw = source.slice(i + 2, j - 1);
356
+ tokens.push({
357
+ type: "word",
358
+ parts: [{
359
+ type: "proc-subst",
360
+ op,
361
+ raw
362
+ }]
363
+ });
364
+ i = j;
365
+ atBoundary = false;
366
+ continue;
367
+ }
368
+ {
369
+ const redir = tryRedirOp(source, i);
370
+ if (redir) {
371
+ tokens.push({
372
+ type: "redir",
373
+ op: redir.op
374
+ });
375
+ i += redir.len;
376
+ atBoundary = true;
377
+ continue;
378
+ }
379
+ }
380
+ if (symbolChars.has(ch)) {
381
+ tokens.push({
382
+ type: "symbol",
383
+ value: ch
384
+ });
385
+ atBoundary = true;
386
+ i += 1;
387
+ continue;
388
+ }
389
+ if (source.startsWith("&&", i)) {
390
+ tokens.push({
391
+ type: "op",
392
+ value: "&&"
393
+ });
394
+ atBoundary = true;
395
+ i += 2;
396
+ continue;
397
+ }
398
+ if (source.startsWith("||", i)) {
399
+ tokens.push({
400
+ type: "op",
401
+ value: "||"
402
+ });
403
+ atBoundary = true;
404
+ i += 2;
405
+ continue;
406
+ }
407
+ if (operatorChars.has(ch)) {
408
+ tokens.push({
409
+ type: "op",
410
+ value: ch
411
+ });
412
+ atBoundary = true;
413
+ i += 1;
414
+ continue;
415
+ }
416
+ const parts = [];
417
+ let current = "";
418
+ const flushLit = () => {
419
+ if (current.length > 0) {
420
+ parts.push({
421
+ type: "lit",
422
+ value: current
423
+ });
424
+ current = "";
425
+ }
426
+ };
427
+ while (i < source.length) {
428
+ const currentChar = source.charAt(i);
429
+ if (currentChar === "\\" && source.charAt(i + 1) === "\n") {
430
+ i += 2;
431
+ continue;
432
+ }
433
+ if (currentChar === "\\" && source.charAt(i + 1) === "\r") {
434
+ if (source.charAt(i + 2) === "\n") {
435
+ i += 3;
436
+ continue;
437
+ }
438
+ }
439
+ if (currentChar === " " || currentChar === " " || currentChar === "\r" || currentChar === "\n" || operatorChars.has(currentChar) || redirChars.has(currentChar) || symbolChars.has(currentChar)) break;
440
+ if (currentChar === "'") {
441
+ flushLit();
442
+ i += 1;
443
+ const start = i;
444
+ while (i < source.length && source.charAt(i) !== "'") i += 1;
445
+ if (i >= source.length) throw new Error("Unclosed single quote");
446
+ parts.push({
447
+ type: "sgl",
448
+ value: source.slice(start, i)
449
+ });
450
+ i += 1;
451
+ continue;
452
+ }
453
+ if (currentChar === "\"") {
454
+ flushLit();
455
+ i += 1;
456
+ const dblParts = [];
457
+ let dblBuf = "";
458
+ const flushDblLit = () => {
459
+ if (dblBuf.length > 0) {
460
+ dblParts.push({
461
+ type: "lit",
462
+ value: dblBuf
463
+ });
464
+ dblBuf = "";
465
+ }
466
+ };
467
+ let closed = false;
468
+ while (i < source.length) {
469
+ const dqChar = source.charAt(i);
470
+ if (dqChar === "\\" && source.charAt(i + 1) === "\n") {
471
+ i += 2;
472
+ continue;
473
+ }
474
+ if (dqChar === "\\" && source.charAt(i + 1) === "\r") {
475
+ if (source.charAt(i + 2) === "\n") {
476
+ i += 3;
477
+ continue;
478
+ }
479
+ }
480
+ if (dqChar === "\\" && i + 1 < source.length) {
481
+ dblBuf += dqChar + source.charAt(i + 1);
482
+ i += 2;
483
+ continue;
484
+ }
485
+ if (dqChar === "$") {
486
+ flushDblLit();
487
+ const exp = scanExpansion(source, i);
488
+ if (exp) {
489
+ dblParts.push(exp.part);
490
+ i = exp.end;
491
+ continue;
492
+ }
493
+ dblBuf += dqChar;
494
+ i += 1;
495
+ continue;
496
+ }
497
+ if (dqChar === "`") {
498
+ flushDblLit();
499
+ const bt = scanBacktick(source, i);
500
+ dblParts.push(bt.part);
501
+ i = bt.end;
502
+ continue;
503
+ }
504
+ if (dqChar === "\"") {
505
+ i += 1;
506
+ closed = true;
507
+ break;
508
+ }
509
+ dblBuf += dqChar;
510
+ i += 1;
511
+ }
512
+ if (!closed) throw new Error("Unclosed double quote");
513
+ flushDblLit();
514
+ parts.push({
515
+ type: "dbl",
516
+ parts: dblParts
517
+ });
518
+ continue;
519
+ }
520
+ if (currentChar === "$") {
521
+ flushLit();
522
+ const exp = scanExpansion(source, i);
523
+ if (exp) {
524
+ parts.push(exp.part);
525
+ i = exp.end;
526
+ continue;
527
+ }
528
+ current += currentChar;
529
+ i += 1;
530
+ continue;
531
+ }
532
+ if (currentChar === "`") {
533
+ flushLit();
534
+ const bt = scanBacktick(source, i);
535
+ parts.push(bt.part);
536
+ i = bt.end;
537
+ continue;
538
+ }
539
+ current += currentChar;
540
+ i += 1;
541
+ }
542
+ flushLit();
543
+ if (parts.length === 0) throw new Error("Unexpected character");
544
+ tokens.push({
545
+ type: "word",
546
+ parts
547
+ });
548
+ atBoundary = false;
549
+ }
550
+ return tokens;
551
+ }
552
+
553
+ //#endregion
554
+ //#region src/parser/constants.ts
555
+ const DECL_KEYWORDS = new Set([
556
+ "declare",
557
+ "local",
558
+ "export",
559
+ "readonly",
560
+ "typeset",
561
+ "nameref"
562
+ ]);
563
+
564
+ //#endregion
565
+ //#region src/parser/parser.ts
566
+ var Parser = class Parser {
567
+ index = 0;
568
+ comments = [];
569
+ constructor(tokens, options = {}) {
570
+ this.tokens = tokens;
571
+ this.options = options;
572
+ }
573
+ parseProgram() {
574
+ const body = [];
575
+ this.skipSeparators();
576
+ while (!this.isEof()) {
577
+ body.push(this.parseStatement());
578
+ this.skipSeparators();
579
+ }
580
+ const program = {
581
+ type: "Program",
582
+ body
583
+ };
584
+ if (this.options.keepComments && this.comments.length > 0) program.comments = this.comments;
585
+ return program;
586
+ }
587
+ assertEof() {
588
+ if (!this.isEof()) {
589
+ const token = this.peek();
590
+ const display = token ? token.type === "op" ? token.value : token.type === "redir" ? token.op : token.type === "symbol" ? token.value : token.type === "arith-cmd" ? "(( ... ))" : token.type === "heredoc-body" ? "<<heredoc>>" : token.type === "comment" ? `#${token.text}` : tokenPartsText(token.parts) : "";
591
+ throw new Error(`Unexpected token: ${display}`);
592
+ }
593
+ }
594
+ parseStatement() {
595
+ let negated = false;
596
+ if (this.matchOp("!")) {
597
+ this.consume();
598
+ negated = true;
599
+ }
600
+ const command = this.parseLogical();
601
+ let background = false;
602
+ if (this.matchOp("&")) {
603
+ this.consume();
604
+ background = true;
605
+ }
606
+ const statement = {
607
+ type: "Statement",
608
+ command
609
+ };
610
+ if (background) statement.background = true;
611
+ if (negated) statement.negated = true;
612
+ return statement;
613
+ }
614
+ parseLogical() {
615
+ let leftCommand = this.parsePipeline();
616
+ while (this.matchOp("&&") || this.matchOp("||")) {
617
+ const opToken = this.consume();
618
+ if (opToken.type !== "op") throw new Error("Expected logical operator");
619
+ const rightCommand = this.parsePipeline();
620
+ leftCommand = {
621
+ type: "Logical",
622
+ op: opToken.value === "&&" ? "and" : "or",
623
+ left: {
624
+ type: "Statement",
625
+ command: leftCommand
626
+ },
627
+ right: {
628
+ type: "Statement",
629
+ command: rightCommand
630
+ }
631
+ };
632
+ }
633
+ return leftCommand;
634
+ }
635
+ parsePipeline() {
636
+ const first = this.parseCommandAtom();
637
+ if (!this.matchOp("|")) return first;
638
+ const commands = [{
639
+ type: "Statement",
640
+ command: first
641
+ }];
642
+ while (this.matchOp("|")) {
643
+ this.consume();
644
+ const next = this.parseCommandAtom();
645
+ commands.push({
646
+ type: "Statement",
647
+ command: next
648
+ });
649
+ }
650
+ return {
651
+ type: "Pipeline",
652
+ commands
653
+ };
654
+ }
655
+ parseCommandAtom() {
656
+ if (this.matchKeyword("if")) return this.parseIfClause();
657
+ if (this.matchKeyword("while")) return this.parseWhileClause(false);
658
+ if (this.matchKeyword("until")) return this.parseWhileClause(true);
659
+ if (this.matchKeyword("for")) return this.parseForOrCStyleLoop();
660
+ if (this.matchKeyword("select")) return this.parseSelectClause();
661
+ if (this.matchKeyword("case")) return this.parseCaseClause();
662
+ if (this.matchKeyword("time")) return this.parseTimeClause();
663
+ if (this.matchKeyword("coproc")) return this.parseCoprocClause();
664
+ if (this.matchKeyword("[[")) return this.parseTestClause();
665
+ if (this.matchKeyword("function") || this.looksLikeFuncDecl()) return this.parseFunctionDecl();
666
+ if (this.matchArithCmd()) return this.consumeArithCmd();
667
+ if (this.matchSymbol("(")) return this.parseSubshell();
668
+ if (this.matchSymbol("{")) return this.parseBlock();
669
+ if (this.matchDeclKeyword()) return this.parseDeclClause();
670
+ if (this.matchKeyword("let")) return this.parseLetClause();
671
+ return this.parseSimpleCommand();
672
+ }
673
+ parseSubshell() {
674
+ this.consumeSymbol("(");
675
+ const body = this.parseStatementList(")");
676
+ this.consumeSymbol(")");
677
+ return {
678
+ type: "Subshell",
679
+ body
680
+ };
681
+ }
682
+ parseBlock() {
683
+ this.consumeSymbol("{");
684
+ const body = this.parseStatementList("}");
685
+ this.consumeSymbol("}");
686
+ return {
687
+ type: "Block",
688
+ body
689
+ };
690
+ }
691
+ parseStatementList(endSymbol) {
692
+ const body = [];
693
+ this.skipSeparators();
694
+ while (!this.matchSymbol(endSymbol)) {
695
+ if (this.isEof()) throw new Error(`Unexpected end of input while looking for ${endSymbol}`);
696
+ body.push(this.parseStatement());
697
+ this.skipSeparators();
698
+ }
699
+ return body;
700
+ }
701
+ parseIfClause() {
702
+ this.consumeKeyword("if");
703
+ const cond = this.parseStatementsUntilKeyword(["then"]);
704
+ this.consumeKeyword("then");
705
+ const thenBranch = this.parseStatementsUntilKeyword([
706
+ "else",
707
+ "elif",
708
+ "fi"
709
+ ]);
710
+ let elseBranch;
711
+ if (this.matchKeyword("elif")) elseBranch = [{
712
+ type: "Statement",
713
+ command: this.parseElifClause()
714
+ }];
715
+ else if (this.matchKeyword("else")) {
716
+ this.consumeKeyword("else");
717
+ elseBranch = this.parseStatementsUntilKeyword(["fi"]);
718
+ }
719
+ this.consumeKeyword("fi");
720
+ return elseBranch ? {
721
+ type: "IfClause",
722
+ cond,
723
+ then: thenBranch,
724
+ else: elseBranch
725
+ } : {
726
+ type: "IfClause",
727
+ cond,
728
+ then: thenBranch
729
+ };
730
+ }
731
+ parseElifClause() {
732
+ this.consumeKeyword("elif");
733
+ const cond = this.parseStatementsUntilKeyword(["then"]);
734
+ this.consumeKeyword("then");
735
+ const thenBranch = this.parseStatementsUntilKeyword([
736
+ "else",
737
+ "elif",
738
+ "fi"
739
+ ]);
740
+ let elseBranch;
741
+ if (this.matchKeyword("elif")) elseBranch = [{
742
+ type: "Statement",
743
+ command: this.parseElifClause()
744
+ }];
745
+ else if (this.matchKeyword("else")) {
746
+ this.consumeKeyword("else");
747
+ elseBranch = this.parseStatementsUntilKeyword(["fi"]);
748
+ }
749
+ return elseBranch ? {
750
+ type: "IfClause",
751
+ cond,
752
+ then: thenBranch,
753
+ else: elseBranch
754
+ } : {
755
+ type: "IfClause",
756
+ cond,
757
+ then: thenBranch
758
+ };
759
+ }
760
+ parseWhileClause(until) {
761
+ this.consumeKeyword(until ? "until" : "while");
762
+ const cond = this.parseStatementsUntilKeyword(["do"]);
763
+ this.consumeKeyword("do");
764
+ const body = this.parseStatementsUntilKeyword(["done"]);
765
+ this.consumeKeyword("done");
766
+ return until ? {
767
+ type: "WhileClause",
768
+ cond,
769
+ body,
770
+ until: true
771
+ } : {
772
+ type: "WhileClause",
773
+ cond,
774
+ body
775
+ };
776
+ }
777
+ parseForOrCStyleLoop() {
778
+ this.consumeKeyword("for");
779
+ if (this.matchArithCmd()) return this.parseCStyleLoop();
780
+ const nameToken = this.consume();
781
+ if (nameToken.type !== "word") throw new Error("Expected loop variable name");
782
+ const name = tokenPartsText(nameToken.parts);
783
+ let items;
784
+ if (this.matchKeyword("in")) {
785
+ this.consumeKeyword("in");
786
+ const collected = [];
787
+ while (this.matchWord() && !this.matchKeyword("do")) {
788
+ const itemToken = this.consume();
789
+ if (itemToken.type !== "word") throw new Error("Expected loop item word");
790
+ collected.push(this.wordFromParts(itemToken.parts));
791
+ }
792
+ if (collected.length > 0) items = collected;
793
+ }
794
+ if (this.matchOp(";")) this.consume();
795
+ this.skipSeparators();
796
+ this.consumeKeyword("do");
797
+ const body = this.parseStatementsUntilKeyword(["done"]);
798
+ this.consumeKeyword("done");
799
+ return items ? {
800
+ type: "ForClause",
801
+ name,
802
+ items,
803
+ body
804
+ } : {
805
+ type: "ForClause",
806
+ name,
807
+ body
808
+ };
809
+ }
810
+ parseCStyleLoop() {
811
+ const token = this.consume();
812
+ if (token.type !== "arith-cmd") throw new Error("Expected (( )) in c-style for");
813
+ const parts = token.expr.split(";").map((s) => s.trim());
814
+ const init = parts[0] || void 0;
815
+ const cond = parts[1] || void 0;
816
+ const post = parts[2] || void 0;
817
+ if (this.matchOp(";")) this.consume();
818
+ this.skipSeparators();
819
+ this.consumeKeyword("do");
820
+ const body = this.parseStatementsUntilKeyword(["done"]);
821
+ this.consumeKeyword("done");
822
+ const loop = {
823
+ type: "CStyleLoop",
824
+ body
825
+ };
826
+ if (init !== void 0) loop.init = init;
827
+ if (cond !== void 0) loop.cond = cond;
828
+ if (post !== void 0) loop.post = post;
829
+ return loop;
830
+ }
831
+ parseSelectClause() {
832
+ this.consumeKeyword("select");
833
+ const nameToken = this.consume();
834
+ if (nameToken.type !== "word") throw new Error("Expected select variable name");
835
+ const name = tokenPartsText(nameToken.parts);
836
+ let items;
837
+ if (this.matchKeyword("in")) {
838
+ this.consumeKeyword("in");
839
+ const collected = [];
840
+ while (this.matchWord() && !this.matchKeyword("do")) {
841
+ const itemToken = this.consume();
842
+ if (itemToken.type !== "word") throw new Error("Expected select item word");
843
+ collected.push(this.wordFromParts(itemToken.parts));
844
+ }
845
+ if (collected.length > 0) items = collected;
846
+ }
847
+ if (this.matchOp(";")) this.consume();
848
+ this.skipSeparators();
849
+ this.consumeKeyword("do");
850
+ const body = this.parseStatementsUntilKeyword(["done"]);
851
+ this.consumeKeyword("done");
852
+ return items ? {
853
+ type: "SelectClause",
854
+ name,
855
+ items,
856
+ body
857
+ } : {
858
+ type: "SelectClause",
859
+ name,
860
+ body
861
+ };
862
+ }
863
+ parseFunctionDecl() {
864
+ if (this.matchKeyword("function")) this.consumeKeyword("function");
865
+ const nameToken = this.consume();
866
+ if (nameToken.type !== "word") throw new Error("Expected function name");
867
+ const name = tokenPartsText(nameToken.parts);
868
+ if (this.matchSymbol("(")) {
869
+ this.consumeSymbol("(");
870
+ this.consumeSymbol(")");
871
+ }
872
+ if (this.matchSymbol("{")) return {
873
+ type: "FunctionDecl",
874
+ name,
875
+ body: this.parseBlock().body
876
+ };
877
+ throw new Error("Expected function body block");
878
+ }
879
+ parseCaseClause() {
880
+ this.consumeKeyword("case");
881
+ const wordToken = this.consume();
882
+ if (wordToken.type !== "word") throw new Error("Expected case word");
883
+ const word = this.wordFromParts(wordToken.parts);
884
+ this.consumeKeyword("in");
885
+ const items = [];
886
+ this.skipSeparators();
887
+ while (!this.matchKeyword("esac")) {
888
+ const patterns = [];
889
+ while (!this.matchSymbol(")")) {
890
+ if (this.matchWord()) {
891
+ const patternToken = this.consume();
892
+ if (patternToken.type !== "word") throw new Error("Expected case pattern");
893
+ patterns.push(this.wordFromParts(patternToken.parts));
894
+ continue;
895
+ }
896
+ if (this.matchOp("|")) {
897
+ this.consume();
898
+ continue;
899
+ }
900
+ throw new Error("Expected case pattern or )");
901
+ }
902
+ this.consumeSymbol(")");
903
+ const body = this.parseCaseItemBody();
904
+ items.push({
905
+ type: "CaseItem",
906
+ patterns,
907
+ body
908
+ });
909
+ if (this.matchOp(";") && this.peekOp(";")) {
910
+ this.consume();
911
+ this.consume();
912
+ }
913
+ this.skipSeparators();
914
+ }
915
+ this.consumeKeyword("esac");
916
+ return {
917
+ type: "CaseClause",
918
+ word,
919
+ items
920
+ };
921
+ }
922
+ parseTimeClause() {
923
+ this.consumeKeyword("time");
924
+ return {
925
+ type: "TimeClause",
926
+ command: this.parseStatement()
927
+ };
928
+ }
929
+ parseTestClause() {
930
+ this.consumeKeyword("[[");
931
+ const words = [];
932
+ while (!this.matchKeyword("]]")) {
933
+ if (this.isEof()) throw new Error("Unclosed [[");
934
+ const token = this.consume();
935
+ if (token.type !== "word") throw new Error("Expected word in [[ ]]");
936
+ words.push(this.wordFromParts(token.parts));
937
+ }
938
+ this.consumeKeyword("]]");
939
+ return {
940
+ type: "TestClause",
941
+ expr: words
942
+ };
943
+ }
944
+ matchArithCmd() {
945
+ return this.peek()?.type === "arith-cmd";
946
+ }
947
+ consumeArithCmd() {
948
+ const token = this.consume();
949
+ if (token.type !== "arith-cmd") throw new Error("Expected arithmetic command");
950
+ return {
951
+ type: "ArithCmd",
952
+ expr: token.expr
953
+ };
954
+ }
955
+ parseCoprocClause() {
956
+ this.consumeKeyword("coproc");
957
+ if (this.matchWord() && this.peekToken(1)?.type === "symbol") {
958
+ const nameToken = this.peek();
959
+ if (nameToken?.type === "word" && this.peekToken(1)?.type === "symbol" && this.peekToken(1).value === "{") {
960
+ const name = tokenPartsText(nameToken.parts);
961
+ this.consume();
962
+ return {
963
+ type: "CoprocClause",
964
+ name,
965
+ body: this.parseStatement()
966
+ };
967
+ }
968
+ }
969
+ return {
970
+ type: "CoprocClause",
971
+ body: this.parseStatement()
972
+ };
973
+ }
974
+ parseCaseItemBody() {
975
+ const body = [];
976
+ this.skipCaseSeparators();
977
+ while (!this.matchKeyword("esac") && !this.isCaseItemEnd()) {
978
+ body.push(this.parseStatement());
979
+ if (this.isCaseItemEnd()) break;
980
+ this.skipCaseSeparators();
981
+ }
982
+ return body;
983
+ }
984
+ isCaseItemEnd() {
985
+ return this.matchOp(";") && this.peekOp(";");
986
+ }
987
+ parseStatementsUntilKeyword(endKeywords) {
988
+ const body = [];
989
+ this.skipSeparators();
990
+ while (!this.matchKeywordIn(endKeywords)) {
991
+ if (this.isEof()) throw new Error(`Unexpected end of input while looking for ${endKeywords.join(", ")}`);
992
+ body.push(this.parseStatement());
993
+ this.skipSeparators();
994
+ }
995
+ return body;
996
+ }
997
+ matchDeclKeyword() {
998
+ const token = this.peek();
999
+ if (token?.type !== "word" || token.parts.length !== 1) return false;
1000
+ const part = token.parts[0];
1001
+ return part?.type === "lit" && DECL_KEYWORDS.has(part.value);
1002
+ }
1003
+ parseDeclClause() {
1004
+ const variantToken = this.consume();
1005
+ if (variantToken.type !== "word") throw new Error("Expected decl keyword");
1006
+ const variant = tokenPartsText(variantToken.parts);
1007
+ const args = [];
1008
+ const assigns = [];
1009
+ const redirects = [];
1010
+ while (true) {
1011
+ if (this.matchRedir()) {
1012
+ const token = this.consume();
1013
+ if (token.type !== "redir") throw new Error("Expected redirect token");
1014
+ const targetToken = this.consume();
1015
+ if (targetToken.type !== "word") throw new Error("Redirect must be followed by a word");
1016
+ const target = this.wordFromParts(targetToken.parts);
1017
+ const redir = token.fd ? {
1018
+ type: "Redirect",
1019
+ op: token.op,
1020
+ fd: token.fd,
1021
+ target
1022
+ } : {
1023
+ type: "Redirect",
1024
+ op: token.op,
1025
+ target
1026
+ };
1027
+ redirects.push(redir);
1028
+ continue;
1029
+ }
1030
+ if (this.matchWord()) {
1031
+ const token = this.peek();
1032
+ if (!token || token.type !== "word") break;
1033
+ const assignment = this.tryParseAssignment(token.parts);
1034
+ if (assignment) {
1035
+ assigns.push(assignment);
1036
+ continue;
1037
+ }
1038
+ this.consume();
1039
+ args.push(this.wordFromParts(token.parts));
1040
+ continue;
1041
+ }
1042
+ break;
1043
+ }
1044
+ const decl = {
1045
+ type: "DeclClause",
1046
+ variant
1047
+ };
1048
+ if (args.length > 0) decl.args = args;
1049
+ if (assigns.length > 0) decl.assigns = assigns;
1050
+ if (redirects.length > 0) decl.redirects = redirects;
1051
+ return decl;
1052
+ }
1053
+ parseLetClause() {
1054
+ this.consumeKeyword("let");
1055
+ const exprs = [];
1056
+ const redirects = [];
1057
+ while (true) {
1058
+ if (this.matchRedir()) {
1059
+ const token = this.consume();
1060
+ if (token.type !== "redir") throw new Error("Expected redirect token");
1061
+ const targetToken = this.consume();
1062
+ if (targetToken.type !== "word") throw new Error("Redirect must be followed by a word");
1063
+ const target = this.wordFromParts(targetToken.parts);
1064
+ const redir = token.fd ? {
1065
+ type: "Redirect",
1066
+ op: token.op,
1067
+ fd: token.fd,
1068
+ target
1069
+ } : {
1070
+ type: "Redirect",
1071
+ op: token.op,
1072
+ target
1073
+ };
1074
+ redirects.push(redir);
1075
+ continue;
1076
+ }
1077
+ if (this.matchWord()) {
1078
+ const token = this.consume();
1079
+ if (token.type !== "word") break;
1080
+ exprs.push(this.wordFromParts(token.parts));
1081
+ continue;
1082
+ }
1083
+ break;
1084
+ }
1085
+ if (exprs.length === 0) throw new Error("let requires at least one expression");
1086
+ const clause = {
1087
+ type: "LetClause",
1088
+ exprs
1089
+ };
1090
+ if (redirects.length > 0) clause.redirects = redirects;
1091
+ return clause;
1092
+ }
1093
+ parseSimpleCommand() {
1094
+ const words = [];
1095
+ const assignments = [];
1096
+ const redirects = [];
1097
+ let sawWord = false;
1098
+ while (true) {
1099
+ if (this.matchWord()) {
1100
+ const token = this.peek();
1101
+ if (!token || token.type !== "word") throw new Error("Expected word token");
1102
+ if (!sawWord) {
1103
+ const assignment = this.tryParseAssignment(token.parts);
1104
+ if (assignment) {
1105
+ assignments.push(assignment);
1106
+ continue;
1107
+ }
1108
+ }
1109
+ this.consume();
1110
+ sawWord = true;
1111
+ words.push(this.wordFromParts(token.parts));
1112
+ continue;
1113
+ }
1114
+ if (this.matchRedir()) {
1115
+ const token = this.consume();
1116
+ if (token.type !== "redir") throw new Error("Expected redirect token");
1117
+ const targetToken = this.consume();
1118
+ if (targetToken.type !== "word") throw new Error("Redirect must be followed by a word");
1119
+ const target = this.wordFromParts(targetToken.parts);
1120
+ const redirect = token.fd ? {
1121
+ type: "Redirect",
1122
+ op: token.op,
1123
+ fd: token.fd,
1124
+ target
1125
+ } : {
1126
+ type: "Redirect",
1127
+ op: token.op,
1128
+ target
1129
+ };
1130
+ if (token.op === "<<" || token.op === "<<-") {
1131
+ this.skipSeparators();
1132
+ if (this.peek()?.type === "heredoc-body") {
1133
+ const bodyToken = this.consume();
1134
+ if (bodyToken.type === "heredoc-body") redirect.heredoc = {
1135
+ type: "Word",
1136
+ parts: [{
1137
+ type: "Literal",
1138
+ value: bodyToken.content
1139
+ }]
1140
+ };
1141
+ }
1142
+ }
1143
+ redirects.push(redirect);
1144
+ continue;
1145
+ }
1146
+ break;
1147
+ }
1148
+ if (words.length === 0 && assignments.length === 0 && redirects.length === 0) throw new Error("Expected a command word");
1149
+ const command = { type: "SimpleCommand" };
1150
+ if (words.length > 0) command.words = words;
1151
+ if (assignments.length > 0) command.assignments = assignments;
1152
+ if (redirects.length > 0) command.redirects = redirects;
1153
+ return command;
1154
+ }
1155
+ convertWordPart(part) {
1156
+ switch (part.type) {
1157
+ case "lit": return {
1158
+ type: "Literal",
1159
+ value: part.value
1160
+ };
1161
+ case "sgl": return {
1162
+ type: "SglQuoted",
1163
+ value: part.value
1164
+ };
1165
+ case "dbl": return {
1166
+ type: "DblQuoted",
1167
+ parts: part.parts.map((p) => this.convertWordPart(p))
1168
+ };
1169
+ case "param": {
1170
+ const paramExp = {
1171
+ type: "ParamExp",
1172
+ short: !part.braced,
1173
+ param: {
1174
+ type: "Literal",
1175
+ value: part.name
1176
+ }
1177
+ };
1178
+ if (part.op) paramExp.op = part.op;
1179
+ if (part.value !== void 0) paramExp.value = {
1180
+ type: "Word",
1181
+ parts: [{
1182
+ type: "Literal",
1183
+ value: part.value
1184
+ }]
1185
+ };
1186
+ return paramExp;
1187
+ }
1188
+ case "cmd-subst": return {
1189
+ type: "CmdSubst",
1190
+ stmts: new Parser(tokenize(part.raw)).parseProgram().body
1191
+ };
1192
+ case "arith-exp": return {
1193
+ type: "ArithExp",
1194
+ expr: part.raw
1195
+ };
1196
+ case "proc-subst": {
1197
+ const prog = new Parser(tokenize(part.raw)).parseProgram();
1198
+ return {
1199
+ type: "ProcSubst",
1200
+ op: part.op,
1201
+ stmts: prog.body
1202
+ };
1203
+ }
1204
+ case "backtick": return {
1205
+ type: "CmdSubst",
1206
+ stmts: new Parser(tokenize(part.raw)).parseProgram().body
1207
+ };
1208
+ }
1209
+ }
1210
+ wordFromParts(parts) {
1211
+ return {
1212
+ type: "Word",
1213
+ parts: parts.map((part) => this.convertWordPart(part))
1214
+ };
1215
+ }
1216
+ /**
1217
+ * Try to parse an assignment from the current token's parts.
1218
+ * If it returns an assignment, it has already consumed all relevant tokens
1219
+ * (the word, and optionally the array `(...)` symbols).
1220
+ * If it returns undefined, nothing was consumed.
1221
+ */
1222
+ tryParseAssignment(parts) {
1223
+ if (parts.length !== 1) return void 0;
1224
+ const part = parts[0];
1225
+ if (!part || part.type !== "lit") return void 0;
1226
+ const raw = part.value;
1227
+ let append = false;
1228
+ let eqIndex = raw.indexOf("+=");
1229
+ if (eqIndex > 0) append = true;
1230
+ else eqIndex = raw.indexOf("=");
1231
+ if (eqIndex <= 0) return void 0;
1232
+ const name = raw.slice(0, eqIndex);
1233
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) return void 0;
1234
+ const afterEq = raw.slice(eqIndex + (append ? 2 : 1));
1235
+ const nextToken = this.peekToken(1);
1236
+ if (afterEq === "" && nextToken?.type === "symbol" && nextToken.value === "(") {
1237
+ this.consume();
1238
+ return this.parseArrayAssignment(name, append);
1239
+ }
1240
+ this.consume();
1241
+ const assignment = {
1242
+ type: "Assignment",
1243
+ name
1244
+ };
1245
+ if (append) assignment.append = true;
1246
+ if (afterEq.length > 0) assignment.value = {
1247
+ type: "Word",
1248
+ parts: [{
1249
+ type: "Literal",
1250
+ value: afterEq
1251
+ }]
1252
+ };
1253
+ return assignment;
1254
+ }
1255
+ parseArrayAssignment(name, append) {
1256
+ this.consumeSymbol("(");
1257
+ const elems = [];
1258
+ while (!this.matchSymbol(")")) {
1259
+ if (this.isEof()) throw new Error("Unclosed array expression");
1260
+ if (this.matchOp(";")) {
1261
+ this.consume();
1262
+ continue;
1263
+ }
1264
+ if (this.matchComment()) {
1265
+ this.consumeComment();
1266
+ continue;
1267
+ }
1268
+ const token = this.consume();
1269
+ if (token.type !== "word") throw new Error("Expected word in array expression");
1270
+ const indexMatch = tokenPartsText(token.parts).match(/^\[([^\]]+)\]=(.*)$/);
1271
+ if (indexMatch) {
1272
+ const indexStr = indexMatch[1];
1273
+ const valStr = indexMatch[2];
1274
+ const elem = {
1275
+ type: "ArrayElem",
1276
+ index: {
1277
+ type: "Word",
1278
+ parts: [{
1279
+ type: "Literal",
1280
+ value: indexStr
1281
+ }]
1282
+ }
1283
+ };
1284
+ if (valStr.length > 0) elem.value = {
1285
+ type: "Word",
1286
+ parts: [{
1287
+ type: "Literal",
1288
+ value: valStr
1289
+ }]
1290
+ };
1291
+ elems.push(elem);
1292
+ } else elems.push({
1293
+ type: "ArrayElem",
1294
+ value: this.wordFromParts(token.parts)
1295
+ });
1296
+ }
1297
+ this.consumeSymbol(")");
1298
+ const assignment = {
1299
+ type: "Assignment",
1300
+ name,
1301
+ array: {
1302
+ type: "ArrayExpr",
1303
+ elems
1304
+ }
1305
+ };
1306
+ if (append) assignment.append = true;
1307
+ return assignment;
1308
+ }
1309
+ skipSeparators() {
1310
+ while (this.matchOp(";") || this.matchComment()) if (this.matchComment()) this.consumeComment();
1311
+ else this.consume();
1312
+ }
1313
+ skipCaseSeparators() {
1314
+ while (this.matchOp(";") && !this.peekOp(";")) this.consume();
1315
+ }
1316
+ matchOp(value) {
1317
+ const token = this.peek();
1318
+ return token?.type === "op" && token.value === value;
1319
+ }
1320
+ matchWord() {
1321
+ return this.peek()?.type === "word";
1322
+ }
1323
+ matchRedir() {
1324
+ return this.peek()?.type === "redir";
1325
+ }
1326
+ matchKeyword(value) {
1327
+ const token = this.peek();
1328
+ if (token?.type !== "word" || token.parts.length !== 1) return false;
1329
+ const part = token.parts[0];
1330
+ return part?.type === "lit" && part.value === value;
1331
+ }
1332
+ matchKeywordIn(values) {
1333
+ return values.some((value) => this.matchKeyword(value));
1334
+ }
1335
+ looksLikeFuncDecl() {
1336
+ const name = this.peek();
1337
+ const next = this.peekToken(1);
1338
+ const nextNext = this.peekToken(2);
1339
+ const after = this.peekToken(3);
1340
+ return name?.type === "word" && next?.type === "symbol" && next.value === "(" && nextNext?.type === "symbol" && nextNext.value === ")" && after?.type === "symbol" && after.value === "{";
1341
+ }
1342
+ matchSymbol(value) {
1343
+ const token = this.peek();
1344
+ return token?.type === "symbol" && token.value === value;
1345
+ }
1346
+ consumeSymbol(value) {
1347
+ const token = this.consume();
1348
+ if (token.type !== "symbol" || token.value !== value) throw new Error(`Expected symbol ${value}`);
1349
+ }
1350
+ consumeKeyword(value) {
1351
+ const token = this.consume();
1352
+ if (token.type !== "word" || token.parts.length !== 1 || token.parts[0]?.type !== "lit" || token.parts[0].value !== value) throw new Error(`Expected keyword ${value}`);
1353
+ }
1354
+ consume() {
1355
+ if (this.isEof()) throw new Error("Unexpected end of input");
1356
+ const token = this.tokens[this.index];
1357
+ if (!token) throw new Error("Unexpected end of input");
1358
+ this.index += 1;
1359
+ return token;
1360
+ }
1361
+ peek() {
1362
+ return this.tokens[this.index];
1363
+ }
1364
+ peekToken(offset) {
1365
+ return this.tokens[this.index + offset];
1366
+ }
1367
+ peekOp(value) {
1368
+ const token = this.peekToken(1);
1369
+ return token?.type === "op" && token.value === value;
1370
+ }
1371
+ matchComment() {
1372
+ return this.peek()?.type === "comment";
1373
+ }
1374
+ consumeComment() {
1375
+ const token = this.consume();
1376
+ if (token.type === "comment") this.comments.push({
1377
+ type: "Comment",
1378
+ text: token.text
1379
+ });
1380
+ }
1381
+ isEof() {
1382
+ return this.index >= this.tokens.length;
1383
+ }
1384
+ };
1385
+
1386
+ //#endregion
1387
+ //#region src/parse.ts
1388
+ function parse(source, options = {}) {
1389
+ const parser = new Parser(tokenize(source, options), options);
1390
+ const ast = parser.parseProgram();
1391
+ parser.assertEof();
1392
+ return { ast };
1393
+ }
1394
+
1395
+ //#endregion
1396
+ export { parse };
1397
+ //# sourceMappingURL=index.js.map