buninu 0.4.18 → 0.4.20
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.
- package/CHANGELOG.md +22 -0
- package/apps/bunmsh/CHANGELOG.md +131 -0
- package/apps/bunmsh/PORTING.md +133 -15
- package/apps/bunmsh/help/awk.md +38 -0
- package/apps/bunmsh/help/break.md +21 -0
- package/apps/bunmsh/help/chmod.md +8 -2
- package/apps/bunmsh/help/cmp.md +29 -0
- package/apps/bunmsh/help/continue.md +22 -0
- package/apps/bunmsh/help/cp.md +4 -0
- package/apps/bunmsh/help/diff.md +30 -0
- package/apps/bunmsh/help/egrep.md +26 -0
- package/apps/bunmsh/help/export.md +2 -2
- package/apps/bunmsh/help/expr.md +30 -0
- package/apps/bunmsh/help/fgrep.md +25 -0
- package/apps/bunmsh/help/grep.md +4 -0
- package/apps/bunmsh/help/ls.md +4 -2
- package/apps/bunmsh/help/mkdir.md +1 -1
- package/apps/bunmsh/help/print.md +3 -0
- package/apps/bunmsh/help/return.md +23 -0
- package/apps/bunmsh/help/sed.md +8 -3
- package/apps/bunmsh/help/set.md +12 -3
- package/apps/bunmsh/help/tr.md +4 -0
- package/apps/bunmsh/help/trap.md +22 -0
- package/apps/bunmsh/help/uniq.md +27 -0
- package/apps/bunmsh/package.json +1 -1
- package/apps/bunmsh/src/awk.js +203 -0
- package/apps/bunmsh/src/diff.js +222 -0
- package/apps/bunmsh/src/fancy-ls.js +15 -4
- package/apps/bunmsh/src/main.js +28 -10
- package/apps/bunmsh/src/sed.js +159 -0
- package/apps/bunmsh/src/shell.js +1714 -462
- package/apps/bunmsh/test/awk-reference.test.js +35 -0
- package/apps/bunmsh/test/awk.test.js +62 -0
- package/apps/bunmsh/test/posix-bootstrap.test.js +595 -0
- package/apps/bunmsh/test/shell1.test.js +64 -0
- package/apps/bunmsh/test/utility-reference-cases.mjs +98 -0
- package/apps/bunmsh/test/utility-reference.test.js +23 -0
- package/apps/jsgotty/client-kitty.mjs +61 -0
- package/apps/jsgotty/client.mjs +16 -0
- package/apps/jsgotty/package-lock.json +2 -2
- package/apps/jsgotty/package.json +1 -1
- package/apps/jsgotty/tests/client-kitty.test.mjs +43 -0
- package/package.json +1 -1
- package/apps/bunmsh/test/shell.test.js +0 -2092
package/apps/bunmsh/src/shell.js
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
|
+
import {compileAwk, executeAwk} from "./awk.js";
|
|
2
|
+
import {normalFileDiff} from "./diff.js";
|
|
3
|
+
import {compileSed, applySed} from "./sed.js";
|
|
1
4
|
import {
|
|
2
5
|
constants as fsConstants,
|
|
3
6
|
accessSync,
|
|
4
7
|
closeSync,
|
|
5
8
|
chmodSync,
|
|
9
|
+
chownSync,
|
|
10
|
+
copyFileSync,
|
|
6
11
|
createReadStream,
|
|
7
12
|
createWriteStream,
|
|
8
13
|
lstatSync,
|
|
9
14
|
linkSync,
|
|
10
15
|
mkdirSync,
|
|
16
|
+
mkdtempSync,
|
|
17
|
+
rmSync,
|
|
18
|
+
writeFileSync,
|
|
11
19
|
openSync,
|
|
12
20
|
readdirSync,
|
|
13
21
|
rmdirSync,
|
|
14
22
|
readFileSync,
|
|
23
|
+
readSync,
|
|
24
|
+
read as fsRead,
|
|
25
|
+
writeSync,
|
|
15
26
|
realpathSync,
|
|
16
27
|
statSync,
|
|
17
28
|
symlinkSync,
|
|
18
29
|
unlinkSync,
|
|
30
|
+
utimesSync,
|
|
19
31
|
} from "node:fs";
|
|
20
32
|
import { createHash } from "node:crypto";
|
|
21
33
|
import {
|
|
34
|
+
tmpdir,
|
|
22
35
|
arch as osArch,
|
|
23
36
|
constants as osConstants,
|
|
24
37
|
hostname as osHostname,
|
|
@@ -33,7 +46,7 @@ import {
|
|
|
33
46
|
isAbsolute,
|
|
34
47
|
resolve as resolvePath,
|
|
35
48
|
} from "node:path";
|
|
36
|
-
import { Readable
|
|
49
|
+
import { Readable } from "node:stream";
|
|
37
50
|
import { format as formatValue } from "node:util";
|
|
38
51
|
import { EXECUTABLE_COMMAND, IS_COMPILED } from "../single-exe/compiled.js";
|
|
39
52
|
import { readAssetText } from "../single-exe/assetsHelper.js";
|
|
@@ -258,13 +271,18 @@ export function tokenize(source, options = {}) {
|
|
|
258
271
|
// word for the indented continuation lines commonly used in exec calls.
|
|
259
272
|
const continuation = lineContinuationLength(source, i);
|
|
260
273
|
if (continuation) {
|
|
274
|
+
options.continuations?.set(i, continuation);
|
|
261
275
|
i += continuation;
|
|
262
276
|
continue;
|
|
263
277
|
}
|
|
264
278
|
|
|
265
279
|
if (ch === "\n") {
|
|
266
280
|
finishWord();
|
|
267
|
-
|
|
281
|
+
// Newlines after AND/OR/pipe operators continue the same command,
|
|
282
|
+
// including when a here-document body lies between the two sides.
|
|
283
|
+
const previous = tokens.at(-1);
|
|
284
|
+
if (!(previous?.type === "op" && ["&&", "||", "|"].includes(previous.value)))
|
|
285
|
+
tokens.push({ type: "op", value: ";", offset: i });
|
|
268
286
|
i++;
|
|
269
287
|
atWordBoundary = true;
|
|
270
288
|
if (pendingHeredocs.length) {
|
|
@@ -283,27 +301,24 @@ export function tokenize(source, options = {}) {
|
|
|
283
301
|
while (i < source.length && source[i] !== "\n") i++;
|
|
284
302
|
continue;
|
|
285
303
|
}
|
|
304
|
+
const fullDup = /^\d+>&\d+(?=$|[\s;|&<>])/.exec(source.slice(i))?.[0];
|
|
305
|
+
if (!current && fullDup) { tokens.push({type:"op",value:fullDup,offset:i}); i += fullDup.length; atWordBoundary=true; continue; }
|
|
306
|
+
const ioOperator = /^(?:[0-9]+)?(?:>&|<&|<>|>>|>(?!>)|<(?!<))/.exec(source.slice(i))?.[0];
|
|
307
|
+
if (!current && ioOperator) {
|
|
308
|
+
tokens.push({type:"op", value:ioOperator, offset:i});
|
|
309
|
+
i += ioOperator.length;
|
|
310
|
+
atWordBoundary = true;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
286
313
|
const two = source.slice(i, i + 2);
|
|
287
|
-
const fdDup = source.slice(i, i + 4);
|
|
288
314
|
const operatorAhead =
|
|
289
315
|
["|", ";", "<", ">"].includes(ch) ||
|
|
290
316
|
two === "&&" ||
|
|
291
|
-
two === "||"
|
|
292
|
-
two === "2>" ||
|
|
293
|
-
fdDup === "1>&2" ||
|
|
294
|
-
fdDup === "2>&1";
|
|
317
|
+
two === "||";
|
|
295
318
|
if (current && operatorAhead) {
|
|
296
319
|
finishWord();
|
|
297
320
|
continue;
|
|
298
321
|
}
|
|
299
|
-
// A descriptor duplication begins with the shorter `2>` redirect token,
|
|
300
|
-
// so it must win before ordinary file redirects are considered.
|
|
301
|
-
if (!current && (fdDup === "1>&2" || fdDup === "2>&1")) {
|
|
302
|
-
tokens.push({ type: "op", value: fdDup, offset: i });
|
|
303
|
-
i += 4;
|
|
304
|
-
atWordBoundary = true;
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
322
|
if (!current && ["&&", "||", ">>", "2>", "2>>"].includes(
|
|
308
323
|
source.slice(i, i + 3) === "2>>" ? source.slice(i, i + 3) : two,
|
|
309
324
|
)) {
|
|
@@ -358,19 +373,20 @@ export function tokenize(source, options = {}) {
|
|
|
358
373
|
}
|
|
359
374
|
const quotedContinuation = lineContinuationLength(source, i);
|
|
360
375
|
if (quotedContinuation) {
|
|
376
|
+
options.continuations?.set(i, quotedContinuation);
|
|
361
377
|
i += quotedContinuation;
|
|
362
378
|
continue;
|
|
363
379
|
}
|
|
364
380
|
if (source[i] === "\\" && i + 1 < source.length) {
|
|
365
381
|
const next = source[i + 1];
|
|
366
|
-
if (next === "$" || next === '"' || next === "\\") {
|
|
382
|
+
if (next === "$" || next === "`" || next === '"' || next === "\\") {
|
|
367
383
|
text += `\u0000${next}`;
|
|
368
384
|
i += 2;
|
|
369
385
|
continue;
|
|
370
386
|
}
|
|
371
387
|
}
|
|
372
388
|
if ((source[i] === "$" && ["(", "{"].includes(source[i + 1])) || source[i] === "`") {
|
|
373
|
-
const end = source[i + 1] === "{" ? parameterEnd(source, i) : substitutionEnd(source, i);
|
|
389
|
+
const end = source[i] === "$" && source[i + 1] === "{" ? parameterEnd(source, i) : substitutionEnd(source, i);
|
|
374
390
|
text += source.slice(i, end);
|
|
375
391
|
i = end;
|
|
376
392
|
continue;
|
|
@@ -408,7 +424,7 @@ export function tokenize(source, options = {}) {
|
|
|
408
424
|
while (i < source.length) {
|
|
409
425
|
const c = source[i];
|
|
410
426
|
if ((c === "$" && ["(", "{"].includes(source[i + 1])) || c === "`") {
|
|
411
|
-
const end = source[i + 1] === "{" ? parameterEnd(source, i) : substitutionEnd(source, i);
|
|
427
|
+
const end = source[i] === "$" && source[i + 1] === "{" ? parameterEnd(source, i) : substitutionEnd(source, i);
|
|
412
428
|
text += source.slice(i, end);
|
|
413
429
|
i = end;
|
|
414
430
|
continue;
|
|
@@ -416,10 +432,7 @@ export function tokenize(source, options = {}) {
|
|
|
416
432
|
const maybeOp =
|
|
417
433
|
["|", ";", "<", ">"].includes(c) ||
|
|
418
434
|
source.slice(i, i + 2) === "&&" ||
|
|
419
|
-
source.slice(i, i + 2) === "||"
|
|
420
|
-
source.slice(i, i + 2) === "2>" ||
|
|
421
|
-
source.slice(i, i + 4) === "1>&2" ||
|
|
422
|
-
source.slice(i, i + 4) === "2>&1";
|
|
435
|
+
source.slice(i, i + 2) === "||";
|
|
423
436
|
if (isSpace(c) || c === "\n" || c === "'" || c === '"' || c === "\\" || maybeOp)
|
|
424
437
|
break;
|
|
425
438
|
text += c;
|
|
@@ -462,16 +475,17 @@ export function parse(source) {
|
|
|
462
475
|
const pipeline = [];
|
|
463
476
|
while (true) {
|
|
464
477
|
const command = emptyCommand();
|
|
478
|
+
command.lineNumber = source.slice(0, tokens[i]?.offset ?? 0).split("\n").length;
|
|
465
479
|
while (i < tokens.length) {
|
|
466
480
|
const token = tokens[i];
|
|
467
481
|
if (token.type === "op" && ["|", ";", "&&", "||"].includes(token.value))
|
|
468
482
|
break;
|
|
469
|
-
if (token.type === "op" &&
|
|
483
|
+
if (token.type === "op" && /^\d+>&\d+$/.test(token.value)) {
|
|
470
484
|
command.redirects.push({ op: token.value, target: null });
|
|
471
485
|
i++;
|
|
472
486
|
continue;
|
|
473
487
|
}
|
|
474
|
-
if (token.type === "op" &&
|
|
488
|
+
if (token.type === "op" && (/^\d*(?:>&|<&|<>|>>|>|<)$/.test(token.value) || ["<<", "<<-", "<<<"].includes(token.value))) {
|
|
475
489
|
const target = tokens[i + 1];
|
|
476
490
|
if (!target || target.type !== "word")
|
|
477
491
|
throw new ShellSyntaxError(
|
|
@@ -497,6 +511,7 @@ export function parse(source) {
|
|
|
497
511
|
|
|
498
512
|
if (tokens[i]?.value === "|") {
|
|
499
513
|
i++;
|
|
514
|
+
while (tokens[i]?.value === ";" && source[tokens[i].offset] === "\n") i++;
|
|
500
515
|
if (!tokens[i])
|
|
501
516
|
throw new ShellSyntaxError("pipeline requires a command", source.length);
|
|
502
517
|
continue;
|
|
@@ -541,16 +556,27 @@ export function parse(source) {
|
|
|
541
556
|
}
|
|
542
557
|
|
|
543
558
|
function parameterValue(name, state) {
|
|
559
|
+
if (name === "LINENO") return String(state.lineNumber ?? 1);
|
|
544
560
|
if (name === "?") return String(state.lastStatus);
|
|
545
561
|
if (name === "$") return String(process.pid);
|
|
546
562
|
if (name === "#") return String(Math.max(0, state.args.length - 1));
|
|
547
|
-
if (name === "-") return "";
|
|
563
|
+
if (name === "-") return Object.entries(state.shellOptions ?? {}).filter(([k,v])=>v && k.length === 1).map(([k])=>k).join("");
|
|
548
564
|
if (name === "0") return state.args[0] ?? "bunmsh";
|
|
549
565
|
if (name === "@" || name === "*") return state.args.slice(1).join((state.env.IFS ?? " ")[0] ?? " ");
|
|
550
566
|
if (/^[1-9][0-9]*$/.test(name)) return state.args[Number(name)] ?? "";
|
|
551
567
|
return state.env[name] ?? "";
|
|
552
568
|
}
|
|
553
569
|
|
|
570
|
+
function checkedParameter(name,state) {
|
|
571
|
+
const unset = name === "LINENO" ? false : /^\d+$/.test(name) ? Number(name) >= state.args.length : /^[A-Za-z_]/.test(name) && !Object.hasOwn(state.env,name);
|
|
572
|
+
if (state.shellOptions.u && unset) {
|
|
573
|
+
state.exitRequested = true;
|
|
574
|
+
state.exitStatus = 1;
|
|
575
|
+
throw new ShellSyntaxError(`${name}: parameter not set`);
|
|
576
|
+
}
|
|
577
|
+
return parameterValue(name,state);
|
|
578
|
+
}
|
|
579
|
+
|
|
554
580
|
function tildeDirectory(name, state) {
|
|
555
581
|
if (name === "") return state.env.HOME ?? "";
|
|
556
582
|
if (name === "+") return state.env.PWD ?? state.cwd;
|
|
@@ -608,28 +634,81 @@ function arithmeticValue(source, state) {
|
|
|
608
634
|
return String(value | 0);
|
|
609
635
|
}
|
|
610
636
|
|
|
611
|
-
function
|
|
637
|
+
async function casePatternRegex(pattern, state) {
|
|
638
|
+
let marked = "";
|
|
639
|
+
const literal = (value) => {
|
|
640
|
+
for (const ch of value) marked += `\u0000${ch}`;
|
|
641
|
+
};
|
|
642
|
+
for (let i = 0; i < pattern.length;) {
|
|
643
|
+
if (pattern[i] === "\\") {
|
|
644
|
+
if (i + 1 < pattern.length) literal(pattern[i + 1]);
|
|
645
|
+
i += 2;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (pattern[i] === "'") {
|
|
649
|
+
const end = pattern.indexOf("'", i + 1);
|
|
650
|
+
if (end < 0) throw new ShellSyntaxError("unterminated single quote in case pattern");
|
|
651
|
+
literal(pattern.slice(i + 1, end));
|
|
652
|
+
i = end + 1;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (pattern[i] === '"') {
|
|
656
|
+
let end = i + 1;
|
|
657
|
+
while (end < pattern.length && pattern[end] !== '"') {
|
|
658
|
+
if (pattern[end] === "\\") end++;
|
|
659
|
+
end++;
|
|
660
|
+
}
|
|
661
|
+
if (end >= pattern.length) throw new ShellSyntaxError("unterminated double quote in case pattern");
|
|
662
|
+
literal(stripExpansionMarkers(await expandText(pattern.slice(i + 1, end), state)));
|
|
663
|
+
i = end + 1;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
let end = i;
|
|
667
|
+
while (end < pattern.length && !["\\", "'", '"'].includes(pattern[end])) end++;
|
|
668
|
+
marked += stripExpansionMarkers(await expandText(pattern.slice(i, end), state));
|
|
669
|
+
i = end;
|
|
670
|
+
}
|
|
612
671
|
let source = "";
|
|
613
|
-
for (
|
|
614
|
-
if (
|
|
615
|
-
|
|
616
|
-
else
|
|
672
|
+
for (let i = 0; i < marked.length; i++) {
|
|
673
|
+
if (marked[i] === "\u0000") {
|
|
674
|
+
source += (marked[++i] ?? "").replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
675
|
+
} else if (marked[i] === "*") source += ".*";
|
|
676
|
+
else if (marked[i] === "?") source += ".";
|
|
677
|
+
else if (marked[i] === "[") {
|
|
678
|
+
let end = i + 1;
|
|
679
|
+
if (marked[end] === "!" || marked[end] === "^") end++;
|
|
680
|
+
if (marked[end] === "]") end++;
|
|
681
|
+
while (end < marked.length && marked[end] !== "]") {
|
|
682
|
+
if (marked[end] === "\u0000" && end + 1 < marked.length) end++;
|
|
683
|
+
end++;
|
|
684
|
+
}
|
|
685
|
+
if (end >= marked.length) source += "\\[";
|
|
686
|
+
else {
|
|
687
|
+
let body = marked.slice(i + 1, end);
|
|
688
|
+
const negate = body[0] === "!" || body[0] === "^";
|
|
689
|
+
if (negate) body = body.slice(1);
|
|
690
|
+
body = body.replace(/\u0000(.)/gs, (_,ch) => /[\\\]\-^]/.test(ch) ? `\\${ch}` : ch);
|
|
691
|
+
source += `[${negate ? "^" : ""}${body}]`;
|
|
692
|
+
i = end;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
else source += marked[i].replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
617
696
|
}
|
|
618
|
-
return new RegExp(
|
|
697
|
+
return new RegExp(`^${source}$`, "s");
|
|
619
698
|
}
|
|
620
699
|
|
|
621
|
-
async function parameterExpansion(body, state) {
|
|
700
|
+
async function parameterExpansion(body, state, quoted = false) {
|
|
622
701
|
if (/^[!%][A-Za-z_][A-Za-z0-9_]*$/.test(body)) {
|
|
623
702
|
const value = parameterValue(body.slice(1), state);
|
|
624
703
|
return body[0] === "!" ? parameterValue(value, state) : String([...value].length);
|
|
625
704
|
}
|
|
626
|
-
if (body.startsWith("#") && /^[A-Za-z_][A-Za-z0-9_]
|
|
705
|
+
if (body.startsWith("#") && /^(?:[A-Za-z_][A-Za-z0-9_]*|[0-9]+|[?$#-])$/.test(body.slice(1)))
|
|
627
706
|
return String(parameterValue(body.slice(1), state).length);
|
|
628
707
|
const trim = /^([A-Za-z_][A-Za-z0-9_]*|[?$#@*]|[0-9]+)(%%|%|##|#)(.*)$/s.exec(body);
|
|
629
708
|
if (trim) {
|
|
630
709
|
const value = parameterValue(trim[1], state);
|
|
631
|
-
const pattern =
|
|
632
|
-
const matches = (part) =>
|
|
710
|
+
const pattern = await casePatternRegex(trim[3], state);
|
|
711
|
+
const matches = (part) => pattern.test(part);
|
|
633
712
|
if (trim[2] === "%") {
|
|
634
713
|
for (let i = value.length; i >= 0; i--) if (matches(value.slice(i))) return value.slice(0, i);
|
|
635
714
|
} else if (trim[2] === "%%") {
|
|
@@ -660,13 +739,14 @@ async function parameterExpansion(body, state) {
|
|
|
660
739
|
if (!match) throw new ShellSyntaxError(`unsupported parameter expansion: \${${body}}`);
|
|
661
740
|
const [, name, operator, operand = ""] = match;
|
|
662
741
|
const value = parameterValue(name, state);
|
|
663
|
-
const unset =
|
|
742
|
+
const unset = /^\d+$/.test(name) ? Number(name) >= state.args.length :
|
|
743
|
+
name !== "LINENO" && !Object.hasOwn(state.env, name) && !/^[?$#@*-]$/.test(name);
|
|
664
744
|
const empty = value === "";
|
|
665
|
-
if (!operator) return
|
|
745
|
+
if (!operator) return checkedParameter(name,state);
|
|
666
746
|
const useEmpty = operator.startsWith(":");
|
|
667
747
|
const op = operator.at(-1);
|
|
668
748
|
const missing = unset || (useEmpty && empty);
|
|
669
|
-
const expandedOperand =
|
|
749
|
+
const expandedOperand = () => expandParameterOperand(operand, state, quoted);
|
|
670
750
|
if (op === "-") return missing ? expandedOperand() : value;
|
|
671
751
|
if (op === "+") return missing ? "" : expandedOperand();
|
|
672
752
|
if (op === "=") {
|
|
@@ -674,7 +754,7 @@ async function parameterExpansion(body, state) {
|
|
|
674
754
|
if (!/^[A-Za-z_]/.test(name)) throw new ShellSyntaxError("cannot assign positional parameter");
|
|
675
755
|
if (state.readonly?.has(name)) throw new ShellSyntaxError(`${name}: is read only`);
|
|
676
756
|
const expanded = await expandedOperand();
|
|
677
|
-
state.env[name] = expanded;
|
|
757
|
+
state.env[name] = stripExpansionMarkers(expanded);
|
|
678
758
|
return expanded;
|
|
679
759
|
}
|
|
680
760
|
return value;
|
|
@@ -684,11 +764,48 @@ async function parameterExpansion(body, state) {
|
|
|
684
764
|
}
|
|
685
765
|
|
|
686
766
|
function markedExpansion(value) {
|
|
687
|
-
return `\
|
|
767
|
+
return `\ufdd0${value}\ufdd1`;
|
|
688
768
|
}
|
|
689
769
|
|
|
690
770
|
function stripExpansionMarkers(value) {
|
|
691
|
-
return value.replace(/[\
|
|
771
|
+
return value.replace(/\ufdd4/g, " ").replace(/[\ufdd0-\ufdd3\ufdd5\ufdd6]/g, "");
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Like mksh eval.c's XARG/XARGSEP, retain quote and argument boundaries
|
|
775
|
+
// until field splitting. A selected ${...word} is shell word syntax, not text.
|
|
776
|
+
async function expandParameterOperand(source, state, outerQuoted) {
|
|
777
|
+
let output = "", plain = "", quote = "none";
|
|
778
|
+
const flush = async () => {
|
|
779
|
+
if (quote === "single") output += `\ufdd2${plain}\ufdd5\ufdd3`;
|
|
780
|
+
else {
|
|
781
|
+
const value = await expandText(plain, state, outerQuoted || quote === "double");
|
|
782
|
+
output += quote === "double" ? `\ufdd2${value}${value === "\ufdd6" ? "" : "\ufdd5"}\ufdd3` : value;
|
|
783
|
+
}
|
|
784
|
+
plain = "";
|
|
785
|
+
};
|
|
786
|
+
for (let i = 0; i < source.length;) {
|
|
787
|
+
const ch = source[i];
|
|
788
|
+
if (quote === "single") {
|
|
789
|
+
if (ch === "'") { await flush(); quote = "none"; i++; }
|
|
790
|
+
else plain += source[i++];
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
if ((ch === '"' && quote !== "single") || (ch === "'" && quote === "none" && !outerQuoted)) {
|
|
794
|
+
await flush(); quote = quote === "none" ? (ch === '"' ? "double" : "single") : "none"; i++; continue;
|
|
795
|
+
}
|
|
796
|
+
if (ch === "\\" && i + 1 < source.length) {
|
|
797
|
+
if (quote === "none" && !outerQuoted || '$`"\\'.includes(source[i + 1])) {
|
|
798
|
+
plain += `\u0000${source[i + 1]}`; i += 2; continue;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (ch === "`" || ch === "$" && ["{", "("].includes(source[i + 1])) {
|
|
802
|
+
const end = source[i] === "$" && source[i + 1] === "{" ? parameterEnd(source, i) : substitutionEnd(source, i);
|
|
803
|
+
plain += source.slice(i, end); i = end; continue;
|
|
804
|
+
}
|
|
805
|
+
plain += source[i++];
|
|
806
|
+
}
|
|
807
|
+
await flush();
|
|
808
|
+
return output;
|
|
692
809
|
}
|
|
693
810
|
|
|
694
811
|
async function commandSubstitution(source, state) {
|
|
@@ -697,17 +814,24 @@ async function commandSubstitution(source, state) {
|
|
|
697
814
|
env: { ...state.env },
|
|
698
815
|
aliases: { ...state.aliases },
|
|
699
816
|
readonly: new Set(state.readonly),
|
|
817
|
+
exported: new Set(state.exported),
|
|
700
818
|
directoryHistory: [...state.directoryHistory],
|
|
701
819
|
tabs: [...state.tabs],
|
|
702
820
|
exitRequested: false,
|
|
703
821
|
exitStatus: 0,
|
|
822
|
+
flow:null, loopDepth:0, returnDepth:0,
|
|
823
|
+
shellOptions:{...state.shellOptions}, fdBindings:{...state.fdBindings},
|
|
824
|
+
traps:{}, trapListeners:new Map(), pendingTraps:[], ownedFds:new Set(),
|
|
704
825
|
};
|
|
826
|
+
// Command substitution installs its own stdout pipe even inside >file.
|
|
827
|
+
delete child.fdBindings[1];
|
|
705
828
|
const output = await execute(source, child, { capture: true });
|
|
706
|
-
|
|
707
|
-
|
|
829
|
+
const final = await finishShell(child,output.status,{capture:true});
|
|
830
|
+
state.expansionStatus = final.status;
|
|
831
|
+
return decode(concatBytes([output.stdout,final.stdout])).replace(/\n+$/, "");
|
|
708
832
|
}
|
|
709
833
|
|
|
710
|
-
async function expandText(text, state) {
|
|
834
|
+
async function expandText(text, state, quoted = false) {
|
|
711
835
|
let output = "";
|
|
712
836
|
for (let i = 0; i < text.length; ) {
|
|
713
837
|
if (text[i] === "\u0000") {
|
|
@@ -717,7 +841,11 @@ async function expandText(text, state) {
|
|
|
717
841
|
}
|
|
718
842
|
if (text[i] === "`") {
|
|
719
843
|
const end = substitutionEnd(text, i);
|
|
720
|
-
|
|
844
|
+
// mksh lex.c SBQUOTE removes one escape layer before parsing the body,
|
|
845
|
+
// even inside quotes that belong to the inner command. Double-quoted
|
|
846
|
+
// backquotes additionally recognize an escaped double quote.
|
|
847
|
+
const source = text.slice(i + 1, end - 1).replace(quoted ? /\\([$`\\"])/g : /\\([$`\\])/g, "$1");
|
|
848
|
+
output += markedExpansion(await commandSubstitution(source, state));
|
|
721
849
|
i = end;
|
|
722
850
|
continue;
|
|
723
851
|
}
|
|
@@ -727,7 +855,8 @@ async function expandText(text, state) {
|
|
|
727
855
|
}
|
|
728
856
|
if (text.startsWith("$((", i)) {
|
|
729
857
|
const end = substitutionEnd(text, i);
|
|
730
|
-
|
|
858
|
+
const expression = stripExpansionMarkers(await expandText(text.slice(i + 3, end - 2), state));
|
|
859
|
+
output += markedExpansion(arithmeticValue(expression, state));
|
|
731
860
|
i = end;
|
|
732
861
|
continue;
|
|
733
862
|
}
|
|
@@ -739,20 +868,22 @@ async function expandText(text, state) {
|
|
|
739
868
|
}
|
|
740
869
|
if (text[i + 1] === "{") {
|
|
741
870
|
const end = parameterEnd(text, i);
|
|
742
|
-
output += markedExpansion(await parameterExpansion(text.slice(i + 2, end - 1), state));
|
|
871
|
+
output += markedExpansion(await parameterExpansion(text.slice(i + 2, end - 1), state, quoted));
|
|
743
872
|
i = end;
|
|
744
873
|
continue;
|
|
745
874
|
}
|
|
746
875
|
const next = text[i + 1];
|
|
747
876
|
if (["?", "$", "#", "@", "*", "-"].includes(next) || /[0-9]/.test(next ?? "")) {
|
|
748
|
-
output +=
|
|
877
|
+
output += next === "@" && quoted
|
|
878
|
+
? (state.args.length > 1 ? state.args.slice(1).map(arg => `\ufdd2${arg}\ufdd5\ufdd3`).join("\ufdd4") : "\ufdd6")
|
|
879
|
+
: markedExpansion(checkedParameter(next, state));
|
|
749
880
|
i += 2;
|
|
750
881
|
continue;
|
|
751
882
|
}
|
|
752
883
|
if (isNameStart(next)) {
|
|
753
884
|
let end = i + 2;
|
|
754
885
|
while (isNameChar(text[end])) end++;
|
|
755
|
-
output += markedExpansion(
|
|
886
|
+
output += markedExpansion(checkedParameter(text.slice(i + 1, end), state));
|
|
756
887
|
i = end;
|
|
757
888
|
continue;
|
|
758
889
|
}
|
|
@@ -774,6 +905,7 @@ function splitFields(segments, state) {
|
|
|
774
905
|
forced = false;
|
|
775
906
|
};
|
|
776
907
|
for (const segment of segments) {
|
|
908
|
+
if (segment.boundary) { finish(true); continue; }
|
|
777
909
|
if (segment.quoted && segment.text === "") forced = true;
|
|
778
910
|
for (const ch of segment.text) {
|
|
779
911
|
if (!segment.quoted && segment.splittable && ifs.includes(ch)) {
|
|
@@ -838,42 +970,41 @@ function braceFields(field) {
|
|
|
838
970
|
}
|
|
839
971
|
|
|
840
972
|
export async function expandWord(word, state, options = {}) {
|
|
841
|
-
if (!options.single && word.fragments.length === 1 &&
|
|
842
|
-
word.fragments[0].quote === "double" && word.fragments[0].text === "$@")
|
|
843
|
-
return state.args.slice(1);
|
|
844
973
|
const segments = [];
|
|
845
|
-
for (
|
|
846
|
-
const fragment = word.fragments[index];
|
|
974
|
+
for (const fragment of word.fragments) {
|
|
847
975
|
const source = fragment.quote === "none" && !options.noTilde
|
|
848
|
-
? expandTildes(fragment.text, state, Boolean(options.assignment))
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
: await expandText(source, state);
|
|
853
|
-
if (fragment.quote !== "none") {
|
|
854
|
-
segments.push({ text: stripExpansionMarkers(text.replaceAll("\u0000", "")), quoted: true, splittable: false });
|
|
855
|
-
} else {
|
|
856
|
-
let plain = "";
|
|
857
|
-
let splittable = false;
|
|
858
|
-
const flush = () => {
|
|
859
|
-
if (plain) segments.push({ text: plain, quoted: false, splittable });
|
|
860
|
-
plain = "";
|
|
861
|
-
};
|
|
862
|
-
for (let i = 0; i < text.length; i++) {
|
|
863
|
-
if (text[i] === "\u0001") { flush(); splittable = true; continue; }
|
|
864
|
-
if (text[i] === "\u0002") { flush(); splittable = false; continue; }
|
|
865
|
-
if (text[i] !== "\u0000") { plain += text[i]; continue; }
|
|
866
|
-
flush();
|
|
867
|
-
segments.push({ text: text[++i] ?? "", quoted: true, splittable: false });
|
|
868
|
-
}
|
|
869
|
-
flush();
|
|
976
|
+
? expandTildes(fragment.text, state, Boolean(options.assignment)) : fragment.text;
|
|
977
|
+
if (fragment.quote === "single") {
|
|
978
|
+
segments.push({text:source.replaceAll("\u0000", ""),quoted:true,splittable:false});
|
|
979
|
+
continue;
|
|
870
980
|
}
|
|
981
|
+
const text = await expandText(source, state, fragment.quote === "double");
|
|
982
|
+
let plain = "", splitDepth = 0, quoteDepth = fragment.quote === "double" ? 1 : 0;
|
|
983
|
+
const flush = () => {
|
|
984
|
+
if (plain) segments.push({text:plain,quoted:quoteDepth > 0,splittable:splitDepth > 0});
|
|
985
|
+
plain = "";
|
|
986
|
+
};
|
|
987
|
+
if (fragment.quote === "double" && !text.includes("\ufdd6"))
|
|
988
|
+
segments.push({text:"",quoted:true,splittable:false});
|
|
989
|
+
for (let i = 0; i < text.length; i++) {
|
|
990
|
+
const ch = text[i];
|
|
991
|
+
if (ch === "\ufdd0") { flush(); splitDepth++; }
|
|
992
|
+
else if (ch === "\ufdd1") { flush(); splitDepth--; }
|
|
993
|
+
else if (ch === "\ufdd2") { flush(); quoteDepth++; }
|
|
994
|
+
else if (ch === "\ufdd3") { flush(); quoteDepth--; }
|
|
995
|
+
else if (ch === "\ufdd4") { flush(); segments.push({text:options.single ? " " : "",boundary:!options.single}); }
|
|
996
|
+
else if (ch === "\ufdd5") { flush(); segments.push({text:"",quoted:true,splittable:false}); }
|
|
997
|
+
else if (ch === "\ufdd6") { flush(); }
|
|
998
|
+
else if (ch === "\u0000") { flush(); segments.push({text:text[++i] ?? "",quoted:true,splittable:false}); }
|
|
999
|
+
else plain += ch;
|
|
1000
|
+
}
|
|
1001
|
+
flush();
|
|
871
1002
|
}
|
|
872
1003
|
if (options.single) return [segments.map(({ text }) => text).join("")];
|
|
873
1004
|
const fields = splitFields(segments, state);
|
|
874
1005
|
const output = [];
|
|
875
1006
|
for (const field of fields)
|
|
876
|
-
for (const expanded of braceFields(field)) output.push(...await pathnameFields(expanded, state));
|
|
1007
|
+
for (const expanded of braceFields(field)) output.push(...(state.shellOptions.f ? [expanded.map(part=>part.ch).join("")] : await pathnameFields(expanded, state)));
|
|
877
1008
|
return output;
|
|
878
1009
|
}
|
|
879
1010
|
|
|
@@ -999,10 +1130,16 @@ async function sourceFile(argv, state) {
|
|
|
999
1130
|
try {
|
|
1000
1131
|
const source = await Bun.file(path).text();
|
|
1001
1132
|
const previousArgs = state.args;
|
|
1133
|
+
const previousLoopDepth = state.loopDepth;
|
|
1134
|
+
state.loopDepth = 0;
|
|
1002
1135
|
if (argv.length > 2) state.args = [previousArgs[0], ...argv.slice(2)];
|
|
1136
|
+
state.returnDepth++;
|
|
1003
1137
|
try {
|
|
1004
1138
|
return await execute(source, state, { capture: true });
|
|
1005
1139
|
} finally {
|
|
1140
|
+
state.returnDepth--;
|
|
1141
|
+
state.loopDepth = previousLoopDepth;
|
|
1142
|
+
if (state.flow?.kind === "return") state.flow = null;
|
|
1006
1143
|
state.args = previousArgs;
|
|
1007
1144
|
}
|
|
1008
1145
|
} catch (error) {
|
|
@@ -1067,7 +1204,112 @@ function evaluateTest(argv, state) {
|
|
|
1067
1204
|
throw new Error("unsupported expression");
|
|
1068
1205
|
}
|
|
1069
1206
|
|
|
1207
|
+
function loopControl(kind, argv, state) {
|
|
1208
|
+
const levels = argv[1] === undefined ? 1 : Number(argv[1]);
|
|
1209
|
+
if (argv.length > 2 || !Number.isInteger(levels) || levels < 1)
|
|
1210
|
+
return result(2, "", `bunmsh: ${kind}: expected a positive integer\n`);
|
|
1211
|
+
if (!state.loopDepth) return result(2, "", `bunmsh: ${kind}: not in a loop\n`);
|
|
1212
|
+
state.flow = { kind, levels: Math.min(levels, state.loopDepth) };
|
|
1213
|
+
return result();
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function trapName(value) {
|
|
1217
|
+
if (value === "0" || value === "EXIT") return "EXIT";
|
|
1218
|
+
const name = /^\d+$/.test(value)
|
|
1219
|
+
? Object.keys(osConstants.signals).find(key => osConstants.signals[key] === Number(value))
|
|
1220
|
+
: value.startsWith("SIG") ? value : `SIG${value}`;
|
|
1221
|
+
return name && osConstants.signals[name] && !["SIGKILL","SIGSTOP"].includes(name) ? name : null;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function setTrap(argv, state) {
|
|
1225
|
+
let args = argv.slice(1);
|
|
1226
|
+
if (args[0] === "--") args.shift();
|
|
1227
|
+
if (!args.length) return result(0,Object.entries(state.traps).map(([name,command]) => `trap -- ${quoteShellWord(command)} ${name === "EXIT" ? "0" : name.slice(3)}\n`).join(""));
|
|
1228
|
+
let action = args.shift();
|
|
1229
|
+
if (/^\d+$/.test(action)) { args.unshift(action); action = "-"; }
|
|
1230
|
+
if (!args.length) return result(2,"","bunmsh: trap: expected a signal\n");
|
|
1231
|
+
for (const value of args) {
|
|
1232
|
+
const name = trapName(value);
|
|
1233
|
+
if (!name) return result(2,"",`bunmsh: trap: ${value}: invalid or uncatchable signal\n`);
|
|
1234
|
+
const listener = state.trapListeners.get(name);
|
|
1235
|
+
if (listener) process.off(name,listener);
|
|
1236
|
+
state.trapListeners.delete(name);
|
|
1237
|
+
if (action === "-") { delete state.traps[name]; continue; }
|
|
1238
|
+
state.traps[name] = action;
|
|
1239
|
+
if (name !== "EXIT") {
|
|
1240
|
+
// Like mksh's pending-trap flag: callbacks enqueue only. Shell code
|
|
1241
|
+
// runs at command boundaries, never reentrantly from a signal callback.
|
|
1242
|
+
const handler = () => { if (state.traps[name] && !state.pendingTraps.includes(name)) state.pendingTraps.push(name); };
|
|
1243
|
+
process.on(name,handler);
|
|
1244
|
+
state.trapListeners.set(name,handler);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return result();
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
async function runPendingTraps(state, io) {
|
|
1251
|
+
if (state.inTrap) return result();
|
|
1252
|
+
const stdout = [], stderr = [];
|
|
1253
|
+
while (state.pendingTraps.length) {
|
|
1254
|
+
const name = state.pendingTraps.shift();
|
|
1255
|
+
const source = state.traps[name];
|
|
1256
|
+
if (!source) continue;
|
|
1257
|
+
const savedStatus = state.lastStatus;
|
|
1258
|
+
state.inTrap = true;
|
|
1259
|
+
try {
|
|
1260
|
+
const execution = await execute(source,state,io);
|
|
1261
|
+
stdout.push(execution.stdout); stderr.push(execution.stderr);
|
|
1262
|
+
} finally { state.inTrap = false; state.lastStatus = savedStatus; }
|
|
1263
|
+
if (state.exitRequested) break;
|
|
1264
|
+
}
|
|
1265
|
+
return result(state.lastStatus,concatBytes(stdout),concatBytes(stderr));
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
export async function finishShell(state, status = state.lastStatus, io = {}) {
|
|
1269
|
+
if (state.finished) return result(status);
|
|
1270
|
+
state.finished = true;
|
|
1271
|
+
let execution = result(status);
|
|
1272
|
+
try {
|
|
1273
|
+
const handler = state.traps.EXIT;
|
|
1274
|
+
delete state.traps.EXIT;
|
|
1275
|
+
if (handler) {
|
|
1276
|
+
state.exitRequested = false;
|
|
1277
|
+
state.flow = null;
|
|
1278
|
+
state.lastStatus = status;
|
|
1279
|
+
execution = await execute(handler,state,io);
|
|
1280
|
+
execution.status = state.exitRequested ? state.exitStatus : status;
|
|
1281
|
+
}
|
|
1282
|
+
return execution;
|
|
1283
|
+
} finally {
|
|
1284
|
+
for (const [signal,listener] of state.trapListeners) process.off(signal,listener);
|
|
1285
|
+
state.trapListeners.clear();
|
|
1286
|
+
for (const fd of state.ownedFds) { try { closeSync(fd); } catch {} }
|
|
1287
|
+
state.ownedFds.clear();
|
|
1288
|
+
// Background processes deliberately remain unref'ed and accessible by
|
|
1289
|
+
// stable index in Bun.sha.procs. No waiting, killing or registry cleanup.
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function consumeLoopFlow(state) {
|
|
1294
|
+
if (!state.flow) return false;
|
|
1295
|
+
if (state.flow.kind === "return") return true;
|
|
1296
|
+
const kind = state.flow.kind;
|
|
1297
|
+
if (--state.flow.levels > 0) return true;
|
|
1298
|
+
state.flow = null;
|
|
1299
|
+
return kind === "break";
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1070
1302
|
const builtins = {
|
|
1303
|
+
return: async (argv, state) => {
|
|
1304
|
+
if (!state.returnDepth) return result(2, "", "bunmsh: return: not in a function or sourced script\n");
|
|
1305
|
+
const status = argv[1] === undefined ? state.lastStatus : Number(argv[1]);
|
|
1306
|
+
if (argv.length > 2 || !Number.isInteger(status))
|
|
1307
|
+
return result(2, "", "bunmsh: return: expected an integer status\n");
|
|
1308
|
+
state.flow = { kind: "return" };
|
|
1309
|
+
return result(status & 255);
|
|
1310
|
+
},
|
|
1311
|
+
break: async (argv, state) => loopControl("break", argv, state),
|
|
1312
|
+
continue: async (argv, state) => loopControl("continue", argv, state),
|
|
1071
1313
|
":": async () => result(),
|
|
1072
1314
|
true: async () => result(),
|
|
1073
1315
|
false: async () => result(1),
|
|
@@ -1214,11 +1456,13 @@ const builtins = {
|
|
|
1214
1456
|
fd = Number(arg.slice(2));
|
|
1215
1457
|
continue;
|
|
1216
1458
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1459
|
+
for (const flag of arg.slice(1)) {
|
|
1460
|
+
if (flag === "r" || flag === "R") raw = true;
|
|
1461
|
+
else if (flag === "n") newline = false;
|
|
1462
|
+
else if (flag === "l") separator = "\n";
|
|
1463
|
+
else if (flag === "N") separator = terminator = "\0";
|
|
1464
|
+
else return result(1, "", `bunmsh: print: -${flag}: unsupported option\n`);
|
|
1465
|
+
}
|
|
1222
1466
|
}
|
|
1223
1467
|
let output = argv.slice(i).join(separator);
|
|
1224
1468
|
if (!raw) {
|
|
@@ -1282,6 +1526,25 @@ const builtins = {
|
|
|
1282
1526
|
if (state.readLines) {
|
|
1283
1527
|
gotInput = state.readLines.length > 0;
|
|
1284
1528
|
line = state.readLines.shift() ?? "";
|
|
1529
|
+
} else if (typeof input === "number" || input == null) {
|
|
1530
|
+
// Like mksh's normal read mode, consume only through the delimiter.
|
|
1531
|
+
// A fresh buffered stream would read ahead and cancelling it can close
|
|
1532
|
+
// the shared descriptor, breaking subsequent reads and fd duplicates.
|
|
1533
|
+
const byte = Buffer.alloc(1), data = [];
|
|
1534
|
+
gotInput = false;
|
|
1535
|
+
while (true) {
|
|
1536
|
+
let count;
|
|
1537
|
+
try {
|
|
1538
|
+
count = input == null
|
|
1539
|
+
? await new Promise((resolve,reject) => fsRead(0,byte,0,1,null,(error,n) => error ? reject(error) : resolve(n)))
|
|
1540
|
+
: readSync(input, byte, 0, 1, null);
|
|
1541
|
+
}
|
|
1542
|
+
catch (error) { if (error.code === "EINTR") continue; throw error; }
|
|
1543
|
+
if (!count) break;
|
|
1544
|
+
if (byte[0] === 10) { gotInput = true; break; }
|
|
1545
|
+
data.push(byte[0]);
|
|
1546
|
+
}
|
|
1547
|
+
line = decoder.decode(Uint8Array.from(data));
|
|
1285
1548
|
} else {
|
|
1286
1549
|
const reader = fallbackInput(input).getReader();
|
|
1287
1550
|
const chunks = [];
|
|
@@ -1403,8 +1666,9 @@ const builtins = {
|
|
|
1403
1666
|
"..": async (_argv, state) => changeDirectory(state, ".."),
|
|
1404
1667
|
"//": async (_argv, state) => previousChildDirectory(state),
|
|
1405
1668
|
export: async (argv, state) => {
|
|
1406
|
-
if (argv.length === 1) {
|
|
1669
|
+
if (argv.length === 1 || argv.length === 2 && argv[1] === "-p") {
|
|
1407
1670
|
const listing = Object.entries(state.env)
|
|
1671
|
+
.filter(([name]) => !state.shellOptions.posix || state.exported.has(name))
|
|
1408
1672
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
1409
1673
|
.map(([name, value]) => `export ${name}=${JSON.stringify(value)}\n`)
|
|
1410
1674
|
.join("");
|
|
@@ -1419,6 +1683,7 @@ const builtins = {
|
|
|
1419
1683
|
else if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(item))
|
|
1420
1684
|
state.env[item] ??= "";
|
|
1421
1685
|
else return result(1, "", `bunmsh: export: ${item}: invalid name\n`);
|
|
1686
|
+
state.exported.add(assignment?.name ?? item);
|
|
1422
1687
|
}
|
|
1423
1688
|
return result();
|
|
1424
1689
|
},
|
|
@@ -1430,6 +1695,7 @@ const builtins = {
|
|
|
1430
1695
|
return result(1, "", `bunmsh: unset: ${name}: invalid name\n`);
|
|
1431
1696
|
if (state.readonly.has(name)) return readonlyError(name);
|
|
1432
1697
|
delete state.env[name];
|
|
1698
|
+
state.exported.delete(name);
|
|
1433
1699
|
}
|
|
1434
1700
|
return result();
|
|
1435
1701
|
},
|
|
@@ -1455,7 +1721,7 @@ const builtins = {
|
|
|
1455
1721
|
return result();
|
|
1456
1722
|
},
|
|
1457
1723
|
env: async (argv, state, input, context = {}) => {
|
|
1458
|
-
let env = { ...state
|
|
1724
|
+
let env = { ...externalEnvironment(state) };
|
|
1459
1725
|
let i = 1;
|
|
1460
1726
|
for (; i < argv.length; i++) {
|
|
1461
1727
|
const item = argv[i];
|
|
@@ -1477,6 +1743,7 @@ const builtins = {
|
|
|
1477
1743
|
const childState = {
|
|
1478
1744
|
...state,
|
|
1479
1745
|
env,
|
|
1746
|
+
exported: new Set(Object.keys(env)),
|
|
1480
1747
|
directoryHistory: [...state.directoryHistory],
|
|
1481
1748
|
tabs: [...state.tabs],
|
|
1482
1749
|
readonly: new Set(state.readonly),
|
|
@@ -1619,14 +1886,37 @@ const builtins = {
|
|
|
1619
1886
|
}
|
|
1620
1887
|
return result(status, "", stderr);
|
|
1621
1888
|
},
|
|
1889
|
+
trap: async (argv,state) => setTrap(argv,state),
|
|
1622
1890
|
set: async (argv, state) => {
|
|
1623
1891
|
if (argv.length === 1)
|
|
1624
1892
|
return result(0, Object.entries(state.env).sort().map(([k, v]) => `${k}=${JSON.stringify(v)}\n`).join(""));
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1893
|
+
const names = {errexit:"e",nounset:"u",noglob:"f",xtrace:"x",verbose:"v",posix:"posix"};
|
|
1894
|
+
let i = 1;
|
|
1895
|
+
for (; i < argv.length; i++) {
|
|
1896
|
+
const item = argv[i];
|
|
1897
|
+
if (item === "--") { i++; break; }
|
|
1898
|
+
if (item === "-") {
|
|
1899
|
+
// Traditional set - ends options and disables verbose/xtrace. Like
|
|
1900
|
+
// mksh parse_args, preserve positional parameters if none follow.
|
|
1901
|
+
state.shellOptions.x = state.shellOptions.v = false;
|
|
1902
|
+
i++; break;
|
|
1903
|
+
}
|
|
1904
|
+
if (!/^[+-]/.test(item)) break;
|
|
1905
|
+
const enabled = item[0] === "-";
|
|
1906
|
+
if (item === "-o" || item === "+o") {
|
|
1907
|
+
if (argv[i+1] === undefined) return result(0,Object.entries(names).map(([name,key]) => enabled ? `${name} ${state.shellOptions[key] ? "on" : "off"}\n` : `set ${state.shellOptions[key] ? "-" : "+"}o ${name}\n`).join(""));
|
|
1908
|
+
const name = argv[++i];
|
|
1909
|
+
if (!names[name]) return result(2,"",`bunmsh: set: unknown option ${name}\n`);
|
|
1910
|
+
state.shellOptions[names[name]] = enabled;
|
|
1911
|
+
} else {
|
|
1912
|
+
for (const key of item.slice(1)) {
|
|
1913
|
+
if (!"eufxv".includes(key)) return result(2,"",`bunmsh: set: unknown option ${key}\n`);
|
|
1914
|
+
state.shellOptions[key] = enabled;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1628
1917
|
}
|
|
1629
|
-
|
|
1918
|
+
if (i < argv.length || argv.at(-1) === "--") state.args = [state.args[0],...argv.slice(i)];
|
|
1919
|
+
return result();
|
|
1630
1920
|
},
|
|
1631
1921
|
};
|
|
1632
1922
|
|
|
@@ -1644,6 +1934,13 @@ export function bunShellFallbackArgv(argv) {
|
|
|
1644
1934
|
let commandArgv = argv;
|
|
1645
1935
|
if (argv[0] === "ls")
|
|
1646
1936
|
commandArgv = commandArgv.filter((value, index) => index === 0 || value !== "--color=auto");
|
|
1937
|
+
if (["mv", "rm"].includes(argv[0]) && argv.includes("--")) {
|
|
1938
|
+
const end = argv.indexOf("--");
|
|
1939
|
+
// Bun.$ mv/rm do not accept --. Protect option-looking path operands when
|
|
1940
|
+
// removing the delimiter, so a filename cannot become an option.
|
|
1941
|
+
commandArgv = [...argv.slice(0, end), ...argv.slice(end + 1).map(value =>
|
|
1942
|
+
value.startsWith("-") ? `./${value}` : value)];
|
|
1943
|
+
}
|
|
1647
1944
|
if (argv[0] === "cp")
|
|
1648
1945
|
commandArgv = commandArgv.map((value, index) =>
|
|
1649
1946
|
index > 0 && /^-[^-]/.test(value) ? `-${value.slice(1).replaceAll("r", "R")}` : value);
|
|
@@ -1651,9 +1948,36 @@ export function bunShellFallbackArgv(argv) {
|
|
|
1651
1948
|
}
|
|
1652
1949
|
|
|
1653
1950
|
async function runBunShellFallback(argv, state) {
|
|
1951
|
+
if (argv[0] === "mkdir") {
|
|
1952
|
+
const flags = argv.slice(1).filter(arg => /^-[pv]+$/.test(arg) || ["--parents","--vebose"].includes(arg));
|
|
1953
|
+
const operands = argv.slice(1).filter(arg => !flags.includes(arg));
|
|
1954
|
+
// Bun.$ starts sibling mkdir operands concurrently. POSIX callers can
|
|
1955
|
+
// pass parent then child without -p, so preserve operand order here.
|
|
1956
|
+
if (operands.length > 1 && operands.every(arg => !arg.startsWith("-") || arg === "-")) {
|
|
1957
|
+
const output = [], errors = []; let status = 0;
|
|
1958
|
+
for (const operand of operands) {
|
|
1959
|
+
const r = await runBunShellFallback(["mkdir",...flags,operand],state);
|
|
1960
|
+
status ||= r.status; output.push(r.stdout); errors.push(r.stderr);
|
|
1961
|
+
}
|
|
1962
|
+
return result(status,concatBytes(output),concatBytes(errors));
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
// GNU/POSIX rm -f ignores nonexistent operands, including the empty name.
|
|
1966
|
+
// Bun.$ instead resolves that empty name to cwd and reports a directory.
|
|
1967
|
+
if (argv[0] === "rm") {
|
|
1968
|
+
const delimiter = argv.indexOf("--");
|
|
1969
|
+
const options = argv.slice(1, delimiter < 0 ? undefined : delimiter)
|
|
1970
|
+
.filter(arg => /^-[frRi]+$/.test(arg)).join("");
|
|
1971
|
+
if (options.lastIndexOf("f") > options.lastIndexOf("i"))
|
|
1972
|
+
argv = argv.filter((arg, index) => index === 0 || arg !== "");
|
|
1973
|
+
}
|
|
1974
|
+
// Bun.$ rejects an empty operand list even with rm -f. Autoconf probes it.
|
|
1975
|
+
const rmFlags = argv.at(-1) === "--" ? argv.slice(1, -1) : argv.slice(1);
|
|
1976
|
+
if (argv[0] === "rm" && rmFlags.every(arg => /^-[frRi]+$/.test(arg)) &&
|
|
1977
|
+
rmFlags.join("").lastIndexOf("f") > rmFlags.join("").lastIndexOf("i")) return result();
|
|
1654
1978
|
const shell = Bun.$`${bunShellFallbackArgv(argv)}`
|
|
1655
1979
|
.cwd(nativePath(state.cwd))
|
|
1656
|
-
.env(state
|
|
1980
|
+
.env(externalEnvironment(state))
|
|
1657
1981
|
.nothrow();
|
|
1658
1982
|
if (state.pipelineChild) {
|
|
1659
1983
|
const output = await shell;
|
|
@@ -1669,6 +1993,42 @@ async function runFancyLsFallback(argv, state) {
|
|
|
1669
1993
|
}
|
|
1670
1994
|
|
|
1671
1995
|
async function runBunShellCpFallback(argv, state) {
|
|
1996
|
+
let preserve=false, verbose=false, end=false;
|
|
1997
|
+
const files=[], unsupported=[];
|
|
1998
|
+
for(const arg of argv.slice(1)) {
|
|
1999
|
+
if(!end&&arg==='--'){end=true;continue;}
|
|
2000
|
+
if(!end&&arg.startsWith('-')&&arg!=='-') {
|
|
2001
|
+
for(const flag of arg.slice(1)) {
|
|
2002
|
+
if(flag==='p')preserve=true;
|
|
2003
|
+
else if(flag==='v')verbose=true;
|
|
2004
|
+
else unsupported.push(flag);
|
|
2005
|
+
}
|
|
2006
|
+
} else files.push(arg);
|
|
2007
|
+
}
|
|
2008
|
+
if(preserve) {
|
|
2009
|
+
if(unsupported.length)return result(1,'',`bunmsh: cp: unsupported option with -p: -${unsupported[0]}\n`);
|
|
2010
|
+
if(files.length<2)return result(1,'','bunmsh: cp: missing file operand\n');
|
|
2011
|
+
const destination=resolvePath(nativePath(state.cwd),nativePath(files.pop()));
|
|
2012
|
+
let directory=false;
|
|
2013
|
+
try {directory=statSync(destination).isDirectory();}catch(error){if(error.code!=='ENOENT')return result(1,'',`bunmsh: cp: ${error.message}\n`);}
|
|
2014
|
+
if(files.length>1&&!directory)return result(1,'','bunmsh: cp: destination must be a directory\n');
|
|
2015
|
+
let stdout='',stderr='',status=0;
|
|
2016
|
+
for(const file of files)try {
|
|
2017
|
+
const source=resolvePath(nativePath(state.cwd),nativePath(file));
|
|
2018
|
+
const target=directory?resolvePath(destination,pathBasename(source)):destination;
|
|
2019
|
+
const info=statSync(source);
|
|
2020
|
+
if(!info.isFile())throw new Error(`-p currently requires a regular file: ${file}`);
|
|
2021
|
+
let previous;try{previous=statSync(target);}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
2022
|
+
if(previous?.dev===info.dev&&previous?.ino===info.ino)throw new Error(`${file} and destination are the same file`);
|
|
2023
|
+
copyFileSync(source,target);
|
|
2024
|
+
const copied=statSync(target);
|
|
2025
|
+
if(copied.uid!==info.uid||copied.gid!==info.gid)chownSync(target,info.uid,info.gid);
|
|
2026
|
+
chmodSync(target,info.mode&0o7777);
|
|
2027
|
+
utimesSync(target,info.atime,info.mtime);
|
|
2028
|
+
if(verbose)stdout+=`'${file}' -> '${target}'\n`;
|
|
2029
|
+
}catch(error){status=1;stderr+=`bunmsh: cp: ${error.message}\n`;}
|
|
2030
|
+
return result(status,stdout,stderr);
|
|
2031
|
+
}
|
|
1672
2032
|
const cmd = IS_COMPILED
|
|
1673
2033
|
? [...EXECUTABLE_COMMAND, "--bun-shell-fallback", ...bunShellFallbackArgv(argv)]
|
|
1674
2034
|
: [...BUN_RUNTIME_COMMAND, BUNMSH_ENTRY, "--bun-shell-fallback", ...bunShellFallbackArgv(argv)];
|
|
@@ -1676,7 +2036,7 @@ async function runBunShellCpFallback(argv, state) {
|
|
|
1676
2036
|
cmd,
|
|
1677
2037
|
cwd: nativePath(state.cwd),
|
|
1678
2038
|
env: {
|
|
1679
|
-
...state
|
|
2039
|
+
...externalEnvironment(state),
|
|
1680
2040
|
BUN_ENABLE_EXPERIMENTAL_SHELL_BUILTINS: "1",
|
|
1681
2041
|
},
|
|
1682
2042
|
stdin: "inherit",
|
|
@@ -1698,7 +2058,7 @@ async function runReflectedProcess(cmd, state) {
|
|
|
1698
2058
|
const proc = Bun.spawn({
|
|
1699
2059
|
cmd,
|
|
1700
2060
|
cwd: nativePath(state.cwd),
|
|
1701
|
-
env: state
|
|
2061
|
+
env: externalEnvironment(state),
|
|
1702
2062
|
stdin: "inherit",
|
|
1703
2063
|
stdout: inherit ? "inherit" : "pipe",
|
|
1704
2064
|
stderr: inherit ? "inherit" : "pipe",
|
|
@@ -1744,7 +2104,10 @@ async function runCatFallback(argv, state, input) {
|
|
|
1744
2104
|
const chunks = [];
|
|
1745
2105
|
let status = 0;
|
|
1746
2106
|
let stderr = "";
|
|
1747
|
-
|
|
2107
|
+
// A subprocess still needs shell-level descriptor routing for redirects.
|
|
2108
|
+
const endpoint = state.fdBindings?.[1] ?? {channel:1};
|
|
2109
|
+
const writer = state.pipelineChild && endpoint.channel === 1 &&
|
|
2110
|
+
endpoint.fd === undefined && !endpoint.closed ? Bun.stdout.writer() : null;
|
|
1748
2111
|
const emit = async (chunk) => {
|
|
1749
2112
|
if (writer) {
|
|
1750
2113
|
writer.write(chunk);
|
|
@@ -1875,6 +2238,44 @@ async function runHeadFallback(argv, state, input) {
|
|
|
1875
2238
|
return result(0, concatBytes(output));
|
|
1876
2239
|
}
|
|
1877
2240
|
|
|
2241
|
+
async function runUniqFallback(argv, state, input) {
|
|
2242
|
+
let count = false, repeated = false, single = false, i = 1;
|
|
2243
|
+
for (; i < argv.length; i++) {
|
|
2244
|
+
const arg = argv[i];
|
|
2245
|
+
if (arg === "--") { i++; break; }
|
|
2246
|
+
if (arg === "-" || !arg.startsWith("-")) break;
|
|
2247
|
+
if (!/^-[cdu]+$/.test(arg)) return result(1, "", `bunmsh: uniq: unsupported option: ${arg}\n`);
|
|
2248
|
+
count ||= arg.includes("c"); repeated ||= arg.includes("d"); single ||= arg.includes("u");
|
|
2249
|
+
}
|
|
2250
|
+
const operands = argv.slice(i);
|
|
2251
|
+
if (operands.length > 2) return result(1, "", "bunmsh: uniq: too many operands\n");
|
|
2252
|
+
try {
|
|
2253
|
+
const data = await readFallbackFiles([operands[0] ?? "-"], state, input);
|
|
2254
|
+
const output = [];
|
|
2255
|
+
let previous = null, occurrences = 0;
|
|
2256
|
+
const emit = () => {
|
|
2257
|
+
if (previous === null || (repeated && occurrences === 1) || (single && occurrences !== 1)) return;
|
|
2258
|
+
if (count) output.push(String(occurrences).padStart(7) + " ");
|
|
2259
|
+
output.push(previous, "\n");
|
|
2260
|
+
};
|
|
2261
|
+
for (let start = 0; start < data.length;) {
|
|
2262
|
+
const newline = data.indexOf(10, start);
|
|
2263
|
+
const end = newline < 0 ? data.length : newline;
|
|
2264
|
+
const line = data.subarray(start, end);
|
|
2265
|
+
if (previous !== null && previous.length === line.length && line.every((b, j) => b === previous[j])) occurrences++;
|
|
2266
|
+
else { emit(); previous = line; occurrences = 1; }
|
|
2267
|
+
start = end + 1;
|
|
2268
|
+
}
|
|
2269
|
+
emit();
|
|
2270
|
+
const bytes = concatBytes(output);
|
|
2271
|
+
if (operands[1] && operands[1] !== "-") {
|
|
2272
|
+
writeFileSync(resolvePath(state.cwd, operands[1]), bytes);
|
|
2273
|
+
return result(0);
|
|
2274
|
+
}
|
|
2275
|
+
return result(0, bytes);
|
|
2276
|
+
} catch (error) { return result(1, "", `bunmsh: uniq: ${error.message}\n`); }
|
|
2277
|
+
}
|
|
2278
|
+
|
|
1878
2279
|
async function runTextFilter(argv, state, input, kind) {
|
|
1879
2280
|
try {
|
|
1880
2281
|
let args = argv.slice(1), reverse = false, numeric = false, unique = false;
|
|
@@ -1991,17 +2392,29 @@ function expandTrSet(value) {
|
|
|
1991
2392
|
}
|
|
1992
2393
|
|
|
1993
2394
|
async function runTrFallback(argv, _state, input) {
|
|
1994
|
-
let del = false, i = 1;
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2395
|
+
let del = false, squeeze = false, i = 1;
|
|
2396
|
+
while (argv[i]?.startsWith("-") && argv[i] !== "-") {
|
|
2397
|
+
const option = argv[i++];
|
|
2398
|
+
if (option === "--") break;
|
|
2399
|
+
if (!/^-[ds]+$/.test(option)) return result(1, "", `bunmsh: tr: unsupported option: ${option}\n`);
|
|
2400
|
+
del ||= option.includes("d"); squeeze ||= option.includes("s");
|
|
2401
|
+
}
|
|
2402
|
+
const operands = argv.slice(i);
|
|
2403
|
+
const count = del ? (squeeze ? 2 : 1) : (squeeze && operands.length === 1 ? 1 : 2);
|
|
2404
|
+
if (operands.length !== count) return result(1, "", "bunmsh: tr: wrong number of operands\n");
|
|
2405
|
+
const from = expandTrSet(operands[0]);
|
|
2406
|
+
const to = expandTrSet(operands[1] ?? "");
|
|
2407
|
+
const translate = !del && operands.length === 2;
|
|
2408
|
+
if (translate && !to) return result(1, "", "bunmsh: tr: empty translation set\n");
|
|
2409
|
+
const squeezed = new Set(operands.length === 2 ? to : from);
|
|
1999
2410
|
const text = decoder.decode(await readFallbackInput(input));
|
|
2000
|
-
let output = "";
|
|
2411
|
+
let output = "", previous;
|
|
2001
2412
|
for (const ch of text) {
|
|
2002
2413
|
const index = from.indexOf(ch);
|
|
2003
|
-
if (index
|
|
2004
|
-
|
|
2414
|
+
if (del && index >= 0) continue;
|
|
2415
|
+
const value = translate && index >= 0 ? to[Math.min(index, to.length - 1)] : ch;
|
|
2416
|
+
if (squeeze && value === previous && squeezed.has(value)) continue;
|
|
2417
|
+
output += value; previous = value;
|
|
2005
2418
|
}
|
|
2006
2419
|
return result(0, output);
|
|
2007
2420
|
}
|
|
@@ -2018,7 +2431,7 @@ async function runCurlFallback(argv, state, input, context = {}) {
|
|
|
2018
2431
|
const stderr = [];
|
|
2019
2432
|
const io = {
|
|
2020
2433
|
cwd: nativePath(state.cwd),
|
|
2021
|
-
env: state
|
|
2434
|
+
env: externalEnvironment(state),
|
|
2022
2435
|
stdoutIsTTY: streamStdout && Boolean(process.stdout.isTTY),
|
|
2023
2436
|
readStdin: () => readFallbackInput(input),
|
|
2024
2437
|
async writeStdout(chunk) {
|
|
@@ -2148,17 +2561,24 @@ function grepFiles(operands, state, recursive) {
|
|
|
2148
2561
|
}
|
|
2149
2562
|
|
|
2150
2563
|
function grepBasicRegex(pattern) {
|
|
2151
|
-
let output = "", inClass = false;
|
|
2564
|
+
let output = "", inClass = false, branchStart = true;
|
|
2152
2565
|
for (let i = 0; i < pattern.length; i++) {
|
|
2153
2566
|
const ch = pattern[i];
|
|
2154
|
-
if (ch === "
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2567
|
+
if (!inClass && ch === "\\" && i + 1 < pattern.length) {
|
|
2568
|
+
const next=pattern[++i];
|
|
2569
|
+
output += "+?|(){}".includes(next) ? next : `\\${next}`;
|
|
2570
|
+
branchStart = next === "(" || next === "|";
|
|
2158
2571
|
continue;
|
|
2159
2572
|
}
|
|
2160
|
-
if (!inClass && "
|
|
2573
|
+
if (!inClass && ch === "$") {
|
|
2574
|
+
const anchored = i === pattern.length-1 || pattern.startsWith("\\)",i+1) || pattern.startsWith("\\|",i+1);
|
|
2575
|
+
output += anchored ? "$" : "\\$";
|
|
2576
|
+
} else if (!inClass && ch === "^") output += branchStart ? "^" : "\\^";
|
|
2577
|
+
else if (!inClass && "+?|(){}".includes(ch)) output += `\\${ch}`;
|
|
2161
2578
|
else output += ch;
|
|
2579
|
+
if (ch === "[") inClass = true;
|
|
2580
|
+
else if (ch === "]" && inClass) inClass = false;
|
|
2581
|
+
branchStart=false;
|
|
2162
2582
|
}
|
|
2163
2583
|
return output;
|
|
2164
2584
|
}
|
|
@@ -2175,6 +2595,7 @@ function grepPosixClasses(pattern) {
|
|
|
2175
2595
|
|
|
2176
2596
|
async function runGrepFallback(argv, state, input, context = {}) {
|
|
2177
2597
|
let flags = "", colorMode = "never", i = 1;
|
|
2598
|
+
const patterns=[];
|
|
2178
2599
|
while (argv[i]?.startsWith("-") && argv[i] !== "-") {
|
|
2179
2600
|
if (argv[i] === "--") { i++; break; }
|
|
2180
2601
|
const colorOption = /^--colou?r(?:=(always|auto|never))?$/.exec(argv[i]);
|
|
@@ -2183,16 +2604,24 @@ async function runGrepFallback(argv, state, input, context = {}) {
|
|
|
2183
2604
|
i++;
|
|
2184
2605
|
continue;
|
|
2185
2606
|
}
|
|
2186
|
-
if (
|
|
2607
|
+
if (argv[i] === "-e" || argv[i].startsWith("-e")) {
|
|
2608
|
+
const option=argv[i++]; const pattern=option.length>2 ? option.slice(2) : argv[i++];
|
|
2609
|
+
if (pattern === undefined) return result(2,"","bunmsh: grep: -e requires a pattern\n");
|
|
2610
|
+
patterns.push(...pattern.split("\n")); continue;
|
|
2611
|
+
}
|
|
2612
|
+
if (!/^-[EFiqvnorxc]+$/.test(argv[i]))
|
|
2187
2613
|
return result(2, "", `bunmsh: grep: unsupported option: ${argv[i]}\n`);
|
|
2188
2614
|
flags += argv[i++].slice(1);
|
|
2189
2615
|
}
|
|
2190
|
-
|
|
2191
|
-
|
|
2616
|
+
if (!patterns.length) {
|
|
2617
|
+
const pattern=argv[i++];
|
|
2618
|
+
if (pattern === undefined) return result(2,"","bunmsh: grep: missing pattern\n");
|
|
2619
|
+
patterns.push(...pattern.split("\n"));
|
|
2620
|
+
}
|
|
2192
2621
|
let regex;
|
|
2193
|
-
const regexSource
|
|
2194
|
-
? pattern.replace(/[\\^$.*+?()[\]{}|]/g,
|
|
2195
|
-
: grepPosixClasses(flags.includes("E") ? pattern : grepBasicRegex(pattern));
|
|
2622
|
+
const regexSource=patterns.map(pattern => flags.includes("F")
|
|
2623
|
+
? pattern.replace(/[\\^$.*+?()[\]{}|]/g,"\\$&")
|
|
2624
|
+
: grepPosixClasses(flags.includes("E") ? pattern : grepBasicRegex(pattern))).map(p=>`(?:${p})`).join("|");
|
|
2196
2625
|
try { regex = new RegExp(flags.includes("x") ? `^(?:${regexSource})$` : regexSource, `${flags.includes("i") ? "i" : ""}${flags.includes("o") ? "g" : ""}`); }
|
|
2197
2626
|
catch (error) { return result(2, "", `bunmsh: grep: ${error.message}\n`); }
|
|
2198
2627
|
const invert = flags.includes("v"), quiet = flags.includes("q"), numbers = flags.includes("n"), only = flags.includes("o");
|
|
@@ -2203,15 +2632,16 @@ async function runGrepFallback(argv, state, input, context = {}) {
|
|
|
2203
2632
|
const highlightRegex = color && !only
|
|
2204
2633
|
? new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`)
|
|
2205
2634
|
: null;
|
|
2206
|
-
let matched = false;
|
|
2635
|
+
let matched = false, count = 0;
|
|
2207
2636
|
const output = [];
|
|
2208
2637
|
const processLine = (line, lineNumber, label, showLabel) => {
|
|
2209
2638
|
regex.lastIndex = 0;
|
|
2210
2639
|
const isMatch = regex.test(line);
|
|
2211
2640
|
regex.lastIndex = 0;
|
|
2212
2641
|
if (isMatch === invert) return false;
|
|
2213
|
-
matched = true;
|
|
2642
|
+
matched = true; count++;
|
|
2214
2643
|
if (quiet) return true;
|
|
2644
|
+
if (flags.includes("c")) return false;
|
|
2215
2645
|
const prefix = `${showLabel ? `${label}:` : ""}${numbers ? `${lineNumber}:` : ""}`;
|
|
2216
2646
|
if (only) {
|
|
2217
2647
|
if (invert) return false;
|
|
@@ -2234,11 +2664,13 @@ async function runGrepFallback(argv, state, input, context = {}) {
|
|
|
2234
2664
|
const files = grepFiles(operands, state, flags.includes("r"));
|
|
2235
2665
|
const showLabel = files.length > 1 || flags.includes("r");
|
|
2236
2666
|
for (const file of files) {
|
|
2667
|
+
count=0;
|
|
2237
2668
|
const lines = (await Bun.file(file.path).text()).split("\n");
|
|
2238
2669
|
if (lines.at(-1) === "") lines.pop();
|
|
2239
2670
|
for (let line = 0; line < lines.length; line++)
|
|
2240
2671
|
if (processLine(lines[line], line + 1,
|
|
2241
2672
|
implicitCwd ? file.shown.replace(/^\.\//, "") : file.shown, showLabel)) return result(0);
|
|
2673
|
+
if (flags.includes("c")) output.push(`${showLabel ? `${file.shown}:` : ""}${count}\n`);
|
|
2242
2674
|
}
|
|
2243
2675
|
} catch (error) { return result(2, "", `bunmsh: grep: ${error.message}\n`); }
|
|
2244
2676
|
} else {
|
|
@@ -2258,93 +2690,10 @@ async function runGrepFallback(argv, state, input, context = {}) {
|
|
|
2258
2690
|
}
|
|
2259
2691
|
} finally { try { reader.releaseLock(); } catch {} }
|
|
2260
2692
|
}
|
|
2693
|
+
if (!operands.length && flags.includes("c") && !quiet) output.push(`${count}\n`);
|
|
2261
2694
|
return result(matched ? 0 : 1, output.join(""));
|
|
2262
2695
|
}
|
|
2263
2696
|
|
|
2264
|
-
function splitSedCommands(script) {
|
|
2265
|
-
const commands = [];
|
|
2266
|
-
let start = 0, escaped = false;
|
|
2267
|
-
for (let i = 0; i < script.length; i++) {
|
|
2268
|
-
if (escaped) { escaped = false; continue; }
|
|
2269
|
-
if (script[i] === "\\") { escaped = true; continue; }
|
|
2270
|
-
if (script[i] === ";") {
|
|
2271
|
-
commands.push(script.slice(start, i).trim());
|
|
2272
|
-
start = i + 1;
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
commands.push(script.slice(start).trim());
|
|
2276
|
-
return commands.filter(Boolean);
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
function readSedField(command, start, delimiter) {
|
|
2280
|
-
let value = "";
|
|
2281
|
-
for (let i = start; i < command.length; i++) {
|
|
2282
|
-
if (command[i] === "\\" && i + 1 < command.length) {
|
|
2283
|
-
if (command[i + 1] === delimiter) value += command[++i];
|
|
2284
|
-
else value += command[i] + command[++i];
|
|
2285
|
-
} else if (command[i] === delimiter) return [value, i + 1];
|
|
2286
|
-
else value += command[i];
|
|
2287
|
-
}
|
|
2288
|
-
throw new Error("unterminated substitute command");
|
|
2289
|
-
}
|
|
2290
|
-
|
|
2291
|
-
function sedReplacement(value) {
|
|
2292
|
-
let output = "";
|
|
2293
|
-
for (let i = 0; i < value.length; i++) {
|
|
2294
|
-
if (value[i] === "\\" && /[1-9]/.test(value[i + 1] ?? "")) output += `$${value[++i]}`;
|
|
2295
|
-
else if (value[i] === "\\" && i + 1 < value.length) output += value[++i];
|
|
2296
|
-
else if (value[i] === "&") output += "$&";
|
|
2297
|
-
else if (value[i] === "$") output += "$$";
|
|
2298
|
-
else output += value[i];
|
|
2299
|
-
}
|
|
2300
|
-
return output;
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
function compileSedCommand(source, extended) {
|
|
2304
|
-
const numberedPrint = /^(\d+)p$/.exec(source);
|
|
2305
|
-
if (numberedPrint) return { type: "print", line: Number(numberedPrint[1]) };
|
|
2306
|
-
if (!source.startsWith("s") || source.length < 2)
|
|
2307
|
-
throw new Error(`unsupported command: ${source}`);
|
|
2308
|
-
const delimiter = source[1];
|
|
2309
|
-
const [pattern, replacementStart] = readSedField(source, 2, delimiter);
|
|
2310
|
-
const [replacement, flagsStart] = readSedField(source, replacementStart, delimiter);
|
|
2311
|
-
const flags = source.slice(flagsStart);
|
|
2312
|
-
if (!/^[gp]*$/.test(flags)) throw new Error(`unsupported substitute flags: ${flags}`);
|
|
2313
|
-
const regexSource = grepPosixClasses(extended ? pattern : grepBasicRegex(pattern));
|
|
2314
|
-
return {
|
|
2315
|
-
type: "substitute",
|
|
2316
|
-
regex: new RegExp(regexSource, flags.includes("g") ? "g" : ""),
|
|
2317
|
-
replacement: sedReplacement(replacement),
|
|
2318
|
-
print: flags.includes("p"),
|
|
2319
|
-
};
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
function applySed(text, commands, quiet) {
|
|
2323
|
-
const trailingNewline = text.endsWith("\n");
|
|
2324
|
-
const lines = text.split("\n");
|
|
2325
|
-
if (trailingNewline) lines.pop();
|
|
2326
|
-
const output = [];
|
|
2327
|
-
for (let index = 0; index < lines.length; index++) {
|
|
2328
|
-
let line = lines[index];
|
|
2329
|
-
for (const command of commands) {
|
|
2330
|
-
if (command.type === "print") {
|
|
2331
|
-
if (command.line === index + 1) output.push(line);
|
|
2332
|
-
continue;
|
|
2333
|
-
}
|
|
2334
|
-
command.regex.lastIndex = 0;
|
|
2335
|
-
const matched = command.regex.test(line);
|
|
2336
|
-
command.regex.lastIndex = 0;
|
|
2337
|
-
if (matched) {
|
|
2338
|
-
line = line.replace(command.regex, command.replacement);
|
|
2339
|
-
if (command.print) output.push(line);
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
if (!quiet) output.push(line);
|
|
2343
|
-
}
|
|
2344
|
-
if (!output.length) return "";
|
|
2345
|
-
return output.join("\n") + (trailingNewline ? "\n" : "");
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
2697
|
async function runSedFallback(argv, state, input) {
|
|
2349
2698
|
let quiet = false, extended = false, inPlace = false, i = 1;
|
|
2350
2699
|
const scripts = [];
|
|
@@ -2357,6 +2706,13 @@ async function runSedFallback(argv, state, input) {
|
|
|
2357
2706
|
continue;
|
|
2358
2707
|
}
|
|
2359
2708
|
if (option.startsWith("-e")) { scripts.push(option.slice(2)); continue; }
|
|
2709
|
+
if (option.startsWith("-f")) {
|
|
2710
|
+
const file=option.length>2 ? option.slice(2) : argv[i++];
|
|
2711
|
+
if (file === undefined) return result(1,"","bunmsh: sed: -f requires a file\n");
|
|
2712
|
+
try { scripts.push(readFileSync(resolvePath(nativePath(state.cwd),nativePath(file)),"utf8")); }
|
|
2713
|
+
catch(error) { return result(1,"",`bunmsh: sed: ${error.message}\n`); }
|
|
2714
|
+
continue;
|
|
2715
|
+
}
|
|
2360
2716
|
if (!/^-[nEri]+$/.test(option))
|
|
2361
2717
|
return result(1, "", `bunmsh: sed: unsupported option: ${option}\n`);
|
|
2362
2718
|
quiet ||= option.includes("n");
|
|
@@ -2366,19 +2722,23 @@ async function runSedFallback(argv, state, input) {
|
|
|
2366
2722
|
if (!scripts.length && i < argv.length) scripts.push(argv[i++]);
|
|
2367
2723
|
if (!scripts.length) return result(1, "", "bunmsh: sed: missing script\n");
|
|
2368
2724
|
let commands;
|
|
2369
|
-
try { commands = scripts.
|
|
2725
|
+
try { commands = compileSed(scripts.join("\n"), pattern => grepPosixClasses(extended ? pattern : grepBasicRegex(pattern))); }
|
|
2370
2726
|
catch (error) { return result(1, "", `bunmsh: sed: ${error.message}\n`); }
|
|
2371
2727
|
const operands = argv.slice(i);
|
|
2372
2728
|
if (inPlace && !operands.length) return result(1, "", "bunmsh: sed: -i requires a file operand\n");
|
|
2729
|
+
const readExtra = file => {
|
|
2730
|
+
try { return readFileSync(resolvePath(nativePath(state.cwd),nativePath(file)),"utf8"); }
|
|
2731
|
+
catch { return ""; } // sed r silently skips unreadable or missing files.
|
|
2732
|
+
};
|
|
2373
2733
|
try {
|
|
2374
2734
|
if (!operands.length) {
|
|
2375
2735
|
const text = decoder.decode(await readFallbackInput(input));
|
|
2376
|
-
return result(0, applySed(text, commands, quiet));
|
|
2736
|
+
return result(0, applySed(text, commands, quiet, readExtra));
|
|
2377
2737
|
}
|
|
2378
2738
|
let stdout = "";
|
|
2379
2739
|
for (const operand of operands) {
|
|
2380
2740
|
const file = isAbsolute(nativePath(operand)) ? nativePath(operand) : resolvePath(nativePath(state.cwd), nativePath(operand));
|
|
2381
|
-
const changed = applySed(await Bun.file(file).text(), commands, quiet);
|
|
2741
|
+
const changed = applySed(await Bun.file(file).text(), commands, quiet, readExtra);
|
|
2382
2742
|
if (inPlace) await Bun.write(file, changed);
|
|
2383
2743
|
else stdout += changed;
|
|
2384
2744
|
}
|
|
@@ -2452,16 +2812,47 @@ function runLnFallback(argv, state) {
|
|
|
2452
2812
|
return result(status, "", stderr);
|
|
2453
2813
|
}
|
|
2454
2814
|
|
|
2815
|
+
function symbolicFileMode(spec, stat) {
|
|
2816
|
+
let mode = stat.mode & 0o7777;
|
|
2817
|
+
// Retain the original shorthand; other symbolic forms require explicit who.
|
|
2818
|
+
if (spec === "+x") return mode | 0o111;
|
|
2819
|
+
for (const clause of spec.split(",")) {
|
|
2820
|
+
const match = /^([ugoa]+)((?:[+=-][rwxX]*)+)$/.exec(clause);
|
|
2821
|
+
if (!match) throw new Error(`unsupported mode: ${spec}`);
|
|
2822
|
+
const who = match[1].includes("a") ? "ugo" : match[1];
|
|
2823
|
+
let selected = 0, special = 0;
|
|
2824
|
+
for (const [name, mask, bit] of [["u",0o700,0o4000],["g",0o070,0o2000],["o",0o007,0o1000]])
|
|
2825
|
+
if (who.includes(name)) { selected |= mask; special |= bit; }
|
|
2826
|
+
for (const action of match[2].matchAll(/([+=-])([rwxX]*)/g)) {
|
|
2827
|
+
let bits = 0;
|
|
2828
|
+
for (const ch of action[2]) {
|
|
2829
|
+
if (ch === "r") bits |= 0o444;
|
|
2830
|
+
if (ch === "w") bits |= 0o222;
|
|
2831
|
+
if (ch === "x" || (ch === "X" && (stat.isDirectory() || (mode & 0o111)))) bits |= 0o111;
|
|
2832
|
+
}
|
|
2833
|
+
bits &= selected;
|
|
2834
|
+
if (action[1] === "+") mode |= bits;
|
|
2835
|
+
else if (action[1] === "-") mode &= ~bits;
|
|
2836
|
+
else {
|
|
2837
|
+
// GNU chmod preserves directory set-ID bits unless explicitly changed.
|
|
2838
|
+
const clearedSpecial = stat.isDirectory() ? special & 0o1000 : special;
|
|
2839
|
+
mode = (mode & ~(selected | clearedSpecial)) | bits;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
return mode;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2455
2846
|
function runChmodFallback(argv, state) {
|
|
2456
|
-
|
|
2457
|
-
const mode = argv[
|
|
2847
|
+
let i = argv[1] === "--" ? 2 : 1;
|
|
2848
|
+
const mode = argv[i++];
|
|
2849
|
+
if (argv[i] === "--") i++;
|
|
2850
|
+
if (!mode || i >= argv.length) return result(1, "", "bunmsh: chmod: usage: chmod MODE FILE...\n");
|
|
2458
2851
|
let status = 0, stderr = "";
|
|
2459
|
-
for (const operand of argv.slice(
|
|
2852
|
+
for (const operand of argv.slice(i)) {
|
|
2460
2853
|
const path = isAbsolute(nativePath(operand)) ? nativePath(operand) : resolvePath(nativePath(state.cwd), nativePath(operand));
|
|
2461
2854
|
try {
|
|
2462
|
-
|
|
2463
|
-
else if (mode === "+x" || mode === "a+x") chmodSync(path, statSync(path).mode | 0o111);
|
|
2464
|
-
else throw new Error(`unsupported mode: ${mode}`);
|
|
2855
|
+
chmodSync(path, /^[0-7]{1,4}$/.test(mode) ? Number.parseInt(mode, 8) : symbolicFileMode(mode, statSync(path)));
|
|
2465
2856
|
} catch (error) { status = 1; stderr += `bunmsh: chmod: ${operand}: ${error.message}\n`; }
|
|
2466
2857
|
}
|
|
2467
2858
|
return result(status, "", stderr);
|
|
@@ -2582,7 +2973,7 @@ async function queryProcessList(name, state) {
|
|
|
2582
2973
|
proc = Bun.spawn({
|
|
2583
2974
|
cmd: command,
|
|
2584
2975
|
cwd: nativePath(state.cwd),
|
|
2585
|
-
env: state
|
|
2976
|
+
env: externalEnvironment(state),
|
|
2586
2977
|
stdin: "ignore",
|
|
2587
2978
|
stdout: "pipe",
|
|
2588
2979
|
stderr: "pipe",
|
|
@@ -2764,12 +3155,157 @@ async function runPspacFallback(argv, state) {
|
|
|
2764
3155
|
return result(0, colorProcessTable(rows), query.stderr);
|
|
2765
3156
|
}
|
|
2766
3157
|
|
|
3158
|
+
function runExprFallback(argv) {
|
|
3159
|
+
const args = argv.slice(argv[1] === "--" ? 2 : 1);
|
|
3160
|
+
let i = 0;
|
|
3161
|
+
const precedence = {"|":1,"&":2,"=":3,"!=":3,"<":3,"<=":3,">":3,">=":3,"+":4,"-":4,"*":5,"/":5,"%":5,":":6};
|
|
3162
|
+
const truth = value => value !== "" && !/^-?0+$/.test(value);
|
|
3163
|
+
const integer = value => { if (!/^-?\d+$/.test(value)) throw new Error("non-integer argument"); return BigInt(value); };
|
|
3164
|
+
const atom = () => {
|
|
3165
|
+
if (i >= args.length) throw new Error("missing operand");
|
|
3166
|
+
const value = args[i++];
|
|
3167
|
+
if (value !== "(") return value;
|
|
3168
|
+
const nested = expression(1);
|
|
3169
|
+
if (args[i++] !== ")") throw new Error("expected closing parenthesis");
|
|
3170
|
+
return nested;
|
|
3171
|
+
};
|
|
3172
|
+
const expression = minimum => {
|
|
3173
|
+
let left = atom();
|
|
3174
|
+
while ((precedence[args[i]] ?? 0) >= minimum) {
|
|
3175
|
+
const op = args[i++], right = expression(precedence[op] + 1);
|
|
3176
|
+
if (op === "|") left = truth(left) ? left : truth(right) ? right : "0";
|
|
3177
|
+
else if (op === "&") left = truth(left) && truth(right) ? left : "0";
|
|
3178
|
+
else if (op === ":") {
|
|
3179
|
+
const re = new RegExp(`^(?:${grepPosixClasses(grepBasicRegex(right))})`);
|
|
3180
|
+
const match = re.exec(left);
|
|
3181
|
+
// BRE parentheses are captures; a failed capturing match returns empty.
|
|
3182
|
+
const capture = /(^|[^\\])(?:\\\\)*\\\(/.test(right);
|
|
3183
|
+
left = capture ? (match?.[1] ?? "") : String(match?.[0].length ?? 0);
|
|
3184
|
+
} else if (["+","-","*","/","%"].includes(op)) {
|
|
3185
|
+
const a = integer(left), b = integer(right);
|
|
3186
|
+
if ((op === "/" || op === "%") && b === 0n) throw new Error("division by zero");
|
|
3187
|
+
left = String(op === "+" ? a+b : op === "-" ? a-b : op === "*" ? a*b : op === "/" ? a/b : a%b);
|
|
3188
|
+
} else {
|
|
3189
|
+
const numeric = /^-?\d+$/.test(left) && /^-?\d+$/.test(right);
|
|
3190
|
+
const a = numeric ? BigInt(left) : left, b = numeric ? BigInt(right) : right;
|
|
3191
|
+
left = String(Number(op === "=" ? a===b : op === "!=" ? a!==b : op === "<" ? a<b : op === "<=" ? a<=b : op === ">" ? a>b : a>=b));
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
return left;
|
|
3195
|
+
};
|
|
3196
|
+
try {
|
|
3197
|
+
const value = expression(1);
|
|
3198
|
+
if (i !== args.length) throw new Error("unexpected operand");
|
|
3199
|
+
return result(truth(value) ? 0 : 1, `${value}\n`);
|
|
3200
|
+
} catch (error) { return result(2,"",`bunmsh: expr: ${error.message}\n`); }
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
async function runAwkFallback(argv, state, input) {
|
|
3204
|
+
try {
|
|
3205
|
+
const scripts=[], variables={}; let i=1;
|
|
3206
|
+
const assignment = text => {
|
|
3207
|
+
const m=/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s.exec(text);
|
|
3208
|
+
if (!m) throw new Error(`invalid variable assignment: ${text}`);
|
|
3209
|
+
return [m[1], m[2].replace(/\\([ntr\\])/g, (_,c)=>({n:"\n",t:"\t",r:"\r","\\":"\\"})[c])];
|
|
3210
|
+
};
|
|
3211
|
+
for (; i<argv.length && argv[i].startsWith("-") && argv[i]!=="-"; i++) {
|
|
3212
|
+
const option=argv[i]; if(option==="--") {i++;break;}
|
|
3213
|
+
const flag=option.slice(0,2);
|
|
3214
|
+
if(!["-F","-v","-f","-e"].includes(flag)) throw new Error(`unsupported option: ${option}`);
|
|
3215
|
+
const value=option.length>2?option.slice(2):argv[++i];
|
|
3216
|
+
if(value===undefined)throw new Error(`${flag} requires an argument`);
|
|
3217
|
+
if(flag==="-F")variables.FS=value;
|
|
3218
|
+
else if(flag==="-v") {const [name,text]=assignment(value);variables[name]=text;}
|
|
3219
|
+
else scripts.push(flag==="-f" ? readFileSync(resolvePath(nativePath(state.cwd),nativePath(value)),"utf8") : value);
|
|
3220
|
+
}
|
|
3221
|
+
if(!scripts.length){if(i>=argv.length)throw new Error("missing program");scripts.push(argv[i++]);}
|
|
3222
|
+
const program=compileAwk(scripts.join("\n"));
|
|
3223
|
+
const needsInput = node => node && typeof node==="object" && (node.type==="getline"&&!node.file ||
|
|
3224
|
+
(node instanceof Map ? [...node.values()] : Object.values(node)).some(needsInput));
|
|
3225
|
+
const operands=argv.slice(i), inputs=[];
|
|
3226
|
+
if(program.rules.length || needsInput(program)) {
|
|
3227
|
+
for (const operand of operands.length?operands:["-"]) {
|
|
3228
|
+
if(/^[A-Za-z_][A-Za-z0-9_]*=/.test(operand))inputs.push({assignment:assignment(operand)});
|
|
3229
|
+
else inputs.push({name:operand,text:operand==="-"?decode(await readFallbackInput(input)):readFileSync(resolvePath(nativePath(state.cwd),nativePath(operand)),"utf8")});
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
const path = file => resolvePath(nativePath(state.cwd),nativePath(file));
|
|
3233
|
+
const value=executeAwk(program,inputs,{variables,argv:operands,env:externalEnvironment(state),
|
|
3234
|
+
runPipe:(command,text)=>{
|
|
3235
|
+
const directory=mkdtempSync(resolvePath(tmpdir(),"bunmsh-awk-state-"));
|
|
3236
|
+
try {
|
|
3237
|
+
const file=resolvePath(directory,"state.json");
|
|
3238
|
+
writeFileSync(file,pipelineBuiltinState(state).env[PIPELINE_STATE_ENV],{mode:0o600});
|
|
3239
|
+
const p=Bun.spawnSync([process.execPath,BUNMSH_ENTRY,"-c",command],{
|
|
3240
|
+
cwd:nativePath(state.cwd),env:{...externalEnvironment(state),[PIPELINE_STATE_ENV]:`@file:${file}`},
|
|
3241
|
+
stdin:bytes(text),stdout:"pipe",stderr:"pipe"});
|
|
3242
|
+
if(p.error)throw p.error;
|
|
3243
|
+
return {status:p.exitCode,stdout:decode(p.stdout),stderr:decode(p.stderr)};
|
|
3244
|
+
} finally {rmSync(directory,{recursive:true,force:true});}
|
|
3245
|
+
},
|
|
3246
|
+
readFile:file=>readFileSync(path(file),"utf8"),
|
|
3247
|
+
writeFile:(file,text,append)=>writeFileSync(path(file),text,{flag:append?"a":"w"})});
|
|
3248
|
+
return result(value.status,value.stdout,value.stderr??"");
|
|
3249
|
+
} catch(error) {return result(2,"",`bunmsh: ${error.message.startsWith("awk:")?"":"awk: "}${error.message}\n`);}
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
async function runCmpFallback(argv, state, input) {
|
|
3253
|
+
let silent = false, i = 1;
|
|
3254
|
+
for (; i < argv.length && argv[i].startsWith("-") && argv[i] !== "-"; i++) {
|
|
3255
|
+
if (argv[i] === "--") { i++; break; }
|
|
3256
|
+
if (["-s", "--silent", "--quiet"].includes(argv[i])) silent = true;
|
|
3257
|
+
else return result(2, "", `bunmsh: cmp: unsupported option: ${argv[i]}\n`);
|
|
3258
|
+
}
|
|
3259
|
+
const files = argv.slice(i);
|
|
3260
|
+
if (files.length !== 2) return result(2, "", "bunmsh: cmp: expected two files\n");
|
|
3261
|
+
try {
|
|
3262
|
+
let stdin;
|
|
3263
|
+
const read = async file => file === "-" ? (stdin ??= await readFallbackInput(input)) :
|
|
3264
|
+
readFileSync(resolvePath(nativePath(state.cwd), nativePath(file)));
|
|
3265
|
+
const a = await read(files[0]), b = await read(files[1]);
|
|
3266
|
+
let offset = 0, line = 1;
|
|
3267
|
+
while (offset < Math.min(a.length, b.length) && a[offset] === b[offset]) {
|
|
3268
|
+
if (a[offset] === 10) line++;
|
|
3269
|
+
offset++;
|
|
3270
|
+
}
|
|
3271
|
+
if (offset === a.length && offset === b.length) return result();
|
|
3272
|
+
if (silent) return result(1);
|
|
3273
|
+
if (offset < Math.min(a.length, b.length))
|
|
3274
|
+
return result(1, `${files[0]} ${files[1]} differ: char ${offset + 1}, line ${line}\n`);
|
|
3275
|
+
const file = files[a.length < b.length ? 0 : 1];
|
|
3276
|
+
return result(1, "", `cmp: EOF on ${file}${offset ? ` after byte ${offset}, in line ${line}` : " which is empty"}\n`);
|
|
3277
|
+
} catch (error) { return result(2, "", silent ? "" : `bunmsh: cmp: ${error.message}\n`); }
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
async function runDiffFallback(argv, state) {
|
|
3281
|
+
let brief=false, i=1;
|
|
3282
|
+
for (; i<argv.length && argv[i].startsWith("-"); i++) {
|
|
3283
|
+
if (argv[i] === "--") { i++; break; }
|
|
3284
|
+
if (["-q","--brief"].includes(argv[i])) brief=true;
|
|
3285
|
+
else if (!/^--color(?:=(?:always|auto|never))?$/.test(argv[i]))
|
|
3286
|
+
return result(2,"",`bunmsh: diff: unsupported option: ${argv[i]}\n`);
|
|
3287
|
+
}
|
|
3288
|
+
if (argv.length-i !== 2) return result(2,"","bunmsh: diff: expected two files\n");
|
|
3289
|
+
const [left,right]=argv.slice(i);
|
|
3290
|
+
try {
|
|
3291
|
+
const a=readFileSync(resolvePath(nativePath(state.cwd),nativePath(left)));
|
|
3292
|
+
const b=readFileSync(resolvePath(nativePath(state.cwd),nativePath(right)));
|
|
3293
|
+
if (a.equals(b)) return result();
|
|
3294
|
+
if (brief || a.includes(0) || b.includes(0)) return result(1,`Files ${left} and ${right} differ\n`);
|
|
3295
|
+
return result(1, Buffer.from(normalFileDiff(a.toString("latin1"), b.toString("latin1")), "latin1").toString());
|
|
3296
|
+
} catch(error) {return result(2,"",`bunmsh: diff: ${error.message}\n`);}
|
|
3297
|
+
}
|
|
3298
|
+
|
|
2767
3299
|
const fallbackBuiltins = {
|
|
3300
|
+
awk: runAwkFallback,
|
|
3301
|
+
diff: runDiffFallback,
|
|
3302
|
+
cmp: runCmpFallback,
|
|
3303
|
+
expr: runExprFallback,
|
|
2768
3304
|
basename: async (argv) => {
|
|
2769
3305
|
const i = argv[1] === "--" ? 2 : 1;
|
|
2770
3306
|
if (i >= argv.length || argv.length - i > 2)
|
|
2771
3307
|
return result(1, "", "bunmsh: basename: usage: basename string [suffix]\n");
|
|
2772
|
-
let name = pathBasename(argv[i]);
|
|
3308
|
+
let name = /^\/+$/.test(argv[i]) ? "/" : pathBasename(argv[i]);
|
|
2773
3309
|
const suffix = argv[i + 1];
|
|
2774
3310
|
if (suffix && suffix !== name && name.endsWith(suffix)) name = name.slice(0, -suffix.length);
|
|
2775
3311
|
return result(0, `${name}\n`);
|
|
@@ -2809,11 +3345,14 @@ const fallbackBuiltins = {
|
|
|
2809
3345
|
clear: async () => result(0, "\x1b[2J\x1b[H"),
|
|
2810
3346
|
rmdir: async (argv, state) => runRmdirFallback(argv, state),
|
|
2811
3347
|
mktemp: async (argv, state) => runMktempFallback(argv, state),
|
|
3348
|
+
uniq: runUniqFallback,
|
|
2812
3349
|
sort: (argv, state, input) => runTextFilter(argv, state, input, "sort"),
|
|
2813
3350
|
date: async (argv) => runDateFallback(argv),
|
|
2814
3351
|
md5sum: (argv, state, input) => runHashFallback(argv, state, input, "md5"),
|
|
2815
3352
|
sha256sum: (argv, state, input) => runHashFallback(argv, state, input, "sha256"),
|
|
2816
3353
|
grep: runGrepFallback,
|
|
3354
|
+
egrep: (argv, state, input, context) => runGrepFallback(["grep", "-E", ...argv.slice(1)], state, input, context),
|
|
3355
|
+
fgrep: (argv, state, input, context) => runGrepFallback(["grep", "-F", ...argv.slice(1)], state, input, context),
|
|
2817
3356
|
sed: runSedFallback,
|
|
2818
3357
|
cut: runCutFallback,
|
|
2819
3358
|
ln: async (argv, state) => runLnFallback(argv, state),
|
|
@@ -2863,8 +3402,14 @@ function formatMilliseconds(milliseconds, color = Boolean(process.stderr.isTTY))
|
|
|
2863
3402
|
return output;
|
|
2864
3403
|
}
|
|
2865
3404
|
|
|
3405
|
+
function externalEnvironment(state) {
|
|
3406
|
+
if (!state.shellOptions.posix) return state.env;
|
|
3407
|
+
return Object.fromEntries(Object.entries(state.env).filter(([name]) =>
|
|
3408
|
+
state.exported.has(name) || name === PIPELINE_STATE_ENV));
|
|
3409
|
+
}
|
|
3410
|
+
|
|
2866
3411
|
export function createState(options = {}) {
|
|
2867
|
-
const env = canonicalEnvironment({ ...process.env, ...(options.env ?? {}) });
|
|
3412
|
+
const env = canonicalEnvironment({ ...(options.inheritEnvironment === false ? {} : process.env), ...(options.env ?? {}) });
|
|
2868
3413
|
const cwd = shellPath(options.cwd ?? process.cwd());
|
|
2869
3414
|
env.PWD = cwd;
|
|
2870
3415
|
return {
|
|
@@ -2879,17 +3424,29 @@ export function createState(options = {}) {
|
|
|
2879
3424
|
mouseTracking: options.mouseTracking ?? /^(?:1|true|on|yes)$/i.test(env.BUNMSH_MOUSE ?? ""),
|
|
2880
3425
|
pathSearch: options.pathSearch ?? true,
|
|
2881
3426
|
readonly: new Set(options.readonly ?? []),
|
|
3427
|
+
exported: new Set(options.exported ?? Object.keys(env)),
|
|
2882
3428
|
getoptsOffset: 1,
|
|
2883
3429
|
args: options.args ?? ["bunmsh"],
|
|
2884
3430
|
lastStatus: 0,
|
|
2885
3431
|
exitRequested: false,
|
|
2886
3432
|
exitStatus: 0,
|
|
2887
3433
|
pipelineChild: options.pipelineChild ?? false,
|
|
3434
|
+
flow: null,
|
|
3435
|
+
loopDepth: 0,
|
|
3436
|
+
returnDepth: 0,
|
|
3437
|
+
fdBindings: {...(options.fdBindings ?? {})},
|
|
3438
|
+
ownedFds: new Set(),
|
|
3439
|
+
shellOptions: {...(options.shellOptions ?? {})},
|
|
3440
|
+
ignoreErrexit: options.ignoreErrexit ?? 0,
|
|
3441
|
+
traps: {},
|
|
3442
|
+
trapListeners: new Map(),
|
|
3443
|
+
pendingTraps: [],
|
|
3444
|
+
inTrap: false,
|
|
2888
3445
|
};
|
|
2889
3446
|
}
|
|
2890
3447
|
|
|
2891
3448
|
function hasCompoundSyntax(source) {
|
|
2892
|
-
if (/(^|\n)\s*(?:\(|[A-Za-z_][A-Za-z0-9_]*\s*\(\s*\)
|
|
3449
|
+
if (/(^|\n)\s*(?:\(|[A-Za-z_][A-Za-z0-9_]*\s*\(\s*\))/.test(source)) return true;
|
|
2893
3450
|
let commandStart = true;
|
|
2894
3451
|
for (const token of tokenize(source)) {
|
|
2895
3452
|
if (token.type === "op") {
|
|
@@ -2898,14 +3455,15 @@ function hasCompoundSyntax(source) {
|
|
|
2898
3455
|
}
|
|
2899
3456
|
const word = token.fragments.length === 1 && token.fragments[0].quote === "none"
|
|
2900
3457
|
? token.fragments[0].text : null;
|
|
2901
|
-
if (commandStart && ["if", "while", "until", "for", "case"].includes(word)) return true;
|
|
3458
|
+
if (commandStart && ["if", "while", "until", "for", "case", "{"].includes(word)) return true;
|
|
2902
3459
|
if (!(commandStart && /^[A-Za-z_][A-Za-z0-9_]*=/.test(word ?? ""))) commandStart = false;
|
|
2903
3460
|
}
|
|
2904
3461
|
return false;
|
|
2905
3462
|
}
|
|
2906
3463
|
|
|
2907
|
-
function compoundLogicalSource(source) {
|
|
2908
|
-
const
|
|
3464
|
+
function compoundLogicalSource(source, firstLine = 1, sourceLines = null) {
|
|
3465
|
+
const continuations = new Map();
|
|
3466
|
+
const tokens = tokenize(source, { continuations });
|
|
2909
3467
|
const breaks = new Set();
|
|
2910
3468
|
let commandStart = true;
|
|
2911
3469
|
const literal = (token) => token.type === "word" && token.fragments.length === 1 &&
|
|
@@ -2917,6 +3475,24 @@ function compoundLogicalSource(source) {
|
|
|
2917
3475
|
continue;
|
|
2918
3476
|
}
|
|
2919
3477
|
const word = literal(token);
|
|
3478
|
+
// Function headers allow linebreaks before the opening brace. Braces
|
|
3479
|
+
// elsewhere remain ordinary arguments unless at a command boundary.
|
|
3480
|
+
const functionHeader = word === "{" && /[A-Za-z_][A-Za-z0-9_]*\s*\(\s*\)(?:\s|\\\r?\n)*$/.exec(source.slice(0, token.offset));
|
|
3481
|
+
const functionBrace = Boolean(functionHeader);
|
|
3482
|
+
if ((commandStart || functionBrace) && word === "{") {
|
|
3483
|
+
if (!functionBrace) breaks.add(token.offset);
|
|
3484
|
+
else breaks.add(functionHeader.index);
|
|
3485
|
+
if (tokens[index + 1]) breaks.add(tokens[index + 1].offset);
|
|
3486
|
+
commandStart = true;
|
|
3487
|
+
continue;
|
|
3488
|
+
}
|
|
3489
|
+
if (commandStart && word === "}") {
|
|
3490
|
+
breaks.add(token.offset);
|
|
3491
|
+
if (tokens[index + 1]?.value === ";" && tokens[index + 2] &&
|
|
3492
|
+
tokens[index + 2].value !== ";") breaks.add(tokens[index + 2].offset);
|
|
3493
|
+
commandStart = false;
|
|
3494
|
+
continue;
|
|
3495
|
+
}
|
|
2920
3496
|
if (!commandStart) continue;
|
|
2921
3497
|
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word ?? "")) continue;
|
|
2922
3498
|
// Like mksh's KEYWORD lexer mode, these words are special only where a
|
|
@@ -2925,34 +3501,48 @@ function compoundLogicalSource(source) {
|
|
|
2925
3501
|
breaks.add(token.offset);
|
|
2926
3502
|
if (["elif", "else", "fi", "done", "esac"].includes(word))
|
|
2927
3503
|
breaks.add(token.offset);
|
|
3504
|
+
if (["fi", "done", "esac"].includes(word) && tokens[index + 1]?.value === ";" &&
|
|
3505
|
+
tokens[index + 2] && tokens[index + 2].value !== ";")
|
|
3506
|
+
breaks.add(tokens[index + 2].offset);
|
|
2928
3507
|
if (["then", "do", "else"].includes(word) && tokens[index + 1])
|
|
2929
3508
|
breaks.add(tokens[index + 1].offset);
|
|
2930
3509
|
commandStart = ["if", "while", "until", "elif", "then", "do", "else"].includes(word);
|
|
2931
3510
|
}
|
|
2932
|
-
let logical =
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
3511
|
+
let logical = "", line = firstLine, physicalIndex = 0;
|
|
3512
|
+
const lineNumbers = [line];
|
|
3513
|
+
for (let i = 0; i < source.length; i++) {
|
|
3514
|
+
if (breaks.has(i) && logical && !logical.endsWith("\n")) { logical += "\n"; lineNumbers.push(line); }
|
|
3515
|
+
if (continuations.has(i)) { i += continuations.get(i) - 1; line = sourceLines?.[++physicalIndex] ?? line + 1; continue; }
|
|
3516
|
+
logical += source[i];
|
|
3517
|
+
if (source[i] === "\n") { line = sourceLines?.[++physicalIndex] ?? line + 1; lineNumbers.push(line); }
|
|
3518
|
+
}
|
|
3519
|
+
return {logical, lineNumbers};
|
|
2937
3520
|
}
|
|
2938
3521
|
|
|
2939
|
-
// A leaf command
|
|
2940
|
-
//
|
|
2941
|
-
//
|
|
2942
|
-
//
|
|
2943
|
-
|
|
2944
|
-
function growHeredocLine(lines, index) {
|
|
3522
|
+
// A leaf command inside a compound body may continue across physical lines
|
|
3523
|
+
// inside quotes, substitutions, or a here-document. Keep those lines together
|
|
3524
|
+
// until mksh-style lexical tokenization says the command is complete, so
|
|
3525
|
+
// keywords inside a multiline word are never mistaken for compound syntax.
|
|
3526
|
+
function growLogicalCommand(lines, index) {
|
|
2945
3527
|
const line = lines[index];
|
|
2946
|
-
if (!line.includes("<<")) return { source: line, next: index + 1 };
|
|
2947
3528
|
let end = index;
|
|
2948
3529
|
let candidate = line;
|
|
2949
3530
|
while (true) {
|
|
2950
3531
|
try {
|
|
2951
|
-
tokenize(candidate, { strict: true });
|
|
3532
|
+
const tokens = tokenize(candidate, { strict: true });
|
|
3533
|
+
const last = tokens.at(-1);
|
|
3534
|
+
if (last?.type === "op" && ["|", "&&", "||"].includes(last.value) && end + 1 < lines.length) {
|
|
3535
|
+
// Leave a following compound node for parseSequence to attach, but
|
|
3536
|
+
// grow ordinary pipelines and AND/OR continuations across newlines.
|
|
3537
|
+
if (!/^\s*(?:\{|if\b|for\b|while\b|until\b|case\b)/.test(lines[end + 1])) {
|
|
3538
|
+
candidate += `\n${lines[++end]}`;
|
|
3539
|
+
continue;
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
2952
3542
|
return { source: candidate, next: end + 1 };
|
|
2953
3543
|
} catch (error) {
|
|
2954
3544
|
if (!(error instanceof ShellSyntaxError) ||
|
|
2955
|
-
|
|
3545
|
+
!/^(?:unterminated (?:here-document|single quote|double quote|command substitution|parameter expansion|substitution))/.test(error.message))
|
|
2956
3546
|
throw error;
|
|
2957
3547
|
end++;
|
|
2958
3548
|
if (end >= lines.length) throw error;
|
|
@@ -2961,8 +3551,301 @@ function growHeredocLine(lines, index) {
|
|
|
2961
3551
|
}
|
|
2962
3552
|
}
|
|
2963
3553
|
|
|
2964
|
-
|
|
2965
|
-
|
|
3554
|
+
// Heredoc bodies follow the operator's source position even when it is the
|
|
3555
|
+
// final token. Remove only the connector, never the rest of the source.
|
|
3556
|
+
function removeSourceToken(source, token) {
|
|
3557
|
+
return source.slice(0, token.offset) + source.slice(token.offset + token.value.length);
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
function compoundSource(node) {
|
|
3561
|
+
if (node.type === "subshell") return `(${node.source})`;
|
|
3562
|
+
if (node.type === "compoundPostfix")
|
|
3563
|
+
return `${compoundSource(node.compound)} ${node.operator} ${node.right ? compoundSource(node.right) : node.command}`;
|
|
3564
|
+
if (typeof node.source !== "string") throw new ShellSyntaxError(`missing source for ${node.type}`);
|
|
3565
|
+
// A following serialized connector must not become part of a heredoc's
|
|
3566
|
+
// delimiter line. Braces preserve the command's scope and give it a boundary.
|
|
3567
|
+
if (node.type === "command" && tokenize(node.source).some(token => token.heredoc))
|
|
3568
|
+
return `{\n${node.source}\n}`;
|
|
3569
|
+
return node.source;
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
function makeCompoundPostfix(compound, suffix, source, nextLine) {
|
|
3573
|
+
const suffixCommand = growLogicalCommand([suffix ?? "", ...source.slice(nextLine)], 0);
|
|
3574
|
+
suffix = suffixCommand.source;
|
|
3575
|
+
nextLine += suffixCommand.next - 1;
|
|
3576
|
+
const tokens = tokenize(suffix ?? "");
|
|
3577
|
+
const connector = tokens.find(t => t.type === "op" && [";","&&","||","|"].includes(t.value));
|
|
3578
|
+
const redirectionText = (suffix ?? "").slice(0,connector?.offset ?? undefined).trim();
|
|
3579
|
+
if (redirectionText && !redirectionText.startsWith("#")) {
|
|
3580
|
+
if (redirectionText === "&") throw new ShellSyntaxError("&: only a single external command is supported");
|
|
3581
|
+
const parsed = parse(`: ${redirectionText}`)[0]?.pipeline[0];
|
|
3582
|
+
if (!parsed || parsed.words.length !== 1) throw new ShellSyntaxError(`unexpected compound suffix: ${redirectionText} after ${compound.source.slice(0,120)}`);
|
|
3583
|
+
compound = {type:"redirected", compound, redirects:parsed.redirects, source:compoundSource(compound) + " " + redirectionText};
|
|
3584
|
+
suffix = connector ? suffix.slice(connector.offset) : "";
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
const match = /^\s*(\|\||&&|\||;)\s*(.*)$/s.exec(suffix ?? "");
|
|
3588
|
+
if (!match) return { node: compound, nextLine };
|
|
3589
|
+
let command = match[2].trim();
|
|
3590
|
+
let commandLine = null;
|
|
3591
|
+
if (match[1] === ";" && (!command || command.startsWith("#"))) return { node: compound, nextLine };
|
|
3592
|
+
while (!command && nextLine < source.length) {
|
|
3593
|
+
commandLine = nextLine;
|
|
3594
|
+
command = source[nextLine++].trim();
|
|
3595
|
+
if (!command || command.startsWith("#")) command = "";
|
|
3596
|
+
}
|
|
3597
|
+
if (!command)
|
|
3598
|
+
throw new ShellSyntaxError(`compound command requires a command after ${match[1]}`);
|
|
3599
|
+
if (commandLine !== null && /^(?:\{|if\b|case\b|for\b|while\b|until\b|\()/.test(command))
|
|
3600
|
+
return {node:{type:"compoundPostfix",compound,operator:match[1],rightPending:true},nextLine:commandLine};
|
|
3601
|
+
// A postfix command can contain multiline quotes, substitutions or heredocs.
|
|
3602
|
+
// Grow it just like an ordinary command before resuming the outer parser.
|
|
3603
|
+
const remaining = [command, ...source.slice(nextLine)];
|
|
3604
|
+
const grown = growLogicalCommand(remaining, 0);
|
|
3605
|
+
command = grown.source;
|
|
3606
|
+
nextLine += grown.next - 1;
|
|
3607
|
+
const trailing = tokenize(command).at(-1);
|
|
3608
|
+
if (trailing?.type === "op" && ["&&", "||", "|"].includes(trailing.value)) {
|
|
3609
|
+
const middle = {type:"command",source:removeSourceToken(command,trailing)};
|
|
3610
|
+
// Pipes bind more tightly; AND/OR lists associate left-to-right.
|
|
3611
|
+
if (trailing.value !== "|") return {
|
|
3612
|
+
node:{type:"compoundPostfix",compound:{type:"compoundPostfix",compound,operator:match[1],right:middle},operator:trailing.value,rightPending:true},
|
|
3613
|
+
nextLine,
|
|
3614
|
+
};
|
|
3615
|
+
return {
|
|
3616
|
+
node: {type:"compoundPostfix", compound, operator:match[1], right:{
|
|
3617
|
+
type:"compoundPostfix", compound:middle,
|
|
3618
|
+
operator:trailing.value, rightPending:true,
|
|
3619
|
+
}},
|
|
3620
|
+
nextLine,
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
return {
|
|
3624
|
+
node: {
|
|
3625
|
+
type: "compoundPostfix",
|
|
3626
|
+
compound,
|
|
3627
|
+
operator: match[1],
|
|
3628
|
+
command,
|
|
3629
|
+
},
|
|
3630
|
+
nextLine,
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
function findShellKeyword(source, keyword) {
|
|
3635
|
+
if (source.trimStart().startsWith("#")) return -1;
|
|
3636
|
+
let quote = null;
|
|
3637
|
+
for (let i = 0; i < source.length;) {
|
|
3638
|
+
const ch = source[i];
|
|
3639
|
+
if (quote === "'") {
|
|
3640
|
+
if (ch === "'") quote = null;
|
|
3641
|
+
i++;
|
|
3642
|
+
continue;
|
|
3643
|
+
}
|
|
3644
|
+
if (quote === '"') {
|
|
3645
|
+
if (ch === "\\" && /[\\"$`\n]/.test(source[i + 1] ?? "")) { i += 2; continue; }
|
|
3646
|
+
if (ch === '"') quote = null;
|
|
3647
|
+
i++;
|
|
3648
|
+
continue;
|
|
3649
|
+
}
|
|
3650
|
+
if (ch === "\\") { i += 2; continue; }
|
|
3651
|
+
if (ch === "'" || ch === '"') { quote = ch; i++; continue; }
|
|
3652
|
+
if (ch === "`" || (ch === "$" && source[i + 1] === "(")) {
|
|
3653
|
+
try { i = substitutionEnd(source, i); }
|
|
3654
|
+
catch { return -1; } // This physical line may continue the substitution.
|
|
3655
|
+
continue;
|
|
3656
|
+
}
|
|
3657
|
+
if (ch === "$" && source[i + 1] === "{") {
|
|
3658
|
+
try { i = parameterEnd(source, i); }
|
|
3659
|
+
catch { return -1; }
|
|
3660
|
+
continue;
|
|
3661
|
+
}
|
|
3662
|
+
if (/[A-Za-z_]/.test(ch)) {
|
|
3663
|
+
const start = i++;
|
|
3664
|
+
while (/[A-Za-z0-9_]/.test(source[i] ?? "")) i++;
|
|
3665
|
+
if (source.slice(start, i) === keyword) {
|
|
3666
|
+
let previous = start - 1;
|
|
3667
|
+
while (previous >= 0 && /\s/.test(source[previous])) previous--;
|
|
3668
|
+
if (keyword !== "esac" || previous < 0 || source[previous] === ";" || source[previous] === "\n")
|
|
3669
|
+
return start;
|
|
3670
|
+
}
|
|
3671
|
+
continue;
|
|
3672
|
+
}
|
|
3673
|
+
i++;
|
|
3674
|
+
}
|
|
3675
|
+
return -1;
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
function splitCasePatterns(source) {
|
|
3679
|
+
const patterns = [];
|
|
3680
|
+
let start = 0;
|
|
3681
|
+
let quote = null;
|
|
3682
|
+
for (let i = 0; i < source.length; i++) {
|
|
3683
|
+
const ch = source[i];
|
|
3684
|
+
if (ch === "\\") { i++; continue; }
|
|
3685
|
+
if (quote) {
|
|
3686
|
+
if (ch === quote) quote = null;
|
|
3687
|
+
continue;
|
|
3688
|
+
}
|
|
3689
|
+
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
3690
|
+
if (ch === "$" && (source[i + 1] === "(" || source[i + 1] === "{")) {
|
|
3691
|
+
i = (source[i + 1] === "(" ? substitutionEnd(source, i) : parameterEnd(source, i)) - 1;
|
|
3692
|
+
continue;
|
|
3693
|
+
}
|
|
3694
|
+
if (ch === "`") { i = substitutionEnd(source, i) - 1; continue; }
|
|
3695
|
+
if (ch === "|") { patterns.push(source.slice(start, i).trim()); start = i + 1; }
|
|
3696
|
+
}
|
|
3697
|
+
patterns.push(source.slice(start).trim());
|
|
3698
|
+
return patterns;
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
// Scan case clauses in the same lexical spirit as mksh's CASEPAT/KEYWORD
|
|
3702
|
+
// modes. In particular, clause boundaries are shell tokens, not line
|
|
3703
|
+
// boundaries: Autoconf commonly writes an entire clause on one line.
|
|
3704
|
+
function shellLiteral(token) {
|
|
3705
|
+
return token?.type === "word" && token.fragments.length === 1 && token.fragments[0].quote === "none" ? token.fragments[0].text : null;
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
function caseKeywords(tokens) {
|
|
3709
|
+
const words = new Map();
|
|
3710
|
+
let start = true;
|
|
3711
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
3712
|
+
const token = tokens[i];
|
|
3713
|
+
let word = shellLiteral(token);
|
|
3714
|
+
if (token.type === "op") {
|
|
3715
|
+
if ([";", "|", "&&", "||"].includes(token.value)) start = true;
|
|
3716
|
+
continue;
|
|
3717
|
+
}
|
|
3718
|
+
// Parentheses are retained in word tokens by this tokenizer. At command
|
|
3719
|
+
// start they can introduce subshells without whitespace before `case`.
|
|
3720
|
+
const openings = start ? /^\(+/.exec(word ?? "")?.[0].length ?? 0 : 0;
|
|
3721
|
+
if (openings) {
|
|
3722
|
+
word = word.slice(openings);
|
|
3723
|
+
if (!word) continue;
|
|
3724
|
+
}
|
|
3725
|
+
if (start && word === "case" && shellLiteral(tokens[i + 2]) === "in") words.set(token.offset + openings, "case");
|
|
3726
|
+
if (start && /^esac\)*$/.test(word ?? "")) words.set(token.offset + openings, "esac");
|
|
3727
|
+
start = ["then", "do", "else", "{"].includes(word) || Boolean(word?.endsWith(")"));
|
|
3728
|
+
}
|
|
3729
|
+
return words;
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
function parseCaseArms(source, sourceLines) {
|
|
3733
|
+
const arms = [];
|
|
3734
|
+
let i = 0;
|
|
3735
|
+
const skipSpace = () => {
|
|
3736
|
+
while (/\s/.test(source[i] ?? "")) i++;
|
|
3737
|
+
};
|
|
3738
|
+
while (i < source.length) {
|
|
3739
|
+
skipSpace();
|
|
3740
|
+
if (i >= source.length) break;
|
|
3741
|
+
if (source[i] === "#") {
|
|
3742
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
3743
|
+
continue;
|
|
3744
|
+
}
|
|
3745
|
+
if (source[i] === "(") { i++; skipSpace(); }
|
|
3746
|
+
const patternStart = i;
|
|
3747
|
+
let quote = null;
|
|
3748
|
+
let extensionDepth = 0;
|
|
3749
|
+
for (; i < source.length; i++) {
|
|
3750
|
+
const ch = source[i];
|
|
3751
|
+
if (ch === "\\") { i++; continue; }
|
|
3752
|
+
if (quote) {
|
|
3753
|
+
if (ch === quote) quote = null;
|
|
3754
|
+
continue;
|
|
3755
|
+
}
|
|
3756
|
+
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
3757
|
+
if (ch === "$" && (source[i + 1] === "(" || source[i + 1] === "{")) {
|
|
3758
|
+
i = (source[i + 1] === "(" ? substitutionEnd(source, i) : parameterEnd(source, i)) - 1;
|
|
3759
|
+
continue;
|
|
3760
|
+
}
|
|
3761
|
+
if (ch === "`") { i = substitutionEnd(source, i) - 1; continue; }
|
|
3762
|
+
if (ch === "(" && /[?*+@!]/.test(source[i - 1] ?? "")) { extensionDepth++; continue; }
|
|
3763
|
+
if (ch === ")" && extensionDepth) { extensionDepth--; continue; }
|
|
3764
|
+
if (ch === ")") break;
|
|
3765
|
+
}
|
|
3766
|
+
if (i >= source.length)
|
|
3767
|
+
throw new ShellSyntaxError(`case pattern requires ): ${source.slice(patternStart, Math.min(source.length, patternStart + 120))}`);
|
|
3768
|
+
const patterns = splitCasePatterns(source.slice(patternStart, i));
|
|
3769
|
+
if (!patterns.length || patterns.some((pattern) => !pattern))
|
|
3770
|
+
throw new ShellSyntaxError("case requires a pattern");
|
|
3771
|
+
i++;
|
|
3772
|
+
|
|
3773
|
+
const bodyStart = i;
|
|
3774
|
+
let nestedCases = 0;
|
|
3775
|
+
let terminator = null;
|
|
3776
|
+
const bodyTokens = tokenize(source.slice(bodyStart));
|
|
3777
|
+
const keywords = caseKeywords(bodyTokens);
|
|
3778
|
+
i = source.length;
|
|
3779
|
+
for (let t = 0; t < bodyTokens.length; t++) {
|
|
3780
|
+
const token = bodyTokens[t];
|
|
3781
|
+
const word = keywords.get(token.offset);
|
|
3782
|
+
if (word === "case") nestedCases++;
|
|
3783
|
+
else if (word === "esac" && nestedCases) nestedCases--;
|
|
3784
|
+
if (!nestedCases && token.type === "op" && token.value === ";") {
|
|
3785
|
+
const offset = bodyStart + token.offset;
|
|
3786
|
+
if ([";", "&", "|"].includes(source[offset + 1]) && source[offset] === ";") {
|
|
3787
|
+
terminator = source.slice(offset, offset + 2);
|
|
3788
|
+
i = offset;
|
|
3789
|
+
break;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
// POSIX allows the final clause to omit `;;` immediately before esac.
|
|
3794
|
+
if (!terminator) {
|
|
3795
|
+
if (source.slice(bodyStart, i).trim() || arms.length) terminator = ";;";
|
|
3796
|
+
else throw new ShellSyntaxError("unterminated case clause");
|
|
3797
|
+
}
|
|
3798
|
+
const bodySource = source.slice(bodyStart, i);
|
|
3799
|
+
let body;
|
|
3800
|
+
try {
|
|
3801
|
+
const first = source.slice(0,bodyStart).split("\n").length - 1;
|
|
3802
|
+
body = parseCompoundScript(bodySource, sourceLines?.[first] ?? 1, sourceLines?.slice(first));
|
|
3803
|
+
} catch (error) {
|
|
3804
|
+
if (error instanceof ShellSyntaxError)
|
|
3805
|
+
error.message += ` in case arm ${patterns.join("|")}`;
|
|
3806
|
+
throw error;
|
|
3807
|
+
}
|
|
3808
|
+
arms.push({ patterns, body, terminator });
|
|
3809
|
+
if (i < source.length) i += 2;
|
|
3810
|
+
}
|
|
3811
|
+
return arms;
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
function readCompoundHeader(lines, start, keyword) {
|
|
3815
|
+
let source = "";
|
|
3816
|
+
for (let end = start; end < lines.length; end++) {
|
|
3817
|
+
source += (end === start ? "" : "\n") + lines[end];
|
|
3818
|
+
let tokens;
|
|
3819
|
+
try { tokens = tokenize(source, {strict:true}); } catch (error) {
|
|
3820
|
+
if (error.message.startsWith("unterminated")) continue;
|
|
3821
|
+
throw error;
|
|
3822
|
+
}
|
|
3823
|
+
const stack = [];
|
|
3824
|
+
let commandStart = true;
|
|
3825
|
+
for (let index = 1; index < tokens.length; index++) {
|
|
3826
|
+
const token = tokens[index];
|
|
3827
|
+
if (token.type === "op") {
|
|
3828
|
+
if ([";", "&&", "||", "|"].includes(token.value)) commandStart = true;
|
|
3829
|
+
continue;
|
|
3830
|
+
}
|
|
3831
|
+
const word = token.fragments.length === 1 && token.fragments[0].quote === "none" ? token.fragments[0].text : null;
|
|
3832
|
+
if (commandStart && word === keyword && !stack.length) {
|
|
3833
|
+
return {condition:source.slice(tokens[0].offset + tokens[0].fragments[0].text.length, token.offset).trim(),
|
|
3834
|
+
tail:source.slice(token.offset + keyword.length).trim(), end};
|
|
3835
|
+
}
|
|
3836
|
+
if (commandStart && ["{", "if", "case", "while", "until", "for"].includes(word)) {
|
|
3837
|
+
stack.push(word === "{" ? "}" : word === "if" ? "fi" : word === "case" ? "esac" : "done");
|
|
3838
|
+
} else if (word === stack.at(-1)) stack.pop();
|
|
3839
|
+
if (commandStart && /^[A-Za-z_][A-Za-z0-9_]*=/.test(word ?? "")) continue;
|
|
3840
|
+
commandStart = ["{", "if", "while", "until", "then", "do", "else", "elif"].includes(word);
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
throw new ShellSyntaxError(`unterminated condition: expected ${keyword}`);
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
export function parseCompoundScript(source, firstLine = 1, sourceLines = null) {
|
|
3847
|
+
const {logical, lineNumbers} = compoundLogicalSource(source, firstLine, sourceLines);
|
|
3848
|
+
const lines = logical.split(/\r?\n/);
|
|
2966
3849
|
const parseSequence = (start, stops = []) => {
|
|
2967
3850
|
const nodes = [];
|
|
2968
3851
|
let i = start;
|
|
@@ -2972,122 +3855,177 @@ function parseCompoundScript(source) {
|
|
|
2972
3855
|
if (stops.some((stop) => stop.test(trimmed))) break;
|
|
2973
3856
|
if (!trimmed || trimmed.startsWith("#")) { i++; continue; }
|
|
2974
3857
|
|
|
2975
|
-
const functionMatch = /^([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\)\s
|
|
3858
|
+
const functionMatch = /^([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\)\s*(\{)?\s*(?:#.*)?$/.exec(trimmed);
|
|
2976
3859
|
if (functionMatch) {
|
|
2977
|
-
|
|
3860
|
+
if (!functionMatch[2]) {
|
|
3861
|
+
do { i++; } while (i < lines.length && (!lines[i].trim() || lines[i].trim().startsWith("#")));
|
|
3862
|
+
if (lines[i]?.trim() !== "{") throw new ShellSyntaxError(`unterminated function ${functionMatch[1]}: expected {`);
|
|
3863
|
+
}
|
|
3864
|
+
const body = parseSequence(i + 1, [/^}(?:\s|;|$)/]);
|
|
2978
3865
|
if (body.index >= lines.length) throw new ShellSyntaxError(`unterminated function ${functionMatch[1]}`);
|
|
2979
3866
|
nodes.push({ type: "function", name: functionMatch[1], body: body.nodes });
|
|
2980
3867
|
i = body.index + 1;
|
|
2981
3868
|
continue;
|
|
2982
3869
|
}
|
|
2983
3870
|
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
i
|
|
3871
|
+
if (trimmed === "{") {
|
|
3872
|
+
const body = parseSequence(i + 1, [/^}(?:\s|;|$)/]);
|
|
3873
|
+
if (body.index >= lines.length) throw new ShellSyntaxError(`unterminated brace group near ${lines.slice(i, i + 5).join("\n")}`);
|
|
3874
|
+
const compound = { type: "group", body: body.nodes, source: [...lines.slice(i, body.index), "}"].join("\n") };
|
|
3875
|
+
const postfix = makeCompoundPostfix(compound, lines[body.index].trim().slice(1), lines, body.index + 1);
|
|
3876
|
+
nodes.push(postfix.node);
|
|
3877
|
+
i = postfix.nextLine;
|
|
2988
3878
|
continue;
|
|
2989
3879
|
}
|
|
2990
3880
|
|
|
3881
|
+
if (trimmed.startsWith("(")) {
|
|
3882
|
+
let end=i, text=line, group=leadingSubshell(text);
|
|
3883
|
+
while (!group && ++end < lines.length) { text += `\n${lines[end]}`; group=leadingSubshell(text); }
|
|
3884
|
+
if (!group) throw new ShellSyntaxError("unterminated subshell");
|
|
3885
|
+
// Preserve the existing rewriteSubshells -> external Bun path for &.
|
|
3886
|
+
if (group.suffix.trim() === "&") {
|
|
3887
|
+
nodes.push({type:"command",source:text,lineNumber:lineNumbers[i]}); i=end+1; continue;
|
|
3888
|
+
}
|
|
3889
|
+
const compound={type:"subshell",source:group.body};
|
|
3890
|
+
const postfix=makeCompoundPostfix(compound,group.suffix,lines,end+1);
|
|
3891
|
+
nodes.push(postfix.node); i=postfix.nextLine; continue;
|
|
3892
|
+
}
|
|
3893
|
+
|
|
2991
3894
|
if (/^if(?:\s|$)/.test(trimmed)) {
|
|
3895
|
+
const compoundStart = i;
|
|
2992
3896
|
const branches = [];
|
|
2993
|
-
let header =
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
let condition = header.replace(/^if\s*/, "").replace(/;\s*then\s*$/, "");
|
|
2998
|
-
i++;
|
|
3897
|
+
let header = readCompoundHeader(lines, i, "then");
|
|
3898
|
+
let condition = header.condition;
|
|
3899
|
+
let thenCommand = header.tail;
|
|
3900
|
+
i = header.end + 1;
|
|
2999
3901
|
while (true) {
|
|
3000
|
-
const body = parseSequence(i, [/^elif(?:\s|$)/, /^else
|
|
3001
|
-
branches.push({ condition, body: body.nodes });
|
|
3902
|
+
const body = parseSequence(i, [/^elif(?:\s|$)/, /^else(?:\s*;\s*|\s+|$)(?:#.*)?$/, /^fi\b/]);
|
|
3903
|
+
branches.push({ condition, body: body.nodes, thenCommand });
|
|
3002
3904
|
i = body.index;
|
|
3003
3905
|
const stop = lines[i]?.trim() ?? "";
|
|
3004
3906
|
if (/^elif(?:\s|$)/.test(stop)) {
|
|
3005
|
-
header =
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
condition = header.replace(/^elif\s*/, "").replace(/;\s*then\s*$/, "");
|
|
3010
|
-
i++;
|
|
3907
|
+
header = readCompoundHeader(lines, i, "then");
|
|
3908
|
+
condition = header.condition;
|
|
3909
|
+
thenCommand = header.tail;
|
|
3910
|
+
i = header.end + 1;
|
|
3011
3911
|
continue;
|
|
3012
3912
|
}
|
|
3013
3913
|
let otherwise = [];
|
|
3014
|
-
if (/^else
|
|
3015
|
-
const body = parseSequence(i + 1, [/^fi\
|
|
3914
|
+
if (/^else(?:\s*;\s*|\s+|$)(?:#.*)?$/.test(stop)) {
|
|
3915
|
+
const body = parseSequence(i + 1, [/^fi\b/]);
|
|
3016
3916
|
otherwise = body.nodes;
|
|
3017
3917
|
i = body.index;
|
|
3018
3918
|
}
|
|
3019
|
-
if (!/^fi\
|
|
3919
|
+
if (!/^fi\b/.test(lines[i]?.trim() ?? ""))
|
|
3020
3920
|
throw new ShellSyntaxError("unterminated if");
|
|
3021
|
-
|
|
3022
|
-
i
|
|
3921
|
+
const compound = { type: "if", branches, otherwise };
|
|
3922
|
+
const close = /^fi\b(.*)$/.exec(lines[i].trim());
|
|
3923
|
+
const result = makeCompoundPostfix(
|
|
3924
|
+
{ ...compound, source: [...lines.slice(compoundStart, i), "fi"].join("\n") },
|
|
3925
|
+
close?.[1] ?? "",
|
|
3926
|
+
lines,
|
|
3927
|
+
i + 1,
|
|
3928
|
+
);
|
|
3929
|
+
nodes.push(result.node);
|
|
3930
|
+
i = result.nextLine;
|
|
3023
3931
|
break;
|
|
3024
3932
|
}
|
|
3025
3933
|
continue;
|
|
3026
3934
|
}
|
|
3027
3935
|
if (/^(?:while|until)(?:\s|$)/.test(trimmed)) {
|
|
3936
|
+
const compoundStart = i;
|
|
3028
3937
|
const kind = trimmed.startsWith("until") ? "until" : "while";
|
|
3029
3938
|
let header = trimmed;
|
|
3030
|
-
while (
|
|
3939
|
+
while (!/(?:;\s*|\s+)do\s*$/.test(header) && ++i < lines.length)
|
|
3031
3940
|
header += ` ${lines[i].trim()}`;
|
|
3032
|
-
if (
|
|
3033
|
-
const condition = header.replace(new RegExp(`^${kind}\\s*`), "").replace(
|
|
3034
|
-
const body = parseSequence(i + 1, [/^done
|
|
3035
|
-
const done = /^done(?:\s*<\s*(.+?))?\s
|
|
3941
|
+
if (!/(?:;\s*|\s+)do\s*$/.test(header)) throw new ShellSyntaxError(`${kind} requires do`);
|
|
3942
|
+
const condition = header.replace(new RegExp(`^${kind}\\s*`), "").replace(/(?:;\s*|\s+)do\s*$/, "");
|
|
3943
|
+
const body = parseSequence(i + 1, [/^done\b/]);
|
|
3944
|
+
const done = /^done(?=$|[\s;<|&])(?:\s*<\s*(.+?))?\s*;?\s*((?:\|\||&&|\||;|#).*)?$/.exec(lines[body.index]?.trim() ?? "");
|
|
3036
3945
|
if (!done)
|
|
3037
3946
|
throw new ShellSyntaxError(`unterminated ${kind}`);
|
|
3038
|
-
|
|
3039
|
-
|
|
3947
|
+
const compound = {
|
|
3948
|
+
type: "loop", kind, condition, body: body.nodes, input: done[1] ?? null,
|
|
3949
|
+
source: [...lines.slice(compoundStart, body.index), `done${done[1] ? ` < ${done[1]}` : ""}`].join("\n"),
|
|
3950
|
+
};
|
|
3951
|
+
const result = makeCompoundPostfix(compound, done[2] ?? "", lines, body.index + 1);
|
|
3952
|
+
nodes.push(result.node);
|
|
3953
|
+
i = result.nextLine;
|
|
3040
3954
|
continue;
|
|
3041
3955
|
}
|
|
3042
3956
|
if (/^for(?:\s|$)/.test(trimmed)) {
|
|
3957
|
+
const compoundStart = i;
|
|
3043
3958
|
let header = trimmed;
|
|
3044
|
-
|
|
3959
|
+
let bodyStart = -1;
|
|
3960
|
+
while (i < lines.length) {
|
|
3961
|
+
if (/;\s*do\s*$/.test(header)) {
|
|
3962
|
+
header = header.replace(/;\s*do\s*$/, "");
|
|
3963
|
+
bodyStart = i + 1;
|
|
3964
|
+
break;
|
|
3965
|
+
}
|
|
3966
|
+
if (lines[i + 1]?.trim() === "do") {
|
|
3967
|
+
bodyStart = i + 2;
|
|
3968
|
+
i++;
|
|
3969
|
+
break;
|
|
3970
|
+
}
|
|
3971
|
+
if (++i >= lines.length) break;
|
|
3045
3972
|
header += ` ${lines[i].trim()}`;
|
|
3046
|
-
|
|
3973
|
+
}
|
|
3974
|
+
const match = /^for\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s+in(?:\s+(.*?))?)?\s*;?$/.exec(header);
|
|
3047
3975
|
if (!match) throw new ShellSyntaxError("invalid for command");
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3976
|
+
if (bodyStart < 0) throw new ShellSyntaxError("for requires do");
|
|
3977
|
+
const body = parseSequence(bodyStart, [/^done\b/]);
|
|
3978
|
+
if (!/^done\b/.test(lines[body.index]?.trim() ?? ""))
|
|
3979
|
+
throw new ShellSyntaxError(`unterminated for: ${lines[compoundStart]?.trim()} (stopped at ${lines[body.index]?.trim()})`);
|
|
3980
|
+
const compound = {
|
|
3981
|
+
type: "for", name: match[1], words: match[2] ?? null, body: body.nodes,
|
|
3982
|
+
source: [...lines.slice(compoundStart, body.index), "done"].join("\n"),
|
|
3983
|
+
};
|
|
3984
|
+
const close = /^done\b(.*)$/.exec(lines[body.index].trim());
|
|
3985
|
+
const result = makeCompoundPostfix(compound, close?.[1] ?? "", lines, body.index + 1);
|
|
3986
|
+
nodes.push(result.node);
|
|
3987
|
+
i = result.nextLine;
|
|
3053
3988
|
continue;
|
|
3054
3989
|
}
|
|
3055
3990
|
if (/^case(?:\s|$)/.test(trimmed)) {
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
const
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
if (
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
i++;
|
|
3069
|
-
while (i < lines.length && !/^esac\s*;?$/.test(lines[i].trim())) {
|
|
3070
|
-
const current = lines[i];
|
|
3071
|
-
const terminator = current.indexOf(";;");
|
|
3072
|
-
if (terminator >= 0) {
|
|
3073
|
-
if (current.slice(0, terminator).trim()) bodyLines.push(current.slice(0, terminator));
|
|
3074
|
-
i++;
|
|
3075
|
-
break;
|
|
3076
|
-
}
|
|
3077
|
-
bodyLines.push(current);
|
|
3078
|
-
i++;
|
|
3991
|
+
const remaining = lines.slice(i).join("\n");
|
|
3992
|
+
const headerTokens=tokenize(remaining).slice(0,3);
|
|
3993
|
+
if (headerTokens[2]?.fragments?.map(f=>f.text).join("") !== "in")
|
|
3994
|
+
throw new ShellSyntaxError(`case requires in: ${remaining.slice(0,180)}`);
|
|
3995
|
+
const caseWord=remaining.slice(headerTokens[1].offset,headerTokens[2].offset).trim();
|
|
3996
|
+
const start = headerTokens[2].offset + 2;
|
|
3997
|
+
let depth = 0, end = -1;
|
|
3998
|
+
for (const [offset, word] of caseKeywords(tokenize(remaining.slice(start)))) {
|
|
3999
|
+
if (word === "case") depth++;
|
|
4000
|
+
if (word === "esac") {
|
|
4001
|
+
if (!depth) { end = start + offset; break; }
|
|
4002
|
+
depth--;
|
|
3079
4003
|
}
|
|
3080
|
-
arms.push({ patterns: arm[1].split("|").map((pattern) => pattern.trim()), body: parseCompoundScript(bodyLines.join("\n")) });
|
|
3081
4004
|
}
|
|
3082
|
-
if (
|
|
3083
|
-
|
|
3084
|
-
i
|
|
4005
|
+
if (end < 0) throw new ShellSyntaxError(`unterminated case: ${trimmed}`);
|
|
4006
|
+
const compound = {type:"case", word:caseWord, arms:parseCaseArms(remaining.slice(start,end),lineNumbers.slice(i + remaining.slice(0,start).split("\n").length - 1)), source:remaining.slice(0,end+4)};
|
|
4007
|
+
const nextLine = i + remaining.slice(0,end+4).split("\n").length;
|
|
4008
|
+
const suffix = remaining.slice(end+4).split("\n")[0];
|
|
4009
|
+
const postfix = makeCompoundPostfix(compound,suffix,lines,nextLine);
|
|
4010
|
+
nodes.push(postfix.node);
|
|
4011
|
+
i = postfix.nextLine;
|
|
3085
4012
|
continue;
|
|
3086
4013
|
}
|
|
3087
|
-
const grown =
|
|
3088
|
-
nodes.push({ type: "command", source: grown.source });
|
|
4014
|
+
const grown = growLogicalCommand(lines, i);
|
|
4015
|
+
nodes.push({ type: "command", source: grown.source, lineNumber:lineNumbers[i], sourceLines:lineNumbers.slice(i,grown.next) });
|
|
3089
4016
|
i = grown.next;
|
|
3090
4017
|
}
|
|
4018
|
+
for (let n = nodes.length - 2; n >= 0; n--) {
|
|
4019
|
+
let pending = nodes[n];
|
|
4020
|
+
while (pending.right) pending = pending.right;
|
|
4021
|
+
if (pending.rightPending) {
|
|
4022
|
+
pending.right = nodes[n+1]; delete pending.rightPending; nodes.splice(n+1,1); continue;
|
|
4023
|
+
}
|
|
4024
|
+
if (nodes[n].type !== "command") continue;
|
|
4025
|
+
const last = tokenize(nodes[n].source).at(-1);
|
|
4026
|
+
if (last?.type !== "op" || !["&&", "||", "|"].includes(last.value)) continue;
|
|
4027
|
+
nodes.splice(n,2,{type:"compoundPostfix",compound:{...nodes[n],source:removeSourceToken(nodes[n].source,last)},operator:last.value,right:nodes[n+1]});
|
|
4028
|
+
}
|
|
3091
4029
|
return { nodes, index: i };
|
|
3092
4030
|
};
|
|
3093
4031
|
const parsed = parseSequence(0);
|
|
@@ -3124,39 +4062,119 @@ export function needsMoreInput(source) {
|
|
|
3124
4062
|
return false;
|
|
3125
4063
|
}
|
|
3126
4064
|
|
|
4065
|
+
// Buffered compound pipelines need the same descriptor routing as leaf commands.
|
|
4066
|
+
function routeCommandOutput(execution, bindings) {
|
|
4067
|
+
const output = [], errors = [];
|
|
4068
|
+
for (const [number, data] of [[1, execution.stdout], [2, execution.stderr]]) {
|
|
4069
|
+
if (!data.byteLength) continue;
|
|
4070
|
+
const endpoint = bindings[number] ?? {channel:number};
|
|
4071
|
+
if (endpoint.closed) { execution.status = 1; errors.push(bytes("bunmsh: bad file descriptor\n")); }
|
|
4072
|
+
else if (endpoint.fd !== undefined) writeSync(endpoint.fd, data);
|
|
4073
|
+
else (endpoint.channel === 2 ? errors : output).push(data);
|
|
4074
|
+
}
|
|
4075
|
+
execution.stdout = concatBytes(output);
|
|
4076
|
+
execution.stderr = concatBytes(errors);
|
|
4077
|
+
return execution;
|
|
4078
|
+
}
|
|
4079
|
+
|
|
3127
4080
|
async function executeNodeList(nodes, state, options = {}) {
|
|
3128
4081
|
const capture = options.capture ?? true;
|
|
3129
4082
|
const stdout = [], stderr = [];
|
|
3130
|
-
let execution = result(
|
|
4083
|
+
let execution = result();
|
|
3131
4084
|
const append = (value) => {
|
|
3132
4085
|
if (!capture) return;
|
|
3133
4086
|
if (value.stdout.byteLength) stdout.push(value.stdout);
|
|
3134
4087
|
if (value.stderr.byteLength) stderr.push(value.stderr);
|
|
3135
4088
|
};
|
|
3136
4089
|
for (const node of nodes) {
|
|
4090
|
+
if (state.exitRequested || state.flow) break;
|
|
4091
|
+
append(await runPendingTraps(state,{capture}));
|
|
3137
4092
|
if (state.exitRequested) break;
|
|
4093
|
+
if (node.type === "redirected") {
|
|
4094
|
+
execution = await runCommand({words:parse(":")[0].pipeline[0].words,redirects:node.redirects,body:[node.compound]},state,{captureStdout:capture,captureStderr:capture,input:options.input});
|
|
4095
|
+
append(execution);
|
|
4096
|
+
if (!capture) {
|
|
4097
|
+
if (execution.stdout.byteLength) await writeStream(process.stdout,execution.stdout);
|
|
4098
|
+
if (execution.stderr.byteLength) await writeStream(process.stderr,execution.stderr);
|
|
4099
|
+
}
|
|
4100
|
+
continue;
|
|
4101
|
+
}
|
|
4102
|
+
if (node.type === "group") {
|
|
4103
|
+
execution = await executeNodeList(node.body, state, options);
|
|
4104
|
+
append(execution);
|
|
4105
|
+
continue;
|
|
4106
|
+
}
|
|
4107
|
+
if (node.type === "compoundPostfix") {
|
|
4108
|
+
if (node.operator === "|") {
|
|
4109
|
+
const link = new TransformStream();
|
|
4110
|
+
const leftPromise = runExternal(
|
|
4111
|
+
pipelineCompoundArgv(compoundSource(node.compound)),
|
|
4112
|
+
pipelineBuiltinState(state),
|
|
4113
|
+
options.input ?? null,
|
|
4114
|
+
false,
|
|
4115
|
+
true,
|
|
4116
|
+
link.writable,
|
|
4117
|
+
{ pipelineKillSignal: "SIGTERM" },
|
|
4118
|
+
);
|
|
4119
|
+
const rightState = {
|
|
4120
|
+
...state,
|
|
4121
|
+
env: { ...state.env },
|
|
4122
|
+
aliases: { ...state.aliases },
|
|
4123
|
+
functions: { ...state.functions },
|
|
4124
|
+
readonly: new Set(state.readonly),
|
|
4125
|
+
exported: new Set(state.exported),
|
|
4126
|
+
directoryHistory: [...state.directoryHistory],
|
|
4127
|
+
tabs: [...state.tabs],
|
|
4128
|
+
exitRequested: false,
|
|
4129
|
+
exitStatus: 0,
|
|
4130
|
+
};
|
|
4131
|
+
const right = await runExternal(
|
|
4132
|
+
pipelineCompoundArgv(node.right ? compoundSource(node.right) : node.command),
|
|
4133
|
+
pipelineBuiltinState(rightState),
|
|
4134
|
+
link.readable,
|
|
4135
|
+
true,
|
|
4136
|
+
true,
|
|
4137
|
+
);
|
|
4138
|
+
const left = await leftPromise;
|
|
4139
|
+
execution = routeCommandOutput(result(right.status, right.stdout, concatBytes([left.stderr, right.stderr])), state.fdBindings ?? {});
|
|
4140
|
+
} else {
|
|
4141
|
+
const previousIgnore = state.ignoreErrexit;
|
|
4142
|
+
if (["&&","||"].includes(node.operator)) state.ignoreErrexit++;
|
|
4143
|
+
try { execution = await executeNodeList([node.compound], state, { capture }); }
|
|
4144
|
+
finally { state.ignoreErrexit=previousIgnore; }
|
|
4145
|
+
append(execution);
|
|
4146
|
+
const shouldRun = node.operator === "&&"
|
|
4147
|
+
? execution.status === 0
|
|
4148
|
+
: node.operator === "||"
|
|
4149
|
+
? execution.status !== 0
|
|
4150
|
+
: true;
|
|
4151
|
+
if (shouldRun && !state.flow && !state.exitRequested) {
|
|
4152
|
+
execution = node.right ? await executeNodeList([node.right],state,{capture}) : await execute(node.command, state, { capture });
|
|
4153
|
+
append(execution);
|
|
4154
|
+
}
|
|
4155
|
+
state.lastStatus = execution.status;
|
|
4156
|
+
continue;
|
|
4157
|
+
}
|
|
4158
|
+
state.lastStatus = execution.status;
|
|
4159
|
+
if (capture) append(execution);
|
|
4160
|
+
else {
|
|
4161
|
+
if (execution.stdout.byteLength) stdout.push(execution.stdout);
|
|
4162
|
+
if (execution.stderr.byteLength) stderr.push(execution.stderr);
|
|
4163
|
+
}
|
|
4164
|
+
continue;
|
|
4165
|
+
}
|
|
3138
4166
|
if (node.type === "function") {
|
|
3139
4167
|
state.functions[node.name] = node.body;
|
|
4168
|
+
state.lastStatus = 0; execution = result();
|
|
3140
4169
|
continue;
|
|
3141
4170
|
}
|
|
3142
4171
|
if (node.type === "command") {
|
|
3143
|
-
execution = await execute(node.source, state, { capture, simple: true });
|
|
4172
|
+
execution = await execute(node.source, state, { capture, simple: true, firstLine:node.lineNumber, sourceLines:node.sourceLines });
|
|
3144
4173
|
append(execution);
|
|
3145
4174
|
continue;
|
|
3146
4175
|
}
|
|
3147
4176
|
if (node.type === "subshell") {
|
|
3148
|
-
|
|
3149
|
-
...state,
|
|
3150
|
-
env: { ...state.env },
|
|
3151
|
-
aliases: { ...state.aliases },
|
|
3152
|
-
functions: { ...state.functions },
|
|
3153
|
-
readonly: new Set(state.readonly),
|
|
3154
|
-
directoryHistory: [...state.directoryHistory],
|
|
3155
|
-
tabs: [...state.tabs],
|
|
3156
|
-
exitRequested: false,
|
|
3157
|
-
exitStatus: 0,
|
|
3158
|
-
};
|
|
3159
|
-
execution = await execute(node.source, child, { capture });
|
|
4177
|
+
execution = await execute(`(${node.source})`,state,{capture,input:options.input});
|
|
3160
4178
|
append(execution);
|
|
3161
4179
|
state.lastStatus = execution.status;
|
|
3162
4180
|
continue;
|
|
@@ -3164,6 +4182,7 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3164
4182
|
if (node.type === "loop") {
|
|
3165
4183
|
const previousReadLines = state.readLines;
|
|
3166
4184
|
let ranBody = false;
|
|
4185
|
+
state.loopDepth++;
|
|
3167
4186
|
try {
|
|
3168
4187
|
if (node.input !== null) {
|
|
3169
4188
|
const parsed = parse(`__input ${node.input}`)[0]?.pipeline[0];
|
|
@@ -3174,7 +4193,7 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3174
4193
|
if (state.readLines.at(-1) === "") state.readLines.pop();
|
|
3175
4194
|
}
|
|
3176
4195
|
while (true) {
|
|
3177
|
-
const checked = await execute(node.condition, state, { capture,
|
|
4196
|
+
const checked = await execute(node.condition, state, { capture, errexitIgnored:true });
|
|
3178
4197
|
append(checked);
|
|
3179
4198
|
const proceed = node.kind === "until" ? checked.status !== 0 : checked.status === 0;
|
|
3180
4199
|
if (!proceed || state.exitRequested) {
|
|
@@ -3184,12 +4203,13 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3184
4203
|
execution = await executeNodeList(node.body, state, { capture });
|
|
3185
4204
|
ranBody = true;
|
|
3186
4205
|
append(execution);
|
|
3187
|
-
if (state.exitRequested) break;
|
|
4206
|
+
if (state.exitRequested || consumeLoopFlow(state)) break;
|
|
3188
4207
|
}
|
|
3189
4208
|
} catch (error) {
|
|
3190
4209
|
execution = result(1, "", `bunmsh: ${error.message}\n`);
|
|
3191
4210
|
append(execution);
|
|
3192
4211
|
} finally {
|
|
4212
|
+
state.loopDepth--;
|
|
3193
4213
|
state.readLines = previousReadLines;
|
|
3194
4214
|
}
|
|
3195
4215
|
state.lastStatus = execution.status;
|
|
@@ -3204,13 +4224,14 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3204
4224
|
for (const word of parsed?.words.slice(1) ?? []) values.push(...await expandWord(word, state));
|
|
3205
4225
|
}
|
|
3206
4226
|
execution = result();
|
|
3207
|
-
|
|
4227
|
+
state.loopDepth++;
|
|
4228
|
+
try { for (const value of values) {
|
|
3208
4229
|
if (state.readonly.has(node.name)) { execution = readonlyError(node.name); break; }
|
|
3209
4230
|
state.env[node.name] = value;
|
|
3210
4231
|
execution = await executeNodeList(node.body, state, { capture });
|
|
3211
4232
|
append(execution);
|
|
3212
|
-
if (state.exitRequested) break;
|
|
3213
|
-
}
|
|
4233
|
+
if (state.exitRequested || consumeLoopFlow(state)) break;
|
|
4234
|
+
} } finally { state.loopDepth--; }
|
|
3214
4235
|
state.lastStatus = execution.status;
|
|
3215
4236
|
continue;
|
|
3216
4237
|
}
|
|
@@ -3218,29 +4239,37 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3218
4239
|
const parsed = parse(`__case ${node.word}`)[0]?.pipeline[0];
|
|
3219
4240
|
const value = (await expandWord(parsed.words[1], state, { single: true }))[0] ?? "";
|
|
3220
4241
|
execution = result();
|
|
4242
|
+
let fallThrough = false;
|
|
3221
4243
|
for (const arm of node.arms) {
|
|
3222
|
-
let matches =
|
|
3223
|
-
for (
|
|
3224
|
-
|
|
3225
|
-
pattern = stripExpansionMarkers(await expandText(pattern, state));
|
|
3226
|
-
if (globPatternRegex(pattern, true, true, true).test(value)) { matches = true; break; }
|
|
4244
|
+
let matches = fallThrough;
|
|
4245
|
+
if (!matches) for (const pattern of arm.patterns) {
|
|
4246
|
+
if ((await casePatternRegex(pattern, state)).test(value)) { matches = true; break; }
|
|
3227
4247
|
}
|
|
3228
4248
|
if (!matches) continue;
|
|
3229
4249
|
execution = await executeNodeList(arm.body, state, { capture });
|
|
3230
4250
|
append(execution);
|
|
4251
|
+
if (arm.terminator === ";&") { fallThrough = true; continue; }
|
|
4252
|
+
if (arm.terminator === ";|") { fallThrough = false; continue; }
|
|
3231
4253
|
break;
|
|
3232
4254
|
}
|
|
3233
4255
|
state.lastStatus = execution.status;
|
|
3234
4256
|
continue;
|
|
3235
4257
|
}
|
|
3236
4258
|
let selected = null;
|
|
4259
|
+
let selectedThen = "";
|
|
3237
4260
|
for (const branch of node.branches) {
|
|
3238
4261
|
let condition = branch.condition.trim();
|
|
3239
|
-
|
|
3240
|
-
while (/^!\s+/.test(condition)) { negate = !negate; condition = condition.replace(/^!\s+/, ""); }
|
|
3241
|
-
const checked = await execute(condition, state, { capture, simple: true });
|
|
4262
|
+
const checked = await execute(condition, state, { capture, errexitIgnored:true });
|
|
3242
4263
|
append(checked);
|
|
3243
|
-
if (
|
|
4264
|
+
if (checked.status === 0) {
|
|
4265
|
+
selected = branch.body;
|
|
4266
|
+
selectedThen = branch.thenCommand ?? "";
|
|
4267
|
+
break;
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
if (selectedThen) {
|
|
4271
|
+
const initial = await execute(selectedThen, state, { capture });
|
|
4272
|
+
append(initial);
|
|
3244
4273
|
}
|
|
3245
4274
|
execution = selected
|
|
3246
4275
|
? await executeNodeList(selected, state, { capture })
|
|
@@ -3253,9 +4282,17 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3253
4282
|
|
|
3254
4283
|
async function executeFunction(name, argv, state) {
|
|
3255
4284
|
const previousArgs = state.args;
|
|
4285
|
+
const previousLoopDepth = state.loopDepth;
|
|
4286
|
+
state.loopDepth = 0;
|
|
3256
4287
|
state.args = [previousArgs[0], ...argv.slice(1)];
|
|
4288
|
+
state.returnDepth++;
|
|
3257
4289
|
try { return await executeNodeList(state.functions[name], state); }
|
|
3258
|
-
finally {
|
|
4290
|
+
finally {
|
|
4291
|
+
state.returnDepth--;
|
|
4292
|
+
state.loopDepth = previousLoopDepth;
|
|
4293
|
+
if (state.flow?.kind === "return") state.flow = null;
|
|
4294
|
+
state.args = previousArgs;
|
|
4295
|
+
}
|
|
3259
4296
|
}
|
|
3260
4297
|
|
|
3261
4298
|
async function runExternal(
|
|
@@ -3275,7 +4312,7 @@ async function runExternal(
|
|
|
3275
4312
|
const options = {
|
|
3276
4313
|
cmd: spawnArgv,
|
|
3277
4314
|
cwd: nativePath(state.cwd),
|
|
3278
|
-
env: state
|
|
4315
|
+
env: externalEnvironment(state),
|
|
3279
4316
|
stdin: runtimeOptions.background ? "ignore" : input === null ? "inherit" : input,
|
|
3280
4317
|
stdout: runtimeOptions.background ? "inherit" : captureStdout || stdoutSink ? "pipe" : "inherit",
|
|
3281
4318
|
stderr: runtimeOptions.background ? "inherit" : captureStderr || runtimeOptions.stderrSink ? "pipe" : "inherit",
|
|
@@ -3283,7 +4320,30 @@ async function runExternal(
|
|
|
3283
4320
|
runtimeOptions.onExit?.(proc, exitCode, signalCode, error);
|
|
3284
4321
|
},
|
|
3285
4322
|
};
|
|
4323
|
+
let stateDirectory;
|
|
3286
4324
|
try {
|
|
4325
|
+
if (options.env[PIPELINE_STATE_ENV] === "@current")
|
|
4326
|
+
options.env[PIPELINE_STATE_ENV] = pipelineBuiltinState(state).env[PIPELINE_STATE_ENV];
|
|
4327
|
+
const serialized = options.env[PIPELINE_STATE_ENV];
|
|
4328
|
+
if (serialized?.length > 2048 && !runtimeOptions.background) {
|
|
4329
|
+
stateDirectory = mkdtempSync(resolvePath(tmpdir(), "bunmsh-state-"));
|
|
4330
|
+
const file = resolvePath(stateDirectory, "state.json");
|
|
4331
|
+
writeFileSync(file, serialized, {mode:0o600});
|
|
4332
|
+
options.env = {...options.env, [PIPELINE_STATE_ENV]: `@file:${file}`};
|
|
4333
|
+
}
|
|
4334
|
+
if (!runtimeOptions.background) {
|
|
4335
|
+
options.stdout = runtimeOptions.stdoutFd ?? options.stdout;
|
|
4336
|
+
options.stderr = runtimeOptions.stderrFd ?? options.stderr;
|
|
4337
|
+
const extra = Object.keys(state.fdBindings ?? {}).map(Number).filter(n => n > 2);
|
|
4338
|
+
if (extra.length) {
|
|
4339
|
+
options.stdio = [options.stdin, options.stdout, options.stderr];
|
|
4340
|
+
for (let n = 3; n <= Math.max(...extra); n++) {
|
|
4341
|
+
const endpoint = state.fdBindings[n];
|
|
4342
|
+
options.stdio[n] = endpoint?.fd ?? endpoint?.channel ?? "ignore";
|
|
4343
|
+
}
|
|
4344
|
+
delete options.stdin; delete options.stdout; delete options.stderr;
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
3287
4347
|
const proc = Bun.spawn(options);
|
|
3288
4348
|
if (runtimeOptions.background) {
|
|
3289
4349
|
if (!Array.isArray(Bun.sha.procs)) Bun.sha.procs = [];
|
|
@@ -3308,10 +4368,21 @@ async function runExternal(
|
|
|
3308
4368
|
const abandonStreaming = new Promise((resolve) => {
|
|
3309
4369
|
runtimeOptions.onSpawn?.(proc, pipelineKillSignal, resolve);
|
|
3310
4370
|
});
|
|
3311
|
-
const
|
|
4371
|
+
const sharedSink = stdoutSink && stdoutSink === runtimeOptions.stderrSink;
|
|
4372
|
+
const sharedStreamPromise = sharedSink ? Promise.race([
|
|
4373
|
+
(async()=>{
|
|
4374
|
+
const writer=stdoutSink.getWriter();
|
|
4375
|
+
const pump=async stream=>{const reader=stream.getReader();try{while(true){const chunk=await reader.read();if(chunk.done)break;await writer.write(chunk.value);}}finally{reader.releaseLock();}};
|
|
4376
|
+
try {await Promise.all([pump(proc.stdout),pump(proc.stderr)]);await writer.close();}
|
|
4377
|
+
catch {try{proc.kill(pipelineKillSignal);}catch{}}
|
|
4378
|
+
finally {writer.releaseLock();}
|
|
4379
|
+
})(),
|
|
4380
|
+
abandonStreaming,
|
|
4381
|
+
]) : Promise.resolve();
|
|
4382
|
+
const outputPromise = captureStdout && !stdoutSink
|
|
3312
4383
|
? new Response(proc.stdout).arrayBuffer()
|
|
3313
4384
|
: Promise.resolve(new ArrayBuffer(0));
|
|
3314
|
-
const streamPromise = stdoutSink
|
|
4385
|
+
const streamPromise = stdoutSink && !sharedSink
|
|
3315
4386
|
? Promise.race([
|
|
3316
4387
|
proc.stdout.pipeTo(stdoutSink).catch(() => {
|
|
3317
4388
|
try { proc.kill(pipelineKillSignal); } catch {}
|
|
@@ -3319,13 +4390,13 @@ async function runExternal(
|
|
|
3319
4390
|
abandonStreaming,
|
|
3320
4391
|
])
|
|
3321
4392
|
: Promise.resolve();
|
|
3322
|
-
const errorStreamPromise = runtimeOptions.stderrSink
|
|
4393
|
+
const errorStreamPromise = runtimeOptions.stderrSink && !sharedSink
|
|
3323
4394
|
? Promise.race([
|
|
3324
4395
|
proc.stderr.pipeTo(runtimeOptions.stderrSink).catch(() => {}),
|
|
3325
4396
|
abandonStreaming,
|
|
3326
4397
|
])
|
|
3327
4398
|
: Promise.resolve();
|
|
3328
|
-
const errorPromise = captureStderr
|
|
4399
|
+
const errorPromise = captureStderr && !runtimeOptions.stderrSink
|
|
3329
4400
|
? new Response(proc.stderr).arrayBuffer()
|
|
3330
4401
|
: Promise.resolve(new ArrayBuffer(0));
|
|
3331
4402
|
const [status, output, error] = await Promise.all([
|
|
@@ -3334,13 +4405,18 @@ async function runExternal(
|
|
|
3334
4405
|
errorPromise,
|
|
3335
4406
|
streamPromise,
|
|
3336
4407
|
errorStreamPromise,
|
|
4408
|
+
sharedStreamPromise,
|
|
3337
4409
|
]);
|
|
3338
4410
|
const execution = result(status, new Uint8Array(output), new Uint8Array(error));
|
|
3339
|
-
execution.stdoutStreamed = Boolean(stdoutSink);
|
|
4411
|
+
execution.stdoutStreamed = Boolean(stdoutSink || runtimeOptions.stderrSink);
|
|
3340
4412
|
execution.stderrStreamed = Boolean(runtimeOptions.stderrSink);
|
|
3341
4413
|
return execution;
|
|
3342
4414
|
} catch (error) {
|
|
3343
4415
|
return result(127, "", `bunmsh: ${argv[0]}: ${error.message}\n`);
|
|
4416
|
+
} finally {
|
|
4417
|
+
if (stateDirectory) {
|
|
4418
|
+
try { unlinkSync(resolvePath(stateDirectory,"state.json")); rmdirSync(stateDirectory); } catch {}
|
|
4419
|
+
}
|
|
3344
4420
|
}
|
|
3345
4421
|
}
|
|
3346
4422
|
|
|
@@ -3359,17 +4435,144 @@ function pipelineBuiltinArgv(argv) {
|
|
|
3359
4435
|
: [...BUN_RUNTIME_COMMAND, BUNMSH_ENTRY, "-cc", ...argv];
|
|
3360
4436
|
}
|
|
3361
4437
|
|
|
4438
|
+
function pipelineCompoundArgv(source) {
|
|
4439
|
+
return IS_COMPILED
|
|
4440
|
+
? [...EXECUTABLE_COMMAND, "-c", source]
|
|
4441
|
+
: [...BUN_RUNTIME_COMMAND, BUNMSH_ENTRY, "-c", source];
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
function heredocRanges(source, tokens = tokenize(source)) {
|
|
4445
|
+
const ranges = new Map();
|
|
4446
|
+
let previousEnd = 0;
|
|
4447
|
+
for (const token of tokens) {
|
|
4448
|
+
if (!token.heredoc) continue;
|
|
4449
|
+
const delimiter = token.fragments.map(f=>f.text.replaceAll("\u0000", "")).join("");
|
|
4450
|
+
const start = Math.max(source.indexOf("\n",token.offset)+1, previousEnd);
|
|
4451
|
+
if (!start) continue;
|
|
4452
|
+
let end=start;
|
|
4453
|
+
while (end < source.length) {
|
|
4454
|
+
const newline=source.indexOf("\n",end);
|
|
4455
|
+
const lineEnd=newline<0 ? source.length : newline;
|
|
4456
|
+
const line=source.slice(end,lineEnd);
|
|
4457
|
+
end=newline<0 ? source.length : newline+1;
|
|
4458
|
+
if (line === delimiter || line.replace(/^\t+/,"") === delimiter) break;
|
|
4459
|
+
}
|
|
4460
|
+
ranges.set(start,end); previousEnd=end;
|
|
4461
|
+
}
|
|
4462
|
+
return ranges;
|
|
4463
|
+
}
|
|
4464
|
+
|
|
4465
|
+
function leadingSubshell(source) {
|
|
4466
|
+
const leading = source.length - source.trimStart().length;
|
|
4467
|
+
if (source[leading] !== "(") return null;
|
|
4468
|
+
let depth = 0, quote = null;
|
|
4469
|
+
let keywords, ranges;
|
|
4470
|
+
try { const tokens=tokenize(source); keywords = caseKeywords(tokens); ranges=heredocRanges(source,tokens); } catch (error) {
|
|
4471
|
+
if (error.message.startsWith("unterminated")) return null;
|
|
4472
|
+
throw error;
|
|
4473
|
+
}
|
|
4474
|
+
const cases = [];
|
|
4475
|
+
for (let i = leading; i < source.length; i++) {
|
|
4476
|
+
if (ranges.has(i)) { i=ranges.get(i)-1; continue; }
|
|
4477
|
+
const ch = source[i];
|
|
4478
|
+
if (quote === "'") { if (ch === "'") quote = null; continue; }
|
|
4479
|
+
if (quote === '"') {
|
|
4480
|
+
if (ch === "\\" && /[\\"$`\n]/.test(source[i + 1] ?? "")) { i++; continue; }
|
|
4481
|
+
if (ch === '"') quote = null;
|
|
4482
|
+
continue;
|
|
4483
|
+
}
|
|
4484
|
+
if (ch === "#" && (i === leading || /[\s;|&]/.test(source[i-1]))) {
|
|
4485
|
+
const end=source.indexOf("\n",i); if (end<0) return null; i=end; continue;
|
|
4486
|
+
}
|
|
4487
|
+
const keyword=keywords.get(i);
|
|
4488
|
+
if (keyword === "case") cases.push("head");
|
|
4489
|
+
else if (keyword === "esac") cases.pop();
|
|
4490
|
+
if (cases.at(-1) === "head" && source.slice(i).match(/^in(?:\s|$)/) && /\s/.test(source[i-1] ?? "")) cases[cases.length-1]="pattern";
|
|
4491
|
+
if (cases.at(-1) === "body" && source.startsWith(";;",i)) cases[cases.length-1]="pattern";
|
|
4492
|
+
if (ch === "\\") { i++; continue; }
|
|
4493
|
+
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
4494
|
+
if (ch === "`") {
|
|
4495
|
+
try { i = substitutionEnd(source, i) - 1; } catch { return null; }
|
|
4496
|
+
continue;
|
|
4497
|
+
}
|
|
4498
|
+
if (ch === "$" && source[i + 1] === "(") {
|
|
4499
|
+
try { i = substitutionEnd(source, i) - 1; } catch { return null; }
|
|
4500
|
+
continue;
|
|
4501
|
+
}
|
|
4502
|
+
if (cases.at(-1) === "pattern" && (ch === "(" || ch === ")")) {
|
|
4503
|
+
if (ch === ")") cases[cases.length-1]="body";
|
|
4504
|
+
continue;
|
|
4505
|
+
}
|
|
4506
|
+
if (ch === "(") depth++;
|
|
4507
|
+
else if (ch === ")" && --depth === 0)
|
|
4508
|
+
return { body: source.slice(leading + 1, i), suffix: source.slice(i + 1) };
|
|
4509
|
+
}
|
|
4510
|
+
return null;
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
function rewriteSubshells(source, state) {
|
|
4514
|
+
let output = "", quote = null;
|
|
4515
|
+
const ranges=heredocRanges(source);
|
|
4516
|
+
for (let i = 0; i < source.length;) {
|
|
4517
|
+
if (ranges.has(i)) { const end=ranges.get(i); output+=source.slice(i,end); i=end; continue; }
|
|
4518
|
+
const ch = source[i];
|
|
4519
|
+
if (quote === "'") {
|
|
4520
|
+
output += ch;
|
|
4521
|
+
if (ch === "'") quote = null;
|
|
4522
|
+
i++;
|
|
4523
|
+
continue;
|
|
4524
|
+
}
|
|
4525
|
+
if (quote === '"') {
|
|
4526
|
+
output += ch;
|
|
4527
|
+
if (ch === "\\" && /[\\"$`\n]/.test(source[i + 1] ?? "")) output += source[++i];
|
|
4528
|
+
else if (ch === '"') quote = null;
|
|
4529
|
+
i++;
|
|
4530
|
+
continue;
|
|
4531
|
+
}
|
|
4532
|
+
if (ch === "\\") { output += source.slice(i, i + 2); i += 2; continue; }
|
|
4533
|
+
if (ch === "'" || ch === '"') { quote = ch; output += ch; i++; continue; }
|
|
4534
|
+
if (ch === "`") {
|
|
4535
|
+
try { const end = substitutionEnd(source, i); output += source.slice(i, end); i = end; continue; }
|
|
4536
|
+
catch { output += ch; i++; continue; }
|
|
4537
|
+
}
|
|
4538
|
+
if (ch === "$" && source[i + 1] === "(") {
|
|
4539
|
+
try { const end = substitutionEnd(source, i); output += source.slice(i, end); i = end; continue; }
|
|
4540
|
+
catch { output += ch; i++; continue; }
|
|
4541
|
+
}
|
|
4542
|
+
const boundary = i === 0 || /[\s;|&]/.test(source[i - 1]);
|
|
4543
|
+
if (ch === "(" && boundary) {
|
|
4544
|
+
const group = leadingSubshell(source.slice(i));
|
|
4545
|
+
if (group) {
|
|
4546
|
+
const argv = pipelineCompoundArgv(group.body);
|
|
4547
|
+
// Snapshot only when this command runs: earlier commands in the same
|
|
4548
|
+
// list may still change variables, functions, options or positional args.
|
|
4549
|
+
output += `BUNMSH_INTERNAL_PIPELINE_STATE=@current ${argv.map(quoteShellWord).join(" ")}`;
|
|
4550
|
+
i += group.body.length + 2;
|
|
4551
|
+
continue;
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
output += ch;
|
|
4555
|
+
i++;
|
|
4556
|
+
}
|
|
4557
|
+
return output;
|
|
4558
|
+
}
|
|
4559
|
+
|
|
3362
4560
|
function pipelineBuiltinState(state) {
|
|
3363
4561
|
return {
|
|
3364
4562
|
...state,
|
|
3365
4563
|
env: {
|
|
3366
4564
|
...state.env,
|
|
3367
4565
|
[PIPELINE_STATE_ENV]: JSON.stringify({
|
|
4566
|
+
env: Object.fromEntries(Object.entries(state.env).filter(([name]) => name !== PIPELINE_STATE_ENV)),
|
|
4567
|
+
exported: [...state.exported],
|
|
3368
4568
|
aliases: state.aliases,
|
|
3369
4569
|
functions: state.functions,
|
|
3370
4570
|
readonly: [...state.readonly],
|
|
3371
4571
|
args: state.args,
|
|
3372
4572
|
pathSearch: state.pathSearch,
|
|
4573
|
+
shellOptions:state.shellOptions,
|
|
4574
|
+
ignoreErrexit:state.ignoreErrexit,
|
|
4575
|
+
fdBindings: Object.fromEntries(Object.entries(state.fdBindings ?? {}).filter(([n]) => Number(n)>2).map(([n,v]) => [n, v.fd === undefined ? v : {fd:Number(n)}])),
|
|
3373
4576
|
}),
|
|
3374
4577
|
},
|
|
3375
4578
|
};
|
|
@@ -3463,7 +4666,12 @@ async function runCommandArgv(
|
|
|
3463
4666
|
)) return result(2, "", "bunmsh: &: only a single external command is supported\n");
|
|
3464
4667
|
|
|
3465
4668
|
let commandState = state;
|
|
3466
|
-
|
|
4669
|
+
const ioCommand = ["builtin", "__builtin", "command"].includes(commandArgv[0])
|
|
4670
|
+
? commandArgv.slice(1).filter(arg => arg !== "--")[0] : commandArgv[0];
|
|
4671
|
+
const redirectedStream = (options.stdoutFd !== undefined || options.stderrFd !== undefined) &&
|
|
4672
|
+
!Object.hasOwn(state.functions, ioCommand) &&
|
|
4673
|
+
(ioCommand === "yes" || Object.hasOwn(fallbackBuiltins, ioCommand));
|
|
4674
|
+
if ((options.pipelineStage || redirectedStream) && runsInPipelineSubprocess(commandArgv, commandState))
|
|
3467
4675
|
return runExternal(
|
|
3468
4676
|
pipelineBuiltinArgv(commandArgv),
|
|
3469
4677
|
pipelineBuiltinState(commandState),
|
|
@@ -3473,6 +4681,8 @@ async function runCommandArgv(
|
|
|
3473
4681
|
options.stdoutSink,
|
|
3474
4682
|
{ ...options, pipelineKillSignal: "SIGTERM" },
|
|
3475
4683
|
);
|
|
4684
|
+
if (typeof input === "number" && ioCommand !== "read" && runsInPipelineSubprocess(commandArgv,commandState))
|
|
4685
|
+
input = Readable.toWeb(createReadStream("",{fd:input,autoClose:false}));
|
|
3476
4686
|
while (["command", "builtin", "__builtin"].includes(commandArgv[0])) {
|
|
3477
4687
|
if (requestsBuiltinHelp(commandArgv) && hasBuiltinHelp(commandArgv[0]))
|
|
3478
4688
|
return builtinHelp(commandArgv[0]);
|
|
@@ -3578,16 +4788,6 @@ async function runCommandArgv(
|
|
|
3578
4788
|
);
|
|
3579
4789
|
}
|
|
3580
4790
|
|
|
3581
|
-
function redirectInput(path) {
|
|
3582
|
-
const fd = openSync(path, "r");
|
|
3583
|
-
return Readable.toWeb(createReadStream(path, { fd, autoClose: true }));
|
|
3584
|
-
}
|
|
3585
|
-
|
|
3586
|
-
function redirectOutput(path, append) {
|
|
3587
|
-
const fd = openSync(path, append ? "a" : "w");
|
|
3588
|
-
return Writable.toWeb(createWriteStream(path, { fd, autoClose: true }));
|
|
3589
|
-
}
|
|
3590
|
-
|
|
3591
4791
|
// Mirrors the double-quote scanning in tokenize(): an unquoted heredoc body
|
|
3592
4792
|
// only lets backslash escape $, `, \, and a trailing newline before the rest
|
|
3593
4793
|
// goes through the normal $/`` expansion pipeline.
|
|
@@ -3620,6 +4820,7 @@ function redirectHeredoc(content) {
|
|
|
3620
4820
|
|
|
3621
4821
|
async function runCommand(command, state, options = {}) {
|
|
3622
4822
|
state.expansionStatus = null;
|
|
4823
|
+
if (command.lineNumber != null) state.lineNumber = command.lineNumber;
|
|
3623
4824
|
const standaloneTilde =
|
|
3624
4825
|
command.words.length === 1 &&
|
|
3625
4826
|
command.words[0].fragments.length === 1 &&
|
|
@@ -3648,128 +4849,132 @@ async function runCommand(command, state, options = {}) {
|
|
|
3648
4849
|
localEnv[assignment.name] = assignment.value;
|
|
3649
4850
|
expanded.shift();
|
|
3650
4851
|
}
|
|
3651
|
-
|
|
4852
|
+
const assignmentOnly = expanded.length === 0;
|
|
4853
|
+
if (assignmentOnly) {
|
|
3652
4854
|
for (const name of Object.keys(localEnv))
|
|
3653
4855
|
if (state.readonly.has(name)) return readonlyError(name);
|
|
3654
4856
|
Object.assign(state.env, localEnv);
|
|
3655
|
-
return result(state.expansionStatus ?? 0);
|
|
4857
|
+
if (!command.redirects.length) return result(state.expansionStatus ?? 0);
|
|
4858
|
+
expanded.push(":");
|
|
3656
4859
|
}
|
|
3657
4860
|
const alias = aliasEligible ? state.aliases[expanded[0]] : null;
|
|
3658
4861
|
if (alias) expanded.splice(0, 1, ...alias);
|
|
3659
4862
|
|
|
4863
|
+
const prefixSaved = new Map();
|
|
4864
|
+
const special = [":", ".", "break", "continue", "eval", "exec", "exit", "export", "readonly", "return", "set", "shift", "trap", "unset"].includes(expanded[0]);
|
|
4865
|
+
const temporaryPrefix = state.shellOptions.posix && !assignmentOnly && !special;
|
|
4866
|
+
const previousBindings = state.fdBindings;
|
|
4867
|
+
const bindings = {...previousBindings};
|
|
4868
|
+
if (options.stdoutSink) bindings[1] = {channel:1};
|
|
4869
|
+
if (options.input !== null && options.input !== undefined) delete bindings[0];
|
|
4870
|
+
const opened = [];
|
|
4871
|
+
const redirectedFds = new Set();
|
|
3660
4872
|
let input = options.input ?? null;
|
|
3661
|
-
let
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
const heredoc = redirect.heredoc ?? { body: "", expand: false };
|
|
3672
|
-
const content = heredoc.expand
|
|
3673
|
-
? stripExpansionMarkers(
|
|
3674
|
-
(await expandText(heredocLiteralPass(heredoc.body), state)).replaceAll("\u0000", ""),
|
|
3675
|
-
)
|
|
3676
|
-
: heredoc.body;
|
|
3677
|
-
if (input instanceof ReadableStream) {
|
|
3678
|
-
try { await input.cancel(); } catch {}
|
|
3679
|
-
}
|
|
3680
|
-
input = redirectHeredoc(content);
|
|
3681
|
-
continue;
|
|
3682
|
-
}
|
|
3683
|
-
if (redirect.op === "<<<") {
|
|
3684
|
-
const [word] = await expandWord(redirect.target, state, { single: true, noTilde: true });
|
|
3685
|
-
if (input instanceof ReadableStream) {
|
|
3686
|
-
try { await input.cancel(); } catch {}
|
|
3687
|
-
}
|
|
3688
|
-
input = redirectHeredoc(`${word}\n`);
|
|
3689
|
-
continue;
|
|
3690
|
-
}
|
|
3691
|
-
const targets = await expandWord(redirect.target, state, { single: true });
|
|
3692
|
-
const target = targets[0];
|
|
3693
|
-
const path = target.startsWith("/") ? target : `${state.cwd}/${target}`;
|
|
3694
|
-
if (redirect.op === "<") {
|
|
3695
|
-
try {
|
|
3696
|
-
if (input instanceof ReadableStream) {
|
|
3697
|
-
try { await input.cancel(); } catch {}
|
|
4873
|
+
let persistent = expanded[0] === "exec" && expanded.length === 1;
|
|
4874
|
+
try {
|
|
4875
|
+
for (const redirect of command.redirects) {
|
|
4876
|
+
if (["<<", "<<-", "<<<"].includes(redirect.op)) {
|
|
4877
|
+
redirectedFds.add(0);
|
|
4878
|
+
let content;
|
|
4879
|
+
if (redirect.op === "<<<") content = `${(await expandWord(redirect.target,state,{single:true,noTilde:true}))[0]}\n`;
|
|
4880
|
+
else {
|
|
4881
|
+
const heredoc = redirect.heredoc ?? {body:"",expand:false};
|
|
4882
|
+
content = heredoc.expand ? stripExpansionMarkers((await expandText(heredocLiteralPass(heredoc.body),state)).replaceAll("\u0000","")) : heredoc.body;
|
|
3698
4883
|
}
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
return result(1, "", `bunmsh: ${target}: ${error.message}\n`);
|
|
4884
|
+
bindings[0] = {content};
|
|
4885
|
+
continue;
|
|
3702
4886
|
}
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
4887
|
+
const dup = /^(\d+)([<>]&)(\d+|-)$/.exec(redirect.op);
|
|
4888
|
+
const match = /^(\d*)(>&|<&|<>|>>|>|<)$/.exec(redirect.op);
|
|
4889
|
+
if (!dup && !match) throw new Error(`unsupported redirection ${redirect.op}`);
|
|
4890
|
+
const op = dup ? dup[2] : match[2];
|
|
4891
|
+
const fd = Number((dup ? dup[1] : match[1]) || (op.startsWith("<") ? 0 : 1));
|
|
4892
|
+
redirectedFds.add(fd);
|
|
4893
|
+
if (fd > 1023) throw new Error("file descriptor out of range");
|
|
4894
|
+
const target = dup ? dup[3] : (await expandWord(redirect.target,state,{single:true}))[0];
|
|
4895
|
+
if (op.endsWith("&")) {
|
|
4896
|
+
if (target === "-") bindings[fd] = {closed:true};
|
|
4897
|
+
else {
|
|
4898
|
+
if (!/^\d+$/.test(target)) throw new Error(`${target}: bad file descriptor`);
|
|
4899
|
+
const from = bindings[Number(target)] ?? (Number(target) <= 2 ? {channel:Number(target)} : null);
|
|
4900
|
+
if (!from || from.closed) throw new Error(`${target}: bad file descriptor`);
|
|
4901
|
+
bindings[fd] = from;
|
|
4902
|
+
}
|
|
4903
|
+
} else {
|
|
4904
|
+
const path = isAbsolute(nativePath(target)) ? nativePath(target) : resolvePath(nativePath(state.cwd),nativePath(target));
|
|
4905
|
+
const handle = openSync(path, op === "<" ? "r" : op === ">>" ? "a" : op === "<>" ? fsConstants.O_RDWR | fsConstants.O_CREAT : "w");
|
|
4906
|
+
opened.push(handle);
|
|
4907
|
+
bindings[fd] = {fd:handle};
|
|
3721
4908
|
}
|
|
3722
|
-
stdoutRedirect = redirectOutput(stdoutPath, stdoutAppend);
|
|
3723
4909
|
}
|
|
3724
|
-
|
|
3725
|
-
|
|
4910
|
+
const commandState = options.subshell ? {...state,env:{...state.env,...localEnv},exported:new Set(state.exported),fdBindings:bindings,flow:null,loopDepth:0,returnDepth:0} : state;
|
|
4911
|
+
if (!options.subshell) {
|
|
4912
|
+
for (const name of Object.keys(localEnv)) if (state.readonly.has(name)) return readonlyError(name);
|
|
4913
|
+
// Internal subprocess transport is command-scoped in both modes.
|
|
4914
|
+
// Otherwise an unrelated later shell inherits the parent's functions/options.
|
|
4915
|
+
for (const name of Object.keys(localEnv)) if (temporaryPrefix || name === PIPELINE_STATE_ENV)
|
|
4916
|
+
prefixSaved.set(name, {present:Object.hasOwn(state.env,name),value:state.env[name],exported:state.exported.has(name)});
|
|
4917
|
+
Object.assign(commandState.env,localEnv);
|
|
4918
|
+
}
|
|
4919
|
+
if (!assignmentOnly) for (const name of Object.keys(localEnv)) commandState.exported.add(name);
|
|
4920
|
+
commandState.fdBindings = bindings;
|
|
4921
|
+
if (persistent) {
|
|
4922
|
+
for (const fd of opened) state.ownedFds.add(fd);
|
|
4923
|
+
return result();
|
|
4924
|
+
}
|
|
4925
|
+
const stdin = bindings[0];
|
|
4926
|
+
if (stdin?.content !== undefined) input = redirectHeredoc(stdin.content);
|
|
4927
|
+
else if (stdin?.closed) input = redirectHeredoc("");
|
|
4928
|
+
else if (stdin?.fd !== undefined) input = stdin.fd;
|
|
4929
|
+
if (state.shellOptions.x) {
|
|
4930
|
+
const line = bytes(`${state.env.PS4 ?? "+ "}${expanded.join(" ")}\n`);
|
|
4931
|
+
const traceFd = bindings[2];
|
|
4932
|
+
if (traceFd?.fd !== undefined) writeSync(traceFd.fd,line);
|
|
4933
|
+
else await writeStream(process.stderr,line);
|
|
4934
|
+
}
|
|
4935
|
+
const nested = Boolean(command.body) || Object.hasOwn(commandState.functions,expanded[0]) || ["eval",".","source"].includes(expanded[0]);
|
|
4936
|
+
const stdout = bindings[1] ?? {channel:1};
|
|
4937
|
+
const stderr = bindings[2] ?? {channel:2};
|
|
4938
|
+
const direct = (endpoint) => endpoint.fd !== undefined || endpoint.closed;
|
|
4939
|
+
const execution = command.body ? await executeNodeList(command.body,commandState,{capture:true,input}) : await runCommandArgv(expanded,commandState,input,
|
|
4940
|
+
direct(stdout) ? false : Boolean(options.captureStdout || stdout.channel !== 1),
|
|
4941
|
+
direct(stderr) ? false : Boolean(options.captureStderr || stderr.channel !== 2), {
|
|
4942
|
+
...options,
|
|
4943
|
+
pipelineStage: Boolean(options.pipelineStage),
|
|
4944
|
+
stdoutSink: !direct(stdout)&&stdout.channel===1 ? options.stdoutSink : null,
|
|
4945
|
+
stderrSink: !direct(stderr)&&stderr.channel===1 ? options.stdoutSink : null,
|
|
4946
|
+
stdoutFd: stdout.fd ?? (stdout.closed ? "ignore" : undefined),
|
|
4947
|
+
stderrFd: stderr.fd ?? (stderr.closed ? "ignore" : undefined),
|
|
4948
|
+
});
|
|
4949
|
+
if (!nested) routeCommandOutput(execution, bindings);
|
|
4950
|
+
return execution;
|
|
3726
4951
|
} catch (error) {
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
4952
|
+
persistent = false;
|
|
4953
|
+
return result(1,"",`bunmsh: ${error.message}\n`);
|
|
4954
|
+
} finally {
|
|
4955
|
+
for (const [name,saved] of prefixSaved) {
|
|
4956
|
+
if (saved.present) state.env[name]=saved.value; else delete state.env[name];
|
|
4957
|
+
if (saved.exported) state.exported.add(name); else state.exported.delete(name);
|
|
4958
|
+
}
|
|
4959
|
+
if (!persistent) {
|
|
4960
|
+
if (options.subshell) state.fdBindings = previousBindings;
|
|
4961
|
+
else if (command.redirects.length) {
|
|
4962
|
+
// Restore just the redirected descriptors (mksh's savedfd scope),
|
|
4963
|
+
// not descriptors opened independently by exec inside the body.
|
|
4964
|
+
const restored = {...state.fdBindings};
|
|
4965
|
+
for (const fd of redirectedFds) {
|
|
4966
|
+
if (Object.hasOwn(previousBindings,fd)) restored[fd] = previousBindings[fd];
|
|
4967
|
+
else delete restored[fd];
|
|
4968
|
+
}
|
|
4969
|
+
state.fdBindings = restored;
|
|
3737
4970
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
}
|
|
3744
|
-
|
|
3745
|
-
const captureStdout = (options.captureStdout || stdoutToStderr) && stdoutRedirect === null;
|
|
3746
|
-
const captureStderr = (options.captureStderr || stderrToStdout) && stderrRedirect === null;
|
|
3747
|
-
const execution = await runCommandArgv(
|
|
3748
|
-
expanded,
|
|
3749
|
-
commandState,
|
|
3750
|
-
input,
|
|
3751
|
-
captureStdout,
|
|
3752
|
-
captureStderr,
|
|
3753
|
-
{
|
|
3754
|
-
pipelineStage: Boolean(options.pipelineStage || stdoutRedirect || stderrRedirect),
|
|
3755
|
-
stdoutSink: stdoutRedirect ?? options.stdoutSink ?? null,
|
|
3756
|
-
stderrSink: stderrRedirect,
|
|
3757
|
-
onSpawn: options.onSpawn,
|
|
3758
|
-
onExit: options.onExit,
|
|
3759
|
-
background: Boolean(options.background),
|
|
3760
|
-
},
|
|
3761
|
-
);
|
|
3762
|
-
|
|
3763
|
-
if (stdoutToStderr && execution.stdout.byteLength) {
|
|
3764
|
-
execution.stderr = concatBytes([execution.stderr, execution.stdout]);
|
|
3765
|
-
execution.stdout = bytes();
|
|
3766
|
-
}
|
|
3767
|
-
if (stderrToStdout && execution.stderr.byteLength) {
|
|
3768
|
-
execution.stdout = concatBytes([execution.stdout, execution.stderr]);
|
|
3769
|
-
execution.stderr = bytes();
|
|
4971
|
+
for (const fd of opened) {
|
|
4972
|
+
if (Object.values(state.fdBindings).some(endpoint => endpoint.fd === fd)) state.ownedFds.add(fd);
|
|
4973
|
+
else { try { closeSync(fd); } catch {} }
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
3770
4976
|
}
|
|
3771
4977
|
|
|
3772
|
-
return execution;
|
|
3773
4978
|
}
|
|
3774
4979
|
|
|
3775
4980
|
async function runPipeline(pipeline, state, options = {}) {
|
|
@@ -3777,6 +4982,8 @@ async function runPipeline(pipeline, state, options = {}) {
|
|
|
3777
4982
|
return result(2, "", "bunmsh: &: only a single external command without redirections is supported\n");
|
|
3778
4983
|
if (pipeline.length === 1)
|
|
3779
4984
|
return runCommand(pipeline[0], state, {
|
|
4985
|
+
input: options.input ?? null,
|
|
4986
|
+
stdoutSink: options.stdoutSink ?? null,
|
|
3780
4987
|
captureStdout: Boolean(options.capture),
|
|
3781
4988
|
captureStderr: Boolean(options.capture),
|
|
3782
4989
|
background: Boolean(options.background),
|
|
@@ -3788,9 +4995,9 @@ async function runPipeline(pipeline, state, options = {}) {
|
|
|
3788
4995
|
);
|
|
3789
4996
|
const linksConnected = links.map((_, i) => {
|
|
3790
4997
|
const stdoutRedirected = pipeline[i].redirects.some((redirect) =>
|
|
3791
|
-
redirect.op
|
|
4998
|
+
/^(?:1)?>(?:>|&.*)?$/.test(redirect.op));
|
|
3792
4999
|
const stdinRedirected = pipeline[i + 1].redirects.some((redirect) =>
|
|
3793
|
-
|
|
5000
|
+
/^(?:0)?<(?:<<?-?|&.*|>)?$/.test(redirect.op));
|
|
3794
5001
|
return !stdoutRedirected && !stdinRedirected;
|
|
3795
5002
|
});
|
|
3796
5003
|
const processes = Array(pipeline.length).fill(null);
|
|
@@ -3812,7 +5019,7 @@ async function runPipeline(pipeline, state, options = {}) {
|
|
|
3812
5019
|
const stages = pipeline.map((command, i) => {
|
|
3813
5020
|
const last = i === pipeline.length - 1;
|
|
3814
5021
|
return runCommand(command, state, {
|
|
3815
|
-
input: i === 0 ? null : links[i - 1].readable,
|
|
5022
|
+
input: i === 0 ? options.input ?? null : links[i - 1].readable,
|
|
3816
5023
|
captureStdout: last && Boolean(options.capture),
|
|
3817
5024
|
captureStderr: Boolean(options.capture),
|
|
3818
5025
|
stdoutSink: last ? null : links[i].writable,
|
|
@@ -3852,6 +5059,18 @@ async function runPipeline(pipeline, state, options = {}) {
|
|
|
3852
5059
|
}
|
|
3853
5060
|
|
|
3854
5061
|
export async function execute(source, state = createState(), io = {}) {
|
|
5062
|
+
const previous = state.ignoreErrexit;
|
|
5063
|
+
if (io.errexitIgnored) state.ignoreErrexit++;
|
|
5064
|
+
try { return await executeInternal(source,state,io); }
|
|
5065
|
+
catch (error) {
|
|
5066
|
+
const failure = result(state.exitRequested ? state.exitStatus : 2,"",`bunmsh: ${error.message}\n`);
|
|
5067
|
+
if (!io.capture) await writeStream(process.stderr,failure.stderr);
|
|
5068
|
+
return failure;
|
|
5069
|
+
}
|
|
5070
|
+
finally { state.ignoreErrexit = previous; }
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5073
|
+
async function executeInternal(source, state, io) {
|
|
3855
5074
|
source = normalizeSource(source);
|
|
3856
5075
|
if (source === "?") {
|
|
3857
5076
|
const execution = result(0, `${state.lastStatus}\n`);
|
|
@@ -3860,6 +5079,15 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3860
5079
|
return execution;
|
|
3861
5080
|
}
|
|
3862
5081
|
const rawSource = source.trimStart();
|
|
5082
|
+
const group = leadingSubshell(source);
|
|
5083
|
+
if (group) {
|
|
5084
|
+
const previous = state.env[PIPELINE_STATE_ENV];
|
|
5085
|
+
try { return await execute(rewriteSubshells(source,state),state,io); }
|
|
5086
|
+
finally {
|
|
5087
|
+
if (previous === undefined) delete state.env[PIPELINE_STATE_ENV];
|
|
5088
|
+
else state.env[PIPELINE_STATE_ENV] = previous;
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
3863
5091
|
if (isJavaScriptMode(rawSource)) {
|
|
3864
5092
|
let execution;
|
|
3865
5093
|
const previousCwd = process.cwd();
|
|
@@ -3916,7 +5144,7 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3916
5144
|
if (execution.stdout.byteLength) stdout.push(execution.stdout);
|
|
3917
5145
|
if (execution.stderr.byteLength) stderr.push(execution.stderr);
|
|
3918
5146
|
}
|
|
3919
|
-
if (state.exitRequested) break;
|
|
5147
|
+
if (state.exitRequested || state.flow) break;
|
|
3920
5148
|
}
|
|
3921
5149
|
return io.capture
|
|
3922
5150
|
? result(execution.status, concatBytes(stdout), concatBytes(stderr))
|
|
@@ -3928,8 +5156,9 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3928
5156
|
if (compoundSyntax) {
|
|
3929
5157
|
let execution;
|
|
3930
5158
|
try {
|
|
3931
|
-
execution = await executeNodeList(parseCompoundScript(source), state, {
|
|
5159
|
+
execution = await executeNodeList(parseCompoundScript(source, io.firstLine ?? 1, io.sourceLines), state, {
|
|
3932
5160
|
capture: Boolean(io.capture),
|
|
5161
|
+
input: io.input ?? null,
|
|
3933
5162
|
});
|
|
3934
5163
|
}
|
|
3935
5164
|
catch (error) {
|
|
@@ -3944,9 +5173,16 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3944
5173
|
return execution;
|
|
3945
5174
|
}
|
|
3946
5175
|
|
|
5176
|
+
const withSubshells = rewriteSubshells(source, state);
|
|
5177
|
+
if (withSubshells !== source) {
|
|
5178
|
+
source = withSubshells;
|
|
5179
|
+
return execute(source, state, io);
|
|
5180
|
+
}
|
|
5181
|
+
|
|
3947
5182
|
let jobs;
|
|
3948
5183
|
try {
|
|
3949
5184
|
jobs = parse(source);
|
|
5185
|
+
for (const job of jobs) for (const command of job.pipeline) command.lineNumber = io.sourceLines?.[command.lineNumber-1] ?? command.lineNumber + (io.firstLine ?? 1) - 1;
|
|
3950
5186
|
} catch (error) {
|
|
3951
5187
|
const message = error instanceof ShellSyntaxError ? error.message : String(error);
|
|
3952
5188
|
const output = `bunmsh: syntax error: ${message}\n`;
|
|
@@ -3959,13 +5195,24 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3959
5195
|
let execution = result(state.lastStatus);
|
|
3960
5196
|
const capturedStdout = [];
|
|
3961
5197
|
const capturedStderr = [];
|
|
3962
|
-
for (const job of jobs) {
|
|
5198
|
+
for (const [jobIndex,job] of jobs.entries()) {
|
|
5199
|
+
const trapped = await runPendingTraps(state,io);
|
|
5200
|
+
if (io.capture) { capturedStdout.push(trapped.stdout); capturedStderr.push(trapped.stderr); }
|
|
5201
|
+
if (state.exitRequested || state.flow) break;
|
|
3963
5202
|
if (job.connector === "&&" && execution.status !== 0) continue;
|
|
3964
5203
|
if (job.connector === "||" && execution.status === 0) continue;
|
|
3965
|
-
|
|
5204
|
+
const ignored = state.ignoreErrexit || job.negate || ["&&","||"].includes(jobs[jobIndex+1]?.connector);
|
|
5205
|
+
const previousIgnore = state.ignoreErrexit;
|
|
5206
|
+
if (ignored) state.ignoreErrexit++;
|
|
5207
|
+
try { execution = await runPipeline(job.pipeline, state, {
|
|
3966
5208
|
capture: io.capture,
|
|
3967
5209
|
background: job.background,
|
|
3968
|
-
|
|
5210
|
+
input: io.input ?? null,
|
|
5211
|
+
stdoutSink: io.stdoutSink ?? null,
|
|
5212
|
+
}); } catch (error) {
|
|
5213
|
+
execution = result(state.exitRequested ? state.exitStatus : 2,"",`bunmsh: ${error.message}\n`);
|
|
5214
|
+
} finally { state.ignoreErrexit = previousIgnore; }
|
|
5215
|
+
if (!ignored && state.shellOptions.e && execution.status && !state.flow) { state.exitRequested=true; state.exitStatus=execution.status; }
|
|
3969
5216
|
if (job.negate) execution.status = execution.status === 0 ? 1 : 0;
|
|
3970
5217
|
state.lastStatus = execution.status;
|
|
3971
5218
|
if (execution.stdout.byteLength) {
|
|
@@ -3976,7 +5223,7 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3976
5223
|
if (io.capture) capturedStderr.push(execution.stderr);
|
|
3977
5224
|
else await writeStream(process.stderr, execution.stderr);
|
|
3978
5225
|
}
|
|
3979
|
-
if (state.exitRequested) break;
|
|
5226
|
+
if (state.exitRequested || state.flow) break;
|
|
3980
5227
|
}
|
|
3981
5228
|
return io.capture
|
|
3982
5229
|
? result(execution.status, concatBytes(capturedStdout), concatBytes(capturedStderr))
|
|
@@ -4006,11 +5253,13 @@ export function pipelineChildState() {
|
|
|
4006
5253
|
const serialized = process.env[PIPELINE_STATE_ENV];
|
|
4007
5254
|
if (!serialized) return createState();
|
|
4008
5255
|
try {
|
|
4009
|
-
const inherited = JSON.parse(serialized);
|
|
4010
|
-
const env = { ...process.env };
|
|
5256
|
+
const inherited = JSON.parse(serialized.startsWith("@file:") ? readFileSync(serialized.slice(6),"utf8") : serialized);
|
|
5257
|
+
const env = { ...process.env, ...(inherited.env ?? {}) };
|
|
4011
5258
|
delete env[PIPELINE_STATE_ENV];
|
|
4012
5259
|
return createState({
|
|
4013
5260
|
env,
|
|
5261
|
+
inheritEnvironment: false,
|
|
5262
|
+
exported: inherited.exported,
|
|
4014
5263
|
cwd: process.cwd(),
|
|
4015
5264
|
aliases: inherited.aliases,
|
|
4016
5265
|
functions: inherited.functions,
|
|
@@ -4018,6 +5267,9 @@ export function pipelineChildState() {
|
|
|
4018
5267
|
args: inherited.args,
|
|
4019
5268
|
pathSearch: inherited.pathSearch,
|
|
4020
5269
|
pipelineChild: true,
|
|
5270
|
+
fdBindings: inherited.fdBindings,
|
|
5271
|
+
shellOptions:inherited.shellOptions,
|
|
5272
|
+
ignoreErrexit:inherited.ignoreErrexit,
|
|
4021
5273
|
});
|
|
4022
5274
|
} catch {
|
|
4023
5275
|
return createState();
|