buninu-linux 0.8.56 → 0.9.56
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/initramfs/bin/tar +34 -46
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +103 -0
- package/initramfs/buninu/apps/bunmsh/PORTING.md +133 -15
- package/initramfs/buninu/apps/bunmsh/help/awk.md +38 -0
- package/initramfs/buninu/apps/bunmsh/help/break.md +21 -0
- package/initramfs/buninu/apps/bunmsh/help/chmod.md +8 -2
- package/initramfs/buninu/apps/bunmsh/help/cmp.md +29 -0
- package/initramfs/buninu/apps/bunmsh/help/continue.md +22 -0
- package/initramfs/buninu/apps/bunmsh/help/cp.md +4 -0
- package/initramfs/buninu/apps/bunmsh/help/diff.md +30 -0
- package/initramfs/buninu/apps/bunmsh/help/egrep.md +26 -0
- package/initramfs/buninu/apps/bunmsh/help/export.md +2 -2
- package/initramfs/buninu/apps/bunmsh/help/expr.md +30 -0
- package/initramfs/buninu/apps/bunmsh/help/fgrep.md +25 -0
- package/initramfs/buninu/apps/bunmsh/help/grep.md +4 -0
- package/initramfs/buninu/apps/bunmsh/help/ls.md +4 -2
- package/initramfs/buninu/apps/bunmsh/help/mkdir.md +1 -1
- package/initramfs/buninu/apps/bunmsh/help/print.md +3 -0
- package/initramfs/buninu/apps/bunmsh/help/return.md +23 -0
- package/initramfs/buninu/apps/bunmsh/help/sed.md +8 -3
- package/initramfs/buninu/apps/bunmsh/help/set.md +12 -3
- package/initramfs/buninu/apps/bunmsh/help/tr.md +4 -0
- package/initramfs/buninu/apps/bunmsh/help/trap.md +22 -0
- package/initramfs/buninu/apps/bunmsh/help/uniq.md +27 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/awk.js +203 -0
- package/initramfs/buninu/apps/bunmsh/src/diff.js +222 -0
- package/initramfs/buninu/apps/bunmsh/src/fancy-ls.js +15 -4
- package/initramfs/buninu/apps/bunmsh/src/main.js +21 -5
- package/initramfs/buninu/apps/bunmsh/src/sed.js +159 -0
- package/initramfs/buninu/apps/bunmsh/src/shell.js +1283 -540
- package/initramfs/buninu/apps/bunmsh/test/awk-reference.test.js +35 -0
- package/initramfs/buninu/apps/bunmsh/test/awk.test.js +62 -0
- package/initramfs/buninu/apps/bunmsh/test/posix-bootstrap.test.js +595 -0
- package/initramfs/buninu/apps/bunmsh/test/utility-reference-cases.mjs +98 -0
- package/initramfs/buninu/apps/bunmsh/test/utility-reference.test.js +23 -0
- package/initramfs/buninu/apps/jsgotty/client-kitty.mjs +61 -0
- package/initramfs/buninu/apps/jsgotty/client.mjs +16 -0
- package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
- package/initramfs/buninu/apps/jsgotty/package.json +1 -1
- package/initramfs/buninu/apps/jsgotty/tests/client-kitty.test.mjs +43 -0
- package/initramfs/etc/resolv.conf +2 -1
- package/initramfs/usr/share/doc/buninu-linux/tar.md +11 -0
- package/package.json +1 -1
- package/scripts/fetch.js +2 -0
- package/test/tar-strip.test.js +36 -0
|
@@ -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,26 +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 = /^(\d+>&\d+)/.exec(source.slice(i))?.[1] ?? "";
|
|
288
314
|
const operatorAhead =
|
|
289
315
|
["|", ";", "<", ">"].includes(ch) ||
|
|
290
316
|
two === "&&" ||
|
|
291
|
-
two === "||"
|
|
292
|
-
two === "2>" ||
|
|
293
|
-
Boolean(fdDup);
|
|
317
|
+
two === "||";
|
|
294
318
|
if (current && operatorAhead) {
|
|
295
319
|
finishWord();
|
|
296
320
|
continue;
|
|
297
321
|
}
|
|
298
|
-
// A descriptor duplication begins with the shorter `2>` redirect token,
|
|
299
|
-
// so it must win before ordinary file redirects are considered.
|
|
300
|
-
if (!current && fdDup) {
|
|
301
|
-
tokens.push({ type: "op", value: fdDup, offset: i });
|
|
302
|
-
i += fdDup.length;
|
|
303
|
-
atWordBoundary = true;
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
322
|
if (!current && ["&&", "||", ">>", "2>", "2>>"].includes(
|
|
307
323
|
source.slice(i, i + 3) === "2>>" ? source.slice(i, i + 3) : two,
|
|
308
324
|
)) {
|
|
@@ -357,6 +373,7 @@ export function tokenize(source, options = {}) {
|
|
|
357
373
|
}
|
|
358
374
|
const quotedContinuation = lineContinuationLength(source, i);
|
|
359
375
|
if (quotedContinuation) {
|
|
376
|
+
options.continuations?.set(i, quotedContinuation);
|
|
360
377
|
i += quotedContinuation;
|
|
361
378
|
continue;
|
|
362
379
|
}
|
|
@@ -369,7 +386,7 @@ export function tokenize(source, options = {}) {
|
|
|
369
386
|
}
|
|
370
387
|
}
|
|
371
388
|
if ((source[i] === "$" && ["(", "{"].includes(source[i + 1])) || source[i] === "`") {
|
|
372
|
-
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);
|
|
373
390
|
text += source.slice(i, end);
|
|
374
391
|
i = end;
|
|
375
392
|
continue;
|
|
@@ -407,7 +424,7 @@ export function tokenize(source, options = {}) {
|
|
|
407
424
|
while (i < source.length) {
|
|
408
425
|
const c = source[i];
|
|
409
426
|
if ((c === "$" && ["(", "{"].includes(source[i + 1])) || c === "`") {
|
|
410
|
-
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);
|
|
411
428
|
text += source.slice(i, end);
|
|
412
429
|
i = end;
|
|
413
430
|
continue;
|
|
@@ -415,9 +432,7 @@ export function tokenize(source, options = {}) {
|
|
|
415
432
|
const maybeOp =
|
|
416
433
|
["|", ";", "<", ">"].includes(c) ||
|
|
417
434
|
source.slice(i, i + 2) === "&&" ||
|
|
418
|
-
source.slice(i, i + 2) === "||"
|
|
419
|
-
source.slice(i, i + 2) === "2>" ||
|
|
420
|
-
/^\d+>&\d+/.test(source.slice(i));
|
|
435
|
+
source.slice(i, i + 2) === "||";
|
|
421
436
|
if (isSpace(c) || c === "\n" || c === "'" || c === '"' || c === "\\" || maybeOp)
|
|
422
437
|
break;
|
|
423
438
|
text += c;
|
|
@@ -460,6 +475,7 @@ export function parse(source) {
|
|
|
460
475
|
const pipeline = [];
|
|
461
476
|
while (true) {
|
|
462
477
|
const command = emptyCommand();
|
|
478
|
+
command.lineNumber = source.slice(0, tokens[i]?.offset ?? 0).split("\n").length;
|
|
463
479
|
while (i < tokens.length) {
|
|
464
480
|
const token = tokens[i];
|
|
465
481
|
if (token.type === "op" && ["|", ";", "&&", "||"].includes(token.value))
|
|
@@ -469,7 +485,7 @@ export function parse(source) {
|
|
|
469
485
|
i++;
|
|
470
486
|
continue;
|
|
471
487
|
}
|
|
472
|
-
if (token.type === "op" &&
|
|
488
|
+
if (token.type === "op" && (/^\d*(?:>&|<&|<>|>>|>|<)$/.test(token.value) || ["<<", "<<-", "<<<"].includes(token.value))) {
|
|
473
489
|
const target = tokens[i + 1];
|
|
474
490
|
if (!target || target.type !== "word")
|
|
475
491
|
throw new ShellSyntaxError(
|
|
@@ -495,6 +511,7 @@ export function parse(source) {
|
|
|
495
511
|
|
|
496
512
|
if (tokens[i]?.value === "|") {
|
|
497
513
|
i++;
|
|
514
|
+
while (tokens[i]?.value === ";" && source[tokens[i].offset] === "\n") i++;
|
|
498
515
|
if (!tokens[i])
|
|
499
516
|
throw new ShellSyntaxError("pipeline requires a command", source.length);
|
|
500
517
|
continue;
|
|
@@ -539,16 +556,27 @@ export function parse(source) {
|
|
|
539
556
|
}
|
|
540
557
|
|
|
541
558
|
function parameterValue(name, state) {
|
|
559
|
+
if (name === "LINENO") return String(state.lineNumber ?? 1);
|
|
542
560
|
if (name === "?") return String(state.lastStatus);
|
|
543
561
|
if (name === "$") return String(process.pid);
|
|
544
562
|
if (name === "#") return String(Math.max(0, state.args.length - 1));
|
|
545
|
-
if (name === "-") return "";
|
|
563
|
+
if (name === "-") return Object.entries(state.shellOptions ?? {}).filter(([k,v])=>v && k.length === 1).map(([k])=>k).join("");
|
|
546
564
|
if (name === "0") return state.args[0] ?? "bunmsh";
|
|
547
565
|
if (name === "@" || name === "*") return state.args.slice(1).join((state.env.IFS ?? " ")[0] ?? " ");
|
|
548
566
|
if (/^[1-9][0-9]*$/.test(name)) return state.args[Number(name)] ?? "";
|
|
549
567
|
return state.env[name] ?? "";
|
|
550
568
|
}
|
|
551
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
|
+
|
|
552
580
|
function tildeDirectory(name, state) {
|
|
553
581
|
if (name === "") return state.env.HOME ?? "";
|
|
554
582
|
if (name === "+") return state.env.PWD ?? state.cwd;
|
|
@@ -606,16 +634,6 @@ function arithmeticValue(source, state) {
|
|
|
606
634
|
return String(value | 0);
|
|
607
635
|
}
|
|
608
636
|
|
|
609
|
-
function globPatternRegex(pattern, anchoredStart, anchoredEnd, greedy) {
|
|
610
|
-
let source = "";
|
|
611
|
-
for (const ch of pattern) {
|
|
612
|
-
if (ch === "*") source += greedy ? ".*" : ".*?";
|
|
613
|
-
else if (ch === "?") source += ".";
|
|
614
|
-
else source += ch.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
615
|
-
}
|
|
616
|
-
return new RegExp(`${anchoredStart ? "^" : ""}${source}${anchoredEnd ? "$" : ""}`);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
637
|
async function casePatternRegex(pattern, state) {
|
|
620
638
|
let marked = "";
|
|
621
639
|
const literal = (value) => {
|
|
@@ -660,34 +678,37 @@ async function casePatternRegex(pattern, state) {
|
|
|
660
678
|
let end = i + 1;
|
|
661
679
|
if (marked[end] === "!" || marked[end] === "^") end++;
|
|
662
680
|
if (marked[end] === "]") end++;
|
|
663
|
-
while (end < marked.length && marked[end] !== "]")
|
|
681
|
+
while (end < marked.length && marked[end] !== "]") {
|
|
682
|
+
if (marked[end] === "\u0000" && end + 1 < marked.length) end++;
|
|
683
|
+
end++;
|
|
684
|
+
}
|
|
664
685
|
if (end >= marked.length) source += "\\[";
|
|
665
686
|
else {
|
|
666
687
|
let body = marked.slice(i + 1, end);
|
|
667
688
|
const negate = body[0] === "!" || body[0] === "^";
|
|
668
689
|
if (negate) body = body.slice(1);
|
|
669
|
-
body = body.
|
|
690
|
+
body = body.replace(/\u0000(.)/gs, (_,ch) => /[\\\]\-^]/.test(ch) ? `\\${ch}` : ch);
|
|
670
691
|
source += `[${negate ? "^" : ""}${body}]`;
|
|
671
692
|
i = end;
|
|
672
693
|
}
|
|
673
694
|
}
|
|
674
695
|
else source += marked[i].replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
675
696
|
}
|
|
676
|
-
return new RegExp(`^${source}
|
|
697
|
+
return new RegExp(`^${source}$`, "s");
|
|
677
698
|
}
|
|
678
699
|
|
|
679
|
-
async function parameterExpansion(body, state) {
|
|
700
|
+
async function parameterExpansion(body, state, quoted = false) {
|
|
680
701
|
if (/^[!%][A-Za-z_][A-Za-z0-9_]*$/.test(body)) {
|
|
681
702
|
const value = parameterValue(body.slice(1), state);
|
|
682
703
|
return body[0] === "!" ? parameterValue(value, state) : String([...value].length);
|
|
683
704
|
}
|
|
684
|
-
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)))
|
|
685
706
|
return String(parameterValue(body.slice(1), state).length);
|
|
686
707
|
const trim = /^([A-Za-z_][A-Za-z0-9_]*|[?$#@*]|[0-9]+)(%%|%|##|#)(.*)$/s.exec(body);
|
|
687
708
|
if (trim) {
|
|
688
709
|
const value = parameterValue(trim[1], state);
|
|
689
|
-
const pattern =
|
|
690
|
-
const matches = (part) =>
|
|
710
|
+
const pattern = await casePatternRegex(trim[3], state);
|
|
711
|
+
const matches = (part) => pattern.test(part);
|
|
691
712
|
if (trim[2] === "%") {
|
|
692
713
|
for (let i = value.length; i >= 0; i--) if (matches(value.slice(i))) return value.slice(0, i);
|
|
693
714
|
} else if (trim[2] === "%%") {
|
|
@@ -718,13 +739,14 @@ async function parameterExpansion(body, state) {
|
|
|
718
739
|
if (!match) throw new ShellSyntaxError(`unsupported parameter expansion: \${${body}}`);
|
|
719
740
|
const [, name, operator, operand = ""] = match;
|
|
720
741
|
const value = parameterValue(name, state);
|
|
721
|
-
const unset =
|
|
742
|
+
const unset = /^\d+$/.test(name) ? Number(name) >= state.args.length :
|
|
743
|
+
name !== "LINENO" && !Object.hasOwn(state.env, name) && !/^[?$#@*-]$/.test(name);
|
|
722
744
|
const empty = value === "";
|
|
723
|
-
if (!operator) return
|
|
745
|
+
if (!operator) return checkedParameter(name,state);
|
|
724
746
|
const useEmpty = operator.startsWith(":");
|
|
725
747
|
const op = operator.at(-1);
|
|
726
748
|
const missing = unset || (useEmpty && empty);
|
|
727
|
-
const expandedOperand =
|
|
749
|
+
const expandedOperand = () => expandParameterOperand(operand, state, quoted);
|
|
728
750
|
if (op === "-") return missing ? expandedOperand() : value;
|
|
729
751
|
if (op === "+") return missing ? "" : expandedOperand();
|
|
730
752
|
if (op === "=") {
|
|
@@ -732,7 +754,7 @@ async function parameterExpansion(body, state) {
|
|
|
732
754
|
if (!/^[A-Za-z_]/.test(name)) throw new ShellSyntaxError("cannot assign positional parameter");
|
|
733
755
|
if (state.readonly?.has(name)) throw new ShellSyntaxError(`${name}: is read only`);
|
|
734
756
|
const expanded = await expandedOperand();
|
|
735
|
-
state.env[name] = expanded;
|
|
757
|
+
state.env[name] = stripExpansionMarkers(expanded);
|
|
736
758
|
return expanded;
|
|
737
759
|
}
|
|
738
760
|
return value;
|
|
@@ -742,11 +764,48 @@ async function parameterExpansion(body, state) {
|
|
|
742
764
|
}
|
|
743
765
|
|
|
744
766
|
function markedExpansion(value) {
|
|
745
|
-
return `\
|
|
767
|
+
return `\ufdd0${value}\ufdd1`;
|
|
746
768
|
}
|
|
747
769
|
|
|
748
770
|
function stripExpansionMarkers(value) {
|
|
749
|
-
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;
|
|
750
809
|
}
|
|
751
810
|
|
|
752
811
|
async function commandSubstitution(source, state) {
|
|
@@ -755,17 +814,24 @@ async function commandSubstitution(source, state) {
|
|
|
755
814
|
env: { ...state.env },
|
|
756
815
|
aliases: { ...state.aliases },
|
|
757
816
|
readonly: new Set(state.readonly),
|
|
817
|
+
exported: new Set(state.exported),
|
|
758
818
|
directoryHistory: [...state.directoryHistory],
|
|
759
819
|
tabs: [...state.tabs],
|
|
760
820
|
exitRequested: false,
|
|
761
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(),
|
|
762
825
|
};
|
|
826
|
+
// Command substitution installs its own stdout pipe even inside >file.
|
|
827
|
+
delete child.fdBindings[1];
|
|
763
828
|
const output = await execute(source, child, { capture: true });
|
|
764
|
-
|
|
765
|
-
|
|
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+$/, "");
|
|
766
832
|
}
|
|
767
833
|
|
|
768
|
-
async function expandText(text, state) {
|
|
834
|
+
async function expandText(text, state, quoted = false) {
|
|
769
835
|
let output = "";
|
|
770
836
|
for (let i = 0; i < text.length; ) {
|
|
771
837
|
if (text[i] === "\u0000") {
|
|
@@ -775,7 +841,11 @@ async function expandText(text, state) {
|
|
|
775
841
|
}
|
|
776
842
|
if (text[i] === "`") {
|
|
777
843
|
const end = substitutionEnd(text, i);
|
|
778
|
-
|
|
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));
|
|
779
849
|
i = end;
|
|
780
850
|
continue;
|
|
781
851
|
}
|
|
@@ -785,7 +855,8 @@ async function expandText(text, state) {
|
|
|
785
855
|
}
|
|
786
856
|
if (text.startsWith("$((", i)) {
|
|
787
857
|
const end = substitutionEnd(text, i);
|
|
788
|
-
|
|
858
|
+
const expression = stripExpansionMarkers(await expandText(text.slice(i + 3, end - 2), state));
|
|
859
|
+
output += markedExpansion(arithmeticValue(expression, state));
|
|
789
860
|
i = end;
|
|
790
861
|
continue;
|
|
791
862
|
}
|
|
@@ -797,20 +868,22 @@ async function expandText(text, state) {
|
|
|
797
868
|
}
|
|
798
869
|
if (text[i + 1] === "{") {
|
|
799
870
|
const end = parameterEnd(text, i);
|
|
800
|
-
output += markedExpansion(await parameterExpansion(text.slice(i + 2, end - 1), state));
|
|
871
|
+
output += markedExpansion(await parameterExpansion(text.slice(i + 2, end - 1), state, quoted));
|
|
801
872
|
i = end;
|
|
802
873
|
continue;
|
|
803
874
|
}
|
|
804
875
|
const next = text[i + 1];
|
|
805
876
|
if (["?", "$", "#", "@", "*", "-"].includes(next) || /[0-9]/.test(next ?? "")) {
|
|
806
|
-
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));
|
|
807
880
|
i += 2;
|
|
808
881
|
continue;
|
|
809
882
|
}
|
|
810
883
|
if (isNameStart(next)) {
|
|
811
884
|
let end = i + 2;
|
|
812
885
|
while (isNameChar(text[end])) end++;
|
|
813
|
-
output += markedExpansion(
|
|
886
|
+
output += markedExpansion(checkedParameter(text.slice(i + 1, end), state));
|
|
814
887
|
i = end;
|
|
815
888
|
continue;
|
|
816
889
|
}
|
|
@@ -832,6 +905,7 @@ function splitFields(segments, state) {
|
|
|
832
905
|
forced = false;
|
|
833
906
|
};
|
|
834
907
|
for (const segment of segments) {
|
|
908
|
+
if (segment.boundary) { finish(true); continue; }
|
|
835
909
|
if (segment.quoted && segment.text === "") forced = true;
|
|
836
910
|
for (const ch of segment.text) {
|
|
837
911
|
if (!segment.quoted && segment.splittable && ifs.includes(ch)) {
|
|
@@ -896,42 +970,41 @@ function braceFields(field) {
|
|
|
896
970
|
}
|
|
897
971
|
|
|
898
972
|
export async function expandWord(word, state, options = {}) {
|
|
899
|
-
if (!options.single && word.fragments.length === 1 &&
|
|
900
|
-
word.fragments[0].quote === "double" && word.fragments[0].text === "$@")
|
|
901
|
-
return state.args.slice(1);
|
|
902
973
|
const segments = [];
|
|
903
|
-
for (
|
|
904
|
-
const fragment = word.fragments[index];
|
|
974
|
+
for (const fragment of word.fragments) {
|
|
905
975
|
const source = fragment.quote === "none" && !options.noTilde
|
|
906
|
-
? expandTildes(fragment.text, state, Boolean(options.assignment))
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
: await expandText(source, state);
|
|
911
|
-
if (fragment.quote !== "none") {
|
|
912
|
-
segments.push({ text: stripExpansionMarkers(text.replaceAll("\u0000", "")), quoted: true, splittable: false });
|
|
913
|
-
} else {
|
|
914
|
-
let plain = "";
|
|
915
|
-
let splittable = false;
|
|
916
|
-
const flush = () => {
|
|
917
|
-
if (plain) segments.push({ text: plain, quoted: false, splittable });
|
|
918
|
-
plain = "";
|
|
919
|
-
};
|
|
920
|
-
for (let i = 0; i < text.length; i++) {
|
|
921
|
-
if (text[i] === "\u0001") { flush(); splittable = true; continue; }
|
|
922
|
-
if (text[i] === "\u0002") { flush(); splittable = false; continue; }
|
|
923
|
-
if (text[i] !== "\u0000") { plain += text[i]; continue; }
|
|
924
|
-
flush();
|
|
925
|
-
segments.push({ text: text[++i] ?? "", quoted: true, splittable: false });
|
|
926
|
-
}
|
|
927
|
-
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;
|
|
928
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();
|
|
929
1002
|
}
|
|
930
1003
|
if (options.single) return [segments.map(({ text }) => text).join("")];
|
|
931
1004
|
const fields = splitFields(segments, state);
|
|
932
1005
|
const output = [];
|
|
933
1006
|
for (const field of fields)
|
|
934
|
-
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)));
|
|
935
1008
|
return output;
|
|
936
1009
|
}
|
|
937
1010
|
|
|
@@ -1057,10 +1130,16 @@ async function sourceFile(argv, state) {
|
|
|
1057
1130
|
try {
|
|
1058
1131
|
const source = await Bun.file(path).text();
|
|
1059
1132
|
const previousArgs = state.args;
|
|
1133
|
+
const previousLoopDepth = state.loopDepth;
|
|
1134
|
+
state.loopDepth = 0;
|
|
1060
1135
|
if (argv.length > 2) state.args = [previousArgs[0], ...argv.slice(2)];
|
|
1136
|
+
state.returnDepth++;
|
|
1061
1137
|
try {
|
|
1062
1138
|
return await execute(source, state, { capture: true });
|
|
1063
1139
|
} finally {
|
|
1140
|
+
state.returnDepth--;
|
|
1141
|
+
state.loopDepth = previousLoopDepth;
|
|
1142
|
+
if (state.flow?.kind === "return") state.flow = null;
|
|
1064
1143
|
state.args = previousArgs;
|
|
1065
1144
|
}
|
|
1066
1145
|
} catch (error) {
|
|
@@ -1125,7 +1204,112 @@ function evaluateTest(argv, state) {
|
|
|
1125
1204
|
throw new Error("unsupported expression");
|
|
1126
1205
|
}
|
|
1127
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
|
+
|
|
1128
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),
|
|
1129
1313
|
":": async () => result(),
|
|
1130
1314
|
true: async () => result(),
|
|
1131
1315
|
false: async () => result(1),
|
|
@@ -1272,11 +1456,13 @@ const builtins = {
|
|
|
1272
1456
|
fd = Number(arg.slice(2));
|
|
1273
1457
|
continue;
|
|
1274
1458
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
+
}
|
|
1280
1466
|
}
|
|
1281
1467
|
let output = argv.slice(i).join(separator);
|
|
1282
1468
|
if (!raw) {
|
|
@@ -1340,6 +1526,25 @@ const builtins = {
|
|
|
1340
1526
|
if (state.readLines) {
|
|
1341
1527
|
gotInput = state.readLines.length > 0;
|
|
1342
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));
|
|
1343
1548
|
} else {
|
|
1344
1549
|
const reader = fallbackInput(input).getReader();
|
|
1345
1550
|
const chunks = [];
|
|
@@ -1461,8 +1666,9 @@ const builtins = {
|
|
|
1461
1666
|
"..": async (_argv, state) => changeDirectory(state, ".."),
|
|
1462
1667
|
"//": async (_argv, state) => previousChildDirectory(state),
|
|
1463
1668
|
export: async (argv, state) => {
|
|
1464
|
-
if (argv.length === 1) {
|
|
1669
|
+
if (argv.length === 1 || argv.length === 2 && argv[1] === "-p") {
|
|
1465
1670
|
const listing = Object.entries(state.env)
|
|
1671
|
+
.filter(([name]) => !state.shellOptions.posix || state.exported.has(name))
|
|
1466
1672
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
1467
1673
|
.map(([name, value]) => `export ${name}=${JSON.stringify(value)}\n`)
|
|
1468
1674
|
.join("");
|
|
@@ -1477,6 +1683,7 @@ const builtins = {
|
|
|
1477
1683
|
else if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(item))
|
|
1478
1684
|
state.env[item] ??= "";
|
|
1479
1685
|
else return result(1, "", `bunmsh: export: ${item}: invalid name\n`);
|
|
1686
|
+
state.exported.add(assignment?.name ?? item);
|
|
1480
1687
|
}
|
|
1481
1688
|
return result();
|
|
1482
1689
|
},
|
|
@@ -1488,6 +1695,7 @@ const builtins = {
|
|
|
1488
1695
|
return result(1, "", `bunmsh: unset: ${name}: invalid name\n`);
|
|
1489
1696
|
if (state.readonly.has(name)) return readonlyError(name);
|
|
1490
1697
|
delete state.env[name];
|
|
1698
|
+
state.exported.delete(name);
|
|
1491
1699
|
}
|
|
1492
1700
|
return result();
|
|
1493
1701
|
},
|
|
@@ -1513,7 +1721,7 @@ const builtins = {
|
|
|
1513
1721
|
return result();
|
|
1514
1722
|
},
|
|
1515
1723
|
env: async (argv, state, input, context = {}) => {
|
|
1516
|
-
let env = { ...state
|
|
1724
|
+
let env = { ...externalEnvironment(state) };
|
|
1517
1725
|
let i = 1;
|
|
1518
1726
|
for (; i < argv.length; i++) {
|
|
1519
1727
|
const item = argv[i];
|
|
@@ -1535,6 +1743,7 @@ const builtins = {
|
|
|
1535
1743
|
const childState = {
|
|
1536
1744
|
...state,
|
|
1537
1745
|
env,
|
|
1746
|
+
exported: new Set(Object.keys(env)),
|
|
1538
1747
|
directoryHistory: [...state.directoryHistory],
|
|
1539
1748
|
tabs: [...state.tabs],
|
|
1540
1749
|
readonly: new Set(state.readonly),
|
|
@@ -1677,14 +1886,37 @@ const builtins = {
|
|
|
1677
1886
|
}
|
|
1678
1887
|
return result(status, "", stderr);
|
|
1679
1888
|
},
|
|
1889
|
+
trap: async (argv,state) => setTrap(argv,state),
|
|
1680
1890
|
set: async (argv, state) => {
|
|
1681
1891
|
if (argv.length === 1)
|
|
1682
1892
|
return result(0, Object.entries(state.env).sort().map(([k, v]) => `${k}=${JSON.stringify(v)}\n`).join(""));
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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
|
+
}
|
|
1686
1917
|
}
|
|
1687
|
-
|
|
1918
|
+
if (i < argv.length || argv.at(-1) === "--") state.args = [state.args[0],...argv.slice(i)];
|
|
1919
|
+
return result();
|
|
1688
1920
|
},
|
|
1689
1921
|
};
|
|
1690
1922
|
|
|
@@ -1702,6 +1934,13 @@ export function bunShellFallbackArgv(argv) {
|
|
|
1702
1934
|
let commandArgv = argv;
|
|
1703
1935
|
if (argv[0] === "ls")
|
|
1704
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
|
+
}
|
|
1705
1944
|
if (argv[0] === "cp")
|
|
1706
1945
|
commandArgv = commandArgv.map((value, index) =>
|
|
1707
1946
|
index > 0 && /^-[^-]/.test(value) ? `-${value.slice(1).replaceAll("r", "R")}` : value);
|
|
@@ -1709,9 +1948,36 @@ export function bunShellFallbackArgv(argv) {
|
|
|
1709
1948
|
}
|
|
1710
1949
|
|
|
1711
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();
|
|
1712
1978
|
const shell = Bun.$`${bunShellFallbackArgv(argv)}`
|
|
1713
1979
|
.cwd(nativePath(state.cwd))
|
|
1714
|
-
.env(state
|
|
1980
|
+
.env(externalEnvironment(state))
|
|
1715
1981
|
.nothrow();
|
|
1716
1982
|
if (state.pipelineChild) {
|
|
1717
1983
|
const output = await shell;
|
|
@@ -1727,6 +1993,42 @@ async function runFancyLsFallback(argv, state) {
|
|
|
1727
1993
|
}
|
|
1728
1994
|
|
|
1729
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
|
+
}
|
|
1730
2032
|
const cmd = IS_COMPILED
|
|
1731
2033
|
? [...EXECUTABLE_COMMAND, "--bun-shell-fallback", ...bunShellFallbackArgv(argv)]
|
|
1732
2034
|
: [...BUN_RUNTIME_COMMAND, BUNMSH_ENTRY, "--bun-shell-fallback", ...bunShellFallbackArgv(argv)];
|
|
@@ -1734,7 +2036,7 @@ async function runBunShellCpFallback(argv, state) {
|
|
|
1734
2036
|
cmd,
|
|
1735
2037
|
cwd: nativePath(state.cwd),
|
|
1736
2038
|
env: {
|
|
1737
|
-
...state
|
|
2039
|
+
...externalEnvironment(state),
|
|
1738
2040
|
BUN_ENABLE_EXPERIMENTAL_SHELL_BUILTINS: "1",
|
|
1739
2041
|
},
|
|
1740
2042
|
stdin: "inherit",
|
|
@@ -1756,7 +2058,7 @@ async function runReflectedProcess(cmd, state) {
|
|
|
1756
2058
|
const proc = Bun.spawn({
|
|
1757
2059
|
cmd,
|
|
1758
2060
|
cwd: nativePath(state.cwd),
|
|
1759
|
-
env: state
|
|
2061
|
+
env: externalEnvironment(state),
|
|
1760
2062
|
stdin: "inherit",
|
|
1761
2063
|
stdout: inherit ? "inherit" : "pipe",
|
|
1762
2064
|
stderr: inherit ? "inherit" : "pipe",
|
|
@@ -1802,7 +2104,10 @@ async function runCatFallback(argv, state, input) {
|
|
|
1802
2104
|
const chunks = [];
|
|
1803
2105
|
let status = 0;
|
|
1804
2106
|
let stderr = "";
|
|
1805
|
-
|
|
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;
|
|
1806
2111
|
const emit = async (chunk) => {
|
|
1807
2112
|
if (writer) {
|
|
1808
2113
|
writer.write(chunk);
|
|
@@ -1933,6 +2238,44 @@ async function runHeadFallback(argv, state, input) {
|
|
|
1933
2238
|
return result(0, concatBytes(output));
|
|
1934
2239
|
}
|
|
1935
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
|
+
|
|
1936
2279
|
async function runTextFilter(argv, state, input, kind) {
|
|
1937
2280
|
try {
|
|
1938
2281
|
let args = argv.slice(1), reverse = false, numeric = false, unique = false;
|
|
@@ -2049,17 +2392,29 @@ function expandTrSet(value) {
|
|
|
2049
2392
|
}
|
|
2050
2393
|
|
|
2051
2394
|
async function runTrFallback(argv, _state, input) {
|
|
2052
|
-
let del = false, i = 1;
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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);
|
|
2057
2410
|
const text = decoder.decode(await readFallbackInput(input));
|
|
2058
|
-
let output = "";
|
|
2411
|
+
let output = "", previous;
|
|
2059
2412
|
for (const ch of text) {
|
|
2060
2413
|
const index = from.indexOf(ch);
|
|
2061
|
-
if (index
|
|
2062
|
-
|
|
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;
|
|
2063
2418
|
}
|
|
2064
2419
|
return result(0, output);
|
|
2065
2420
|
}
|
|
@@ -2076,7 +2431,7 @@ async function runCurlFallback(argv, state, input, context = {}) {
|
|
|
2076
2431
|
const stderr = [];
|
|
2077
2432
|
const io = {
|
|
2078
2433
|
cwd: nativePath(state.cwd),
|
|
2079
|
-
env: state
|
|
2434
|
+
env: externalEnvironment(state),
|
|
2080
2435
|
stdoutIsTTY: streamStdout && Boolean(process.stdout.isTTY),
|
|
2081
2436
|
readStdin: () => readFallbackInput(input),
|
|
2082
2437
|
async writeStdout(chunk) {
|
|
@@ -2206,17 +2561,24 @@ function grepFiles(operands, state, recursive) {
|
|
|
2206
2561
|
}
|
|
2207
2562
|
|
|
2208
2563
|
function grepBasicRegex(pattern) {
|
|
2209
|
-
let output = "", inClass = false;
|
|
2564
|
+
let output = "", inClass = false, branchStart = true;
|
|
2210
2565
|
for (let i = 0; i < pattern.length; i++) {
|
|
2211
2566
|
const ch = pattern[i];
|
|
2212
|
-
if (ch === "
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2567
|
+
if (!inClass && ch === "\\" && i + 1 < pattern.length) {
|
|
2568
|
+
const next=pattern[++i];
|
|
2569
|
+
output += "+?|(){}".includes(next) ? next : `\\${next}`;
|
|
2570
|
+
branchStart = next === "(" || next === "|";
|
|
2216
2571
|
continue;
|
|
2217
2572
|
}
|
|
2218
|
-
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}`;
|
|
2219
2578
|
else output += ch;
|
|
2579
|
+
if (ch === "[") inClass = true;
|
|
2580
|
+
else if (ch === "]" && inClass) inClass = false;
|
|
2581
|
+
branchStart=false;
|
|
2220
2582
|
}
|
|
2221
2583
|
return output;
|
|
2222
2584
|
}
|
|
@@ -2233,6 +2595,7 @@ function grepPosixClasses(pattern) {
|
|
|
2233
2595
|
|
|
2234
2596
|
async function runGrepFallback(argv, state, input, context = {}) {
|
|
2235
2597
|
let flags = "", colorMode = "never", i = 1;
|
|
2598
|
+
const patterns=[];
|
|
2236
2599
|
while (argv[i]?.startsWith("-") && argv[i] !== "-") {
|
|
2237
2600
|
if (argv[i] === "--") { i++; break; }
|
|
2238
2601
|
const colorOption = /^--colou?r(?:=(always|auto|never))?$/.exec(argv[i]);
|
|
@@ -2241,16 +2604,24 @@ async function runGrepFallback(argv, state, input, context = {}) {
|
|
|
2241
2604
|
i++;
|
|
2242
2605
|
continue;
|
|
2243
2606
|
}
|
|
2244
|
-
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]))
|
|
2245
2613
|
return result(2, "", `bunmsh: grep: unsupported option: ${argv[i]}\n`);
|
|
2246
2614
|
flags += argv[i++].slice(1);
|
|
2247
2615
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
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
|
+
}
|
|
2250
2621
|
let regex;
|
|
2251
|
-
const regexSource
|
|
2252
|
-
? pattern.replace(/[\\^$.*+?()[\]{}|]/g,
|
|
2253
|
-
: 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("|");
|
|
2254
2625
|
try { regex = new RegExp(flags.includes("x") ? `^(?:${regexSource})$` : regexSource, `${flags.includes("i") ? "i" : ""}${flags.includes("o") ? "g" : ""}`); }
|
|
2255
2626
|
catch (error) { return result(2, "", `bunmsh: grep: ${error.message}\n`); }
|
|
2256
2627
|
const invert = flags.includes("v"), quiet = flags.includes("q"), numbers = flags.includes("n"), only = flags.includes("o");
|
|
@@ -2261,15 +2632,16 @@ async function runGrepFallback(argv, state, input, context = {}) {
|
|
|
2261
2632
|
const highlightRegex = color && !only
|
|
2262
2633
|
? new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`)
|
|
2263
2634
|
: null;
|
|
2264
|
-
let matched = false;
|
|
2635
|
+
let matched = false, count = 0;
|
|
2265
2636
|
const output = [];
|
|
2266
2637
|
const processLine = (line, lineNumber, label, showLabel) => {
|
|
2267
2638
|
regex.lastIndex = 0;
|
|
2268
2639
|
const isMatch = regex.test(line);
|
|
2269
2640
|
regex.lastIndex = 0;
|
|
2270
2641
|
if (isMatch === invert) return false;
|
|
2271
|
-
matched = true;
|
|
2642
|
+
matched = true; count++;
|
|
2272
2643
|
if (quiet) return true;
|
|
2644
|
+
if (flags.includes("c")) return false;
|
|
2273
2645
|
const prefix = `${showLabel ? `${label}:` : ""}${numbers ? `${lineNumber}:` : ""}`;
|
|
2274
2646
|
if (only) {
|
|
2275
2647
|
if (invert) return false;
|
|
@@ -2292,11 +2664,13 @@ async function runGrepFallback(argv, state, input, context = {}) {
|
|
|
2292
2664
|
const files = grepFiles(operands, state, flags.includes("r"));
|
|
2293
2665
|
const showLabel = files.length > 1 || flags.includes("r");
|
|
2294
2666
|
for (const file of files) {
|
|
2667
|
+
count=0;
|
|
2295
2668
|
const lines = (await Bun.file(file.path).text()).split("\n");
|
|
2296
2669
|
if (lines.at(-1) === "") lines.pop();
|
|
2297
2670
|
for (let line = 0; line < lines.length; line++)
|
|
2298
2671
|
if (processLine(lines[line], line + 1,
|
|
2299
2672
|
implicitCwd ? file.shown.replace(/^\.\//, "") : file.shown, showLabel)) return result(0);
|
|
2673
|
+
if (flags.includes("c")) output.push(`${showLabel ? `${file.shown}:` : ""}${count}\n`);
|
|
2300
2674
|
}
|
|
2301
2675
|
} catch (error) { return result(2, "", `bunmsh: grep: ${error.message}\n`); }
|
|
2302
2676
|
} else {
|
|
@@ -2316,93 +2690,10 @@ async function runGrepFallback(argv, state, input, context = {}) {
|
|
|
2316
2690
|
}
|
|
2317
2691
|
} finally { try { reader.releaseLock(); } catch {} }
|
|
2318
2692
|
}
|
|
2693
|
+
if (!operands.length && flags.includes("c") && !quiet) output.push(`${count}\n`);
|
|
2319
2694
|
return result(matched ? 0 : 1, output.join(""));
|
|
2320
2695
|
}
|
|
2321
2696
|
|
|
2322
|
-
function splitSedCommands(script) {
|
|
2323
|
-
const commands = [];
|
|
2324
|
-
let start = 0, escaped = false;
|
|
2325
|
-
for (let i = 0; i < script.length; i++) {
|
|
2326
|
-
if (escaped) { escaped = false; continue; }
|
|
2327
|
-
if (script[i] === "\\") { escaped = true; continue; }
|
|
2328
|
-
if (script[i] === ";") {
|
|
2329
|
-
commands.push(script.slice(start, i).trim());
|
|
2330
|
-
start = i + 1;
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
commands.push(script.slice(start).trim());
|
|
2334
|
-
return commands.filter(Boolean);
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
function readSedField(command, start, delimiter) {
|
|
2338
|
-
let value = "";
|
|
2339
|
-
for (let i = start; i < command.length; i++) {
|
|
2340
|
-
if (command[i] === "\\" && i + 1 < command.length) {
|
|
2341
|
-
if (command[i + 1] === delimiter) value += command[++i];
|
|
2342
|
-
else value += command[i] + command[++i];
|
|
2343
|
-
} else if (command[i] === delimiter) return [value, i + 1];
|
|
2344
|
-
else value += command[i];
|
|
2345
|
-
}
|
|
2346
|
-
throw new Error("unterminated substitute command");
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
|
-
function sedReplacement(value) {
|
|
2350
|
-
let output = "";
|
|
2351
|
-
for (let i = 0; i < value.length; i++) {
|
|
2352
|
-
if (value[i] === "\\" && /[1-9]/.test(value[i + 1] ?? "")) output += `$${value[++i]}`;
|
|
2353
|
-
else if (value[i] === "\\" && i + 1 < value.length) output += value[++i];
|
|
2354
|
-
else if (value[i] === "&") output += "$&";
|
|
2355
|
-
else if (value[i] === "$") output += "$$";
|
|
2356
|
-
else output += value[i];
|
|
2357
|
-
}
|
|
2358
|
-
return output;
|
|
2359
|
-
}
|
|
2360
|
-
|
|
2361
|
-
function compileSedCommand(source, extended) {
|
|
2362
|
-
const numberedPrint = /^(\d+)p$/.exec(source);
|
|
2363
|
-
if (numberedPrint) return { type: "print", line: Number(numberedPrint[1]) };
|
|
2364
|
-
if (!source.startsWith("s") || source.length < 2)
|
|
2365
|
-
throw new Error(`unsupported command: ${source}`);
|
|
2366
|
-
const delimiter = source[1];
|
|
2367
|
-
const [pattern, replacementStart] = readSedField(source, 2, delimiter);
|
|
2368
|
-
const [replacement, flagsStart] = readSedField(source, replacementStart, delimiter);
|
|
2369
|
-
const flags = source.slice(flagsStart);
|
|
2370
|
-
if (!/^[gp]*$/.test(flags)) throw new Error(`unsupported substitute flags: ${flags}`);
|
|
2371
|
-
const regexSource = grepPosixClasses(extended ? pattern : grepBasicRegex(pattern));
|
|
2372
|
-
return {
|
|
2373
|
-
type: "substitute",
|
|
2374
|
-
regex: new RegExp(regexSource, flags.includes("g") ? "g" : ""),
|
|
2375
|
-
replacement: sedReplacement(replacement),
|
|
2376
|
-
print: flags.includes("p"),
|
|
2377
|
-
};
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
function applySed(text, commands, quiet) {
|
|
2381
|
-
const trailingNewline = text.endsWith("\n");
|
|
2382
|
-
const lines = text.split("\n");
|
|
2383
|
-
if (trailingNewline) lines.pop();
|
|
2384
|
-
const output = [];
|
|
2385
|
-
for (let index = 0; index < lines.length; index++) {
|
|
2386
|
-
let line = lines[index];
|
|
2387
|
-
for (const command of commands) {
|
|
2388
|
-
if (command.type === "print") {
|
|
2389
|
-
if (command.line === index + 1) output.push(line);
|
|
2390
|
-
continue;
|
|
2391
|
-
}
|
|
2392
|
-
command.regex.lastIndex = 0;
|
|
2393
|
-
const matched = command.regex.test(line);
|
|
2394
|
-
command.regex.lastIndex = 0;
|
|
2395
|
-
if (matched) {
|
|
2396
|
-
line = line.replace(command.regex, command.replacement);
|
|
2397
|
-
if (command.print) output.push(line);
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
if (!quiet) output.push(line);
|
|
2401
|
-
}
|
|
2402
|
-
if (!output.length) return "";
|
|
2403
|
-
return output.join("\n") + (trailingNewline ? "\n" : "");
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
2697
|
async function runSedFallback(argv, state, input) {
|
|
2407
2698
|
let quiet = false, extended = false, inPlace = false, i = 1;
|
|
2408
2699
|
const scripts = [];
|
|
@@ -2415,6 +2706,13 @@ async function runSedFallback(argv, state, input) {
|
|
|
2415
2706
|
continue;
|
|
2416
2707
|
}
|
|
2417
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
|
+
}
|
|
2418
2716
|
if (!/^-[nEri]+$/.test(option))
|
|
2419
2717
|
return result(1, "", `bunmsh: sed: unsupported option: ${option}\n`);
|
|
2420
2718
|
quiet ||= option.includes("n");
|
|
@@ -2424,19 +2722,23 @@ async function runSedFallback(argv, state, input) {
|
|
|
2424
2722
|
if (!scripts.length && i < argv.length) scripts.push(argv[i++]);
|
|
2425
2723
|
if (!scripts.length) return result(1, "", "bunmsh: sed: missing script\n");
|
|
2426
2724
|
let commands;
|
|
2427
|
-
try { commands = scripts.
|
|
2725
|
+
try { commands = compileSed(scripts.join("\n"), pattern => grepPosixClasses(extended ? pattern : grepBasicRegex(pattern))); }
|
|
2428
2726
|
catch (error) { return result(1, "", `bunmsh: sed: ${error.message}\n`); }
|
|
2429
2727
|
const operands = argv.slice(i);
|
|
2430
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
|
+
};
|
|
2431
2733
|
try {
|
|
2432
2734
|
if (!operands.length) {
|
|
2433
2735
|
const text = decoder.decode(await readFallbackInput(input));
|
|
2434
|
-
return result(0, applySed(text, commands, quiet));
|
|
2736
|
+
return result(0, applySed(text, commands, quiet, readExtra));
|
|
2435
2737
|
}
|
|
2436
2738
|
let stdout = "";
|
|
2437
2739
|
for (const operand of operands) {
|
|
2438
2740
|
const file = isAbsolute(nativePath(operand)) ? nativePath(operand) : resolvePath(nativePath(state.cwd), nativePath(operand));
|
|
2439
|
-
const changed = applySed(await Bun.file(file).text(), commands, quiet);
|
|
2741
|
+
const changed = applySed(await Bun.file(file).text(), commands, quiet, readExtra);
|
|
2440
2742
|
if (inPlace) await Bun.write(file, changed);
|
|
2441
2743
|
else stdout += changed;
|
|
2442
2744
|
}
|
|
@@ -2510,16 +2812,47 @@ function runLnFallback(argv, state) {
|
|
|
2510
2812
|
return result(status, "", stderr);
|
|
2511
2813
|
}
|
|
2512
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
|
+
|
|
2513
2846
|
function runChmodFallback(argv, state) {
|
|
2514
|
-
|
|
2515
|
-
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");
|
|
2516
2851
|
let status = 0, stderr = "";
|
|
2517
|
-
for (const operand of argv.slice(
|
|
2852
|
+
for (const operand of argv.slice(i)) {
|
|
2518
2853
|
const path = isAbsolute(nativePath(operand)) ? nativePath(operand) : resolvePath(nativePath(state.cwd), nativePath(operand));
|
|
2519
2854
|
try {
|
|
2520
|
-
|
|
2521
|
-
else if (mode === "+x" || mode === "a+x") chmodSync(path, statSync(path).mode | 0o111);
|
|
2522
|
-
else throw new Error(`unsupported mode: ${mode}`);
|
|
2855
|
+
chmodSync(path, /^[0-7]{1,4}$/.test(mode) ? Number.parseInt(mode, 8) : symbolicFileMode(mode, statSync(path)));
|
|
2523
2856
|
} catch (error) { status = 1; stderr += `bunmsh: chmod: ${operand}: ${error.message}\n`; }
|
|
2524
2857
|
}
|
|
2525
2858
|
return result(status, "", stderr);
|
|
@@ -2640,7 +2973,7 @@ async function queryProcessList(name, state) {
|
|
|
2640
2973
|
proc = Bun.spawn({
|
|
2641
2974
|
cmd: command,
|
|
2642
2975
|
cwd: nativePath(state.cwd),
|
|
2643
|
-
env: state
|
|
2976
|
+
env: externalEnvironment(state),
|
|
2644
2977
|
stdin: "ignore",
|
|
2645
2978
|
stdout: "pipe",
|
|
2646
2979
|
stderr: "pipe",
|
|
@@ -2822,12 +3155,157 @@ async function runPspacFallback(argv, state) {
|
|
|
2822
3155
|
return result(0, colorProcessTable(rows), query.stderr);
|
|
2823
3156
|
}
|
|
2824
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(pipelineCompoundArgv(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
|
+
|
|
2825
3299
|
const fallbackBuiltins = {
|
|
3300
|
+
awk: runAwkFallback,
|
|
3301
|
+
diff: runDiffFallback,
|
|
3302
|
+
cmp: runCmpFallback,
|
|
3303
|
+
expr: runExprFallback,
|
|
2826
3304
|
basename: async (argv) => {
|
|
2827
3305
|
const i = argv[1] === "--" ? 2 : 1;
|
|
2828
3306
|
if (i >= argv.length || argv.length - i > 2)
|
|
2829
3307
|
return result(1, "", "bunmsh: basename: usage: basename string [suffix]\n");
|
|
2830
|
-
let name = pathBasename(argv[i]);
|
|
3308
|
+
let name = /^\/+$/.test(argv[i]) ? "/" : pathBasename(argv[i]);
|
|
2831
3309
|
const suffix = argv[i + 1];
|
|
2832
3310
|
if (suffix && suffix !== name && name.endsWith(suffix)) name = name.slice(0, -suffix.length);
|
|
2833
3311
|
return result(0, `${name}\n`);
|
|
@@ -2867,11 +3345,14 @@ const fallbackBuiltins = {
|
|
|
2867
3345
|
clear: async () => result(0, "\x1b[2J\x1b[H"),
|
|
2868
3346
|
rmdir: async (argv, state) => runRmdirFallback(argv, state),
|
|
2869
3347
|
mktemp: async (argv, state) => runMktempFallback(argv, state),
|
|
3348
|
+
uniq: runUniqFallback,
|
|
2870
3349
|
sort: (argv, state, input) => runTextFilter(argv, state, input, "sort"),
|
|
2871
3350
|
date: async (argv) => runDateFallback(argv),
|
|
2872
3351
|
md5sum: (argv, state, input) => runHashFallback(argv, state, input, "md5"),
|
|
2873
3352
|
sha256sum: (argv, state, input) => runHashFallback(argv, state, input, "sha256"),
|
|
2874
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),
|
|
2875
3356
|
sed: runSedFallback,
|
|
2876
3357
|
cut: runCutFallback,
|
|
2877
3358
|
ln: async (argv, state) => runLnFallback(argv, state),
|
|
@@ -2921,8 +3402,14 @@ function formatMilliseconds(milliseconds, color = Boolean(process.stderr.isTTY))
|
|
|
2921
3402
|
return output;
|
|
2922
3403
|
}
|
|
2923
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
|
+
|
|
2924
3411
|
export function createState(options = {}) {
|
|
2925
|
-
const env = canonicalEnvironment({ ...process.env, ...(options.env ?? {}) });
|
|
3412
|
+
const env = canonicalEnvironment({ ...(options.inheritEnvironment === false ? {} : process.env), ...(options.env ?? {}) });
|
|
2926
3413
|
const cwd = shellPath(options.cwd ?? process.cwd());
|
|
2927
3414
|
env.PWD = cwd;
|
|
2928
3415
|
return {
|
|
@@ -2937,17 +3424,29 @@ export function createState(options = {}) {
|
|
|
2937
3424
|
mouseTracking: options.mouseTracking ?? /^(?:1|true|on|yes)$/i.test(env.BUNMSH_MOUSE ?? ""),
|
|
2938
3425
|
pathSearch: options.pathSearch ?? true,
|
|
2939
3426
|
readonly: new Set(options.readonly ?? []),
|
|
3427
|
+
exported: new Set(options.exported ?? Object.keys(env)),
|
|
2940
3428
|
getoptsOffset: 1,
|
|
2941
3429
|
args: options.args ?? ["bunmsh"],
|
|
2942
3430
|
lastStatus: 0,
|
|
2943
3431
|
exitRequested: false,
|
|
2944
3432
|
exitStatus: 0,
|
|
2945
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,
|
|
2946
3445
|
};
|
|
2947
3446
|
}
|
|
2948
3447
|
|
|
2949
3448
|
function hasCompoundSyntax(source) {
|
|
2950
|
-
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;
|
|
2951
3450
|
let commandStart = true;
|
|
2952
3451
|
for (const token of tokenize(source)) {
|
|
2953
3452
|
if (token.type === "op") {
|
|
@@ -2956,14 +3455,15 @@ function hasCompoundSyntax(source) {
|
|
|
2956
3455
|
}
|
|
2957
3456
|
const word = token.fragments.length === 1 && token.fragments[0].quote === "none"
|
|
2958
3457
|
? token.fragments[0].text : null;
|
|
2959
|
-
if (commandStart && ["if", "while", "until", "for", "case"].includes(word)) return true;
|
|
3458
|
+
if (commandStart && ["if", "while", "until", "for", "case", "{"].includes(word)) return true;
|
|
2960
3459
|
if (!(commandStart && /^[A-Za-z_][A-Za-z0-9_]*=/.test(word ?? ""))) commandStart = false;
|
|
2961
3460
|
}
|
|
2962
3461
|
return false;
|
|
2963
3462
|
}
|
|
2964
3463
|
|
|
2965
|
-
function compoundLogicalSource(source) {
|
|
2966
|
-
const
|
|
3464
|
+
function compoundLogicalSource(source, firstLine = 1, sourceLines = null) {
|
|
3465
|
+
const continuations = new Map();
|
|
3466
|
+
const tokens = tokenize(source, { continuations });
|
|
2967
3467
|
const breaks = new Set();
|
|
2968
3468
|
let commandStart = true;
|
|
2969
3469
|
const literal = (token) => token.type === "word" && token.fragments.length === 1 &&
|
|
@@ -2975,6 +3475,24 @@ function compoundLogicalSource(source) {
|
|
|
2975
3475
|
continue;
|
|
2976
3476
|
}
|
|
2977
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
|
+
}
|
|
2978
3496
|
if (!commandStart) continue;
|
|
2979
3497
|
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word ?? "")) continue;
|
|
2980
3498
|
// Like mksh's KEYWORD lexer mode, these words are special only where a
|
|
@@ -2990,11 +3508,15 @@ function compoundLogicalSource(source) {
|
|
|
2990
3508
|
breaks.add(tokens[index + 1].offset);
|
|
2991
3509
|
commandStart = ["if", "while", "until", "elif", "then", "do", "else"].includes(word);
|
|
2992
3510
|
}
|
|
2993
|
-
let logical =
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
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};
|
|
2998
3520
|
}
|
|
2999
3521
|
|
|
3000
3522
|
// A leaf command inside a compound body may continue across physical lines
|
|
@@ -3007,11 +3529,20 @@ function growLogicalCommand(lines, index) {
|
|
|
3007
3529
|
let candidate = line;
|
|
3008
3530
|
while (true) {
|
|
3009
3531
|
try {
|
|
3010
|
-
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
|
+
}
|
|
3011
3542
|
return { source: candidate, next: end + 1 };
|
|
3012
3543
|
} catch (error) {
|
|
3013
3544
|
if (!(error instanceof ShellSyntaxError) ||
|
|
3014
|
-
!/^(?:unterminated (?:here-document|single quote|double quote|command substitution|substitution))/.test(error.message))
|
|
3545
|
+
!/^(?:unterminated (?:here-document|single quote|double quote|command substitution|parameter expansion|substitution))/.test(error.message))
|
|
3015
3546
|
throw error;
|
|
3016
3547
|
end++;
|
|
3017
3548
|
if (end >= lines.length) throw error;
|
|
@@ -3020,16 +3551,75 @@ function growLogicalCommand(lines, index) {
|
|
|
3020
3551
|
}
|
|
3021
3552
|
}
|
|
3022
3553
|
|
|
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
|
+
|
|
3023
3572
|
function makeCompoundPostfix(compound, suffix, source, nextLine) {
|
|
3024
|
-
const
|
|
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 ?? "");
|
|
3025
3588
|
if (!match) return { node: compound, nextLine };
|
|
3026
3589
|
let command = match[2].trim();
|
|
3590
|
+
let commandLine = null;
|
|
3591
|
+
if (match[1] === ";" && (!command || command.startsWith("#"))) return { node: compound, nextLine };
|
|
3027
3592
|
while (!command && nextLine < source.length) {
|
|
3593
|
+
commandLine = nextLine;
|
|
3028
3594
|
command = source[nextLine++].trim();
|
|
3029
3595
|
if (!command || command.startsWith("#")) command = "";
|
|
3030
3596
|
}
|
|
3031
3597
|
if (!command)
|
|
3032
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
|
+
}
|
|
3033
3623
|
return {
|
|
3034
3624
|
node: {
|
|
3035
3625
|
type: "compoundPostfix",
|
|
@@ -3111,7 +3701,35 @@ function splitCasePatterns(source) {
|
|
|
3111
3701
|
// Scan case clauses in the same lexical spirit as mksh's CASEPAT/KEYWORD
|
|
3112
3702
|
// modes. In particular, clause boundaries are shell tokens, not line
|
|
3113
3703
|
// boundaries: Autoconf commonly writes an entire clause on one line.
|
|
3114
|
-
function
|
|
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) {
|
|
3115
3733
|
const arms = [];
|
|
3116
3734
|
let i = 0;
|
|
3117
3735
|
const skipSpace = () => {
|
|
@@ -3153,43 +3771,23 @@ function parseCaseArms(source) {
|
|
|
3153
3771
|
i++;
|
|
3154
3772
|
|
|
3155
3773
|
const bodyStart = i;
|
|
3156
|
-
quote = null;
|
|
3157
3774
|
let nestedCases = 0;
|
|
3158
|
-
let word = "";
|
|
3159
3775
|
let terminator = null;
|
|
3160
|
-
const
|
|
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);
|
|
3161
3782
|
if (word === "case") nestedCases++;
|
|
3162
3783
|
else if (word === "esac" && nestedCases) nestedCases--;
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
}
|
|
3171
|
-
if (ch === "\\") { word = ""; i++; continue; }
|
|
3172
|
-
if (quote) {
|
|
3173
|
-
if (ch === quote) quote = null;
|
|
3174
|
-
continue;
|
|
3175
|
-
}
|
|
3176
|
-
if (ch === "'" || ch === '"') { finishWord(); quote = ch; continue; }
|
|
3177
|
-
if (ch === "$" && (source[i + 1] === "(" || source[i + 1] === "{")) {
|
|
3178
|
-
finishWord();
|
|
3179
|
-
i = (source[i + 1] === "(" ? substitutionEnd(source, i) : parameterEnd(source, i)) - 1;
|
|
3180
|
-
continue;
|
|
3181
|
-
}
|
|
3182
|
-
if (ch === "`") {
|
|
3183
|
-
finishWord();
|
|
3184
|
-
try { i = substitutionEnd(source, i) - 1; }
|
|
3185
|
-
catch (error) { error.message += ` near ${JSON.stringify(source.slice(Math.max(0, i - 100), i + 180))}`; throw error; }
|
|
3186
|
-
continue;
|
|
3187
|
-
}
|
|
3188
|
-
if (/[A-Za-z_]/.test(ch)) { word += ch; continue; }
|
|
3189
|
-
finishWord();
|
|
3190
|
-
if (!nestedCases && ch === ";" && [";", "&", "|"].includes(source[i + 1])) {
|
|
3191
|
-
terminator = source.slice(i, i + 2);
|
|
3192
|
-
break;
|
|
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
|
+
}
|
|
3193
3791
|
}
|
|
3194
3792
|
}
|
|
3195
3793
|
// POSIX allows the final clause to omit `;;` immediately before esac.
|
|
@@ -3197,10 +3795,11 @@ function parseCaseArms(source) {
|
|
|
3197
3795
|
if (source.slice(bodyStart, i).trim() || arms.length) terminator = ";;";
|
|
3198
3796
|
else throw new ShellSyntaxError("unterminated case clause");
|
|
3199
3797
|
}
|
|
3200
|
-
const bodySource = source.slice(bodyStart, i)
|
|
3798
|
+
const bodySource = source.slice(bodyStart, i);
|
|
3201
3799
|
let body;
|
|
3202
3800
|
try {
|
|
3203
|
-
|
|
3801
|
+
const first = source.slice(0,bodyStart).split("\n").length - 1;
|
|
3802
|
+
body = parseCompoundScript(bodySource, sourceLines?.[first] ?? 1, sourceLines?.slice(first));
|
|
3204
3803
|
} catch (error) {
|
|
3205
3804
|
if (error instanceof ShellSyntaxError)
|
|
3206
3805
|
error.message += ` in case arm ${patterns.join("|")}`;
|
|
@@ -3212,8 +3811,41 @@ function parseCaseArms(source) {
|
|
|
3212
3811
|
return arms;
|
|
3213
3812
|
}
|
|
3214
3813
|
|
|
3215
|
-
|
|
3216
|
-
|
|
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/);
|
|
3217
3849
|
const parseSequence = (start, stops = []) => {
|
|
3218
3850
|
const nodes = [];
|
|
3219
3851
|
let i = start;
|
|
@@ -3223,51 +3855,63 @@ export function parseCompoundScript(source) {
|
|
|
3223
3855
|
if (stops.some((stop) => stop.test(trimmed))) break;
|
|
3224
3856
|
if (!trimmed || trimmed.startsWith("#")) { i++; continue; }
|
|
3225
3857
|
|
|
3226
|
-
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);
|
|
3227
3859
|
if (functionMatch) {
|
|
3228
|
-
|
|
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|;|$)/]);
|
|
3229
3865
|
if (body.index >= lines.length) throw new ShellSyntaxError(`unterminated function ${functionMatch[1]}`);
|
|
3230
3866
|
nodes.push({ type: "function", name: functionMatch[1], body: body.nodes });
|
|
3231
3867
|
i = body.index + 1;
|
|
3232
3868
|
continue;
|
|
3233
3869
|
}
|
|
3234
3870
|
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
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;
|
|
3239
3878
|
continue;
|
|
3240
3879
|
}
|
|
3241
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
|
+
|
|
3242
3894
|
if (/^if(?:\s|$)/.test(trimmed)) {
|
|
3243
3895
|
const compoundStart = i;
|
|
3244
3896
|
const branches = [];
|
|
3245
|
-
let header =
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
if (!then) throw new ShellSyntaxError("if requires then");
|
|
3250
|
-
let condition = header.slice(0, then.index).replace(/^if\s*/, "").trim();
|
|
3251
|
-
let thenCommand = then[1]?.trim() ?? "";
|
|
3252
|
-
i++;
|
|
3897
|
+
let header = readCompoundHeader(lines, i, "then");
|
|
3898
|
+
let condition = header.condition;
|
|
3899
|
+
let thenCommand = header.tail;
|
|
3900
|
+
i = header.end + 1;
|
|
3253
3901
|
while (true) {
|
|
3254
|
-
const body = parseSequence(i, [/^elif(?:\s|$)/, /^else
|
|
3902
|
+
const body = parseSequence(i, [/^elif(?:\s|$)/, /^else(?:\s*;\s*|\s+|$)(?:#.*)?$/, /^fi\b/]);
|
|
3255
3903
|
branches.push({ condition, body: body.nodes, thenCommand });
|
|
3256
3904
|
i = body.index;
|
|
3257
3905
|
const stop = lines[i]?.trim() ?? "";
|
|
3258
3906
|
if (/^elif(?:\s|$)/.test(stop)) {
|
|
3259
|
-
header =
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
if (!then) throw new ShellSyntaxError("elif requires then");
|
|
3264
|
-
condition = header.slice(0, then.index).replace(/^elif\s*/, "").trim();
|
|
3265
|
-
thenCommand = then[1]?.trim() ?? "";
|
|
3266
|
-
i++;
|
|
3907
|
+
header = readCompoundHeader(lines, i, "then");
|
|
3908
|
+
condition = header.condition;
|
|
3909
|
+
thenCommand = header.tail;
|
|
3910
|
+
i = header.end + 1;
|
|
3267
3911
|
continue;
|
|
3268
3912
|
}
|
|
3269
3913
|
let otherwise = [];
|
|
3270
|
-
if (/^else
|
|
3914
|
+
if (/^else(?:\s*;\s*|\s+|$)(?:#.*)?$/.test(stop)) {
|
|
3271
3915
|
const body = parseSequence(i + 1, [/^fi\b/]);
|
|
3272
3916
|
otherwise = body.nodes;
|
|
3273
3917
|
i = body.index;
|
|
@@ -3297,7 +3941,7 @@ export function parseCompoundScript(source) {
|
|
|
3297
3941
|
if (!/(?:;\s*|\s+)do\s*$/.test(header)) throw new ShellSyntaxError(`${kind} requires do`);
|
|
3298
3942
|
const condition = header.replace(new RegExp(`^${kind}\\s*`), "").replace(/(?:;\s*|\s+)do\s*$/, "");
|
|
3299
3943
|
const body = parseSequence(i + 1, [/^done\b/]);
|
|
3300
|
-
const done = /^done(?:\s*<\s*(.+?))?\s*;?\s*((
|
|
3944
|
+
const done = /^done(?=$|[\s;<|&])(?:\s*<\s*(.+?))?\s*;?\s*((?:\|\||&&|\||;|#).*)?$/.exec(lines[body.index]?.trim() ?? "");
|
|
3301
3945
|
if (!done)
|
|
3302
3946
|
throw new ShellSyntaxError(`unterminated ${kind}`);
|
|
3303
3947
|
const compound = {
|
|
@@ -3344,62 +3988,44 @@ export function parseCompoundScript(source) {
|
|
|
3344
3988
|
continue;
|
|
3345
3989
|
}
|
|
3346
3990
|
if (/^case(?:\s|$)/.test(trimmed)) {
|
|
3347
|
-
const
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
const
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
word: match[1],
|
|
3360
|
-
arms: parseCaseArms(inlineArms),
|
|
3361
|
-
source: `case ${match[1]} in\n${inlineArms}\nesac`,
|
|
3362
|
-
};
|
|
3363
|
-
const result = makeCompoundPostfix(compound, suffix, lines, i + 1);
|
|
3364
|
-
nodes.push(result.node);
|
|
3365
|
-
i = result.nextLine;
|
|
3366
|
-
continue;
|
|
3367
|
-
}
|
|
3368
|
-
const bodyLines = match[2] ? [match[2]] : [];
|
|
3369
|
-
i++;
|
|
3370
|
-
let nestedCases = 0;
|
|
3371
|
-
while (i < lines.length) {
|
|
3372
|
-
const current = lines[i].trim();
|
|
3373
|
-
const nestedCase = findShellKeyword(current, "case");
|
|
3374
|
-
if (nestedCase >= 0 && /\s/.test(current[nestedCase + "case".length] ?? ""))
|
|
3375
|
-
nestedCases++;
|
|
3376
|
-
// In an enclosing case arm, mksh tokenises `esac;;` as two
|
|
3377
|
-
// independent tokens: ESAC followed by the enclosing arm's BREAK.
|
|
3378
|
-
// Count that as the end of the nested case while retaining the
|
|
3379
|
-
// whole line for parseCaseArms to assign the `;;` to its parent.
|
|
3380
|
-
const closingCase = findShellKeyword(current, "esac");
|
|
3381
|
-
if (closingCase >= 0) {
|
|
3382
|
-
if (!nestedCases) break;
|
|
3383
|
-
nestedCases--;
|
|
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--;
|
|
3384
4003
|
}
|
|
3385
|
-
bodyLines.push(lines[i++]);
|
|
3386
4004
|
}
|
|
3387
|
-
if (
|
|
3388
|
-
|
|
3389
|
-
const
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
const result = makeCompoundPostfix(compound, close?.[1] ?? "", lines, i + 1);
|
|
3395
|
-
nodes.push(result.node);
|
|
3396
|
-
i = result.nextLine;
|
|
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;
|
|
3397
4012
|
continue;
|
|
3398
4013
|
}
|
|
3399
4014
|
const grown = growLogicalCommand(lines, i);
|
|
3400
|
-
nodes.push({ type: "command", source: grown.source });
|
|
4015
|
+
nodes.push({ type: "command", source: grown.source, lineNumber:lineNumbers[i], sourceLines:lineNumbers.slice(i,grown.next) });
|
|
3401
4016
|
i = grown.next;
|
|
3402
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
|
+
}
|
|
3403
4029
|
return { nodes, index: i };
|
|
3404
4030
|
};
|
|
3405
4031
|
const parsed = parseSequence(0);
|
|
@@ -3436,22 +4062,53 @@ export function needsMoreInput(source) {
|
|
|
3436
4062
|
return false;
|
|
3437
4063
|
}
|
|
3438
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
|
+
|
|
3439
4080
|
async function executeNodeList(nodes, state, options = {}) {
|
|
3440
4081
|
const capture = options.capture ?? true;
|
|
3441
4082
|
const stdout = [], stderr = [];
|
|
3442
|
-
let execution = result(
|
|
4083
|
+
let execution = result();
|
|
3443
4084
|
const append = (value) => {
|
|
3444
4085
|
if (!capture) return;
|
|
3445
4086
|
if (value.stdout.byteLength) stdout.push(value.stdout);
|
|
3446
4087
|
if (value.stderr.byteLength) stderr.push(value.stderr);
|
|
3447
4088
|
};
|
|
3448
4089
|
for (const node of nodes) {
|
|
4090
|
+
if (state.exitRequested || state.flow) break;
|
|
4091
|
+
append(await runPendingTraps(state,{capture}));
|
|
3449
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
|
+
}
|
|
3450
4107
|
if (node.type === "compoundPostfix") {
|
|
3451
4108
|
if (node.operator === "|") {
|
|
3452
4109
|
const link = new TransformStream();
|
|
3453
4110
|
const leftPromise = runExternal(
|
|
3454
|
-
pipelineCompoundArgv(node.compound
|
|
4111
|
+
pipelineCompoundArgv(compoundSource(node.compound)),
|
|
3455
4112
|
pipelineBuiltinState(state),
|
|
3456
4113
|
options.input ?? null,
|
|
3457
4114
|
false,
|
|
@@ -3465,30 +4122,34 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3465
4122
|
aliases: { ...state.aliases },
|
|
3466
4123
|
functions: { ...state.functions },
|
|
3467
4124
|
readonly: new Set(state.readonly),
|
|
4125
|
+
exported: new Set(state.exported),
|
|
3468
4126
|
directoryHistory: [...state.directoryHistory],
|
|
3469
4127
|
tabs: [...state.tabs],
|
|
3470
4128
|
exitRequested: false,
|
|
3471
4129
|
exitStatus: 0,
|
|
3472
4130
|
};
|
|
3473
4131
|
const right = await runExternal(
|
|
3474
|
-
pipelineCompoundArgv(node.command),
|
|
4132
|
+
pipelineCompoundArgv(node.right ? compoundSource(node.right) : node.command),
|
|
3475
4133
|
pipelineBuiltinState(rightState),
|
|
3476
4134
|
link.readable,
|
|
3477
4135
|
true,
|
|
3478
4136
|
true,
|
|
3479
4137
|
);
|
|
3480
4138
|
const left = await leftPromise;
|
|
3481
|
-
execution = result(right.status, right.stdout, concatBytes([left.stderr, right.stderr]));
|
|
4139
|
+
execution = routeCommandOutput(result(right.status, right.stdout, concatBytes([left.stderr, right.stderr])), state.fdBindings ?? {});
|
|
3482
4140
|
} else {
|
|
3483
|
-
|
|
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; }
|
|
3484
4145
|
append(execution);
|
|
3485
4146
|
const shouldRun = node.operator === "&&"
|
|
3486
4147
|
? execution.status === 0
|
|
3487
4148
|
: node.operator === "||"
|
|
3488
4149
|
? execution.status !== 0
|
|
3489
4150
|
: true;
|
|
3490
|
-
if (shouldRun) {
|
|
3491
|
-
execution = await execute(node.command, state, { capture });
|
|
4151
|
+
if (shouldRun && !state.flow && !state.exitRequested) {
|
|
4152
|
+
execution = node.right ? await executeNodeList([node.right],state,{capture}) : await execute(node.command, state, { capture });
|
|
3492
4153
|
append(execution);
|
|
3493
4154
|
}
|
|
3494
4155
|
state.lastStatus = execution.status;
|
|
@@ -3504,26 +4165,16 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3504
4165
|
}
|
|
3505
4166
|
if (node.type === "function") {
|
|
3506
4167
|
state.functions[node.name] = node.body;
|
|
4168
|
+
state.lastStatus = 0; execution = result();
|
|
3507
4169
|
continue;
|
|
3508
4170
|
}
|
|
3509
4171
|
if (node.type === "command") {
|
|
3510
|
-
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 });
|
|
3511
4173
|
append(execution);
|
|
3512
4174
|
continue;
|
|
3513
4175
|
}
|
|
3514
4176
|
if (node.type === "subshell") {
|
|
3515
|
-
|
|
3516
|
-
...state,
|
|
3517
|
-
env: { ...state.env },
|
|
3518
|
-
aliases: { ...state.aliases },
|
|
3519
|
-
functions: { ...state.functions },
|
|
3520
|
-
readonly: new Set(state.readonly),
|
|
3521
|
-
directoryHistory: [...state.directoryHistory],
|
|
3522
|
-
tabs: [...state.tabs],
|
|
3523
|
-
exitRequested: false,
|
|
3524
|
-
exitStatus: 0,
|
|
3525
|
-
};
|
|
3526
|
-
execution = await execute(node.source, child, { capture });
|
|
4177
|
+
execution = await execute(`(${node.source})`,state,{capture,input:options.input});
|
|
3527
4178
|
append(execution);
|
|
3528
4179
|
state.lastStatus = execution.status;
|
|
3529
4180
|
continue;
|
|
@@ -3531,6 +4182,7 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3531
4182
|
if (node.type === "loop") {
|
|
3532
4183
|
const previousReadLines = state.readLines;
|
|
3533
4184
|
let ranBody = false;
|
|
4185
|
+
state.loopDepth++;
|
|
3534
4186
|
try {
|
|
3535
4187
|
if (node.input !== null) {
|
|
3536
4188
|
const parsed = parse(`__input ${node.input}`)[0]?.pipeline[0];
|
|
@@ -3541,7 +4193,7 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3541
4193
|
if (state.readLines.at(-1) === "") state.readLines.pop();
|
|
3542
4194
|
}
|
|
3543
4195
|
while (true) {
|
|
3544
|
-
const checked = await execute(node.condition, state, { capture,
|
|
4196
|
+
const checked = await execute(node.condition, state, { capture, errexitIgnored:true });
|
|
3545
4197
|
append(checked);
|
|
3546
4198
|
const proceed = node.kind === "until" ? checked.status !== 0 : checked.status === 0;
|
|
3547
4199
|
if (!proceed || state.exitRequested) {
|
|
@@ -3551,12 +4203,13 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3551
4203
|
execution = await executeNodeList(node.body, state, { capture });
|
|
3552
4204
|
ranBody = true;
|
|
3553
4205
|
append(execution);
|
|
3554
|
-
if (state.exitRequested) break;
|
|
4206
|
+
if (state.exitRequested || consumeLoopFlow(state)) break;
|
|
3555
4207
|
}
|
|
3556
4208
|
} catch (error) {
|
|
3557
4209
|
execution = result(1, "", `bunmsh: ${error.message}\n`);
|
|
3558
4210
|
append(execution);
|
|
3559
4211
|
} finally {
|
|
4212
|
+
state.loopDepth--;
|
|
3560
4213
|
state.readLines = previousReadLines;
|
|
3561
4214
|
}
|
|
3562
4215
|
state.lastStatus = execution.status;
|
|
@@ -3571,13 +4224,14 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3571
4224
|
for (const word of parsed?.words.slice(1) ?? []) values.push(...await expandWord(word, state));
|
|
3572
4225
|
}
|
|
3573
4226
|
execution = result();
|
|
3574
|
-
|
|
4227
|
+
state.loopDepth++;
|
|
4228
|
+
try { for (const value of values) {
|
|
3575
4229
|
if (state.readonly.has(node.name)) { execution = readonlyError(node.name); break; }
|
|
3576
4230
|
state.env[node.name] = value;
|
|
3577
4231
|
execution = await executeNodeList(node.body, state, { capture });
|
|
3578
4232
|
append(execution);
|
|
3579
|
-
if (state.exitRequested) break;
|
|
3580
|
-
}
|
|
4233
|
+
if (state.exitRequested || consumeLoopFlow(state)) break;
|
|
4234
|
+
} } finally { state.loopDepth--; }
|
|
3581
4235
|
state.lastStatus = execution.status;
|
|
3582
4236
|
continue;
|
|
3583
4237
|
}
|
|
@@ -3605,11 +4259,9 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3605
4259
|
let selectedThen = "";
|
|
3606
4260
|
for (const branch of node.branches) {
|
|
3607
4261
|
let condition = branch.condition.trim();
|
|
3608
|
-
|
|
3609
|
-
while (/^!\s+/.test(condition)) { negate = !negate; condition = condition.replace(/^!\s+/, ""); }
|
|
3610
|
-
const checked = await execute(condition, state, { capture, simple: true });
|
|
4262
|
+
const checked = await execute(condition, state, { capture, errexitIgnored:true });
|
|
3611
4263
|
append(checked);
|
|
3612
|
-
if (
|
|
4264
|
+
if (checked.status === 0) {
|
|
3613
4265
|
selected = branch.body;
|
|
3614
4266
|
selectedThen = branch.thenCommand ?? "";
|
|
3615
4267
|
break;
|
|
@@ -3630,9 +4282,17 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3630
4282
|
|
|
3631
4283
|
async function executeFunction(name, argv, state) {
|
|
3632
4284
|
const previousArgs = state.args;
|
|
4285
|
+
const previousLoopDepth = state.loopDepth;
|
|
4286
|
+
state.loopDepth = 0;
|
|
3633
4287
|
state.args = [previousArgs[0], ...argv.slice(1)];
|
|
4288
|
+
state.returnDepth++;
|
|
3634
4289
|
try { return await executeNodeList(state.functions[name], state); }
|
|
3635
|
-
finally {
|
|
4290
|
+
finally {
|
|
4291
|
+
state.returnDepth--;
|
|
4292
|
+
state.loopDepth = previousLoopDepth;
|
|
4293
|
+
if (state.flow?.kind === "return") state.flow = null;
|
|
4294
|
+
state.args = previousArgs;
|
|
4295
|
+
}
|
|
3636
4296
|
}
|
|
3637
4297
|
|
|
3638
4298
|
async function runExternal(
|
|
@@ -3652,7 +4312,7 @@ async function runExternal(
|
|
|
3652
4312
|
const options = {
|
|
3653
4313
|
cmd: spawnArgv,
|
|
3654
4314
|
cwd: nativePath(state.cwd),
|
|
3655
|
-
env: state
|
|
4315
|
+
env: externalEnvironment(state),
|
|
3656
4316
|
stdin: runtimeOptions.background ? "ignore" : input === null ? "inherit" : input,
|
|
3657
4317
|
stdout: runtimeOptions.background ? "inherit" : captureStdout || stdoutSink ? "pipe" : "inherit",
|
|
3658
4318
|
stderr: runtimeOptions.background ? "inherit" : captureStderr || runtimeOptions.stderrSink ? "pipe" : "inherit",
|
|
@@ -3660,7 +4320,30 @@ async function runExternal(
|
|
|
3660
4320
|
runtimeOptions.onExit?.(proc, exitCode, signalCode, error);
|
|
3661
4321
|
},
|
|
3662
4322
|
};
|
|
4323
|
+
let stateDirectory;
|
|
3663
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
|
+
}
|
|
3664
4347
|
const proc = Bun.spawn(options);
|
|
3665
4348
|
if (runtimeOptions.background) {
|
|
3666
4349
|
if (!Array.isArray(Bun.sha.procs)) Bun.sha.procs = [];
|
|
@@ -3685,10 +4368,21 @@ async function runExternal(
|
|
|
3685
4368
|
const abandonStreaming = new Promise((resolve) => {
|
|
3686
4369
|
runtimeOptions.onSpawn?.(proc, pipelineKillSignal, resolve);
|
|
3687
4370
|
});
|
|
3688
|
-
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
|
|
3689
4383
|
? new Response(proc.stdout).arrayBuffer()
|
|
3690
4384
|
: Promise.resolve(new ArrayBuffer(0));
|
|
3691
|
-
const streamPromise = stdoutSink
|
|
4385
|
+
const streamPromise = stdoutSink && !sharedSink
|
|
3692
4386
|
? Promise.race([
|
|
3693
4387
|
proc.stdout.pipeTo(stdoutSink).catch(() => {
|
|
3694
4388
|
try { proc.kill(pipelineKillSignal); } catch {}
|
|
@@ -3696,13 +4390,13 @@ async function runExternal(
|
|
|
3696
4390
|
abandonStreaming,
|
|
3697
4391
|
])
|
|
3698
4392
|
: Promise.resolve();
|
|
3699
|
-
const errorStreamPromise = runtimeOptions.stderrSink
|
|
4393
|
+
const errorStreamPromise = runtimeOptions.stderrSink && !sharedSink
|
|
3700
4394
|
? Promise.race([
|
|
3701
4395
|
proc.stderr.pipeTo(runtimeOptions.stderrSink).catch(() => {}),
|
|
3702
4396
|
abandonStreaming,
|
|
3703
4397
|
])
|
|
3704
4398
|
: Promise.resolve();
|
|
3705
|
-
const errorPromise = captureStderr
|
|
4399
|
+
const errorPromise = captureStderr && !runtimeOptions.stderrSink
|
|
3706
4400
|
? new Response(proc.stderr).arrayBuffer()
|
|
3707
4401
|
: Promise.resolve(new ArrayBuffer(0));
|
|
3708
4402
|
const [status, output, error] = await Promise.all([
|
|
@@ -3711,13 +4405,18 @@ async function runExternal(
|
|
|
3711
4405
|
errorPromise,
|
|
3712
4406
|
streamPromise,
|
|
3713
4407
|
errorStreamPromise,
|
|
4408
|
+
sharedStreamPromise,
|
|
3714
4409
|
]);
|
|
3715
4410
|
const execution = result(status, new Uint8Array(output), new Uint8Array(error));
|
|
3716
|
-
execution.stdoutStreamed = Boolean(stdoutSink);
|
|
4411
|
+
execution.stdoutStreamed = Boolean(stdoutSink || runtimeOptions.stderrSink);
|
|
3717
4412
|
execution.stderrStreamed = Boolean(runtimeOptions.stderrSink);
|
|
3718
4413
|
return execution;
|
|
3719
4414
|
} catch (error) {
|
|
3720
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
|
+
}
|
|
3721
4420
|
}
|
|
3722
4421
|
}
|
|
3723
4422
|
|
|
@@ -3742,11 +4441,39 @@ function pipelineCompoundArgv(source) {
|
|
|
3742
4441
|
: [...BUN_RUNTIME_COMMAND, BUNMSH_ENTRY, "-c", source];
|
|
3743
4442
|
}
|
|
3744
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
|
+
|
|
3745
4465
|
function leadingSubshell(source) {
|
|
3746
4466
|
const leading = source.length - source.trimStart().length;
|
|
3747
4467
|
if (source[leading] !== "(") return null;
|
|
3748
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 = [];
|
|
3749
4475
|
for (let i = leading; i < source.length; i++) {
|
|
4476
|
+
if (ranges.has(i)) { i=ranges.get(i)-1; continue; }
|
|
3750
4477
|
const ch = source[i];
|
|
3751
4478
|
if (quote === "'") { if (ch === "'") quote = null; continue; }
|
|
3752
4479
|
if (quote === '"') {
|
|
@@ -3754,6 +4481,14 @@ function leadingSubshell(source) {
|
|
|
3754
4481
|
if (ch === '"') quote = null;
|
|
3755
4482
|
continue;
|
|
3756
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";
|
|
3757
4492
|
if (ch === "\\") { i++; continue; }
|
|
3758
4493
|
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
3759
4494
|
if (ch === "`") {
|
|
@@ -3764,6 +4499,10 @@ function leadingSubshell(source) {
|
|
|
3764
4499
|
try { i = substitutionEnd(source, i) - 1; } catch { return null; }
|
|
3765
4500
|
continue;
|
|
3766
4501
|
}
|
|
4502
|
+
if (cases.at(-1) === "pattern" && (ch === "(" || ch === ")")) {
|
|
4503
|
+
if (ch === ")") cases[cases.length-1]="body";
|
|
4504
|
+
continue;
|
|
4505
|
+
}
|
|
3767
4506
|
if (ch === "(") depth++;
|
|
3768
4507
|
else if (ch === ")" && --depth === 0)
|
|
3769
4508
|
return { body: source.slice(leading + 1, i), suffix: source.slice(i + 1) };
|
|
@@ -3773,7 +4512,9 @@ function leadingSubshell(source) {
|
|
|
3773
4512
|
|
|
3774
4513
|
function rewriteSubshells(source, state) {
|
|
3775
4514
|
let output = "", quote = null;
|
|
4515
|
+
const ranges=heredocRanges(source);
|
|
3776
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; }
|
|
3777
4518
|
const ch = source[i];
|
|
3778
4519
|
if (quote === "'") {
|
|
3779
4520
|
output += ch;
|
|
@@ -3803,8 +4544,9 @@ function rewriteSubshells(source, state) {
|
|
|
3803
4544
|
const group = leadingSubshell(source.slice(i));
|
|
3804
4545
|
if (group) {
|
|
3805
4546
|
const argv = pipelineCompoundArgv(group.body);
|
|
3806
|
-
|
|
3807
|
-
|
|
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(" ")}`;
|
|
3808
4550
|
i += group.body.length + 2;
|
|
3809
4551
|
continue;
|
|
3810
4552
|
}
|
|
@@ -3821,11 +4563,16 @@ function pipelineBuiltinState(state) {
|
|
|
3821
4563
|
env: {
|
|
3822
4564
|
...state.env,
|
|
3823
4565
|
[PIPELINE_STATE_ENV]: JSON.stringify({
|
|
4566
|
+
env: Object.fromEntries(Object.entries(state.env).filter(([name]) => name !== PIPELINE_STATE_ENV)),
|
|
4567
|
+
exported: [...state.exported],
|
|
3824
4568
|
aliases: state.aliases,
|
|
3825
4569
|
functions: state.functions,
|
|
3826
4570
|
readonly: [...state.readonly],
|
|
3827
4571
|
args: state.args,
|
|
3828
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)}])),
|
|
3829
4576
|
}),
|
|
3830
4577
|
},
|
|
3831
4578
|
};
|
|
@@ -3919,7 +4666,12 @@ async function runCommandArgv(
|
|
|
3919
4666
|
)) return result(2, "", "bunmsh: &: only a single external command is supported\n");
|
|
3920
4667
|
|
|
3921
4668
|
let commandState = state;
|
|
3922
|
-
|
|
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))
|
|
3923
4675
|
return runExternal(
|
|
3924
4676
|
pipelineBuiltinArgv(commandArgv),
|
|
3925
4677
|
pipelineBuiltinState(commandState),
|
|
@@ -3929,6 +4681,8 @@ async function runCommandArgv(
|
|
|
3929
4681
|
options.stdoutSink,
|
|
3930
4682
|
{ ...options, pipelineKillSignal: "SIGTERM" },
|
|
3931
4683
|
);
|
|
4684
|
+
if (typeof input === "number" && ioCommand !== "read" && runsInPipelineSubprocess(commandArgv,commandState))
|
|
4685
|
+
input = Readable.toWeb(createReadStream("",{fd:input,autoClose:false}));
|
|
3932
4686
|
while (["command", "builtin", "__builtin"].includes(commandArgv[0])) {
|
|
3933
4687
|
if (requestsBuiltinHelp(commandArgv) && hasBuiltinHelp(commandArgv[0]))
|
|
3934
4688
|
return builtinHelp(commandArgv[0]);
|
|
@@ -4034,16 +4788,6 @@ async function runCommandArgv(
|
|
|
4034
4788
|
);
|
|
4035
4789
|
}
|
|
4036
4790
|
|
|
4037
|
-
function redirectInput(path) {
|
|
4038
|
-
const fd = openSync(path, "r");
|
|
4039
|
-
return Readable.toWeb(createReadStream(path, { fd, autoClose: true }));
|
|
4040
|
-
}
|
|
4041
|
-
|
|
4042
|
-
function redirectOutput(path, append) {
|
|
4043
|
-
const fd = openSync(path, append ? "a" : "w");
|
|
4044
|
-
return Writable.toWeb(createWriteStream(path, { fd, autoClose: true }));
|
|
4045
|
-
}
|
|
4046
|
-
|
|
4047
4791
|
// Mirrors the double-quote scanning in tokenize(): an unquoted heredoc body
|
|
4048
4792
|
// only lets backslash escape $, `, \, and a trailing newline before the rest
|
|
4049
4793
|
// goes through the normal $/`` expansion pipeline.
|
|
@@ -4076,6 +4820,7 @@ function redirectHeredoc(content) {
|
|
|
4076
4820
|
|
|
4077
4821
|
async function runCommand(command, state, options = {}) {
|
|
4078
4822
|
state.expansionStatus = null;
|
|
4823
|
+
if (command.lineNumber != null) state.lineNumber = command.lineNumber;
|
|
4079
4824
|
const standaloneTilde =
|
|
4080
4825
|
command.words.length === 1 &&
|
|
4081
4826
|
command.words[0].fragments.length === 1 &&
|
|
@@ -4104,129 +4849,132 @@ async function runCommand(command, state, options = {}) {
|
|
|
4104
4849
|
localEnv[assignment.name] = assignment.value;
|
|
4105
4850
|
expanded.shift();
|
|
4106
4851
|
}
|
|
4107
|
-
|
|
4852
|
+
const assignmentOnly = expanded.length === 0;
|
|
4853
|
+
if (assignmentOnly) {
|
|
4108
4854
|
for (const name of Object.keys(localEnv))
|
|
4109
4855
|
if (state.readonly.has(name)) return readonlyError(name);
|
|
4110
4856
|
Object.assign(state.env, localEnv);
|
|
4111
|
-
return result(state.expansionStatus ?? 0);
|
|
4857
|
+
if (!command.redirects.length) return result(state.expansionStatus ?? 0);
|
|
4858
|
+
expanded.push(":");
|
|
4112
4859
|
}
|
|
4113
4860
|
const alias = aliasEligible ? state.aliases[expanded[0]] : null;
|
|
4114
4861
|
if (alias) expanded.splice(0, 1, ...alias);
|
|
4115
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();
|
|
4116
4872
|
let input = options.input ?? null;
|
|
4117
|
-
let
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
? stripExpansionMarkers(
|
|
4131
|
-
(await expandText(heredocLiteralPass(heredoc.body), state)).replaceAll("\u0000", ""),
|
|
4132
|
-
)
|
|
4133
|
-
: heredoc.body;
|
|
4134
|
-
if (input instanceof ReadableStream) {
|
|
4135
|
-
try { await input.cancel(); } catch {}
|
|
4136
|
-
}
|
|
4137
|
-
input = redirectHeredoc(content);
|
|
4138
|
-
continue;
|
|
4139
|
-
}
|
|
4140
|
-
if (redirect.op === "<<<") {
|
|
4141
|
-
const [word] = await expandWord(redirect.target, state, { single: true, noTilde: true });
|
|
4142
|
-
if (input instanceof ReadableStream) {
|
|
4143
|
-
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;
|
|
4883
|
+
}
|
|
4884
|
+
bindings[0] = {content};
|
|
4885
|
+
continue;
|
|
4144
4886
|
}
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
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;
|
|
4155
4902
|
}
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
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};
|
|
4159
4908
|
}
|
|
4160
|
-
} else if (redirect.op === ">" || redirect.op === ">>") {
|
|
4161
|
-
stdoutPath = path;
|
|
4162
|
-
stdoutAppend = redirect.op === ">>";
|
|
4163
|
-
} else {
|
|
4164
|
-
stderrPath = path;
|
|
4165
|
-
stderrAppend = redirect.op === "2>>";
|
|
4166
4909
|
}
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
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();
|
|
4180
4924
|
}
|
|
4181
|
-
|
|
4182
|
-
|
|
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;
|
|
4183
4951
|
} catch (error) {
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
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;
|
|
4194
4970
|
}
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
}
|
|
4201
|
-
|
|
4202
|
-
const captureStdout = (options.captureStdout || stdoutToStderr) && stdoutRedirect === null;
|
|
4203
|
-
const captureStderr = (options.captureStderr || stderrToStdout) && stderrRedirect === null;
|
|
4204
|
-
const execution = await runCommandArgv(
|
|
4205
|
-
expanded,
|
|
4206
|
-
commandState,
|
|
4207
|
-
input,
|
|
4208
|
-
captureStdout,
|
|
4209
|
-
captureStderr,
|
|
4210
|
-
{
|
|
4211
|
-
pipelineStage: Boolean(options.pipelineStage || stdoutRedirect || stderrRedirect),
|
|
4212
|
-
stdoutSink: stdoutRedirect ?? options.stdoutSink ?? null,
|
|
4213
|
-
stderrSink: stderrRedirect,
|
|
4214
|
-
onSpawn: options.onSpawn,
|
|
4215
|
-
onExit: options.onExit,
|
|
4216
|
-
background: Boolean(options.background),
|
|
4217
|
-
},
|
|
4218
|
-
);
|
|
4219
|
-
|
|
4220
|
-
if (stdoutToStderr && execution.stdout.byteLength) {
|
|
4221
|
-
execution.stderr = concatBytes([execution.stderr, execution.stdout]);
|
|
4222
|
-
execution.stdout = bytes();
|
|
4223
|
-
}
|
|
4224
|
-
if (stderrToStdout && execution.stderr.byteLength) {
|
|
4225
|
-
execution.stdout = concatBytes([execution.stdout, execution.stderr]);
|
|
4226
|
-
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
|
+
}
|
|
4227
4976
|
}
|
|
4228
4977
|
|
|
4229
|
-
return execution;
|
|
4230
4978
|
}
|
|
4231
4979
|
|
|
4232
4980
|
async function runPipeline(pipeline, state, options = {}) {
|
|
@@ -4247,9 +4995,9 @@ async function runPipeline(pipeline, state, options = {}) {
|
|
|
4247
4995
|
);
|
|
4248
4996
|
const linksConnected = links.map((_, i) => {
|
|
4249
4997
|
const stdoutRedirected = pipeline[i].redirects.some((redirect) =>
|
|
4250
|
-
redirect.op
|
|
4998
|
+
/^(?:1)?>(?:>|&.*)?$/.test(redirect.op));
|
|
4251
4999
|
const stdinRedirected = pipeline[i + 1].redirects.some((redirect) =>
|
|
4252
|
-
|
|
5000
|
+
/^(?:0)?<(?:<<?-?|&.*|>)?$/.test(redirect.op));
|
|
4253
5001
|
return !stdoutRedirected && !stdinRedirected;
|
|
4254
5002
|
});
|
|
4255
5003
|
const processes = Array(pipeline.length).fill(null);
|
|
@@ -4311,6 +5059,18 @@ async function runPipeline(pipeline, state, options = {}) {
|
|
|
4311
5059
|
}
|
|
4312
5060
|
|
|
4313
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) {
|
|
4314
5074
|
source = normalizeSource(source);
|
|
4315
5075
|
if (source === "?") {
|
|
4316
5076
|
const execution = result(0, `${state.lastStatus}\n`);
|
|
@@ -4321,44 +5081,12 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
4321
5081
|
const rawSource = source.trimStart();
|
|
4322
5082
|
const group = leadingSubshell(source);
|
|
4323
5083
|
if (group) {
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
if (redirect[2] === ">" || redirect[2] === ">>") {
|
|
4330
|
-
if (redirect[1] === "2") discardStderr ||= redirect[3] === "/dev/null";
|
|
4331
|
-
else discardStdout ||= redirect[3] === "/dev/null";
|
|
4332
|
-
}
|
|
4333
|
-
suffix = suffix.slice(redirect[0].length);
|
|
4334
|
-
}
|
|
4335
|
-
const child = await runExternal(
|
|
4336
|
-
pipelineCompoundArgv(group.body),
|
|
4337
|
-
pipelineBuiltinState(state),
|
|
4338
|
-
io.input ?? null,
|
|
4339
|
-
true,
|
|
4340
|
-
true,
|
|
4341
|
-
);
|
|
4342
|
-
let execution = result(child.status,
|
|
4343
|
-
discardStdout ? "" : child.stdout,
|
|
4344
|
-
discardStderr ? "" : child.stderr);
|
|
4345
|
-
const connector = /^\s*(\|\||&&|;)\s*([\s\S]*)$/.exec(suffix);
|
|
4346
|
-
if (connector) {
|
|
4347
|
-
const shouldRun = connector[1] === ";" ||
|
|
4348
|
-
(connector[1] === "&&" ? execution.status === 0 : execution.status !== 0);
|
|
4349
|
-
if (shouldRun) {
|
|
4350
|
-
const tail = await execute(connector[2], state, io);
|
|
4351
|
-
execution = result(tail.status,
|
|
4352
|
-
concatBytes([execution.stdout, tail.stdout]),
|
|
4353
|
-
concatBytes([execution.stderr, tail.stderr]));
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4356
|
-
state.lastStatus = execution.status;
|
|
4357
|
-
if (!io.capture) {
|
|
4358
|
-
if (execution.stdout.byteLength) await writeStream(process.stdout, execution.stdout);
|
|
4359
|
-
if (execution.stderr.byteLength) await writeStream(process.stderr, execution.stderr);
|
|
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;
|
|
4360
5089
|
}
|
|
4361
|
-
return execution;
|
|
4362
5090
|
}
|
|
4363
5091
|
if (isJavaScriptMode(rawSource)) {
|
|
4364
5092
|
let execution;
|
|
@@ -4416,7 +5144,7 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
4416
5144
|
if (execution.stdout.byteLength) stdout.push(execution.stdout);
|
|
4417
5145
|
if (execution.stderr.byteLength) stderr.push(execution.stderr);
|
|
4418
5146
|
}
|
|
4419
|
-
if (state.exitRequested) break;
|
|
5147
|
+
if (state.exitRequested || state.flow) break;
|
|
4420
5148
|
}
|
|
4421
5149
|
return io.capture
|
|
4422
5150
|
? result(execution.status, concatBytes(stdout), concatBytes(stderr))
|
|
@@ -4428,7 +5156,7 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
4428
5156
|
if (compoundSyntax) {
|
|
4429
5157
|
let execution;
|
|
4430
5158
|
try {
|
|
4431
|
-
execution = await executeNodeList(parseCompoundScript(source), state, {
|
|
5159
|
+
execution = await executeNodeList(parseCompoundScript(source, io.firstLine ?? 1, io.sourceLines), state, {
|
|
4432
5160
|
capture: Boolean(io.capture),
|
|
4433
5161
|
input: io.input ?? null,
|
|
4434
5162
|
});
|
|
@@ -4454,6 +5182,7 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
4454
5182
|
let jobs;
|
|
4455
5183
|
try {
|
|
4456
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;
|
|
4457
5186
|
} catch (error) {
|
|
4458
5187
|
const message = error instanceof ShellSyntaxError ? error.message : String(error);
|
|
4459
5188
|
const output = `bunmsh: syntax error: ${message}\n`;
|
|
@@ -4466,15 +5195,24 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
4466
5195
|
let execution = result(state.lastStatus);
|
|
4467
5196
|
const capturedStdout = [];
|
|
4468
5197
|
const capturedStderr = [];
|
|
4469
|
-
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;
|
|
4470
5202
|
if (job.connector === "&&" && execution.status !== 0) continue;
|
|
4471
5203
|
if (job.connector === "||" && execution.status === 0) continue;
|
|
4472
|
-
|
|
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, {
|
|
4473
5208
|
capture: io.capture,
|
|
4474
5209
|
background: job.background,
|
|
4475
5210
|
input: io.input ?? null,
|
|
4476
5211
|
stdoutSink: io.stdoutSink ?? null,
|
|
4477
|
-
});
|
|
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; }
|
|
4478
5216
|
if (job.negate) execution.status = execution.status === 0 ? 1 : 0;
|
|
4479
5217
|
state.lastStatus = execution.status;
|
|
4480
5218
|
if (execution.stdout.byteLength) {
|
|
@@ -4485,7 +5223,7 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
4485
5223
|
if (io.capture) capturedStderr.push(execution.stderr);
|
|
4486
5224
|
else await writeStream(process.stderr, execution.stderr);
|
|
4487
5225
|
}
|
|
4488
|
-
if (state.exitRequested) break;
|
|
5226
|
+
if (state.exitRequested || state.flow) break;
|
|
4489
5227
|
}
|
|
4490
5228
|
return io.capture
|
|
4491
5229
|
? result(execution.status, concatBytes(capturedStdout), concatBytes(capturedStderr))
|
|
@@ -4515,11 +5253,13 @@ export function pipelineChildState() {
|
|
|
4515
5253
|
const serialized = process.env[PIPELINE_STATE_ENV];
|
|
4516
5254
|
if (!serialized) return createState();
|
|
4517
5255
|
try {
|
|
4518
|
-
const inherited = JSON.parse(serialized);
|
|
4519
|
-
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 ?? {}) };
|
|
4520
5258
|
delete env[PIPELINE_STATE_ENV];
|
|
4521
5259
|
return createState({
|
|
4522
5260
|
env,
|
|
5261
|
+
inheritEnvironment: false,
|
|
5262
|
+
exported: inherited.exported,
|
|
4523
5263
|
cwd: process.cwd(),
|
|
4524
5264
|
aliases: inherited.aliases,
|
|
4525
5265
|
functions: inherited.functions,
|
|
@@ -4527,6 +5267,9 @@ export function pipelineChildState() {
|
|
|
4527
5267
|
args: inherited.args,
|
|
4528
5268
|
pathSearch: inherited.pathSearch,
|
|
4529
5269
|
pipelineChild: true,
|
|
5270
|
+
fdBindings: inherited.fdBindings,
|
|
5271
|
+
shellOptions:inherited.shellOptions,
|
|
5272
|
+
ignoreErrexit:inherited.ignoreErrexit,
|
|
4530
5273
|
});
|
|
4531
5274
|
} catch {
|
|
4532
5275
|
return createState();
|