buninu 0.4.17 → 0.4.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/apps/bunmsh/CHANGELOG.md +43 -0
- package/apps/bunmsh/PORTING.md +6 -2
- package/apps/bunmsh/help/README.md +3 -1
- package/apps/bunmsh/help/ln.md +3 -1
- package/apps/bunmsh/package.json +1 -1
- package/apps/bunmsh/src/main.js +7 -5
- package/apps/bunmsh/src/shell.js +757 -159
- package/apps/bunmsh/test/reference.test.js +95 -1
- package/apps/bunmsh/test/shell1.test.js +155 -0
- package/apps/jsgotty/LICENSES/MIT-microsoft-terminal-conpty.txt +21 -0
- package/apps/jsgotty/LICENSES/MIT-ws.txt +20 -0
- package/apps/jsgotty/LICENSES/NOTICE-microsoft-terminal-conpty.md +535 -0
- package/apps/jsgotty/NOTICE +100 -0
- package/apps/jsgotty/conpty/OpenConsole.exe +0 -0
- package/apps/jsgotty/conpty/conpty.dll +0 -0
- package/apps/jsgotty/gotty.js +24 -0
- package/apps/jsgotty/package-lock.json +2 -2
- package/apps/jsgotty/package.json +6 -2
- package/apps/jsgotty/windows-conpty.js +135 -0
- package/package.json +1 -1
package/apps/bunmsh/src/shell.js
CHANGED
|
@@ -283,25 +283,23 @@ export function tokenize(source, options = {}) {
|
|
|
283
283
|
while (i < source.length && source[i] !== "\n") i++;
|
|
284
284
|
continue;
|
|
285
285
|
}
|
|
286
|
-
|
|
287
286
|
const two = source.slice(i, i + 2);
|
|
288
|
-
const fdDup = source.slice(i
|
|
287
|
+
const fdDup = /^(\d+>&\d+)/.exec(source.slice(i))?.[1] ?? "";
|
|
289
288
|
const operatorAhead =
|
|
290
289
|
["|", ";", "<", ">"].includes(ch) ||
|
|
291
290
|
two === "&&" ||
|
|
292
291
|
two === "||" ||
|
|
293
292
|
two === "2>" ||
|
|
294
|
-
fdDup
|
|
295
|
-
fdDup === "2>&1";
|
|
293
|
+
Boolean(fdDup);
|
|
296
294
|
if (current && operatorAhead) {
|
|
297
295
|
finishWord();
|
|
298
296
|
continue;
|
|
299
297
|
}
|
|
300
298
|
// A descriptor duplication begins with the shorter `2>` redirect token,
|
|
301
299
|
// so it must win before ordinary file redirects are considered.
|
|
302
|
-
if (!current &&
|
|
300
|
+
if (!current && fdDup) {
|
|
303
301
|
tokens.push({ type: "op", value: fdDup, offset: i });
|
|
304
|
-
i +=
|
|
302
|
+
i += fdDup.length;
|
|
305
303
|
atWordBoundary = true;
|
|
306
304
|
continue;
|
|
307
305
|
}
|
|
@@ -364,7 +362,7 @@ export function tokenize(source, options = {}) {
|
|
|
364
362
|
}
|
|
365
363
|
if (source[i] === "\\" && i + 1 < source.length) {
|
|
366
364
|
const next = source[i + 1];
|
|
367
|
-
if (next === "$" || next === '"' || next === "\\") {
|
|
365
|
+
if (next === "$" || next === "`" || next === '"' || next === "\\") {
|
|
368
366
|
text += `\u0000${next}`;
|
|
369
367
|
i += 2;
|
|
370
368
|
continue;
|
|
@@ -419,8 +417,7 @@ export function tokenize(source, options = {}) {
|
|
|
419
417
|
source.slice(i, i + 2) === "&&" ||
|
|
420
418
|
source.slice(i, i + 2) === "||" ||
|
|
421
419
|
source.slice(i, i + 2) === "2>" ||
|
|
422
|
-
source.slice(i
|
|
423
|
-
source.slice(i, i + 4) === "2>&1";
|
|
420
|
+
/^\d+>&\d+/.test(source.slice(i));
|
|
424
421
|
if (isSpace(c) || c === "\n" || c === "'" || c === '"' || c === "\\" || maybeOp)
|
|
425
422
|
break;
|
|
426
423
|
text += c;
|
|
@@ -448,6 +445,7 @@ function emptyCommand() {
|
|
|
448
445
|
}
|
|
449
446
|
|
|
450
447
|
export function parse(source) {
|
|
448
|
+
source = normalizeSource(source);
|
|
451
449
|
const tokens = tokenize(source);
|
|
452
450
|
const jobs = [];
|
|
453
451
|
let i = 0;
|
|
@@ -466,7 +464,7 @@ export function parse(source) {
|
|
|
466
464
|
const token = tokens[i];
|
|
467
465
|
if (token.type === "op" && ["|", ";", "&&", "||"].includes(token.value))
|
|
468
466
|
break;
|
|
469
|
-
if (token.type === "op" &&
|
|
467
|
+
if (token.type === "op" && /^\d+>&\d+$/.test(token.value)) {
|
|
470
468
|
command.redirects.push({ op: token.value, target: null });
|
|
471
469
|
i++;
|
|
472
470
|
continue;
|
|
@@ -513,8 +511,8 @@ export function parse(source) {
|
|
|
513
511
|
throw new ShellSyntaxError("! requires a command", firstWord.offset);
|
|
514
512
|
negate = true;
|
|
515
513
|
}
|
|
516
|
-
jobs.push({ connector, pipeline, negate });
|
|
517
514
|
const separator = tokens[i]?.value;
|
|
515
|
+
jobs.push({ connector, pipeline, negate, background: false });
|
|
518
516
|
if (!separator) break;
|
|
519
517
|
if (![";", "&&", "||"].includes(separator))
|
|
520
518
|
throw new ShellSyntaxError(`unexpected operator ${separator}`, tokens[i].offset);
|
|
@@ -523,6 +521,20 @@ export function parse(source) {
|
|
|
523
521
|
skipSemicolons();
|
|
524
522
|
if (i >= tokens.length) break;
|
|
525
523
|
}
|
|
524
|
+
const lastJob = jobs.at(-1);
|
|
525
|
+
const lastCommand = lastJob?.pipeline.at(-1);
|
|
526
|
+
const lastWord = lastCommand?.words.at(-1);
|
|
527
|
+
const lastToken = tokens.findLast((token) =>
|
|
528
|
+
token.type !== "op" || token.value !== ";" || source[token.offset] !== "\n");
|
|
529
|
+
if (lastWord === lastToken &&
|
|
530
|
+
lastWord?.fragments.length === 1 &&
|
|
531
|
+
lastWord.fragments[0].quote === "none" &&
|
|
532
|
+
lastWord.fragments[0].text === "&") {
|
|
533
|
+
lastCommand.words.pop();
|
|
534
|
+
if (lastCommand.words.length === 0 && lastCommand.redirects.length === 0)
|
|
535
|
+
throw new ShellSyntaxError("expected a command", lastWord.offset);
|
|
536
|
+
lastJob.background = true;
|
|
537
|
+
}
|
|
526
538
|
return jobs;
|
|
527
539
|
}
|
|
528
540
|
|
|
@@ -604,6 +616,66 @@ function globPatternRegex(pattern, anchoredStart, anchoredEnd, greedy) {
|
|
|
604
616
|
return new RegExp(`${anchoredStart ? "^" : ""}${source}${anchoredEnd ? "$" : ""}`);
|
|
605
617
|
}
|
|
606
618
|
|
|
619
|
+
async function casePatternRegex(pattern, state) {
|
|
620
|
+
let marked = "";
|
|
621
|
+
const literal = (value) => {
|
|
622
|
+
for (const ch of value) marked += `\u0000${ch}`;
|
|
623
|
+
};
|
|
624
|
+
for (let i = 0; i < pattern.length;) {
|
|
625
|
+
if (pattern[i] === "\\") {
|
|
626
|
+
if (i + 1 < pattern.length) literal(pattern[i + 1]);
|
|
627
|
+
i += 2;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (pattern[i] === "'") {
|
|
631
|
+
const end = pattern.indexOf("'", i + 1);
|
|
632
|
+
if (end < 0) throw new ShellSyntaxError("unterminated single quote in case pattern");
|
|
633
|
+
literal(pattern.slice(i + 1, end));
|
|
634
|
+
i = end + 1;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (pattern[i] === '"') {
|
|
638
|
+
let end = i + 1;
|
|
639
|
+
while (end < pattern.length && pattern[end] !== '"') {
|
|
640
|
+
if (pattern[end] === "\\") end++;
|
|
641
|
+
end++;
|
|
642
|
+
}
|
|
643
|
+
if (end >= pattern.length) throw new ShellSyntaxError("unterminated double quote in case pattern");
|
|
644
|
+
literal(stripExpansionMarkers(await expandText(pattern.slice(i + 1, end), state)));
|
|
645
|
+
i = end + 1;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
let end = i;
|
|
649
|
+
while (end < pattern.length && !["\\", "'", '"'].includes(pattern[end])) end++;
|
|
650
|
+
marked += stripExpansionMarkers(await expandText(pattern.slice(i, end), state));
|
|
651
|
+
i = end;
|
|
652
|
+
}
|
|
653
|
+
let source = "";
|
|
654
|
+
for (let i = 0; i < marked.length; i++) {
|
|
655
|
+
if (marked[i] === "\u0000") {
|
|
656
|
+
source += (marked[++i] ?? "").replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
657
|
+
} else if (marked[i] === "*") source += ".*";
|
|
658
|
+
else if (marked[i] === "?") source += ".";
|
|
659
|
+
else if (marked[i] === "[") {
|
|
660
|
+
let end = i + 1;
|
|
661
|
+
if (marked[end] === "!" || marked[end] === "^") end++;
|
|
662
|
+
if (marked[end] === "]") end++;
|
|
663
|
+
while (end < marked.length && marked[end] !== "]") end++;
|
|
664
|
+
if (end >= marked.length) source += "\\[";
|
|
665
|
+
else {
|
|
666
|
+
let body = marked.slice(i + 1, end);
|
|
667
|
+
const negate = body[0] === "!" || body[0] === "^";
|
|
668
|
+
if (negate) body = body.slice(1);
|
|
669
|
+
body = body.replaceAll("\\", "\\\\");
|
|
670
|
+
source += `[${negate ? "^" : ""}${body}]`;
|
|
671
|
+
i = end;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
else source += marked[i].replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
675
|
+
}
|
|
676
|
+
return new RegExp(`^${source}$`);
|
|
677
|
+
}
|
|
678
|
+
|
|
607
679
|
async function parameterExpansion(body, state) {
|
|
608
680
|
if (/^[!%][A-Za-z_][A-Za-z0-9_]*$/.test(body)) {
|
|
609
681
|
const value = parameterValue(body.slice(1), state);
|
|
@@ -1223,33 +1295,37 @@ const builtins = {
|
|
|
1223
1295
|
const format = argv[1];
|
|
1224
1296
|
let ai = 2;
|
|
1225
1297
|
let output = "";
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
i
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1298
|
+
let previous;
|
|
1299
|
+
do {
|
|
1300
|
+
previous = ai;
|
|
1301
|
+
for (let i = 0; i < format.length; i++) {
|
|
1302
|
+
if (format[i] === "\\" && i + 1 < format.length) {
|
|
1303
|
+
const parsed = parsePrintEscapes(format.slice(i, i + 2));
|
|
1304
|
+
output += parsed.text;
|
|
1305
|
+
i++;
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
if (format[i] !== "%") {
|
|
1309
|
+
output += format[i];
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (format[i + 1] === "%") {
|
|
1313
|
+
output += "%";
|
|
1314
|
+
i++;
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
const match = /^%([0-9]*)([sdi])/.exec(format.slice(i));
|
|
1318
|
+
if (!match) return result(2, "", `bunmsh: printf: unsupported format near ${format.slice(i)}\n`);
|
|
1319
|
+
const width = Number(match[1] || 0);
|
|
1320
|
+
const kind = match[2];
|
|
1321
|
+
const value = argv[ai++] ?? (kind === "s" ? "" : "0");
|
|
1322
|
+
let rendered = kind === "s" ? value : String(Number.parseInt(value, 10) || 0);
|
|
1323
|
+
if (width > rendered.length)
|
|
1324
|
+
rendered = (match[1]?.startsWith("0") ? "0" : " ").repeat(width - rendered.length) + rendered;
|
|
1325
|
+
output += rendered;
|
|
1326
|
+
i += match[0].length - 1;
|
|
1241
1327
|
}
|
|
1242
|
-
|
|
1243
|
-
if (!match) return result(2, "", `bunmsh: printf: unsupported format near ${format.slice(i)}\n`);
|
|
1244
|
-
const width = Number(match[1] || 0);
|
|
1245
|
-
const kind = match[2];
|
|
1246
|
-
const value = argv[ai++] ?? (kind === "s" ? "" : "0");
|
|
1247
|
-
let rendered = kind === "s" ? value : String(Number.parseInt(value, 10) || 0);
|
|
1248
|
-
if (width > rendered.length)
|
|
1249
|
-
rendered = (match[1]?.startsWith("0") ? "0" : " ").repeat(width - rendered.length) + rendered;
|
|
1250
|
-
output += rendered;
|
|
1251
|
-
i += match[0].length - 1;
|
|
1252
|
-
}
|
|
1328
|
+
} while (ai > previous && ai < argv.length);
|
|
1253
1329
|
return result(0, output);
|
|
1254
1330
|
},
|
|
1255
1331
|
read: async (argv, state, input) => {
|
|
@@ -1821,11 +1897,19 @@ async function runHeadFallback(argv, state, input) {
|
|
|
1821
1897
|
return result(1, "", "bunmsh: head: invalid line count\n");
|
|
1822
1898
|
const operands = argv.slice(i);
|
|
1823
1899
|
if (operands.length) {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1900
|
+
const output = [];
|
|
1901
|
+
let status = 0, stderr = "";
|
|
1902
|
+
for (const operand of operands) {
|
|
1903
|
+
if (operands.length > 1)
|
|
1904
|
+
output.push(`${output.length ? "\n" : ""}==> ${operand} <==\n`);
|
|
1905
|
+
try {
|
|
1906
|
+
const data = await readFallbackFiles([operand], state, input);
|
|
1907
|
+
output.push(bytesMode
|
|
1908
|
+
? data.slice(0, count)
|
|
1909
|
+
: decoder.decode(data).split(/(?<=\n)/).slice(0, count).join(""));
|
|
1910
|
+
} catch (error) { status = 1; stderr += `bunmsh: head: ${operand}: ${error.message}\n`; }
|
|
1911
|
+
}
|
|
1912
|
+
return result(status, concatBytes(output), stderr);
|
|
1829
1913
|
}
|
|
1830
1914
|
const reader = fallbackInput(input).getReader();
|
|
1831
1915
|
const output = [];
|
|
@@ -1873,19 +1957,32 @@ async function runTextFilter(argv, state, input, kind) {
|
|
|
1873
1957
|
reverse ||= option.includes("r"); numeric ||= option.includes("n"); unique ||= option.includes("u");
|
|
1874
1958
|
}
|
|
1875
1959
|
}
|
|
1876
|
-
const text = decoder.decode(await readFallbackFiles(args, state, input));
|
|
1877
|
-
let lines = text.split("\n");
|
|
1878
|
-
const trailing = lines.at(-1) === "";
|
|
1879
|
-
if (trailing) lines.pop();
|
|
1880
1960
|
if (kind === "tail") {
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1961
|
+
const operands = args.length ? args : ["-"];
|
|
1962
|
+
const output = [];
|
|
1963
|
+
let status = 0, stderr = "";
|
|
1964
|
+
for (const operand of operands) {
|
|
1965
|
+
try {
|
|
1966
|
+
const text = decoder.decode(await readFallbackFiles([operand], state, input));
|
|
1967
|
+
const lines = text.split("\n");
|
|
1968
|
+
const trailing = lines.at(-1) === "";
|
|
1969
|
+
if (trailing) lines.pop();
|
|
1970
|
+
const selected = tailFromStart
|
|
1971
|
+
? lines.slice(Math.max(0, tailCount - 1))
|
|
1972
|
+
: tailCount === 0 ? [] : lines.slice(-tailCount);
|
|
1973
|
+
if (operands.length > 1 && (tailFromStart || tailCount !== 0))
|
|
1974
|
+
output.push(`${output.length ? "\n" : ""}==> ${operand === "-" ? "standard input" : operand} <==\n`);
|
|
1975
|
+
if (selected.length) output.push(`${selected.join("\n")}${trailing ? "\n" : ""}`);
|
|
1976
|
+
} catch (error) { status = 1; stderr += `bunmsh: tail: ${operand}: ${error.message}\n`; }
|
|
1977
|
+
}
|
|
1978
|
+
return result(status, output.join(""), stderr);
|
|
1888
1979
|
}
|
|
1980
|
+
const text = decoder.decode(await readFallbackFiles(args, state, input));
|
|
1981
|
+
let lines = text.split("\n");
|
|
1982
|
+
if (lines.at(-1) === "") lines.pop();
|
|
1983
|
+
lines.sort((a, b) => numeric ? Number(a) - Number(b) : a.localeCompare(b));
|
|
1984
|
+
if (unique) lines = lines.filter((line, index) => index === 0 || line !== lines[index - 1]);
|
|
1985
|
+
if (reverse) lines.reverse();
|
|
1889
1986
|
return result(0, lines.length ? `${lines.join("\n")}\n` : "");
|
|
1890
1987
|
} catch (error) { return result(1, "", `bunmsh: ${kind}: ${error.message}\n`); }
|
|
1891
1988
|
}
|
|
@@ -1894,15 +1991,27 @@ async function runWcFallback(argv, state, input) {
|
|
|
1894
1991
|
let flags = "", i = 1;
|
|
1895
1992
|
while (/^-[lwc]+$/.test(argv[i] ?? "")) flags += argv[i++].slice(1);
|
|
1896
1993
|
if (!flags) flags = "lwc";
|
|
1897
|
-
|
|
1898
|
-
|
|
1994
|
+
const operands = argv.slice(i);
|
|
1995
|
+
const files = operands.length ? operands : ["-"];
|
|
1996
|
+
const rows = [];
|
|
1997
|
+
let status = 0, stderr = "";
|
|
1998
|
+
let totalBytes = 0;
|
|
1999
|
+
for (const operand of files) try {
|
|
2000
|
+
const data = await readFallbackFiles([operand], state, input);
|
|
1899
2001
|
const text = decoder.decode(data);
|
|
1900
2002
|
const values = [];
|
|
1901
2003
|
if (flags.includes("l")) values.push((text.match(/\n/g) ?? []).length);
|
|
1902
2004
|
if (flags.includes("w")) values.push(text.trim() ? text.trim().split(/\s+/).length : 0);
|
|
1903
2005
|
if (flags.includes("c")) values.push(data.byteLength);
|
|
1904
|
-
|
|
1905
|
-
|
|
2006
|
+
totalBytes += data.byteLength;
|
|
2007
|
+
rows.push({ values, operand });
|
|
2008
|
+
} catch (error) { status = 1; stderr += `bunmsh: wc: ${operand}: ${error.message}\n`; }
|
|
2009
|
+
if (operands.length > 1 && rows.length)
|
|
2010
|
+
rows.push({ values: rows.reduce((total, row) => total.map((n, index) => n + row.values[index]),
|
|
2011
|
+
rows[0].values.map(() => 0)), operand: "total" });
|
|
2012
|
+
const width = operands.length > 1 ? String(totalBytes).length : 0;
|
|
2013
|
+
return result(status, rows.map(({ values, operand }) =>
|
|
2014
|
+
`${values.map((value) => String(value).padStart(width)).join(" ")}${operands.length ? ` ${operand}` : ""}\n`).join(""), stderr);
|
|
1906
2015
|
}
|
|
1907
2016
|
|
|
1908
2017
|
function expandTrSet(value) {
|
|
@@ -2007,11 +2116,14 @@ async function runTeeFallback(argv, state, input) {
|
|
|
2007
2116
|
|
|
2008
2117
|
async function runHashFallback(argv, state, input, algorithm) {
|
|
2009
2118
|
const operands = argv.slice(1);
|
|
2010
|
-
|
|
2011
|
-
|
|
2119
|
+
const output = [];
|
|
2120
|
+
let status = 0, stderr = "";
|
|
2121
|
+
for (const operand of operands.length ? operands : ["-"]) try {
|
|
2122
|
+
const data = await readFallbackFiles([operand], state, input);
|
|
2012
2123
|
const digest = createHash(algorithm).update(data).digest("hex");
|
|
2013
|
-
|
|
2014
|
-
} catch (error) {
|
|
2124
|
+
output.push(`${digest} ${operand}\n`);
|
|
2125
|
+
} catch (error) { status = 1; stderr += `bunmsh: ${algorithm}sum: ${operand}: ${error.message}\n`; }
|
|
2126
|
+
return result(status, output.join(""), stderr);
|
|
2015
2127
|
}
|
|
2016
2128
|
|
|
2017
2129
|
function runMktempFallback(argv, state) {
|
|
@@ -2173,6 +2285,8 @@ async function runGrepFallback(argv, state, input, context = {}) {
|
|
|
2173
2285
|
};
|
|
2174
2286
|
|
|
2175
2287
|
const operands = argv.slice(i);
|
|
2288
|
+
const implicitCwd = !operands.length && flags.includes("r");
|
|
2289
|
+
if (implicitCwd) operands.push(".");
|
|
2176
2290
|
if (operands.length) {
|
|
2177
2291
|
try {
|
|
2178
2292
|
const files = grepFiles(operands, state, flags.includes("r"));
|
|
@@ -2181,7 +2295,8 @@ async function runGrepFallback(argv, state, input, context = {}) {
|
|
|
2181
2295
|
const lines = (await Bun.file(file.path).text()).split("\n");
|
|
2182
2296
|
if (lines.at(-1) === "") lines.pop();
|
|
2183
2297
|
for (let line = 0; line < lines.length; line++)
|
|
2184
|
-
if (processLine(lines[line], line + 1,
|
|
2298
|
+
if (processLine(lines[line], line + 1,
|
|
2299
|
+
implicitCwd ? file.shown.replace(/^\.\//, "") : file.shown, showLabel)) return result(0);
|
|
2185
2300
|
}
|
|
2186
2301
|
} catch (error) { return result(2, "", `bunmsh: grep: ${error.message}\n`); }
|
|
2187
2302
|
} else {
|
|
@@ -2341,20 +2456,26 @@ function cutCharacterRanges(spec) {
|
|
|
2341
2456
|
return ranges;
|
|
2342
2457
|
}
|
|
2343
2458
|
|
|
2344
|
-
async function runCutFallback(argv,
|
|
2459
|
+
async function runCutFallback(argv, state, input) {
|
|
2345
2460
|
let spec, i = 1;
|
|
2346
2461
|
if (argv[i] === "-c") spec = argv[++i], i++;
|
|
2347
2462
|
else if (argv[i]?.startsWith("-c")) spec = argv[i++].slice(2);
|
|
2348
|
-
if (!spec
|
|
2463
|
+
if (!spec) return result(1, "", "bunmsh: cut: usage: cut -c LIST [FILE ...]\n");
|
|
2349
2464
|
try {
|
|
2350
2465
|
const ranges = cutCharacterRanges(spec);
|
|
2351
|
-
const
|
|
2352
|
-
|
|
2353
|
-
const
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2466
|
+
const output = [];
|
|
2467
|
+
let status = 0, stderr = "";
|
|
2468
|
+
const operands = argv.slice(i);
|
|
2469
|
+
for (const operand of operands.length ? operands : ["-"]) try {
|
|
2470
|
+
const text = decoder.decode(await readFallbackFiles([operand], state, input));
|
|
2471
|
+
const trailing = text.endsWith("\n");
|
|
2472
|
+
const lines = text.split("\n");
|
|
2473
|
+
if (trailing) lines.pop();
|
|
2474
|
+
output.push(lines.map((line) => [...line].filter((_ch, index) =>
|
|
2475
|
+
ranges.some(([start, end]) => index + 1 >= start && index + 1 <= end)).join("")).join("\n") +
|
|
2476
|
+
(trailing ? "\n" : ""));
|
|
2477
|
+
} catch (error) { status = 1; stderr += `bunmsh: cut: ${operand}: ${error.message}\n`; }
|
|
2478
|
+
return result(status, output.join(""), stderr);
|
|
2358
2479
|
} catch (error) { return result(1, "", `bunmsh: cut: ${error.message}\n`); }
|
|
2359
2480
|
}
|
|
2360
2481
|
|
|
@@ -2366,24 +2487,27 @@ function runLnFallback(argv, state) {
|
|
|
2366
2487
|
noTargetDirectory ||= argv[i].includes("T");
|
|
2367
2488
|
i++;
|
|
2368
2489
|
}
|
|
2369
|
-
if (argv.length - i
|
|
2370
|
-
const
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
try {
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2490
|
+
if (argv.length - i < 2) return result(1, "", "bunmsh: ln: usage: ln [-sf] target... link_name\n");
|
|
2491
|
+
const sources = argv.slice(i, -1);
|
|
2492
|
+
const destination = nativePath(argv.at(-1));
|
|
2493
|
+
const targetPath = isAbsolute(destination) ? destination : resolvePath(nativePath(state.cwd), destination);
|
|
2494
|
+
let directory = false;
|
|
2495
|
+
if (!noTargetDirectory) try { directory = statSync(targetPath).isDirectory(); } catch {}
|
|
2496
|
+
if (sources.length > 1 && !directory)
|
|
2497
|
+
return result(1, "", "bunmsh: ln: target is not a directory\n");
|
|
2498
|
+
let status = 0, stderr = "";
|
|
2499
|
+
for (const operand of sources) {
|
|
2500
|
+
const target = nativePath(operand);
|
|
2501
|
+
const link = directory ? resolvePath(targetPath, pathBasename(target)) : targetPath;
|
|
2502
|
+
try {
|
|
2503
|
+
if (force) try { unlinkSync(link); } catch {}
|
|
2504
|
+
if (symbolic) symlinkSync(target, link); else {
|
|
2505
|
+
const source = isAbsolute(target) ? target : resolvePath(nativePath(state.cwd), target);
|
|
2506
|
+
linkSync(source, link);
|
|
2507
|
+
}
|
|
2508
|
+
} catch (error) { status = 1; stderr += `bunmsh: ln: ${operand}: ${error.message}\n`; }
|
|
2509
|
+
}
|
|
2510
|
+
return result(status, "", stderr);
|
|
2387
2511
|
}
|
|
2388
2512
|
|
|
2389
2513
|
function runChmodFallback(argv, state) {
|
|
@@ -2859,6 +2983,9 @@ function compoundLogicalSource(source) {
|
|
|
2859
2983
|
breaks.add(token.offset);
|
|
2860
2984
|
if (["elif", "else", "fi", "done", "esac"].includes(word))
|
|
2861
2985
|
breaks.add(token.offset);
|
|
2986
|
+
if (["fi", "done", "esac"].includes(word) && tokens[index + 1]?.value === ";" &&
|
|
2987
|
+
tokens[index + 2] && tokens[index + 2].value !== ";")
|
|
2988
|
+
breaks.add(tokens[index + 2].offset);
|
|
2862
2989
|
if (["then", "do", "else"].includes(word) && tokens[index + 1])
|
|
2863
2990
|
breaks.add(tokens[index + 1].offset);
|
|
2864
2991
|
commandStart = ["if", "while", "until", "elif", "then", "do", "else"].includes(word);
|
|
@@ -2870,14 +2997,12 @@ function compoundLogicalSource(source) {
|
|
|
2870
2997
|
return logical;
|
|
2871
2998
|
}
|
|
2872
2999
|
|
|
2873
|
-
// A leaf command
|
|
2874
|
-
//
|
|
2875
|
-
//
|
|
2876
|
-
//
|
|
2877
|
-
|
|
2878
|
-
function growHeredocLine(lines, index) {
|
|
3000
|
+
// A leaf command inside a compound body may continue across physical lines
|
|
3001
|
+
// inside quotes, substitutions, or a here-document. Keep those lines together
|
|
3002
|
+
// until mksh-style lexical tokenization says the command is complete, so
|
|
3003
|
+
// keywords inside a multiline word are never mistaken for compound syntax.
|
|
3004
|
+
function growLogicalCommand(lines, index) {
|
|
2879
3005
|
const line = lines[index];
|
|
2880
|
-
if (!line.includes("<<")) return { source: line, next: index + 1 };
|
|
2881
3006
|
let end = index;
|
|
2882
3007
|
let candidate = line;
|
|
2883
3008
|
while (true) {
|
|
@@ -2886,7 +3011,7 @@ function growHeredocLine(lines, index) {
|
|
|
2886
3011
|
return { source: candidate, next: end + 1 };
|
|
2887
3012
|
} catch (error) {
|
|
2888
3013
|
if (!(error instanceof ShellSyntaxError) ||
|
|
2889
|
-
|
|
3014
|
+
!/^(?:unterminated (?:here-document|single quote|double quote|command substitution|substitution))/.test(error.message))
|
|
2890
3015
|
throw error;
|
|
2891
3016
|
end++;
|
|
2892
3017
|
if (end >= lines.length) throw error;
|
|
@@ -2895,7 +3020,199 @@ function growHeredocLine(lines, index) {
|
|
|
2895
3020
|
}
|
|
2896
3021
|
}
|
|
2897
3022
|
|
|
2898
|
-
function
|
|
3023
|
+
function makeCompoundPostfix(compound, suffix, source, nextLine) {
|
|
3024
|
+
const match = /^\s*(\|\||&&|\||;)\s*(.*)$/.exec(suffix ?? "");
|
|
3025
|
+
if (!match) return { node: compound, nextLine };
|
|
3026
|
+
let command = match[2].trim();
|
|
3027
|
+
while (!command && nextLine < source.length) {
|
|
3028
|
+
command = source[nextLine++].trim();
|
|
3029
|
+
if (!command || command.startsWith("#")) command = "";
|
|
3030
|
+
}
|
|
3031
|
+
if (!command)
|
|
3032
|
+
throw new ShellSyntaxError(`compound command requires a command after ${match[1]}`);
|
|
3033
|
+
return {
|
|
3034
|
+
node: {
|
|
3035
|
+
type: "compoundPostfix",
|
|
3036
|
+
compound,
|
|
3037
|
+
operator: match[1],
|
|
3038
|
+
command,
|
|
3039
|
+
},
|
|
3040
|
+
nextLine,
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
function findShellKeyword(source, keyword) {
|
|
3045
|
+
if (source.trimStart().startsWith("#")) return -1;
|
|
3046
|
+
let quote = null;
|
|
3047
|
+
for (let i = 0; i < source.length;) {
|
|
3048
|
+
const ch = source[i];
|
|
3049
|
+
if (quote === "'") {
|
|
3050
|
+
if (ch === "'") quote = null;
|
|
3051
|
+
i++;
|
|
3052
|
+
continue;
|
|
3053
|
+
}
|
|
3054
|
+
if (quote === '"') {
|
|
3055
|
+
if (ch === "\\" && /[\\"$`\n]/.test(source[i + 1] ?? "")) { i += 2; continue; }
|
|
3056
|
+
if (ch === '"') quote = null;
|
|
3057
|
+
i++;
|
|
3058
|
+
continue;
|
|
3059
|
+
}
|
|
3060
|
+
if (ch === "\\") { i += 2; continue; }
|
|
3061
|
+
if (ch === "'" || ch === '"') { quote = ch; i++; continue; }
|
|
3062
|
+
if (ch === "`" || (ch === "$" && source[i + 1] === "(")) {
|
|
3063
|
+
try { i = substitutionEnd(source, i); }
|
|
3064
|
+
catch { return -1; } // This physical line may continue the substitution.
|
|
3065
|
+
continue;
|
|
3066
|
+
}
|
|
3067
|
+
if (ch === "$" && source[i + 1] === "{") {
|
|
3068
|
+
try { i = parameterEnd(source, i); }
|
|
3069
|
+
catch { return -1; }
|
|
3070
|
+
continue;
|
|
3071
|
+
}
|
|
3072
|
+
if (/[A-Za-z_]/.test(ch)) {
|
|
3073
|
+
const start = i++;
|
|
3074
|
+
while (/[A-Za-z0-9_]/.test(source[i] ?? "")) i++;
|
|
3075
|
+
if (source.slice(start, i) === keyword) {
|
|
3076
|
+
let previous = start - 1;
|
|
3077
|
+
while (previous >= 0 && /\s/.test(source[previous])) previous--;
|
|
3078
|
+
if (keyword !== "esac" || previous < 0 || source[previous] === ";" || source[previous] === "\n")
|
|
3079
|
+
return start;
|
|
3080
|
+
}
|
|
3081
|
+
continue;
|
|
3082
|
+
}
|
|
3083
|
+
i++;
|
|
3084
|
+
}
|
|
3085
|
+
return -1;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
function splitCasePatterns(source) {
|
|
3089
|
+
const patterns = [];
|
|
3090
|
+
let start = 0;
|
|
3091
|
+
let quote = null;
|
|
3092
|
+
for (let i = 0; i < source.length; i++) {
|
|
3093
|
+
const ch = source[i];
|
|
3094
|
+
if (ch === "\\") { i++; continue; }
|
|
3095
|
+
if (quote) {
|
|
3096
|
+
if (ch === quote) quote = null;
|
|
3097
|
+
continue;
|
|
3098
|
+
}
|
|
3099
|
+
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
3100
|
+
if (ch === "$" && (source[i + 1] === "(" || source[i + 1] === "{")) {
|
|
3101
|
+
i = (source[i + 1] === "(" ? substitutionEnd(source, i) : parameterEnd(source, i)) - 1;
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
if (ch === "`") { i = substitutionEnd(source, i) - 1; continue; }
|
|
3105
|
+
if (ch === "|") { patterns.push(source.slice(start, i).trim()); start = i + 1; }
|
|
3106
|
+
}
|
|
3107
|
+
patterns.push(source.slice(start).trim());
|
|
3108
|
+
return patterns;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
// Scan case clauses in the same lexical spirit as mksh's CASEPAT/KEYWORD
|
|
3112
|
+
// modes. In particular, clause boundaries are shell tokens, not line
|
|
3113
|
+
// boundaries: Autoconf commonly writes an entire clause on one line.
|
|
3114
|
+
function parseCaseArms(source) {
|
|
3115
|
+
const arms = [];
|
|
3116
|
+
let i = 0;
|
|
3117
|
+
const skipSpace = () => {
|
|
3118
|
+
while (/\s/.test(source[i] ?? "")) i++;
|
|
3119
|
+
};
|
|
3120
|
+
while (i < source.length) {
|
|
3121
|
+
skipSpace();
|
|
3122
|
+
if (i >= source.length) break;
|
|
3123
|
+
if (source[i] === "#") {
|
|
3124
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
3125
|
+
continue;
|
|
3126
|
+
}
|
|
3127
|
+
if (source[i] === "(") { i++; skipSpace(); }
|
|
3128
|
+
const patternStart = i;
|
|
3129
|
+
let quote = null;
|
|
3130
|
+
let extensionDepth = 0;
|
|
3131
|
+
for (; i < source.length; i++) {
|
|
3132
|
+
const ch = source[i];
|
|
3133
|
+
if (ch === "\\") { i++; continue; }
|
|
3134
|
+
if (quote) {
|
|
3135
|
+
if (ch === quote) quote = null;
|
|
3136
|
+
continue;
|
|
3137
|
+
}
|
|
3138
|
+
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
3139
|
+
if (ch === "$" && (source[i + 1] === "(" || source[i + 1] === "{")) {
|
|
3140
|
+
i = (source[i + 1] === "(" ? substitutionEnd(source, i) : parameterEnd(source, i)) - 1;
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
if (ch === "`") { i = substitutionEnd(source, i) - 1; continue; }
|
|
3144
|
+
if (ch === "(" && /[?*+@!]/.test(source[i - 1] ?? "")) { extensionDepth++; continue; }
|
|
3145
|
+
if (ch === ")" && extensionDepth) { extensionDepth--; continue; }
|
|
3146
|
+
if (ch === ")") break;
|
|
3147
|
+
}
|
|
3148
|
+
if (i >= source.length)
|
|
3149
|
+
throw new ShellSyntaxError(`case pattern requires ): ${source.slice(patternStart, Math.min(source.length, patternStart + 120))}`);
|
|
3150
|
+
const patterns = splitCasePatterns(source.slice(patternStart, i));
|
|
3151
|
+
if (!patterns.length || patterns.some((pattern) => !pattern))
|
|
3152
|
+
throw new ShellSyntaxError("case requires a pattern");
|
|
3153
|
+
i++;
|
|
3154
|
+
|
|
3155
|
+
const bodyStart = i;
|
|
3156
|
+
quote = null;
|
|
3157
|
+
let nestedCases = 0;
|
|
3158
|
+
let word = "";
|
|
3159
|
+
let terminator = null;
|
|
3160
|
+
const finishWord = () => {
|
|
3161
|
+
if (word === "case") nestedCases++;
|
|
3162
|
+
else if (word === "esac" && nestedCases) nestedCases--;
|
|
3163
|
+
word = "";
|
|
3164
|
+
};
|
|
3165
|
+
for (; i < source.length; i++) {
|
|
3166
|
+
const ch = source[i];
|
|
3167
|
+
if (ch === "#" && !word && (i === bodyStart || /[\s;|&]/.test(source[i - 1] ?? ""))) {
|
|
3168
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
3169
|
+
continue;
|
|
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;
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
// POSIX allows the final clause to omit `;;` immediately before esac.
|
|
3196
|
+
if (!terminator) {
|
|
3197
|
+
if (source.slice(bodyStart, i).trim() || arms.length) terminator = ";;";
|
|
3198
|
+
else throw new ShellSyntaxError("unterminated case clause");
|
|
3199
|
+
}
|
|
3200
|
+
const bodySource = source.slice(bodyStart, i).trim();
|
|
3201
|
+
let body;
|
|
3202
|
+
try {
|
|
3203
|
+
body = parseCompoundScript(bodySource);
|
|
3204
|
+
} catch (error) {
|
|
3205
|
+
if (error instanceof ShellSyntaxError)
|
|
3206
|
+
error.message += ` in case arm ${patterns.join("|")}`;
|
|
3207
|
+
throw error;
|
|
3208
|
+
}
|
|
3209
|
+
arms.push({ patterns, body, terminator });
|
|
3210
|
+
if (i < source.length) i += 2;
|
|
3211
|
+
}
|
|
3212
|
+
return arms;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
export function parseCompoundScript(source) {
|
|
2899
3216
|
const lines = compoundLogicalSource(source.replace(/\\\r?\n/g, "")).split(/\r?\n/);
|
|
2900
3217
|
const parseSequence = (start, stops = []) => {
|
|
2901
3218
|
const nodes = [];
|
|
@@ -2923,102 +3240,163 @@ function parseCompoundScript(source) {
|
|
|
2923
3240
|
}
|
|
2924
3241
|
|
|
2925
3242
|
if (/^if(?:\s|$)/.test(trimmed)) {
|
|
3243
|
+
const compoundStart = i;
|
|
2926
3244
|
const branches = [];
|
|
2927
3245
|
let header = trimmed;
|
|
2928
|
-
while (
|
|
3246
|
+
while (!/(?:;\s*|\s+)then(?:\s+.*)?$/.test(header) && ++i < lines.length)
|
|
2929
3247
|
header += ` ${lines[i].trim()}`;
|
|
2930
|
-
|
|
2931
|
-
|
|
3248
|
+
let then = /(?:;\s*|\s+)then(?:\s+(.*))?$/.exec(header);
|
|
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() ?? "";
|
|
2932
3252
|
i++;
|
|
2933
3253
|
while (true) {
|
|
2934
|
-
const body = parseSequence(i, [/^elif(?:\s|$)/, /^else\s*;?$/, /^fi\
|
|
2935
|
-
branches.push({ condition, body: body.nodes });
|
|
3254
|
+
const body = parseSequence(i, [/^elif(?:\s|$)/, /^else\s*;?$/, /^fi\b/]);
|
|
3255
|
+
branches.push({ condition, body: body.nodes, thenCommand });
|
|
2936
3256
|
i = body.index;
|
|
2937
3257
|
const stop = lines[i]?.trim() ?? "";
|
|
2938
3258
|
if (/^elif(?:\s|$)/.test(stop)) {
|
|
2939
3259
|
header = stop;
|
|
2940
|
-
while (
|
|
3260
|
+
while (!/(?:;\s*|\s+)then(?:\s+.*)?$/.test(header) && ++i < lines.length)
|
|
2941
3261
|
header += ` ${lines[i].trim()}`;
|
|
2942
|
-
|
|
2943
|
-
|
|
3262
|
+
then = /(?:;\s*|\s+)then(?:\s+(.*))?$/.exec(header);
|
|
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() ?? "";
|
|
2944
3266
|
i++;
|
|
2945
3267
|
continue;
|
|
2946
3268
|
}
|
|
2947
3269
|
let otherwise = [];
|
|
2948
3270
|
if (/^else\s*;?$/.test(stop)) {
|
|
2949
|
-
const body = parseSequence(i + 1, [/^fi\
|
|
3271
|
+
const body = parseSequence(i + 1, [/^fi\b/]);
|
|
2950
3272
|
otherwise = body.nodes;
|
|
2951
3273
|
i = body.index;
|
|
2952
3274
|
}
|
|
2953
|
-
if (!/^fi\
|
|
3275
|
+
if (!/^fi\b/.test(lines[i]?.trim() ?? ""))
|
|
2954
3276
|
throw new ShellSyntaxError("unterminated if");
|
|
2955
|
-
|
|
2956
|
-
i
|
|
3277
|
+
const compound = { type: "if", branches, otherwise };
|
|
3278
|
+
const close = /^fi\b(.*)$/.exec(lines[i].trim());
|
|
3279
|
+
const result = makeCompoundPostfix(
|
|
3280
|
+
{ ...compound, source: [...lines.slice(compoundStart, i), "fi"].join("\n") },
|
|
3281
|
+
close?.[1] ?? "",
|
|
3282
|
+
lines,
|
|
3283
|
+
i + 1,
|
|
3284
|
+
);
|
|
3285
|
+
nodes.push(result.node);
|
|
3286
|
+
i = result.nextLine;
|
|
2957
3287
|
break;
|
|
2958
3288
|
}
|
|
2959
3289
|
continue;
|
|
2960
3290
|
}
|
|
2961
3291
|
if (/^(?:while|until)(?:\s|$)/.test(trimmed)) {
|
|
3292
|
+
const compoundStart = i;
|
|
2962
3293
|
const kind = trimmed.startsWith("until") ? "until" : "while";
|
|
2963
3294
|
let header = trimmed;
|
|
2964
|
-
while (
|
|
3295
|
+
while (!/(?:;\s*|\s+)do\s*$/.test(header) && ++i < lines.length)
|
|
2965
3296
|
header += ` ${lines[i].trim()}`;
|
|
2966
|
-
if (
|
|
2967
|
-
const condition = header.replace(new RegExp(`^${kind}\\s*`), "").replace(
|
|
2968
|
-
const body = parseSequence(i + 1, [/^done
|
|
2969
|
-
const done = /^done(?:\s*<\s*(.+?))?\s
|
|
3297
|
+
if (!/(?:;\s*|\s+)do\s*$/.test(header)) throw new ShellSyntaxError(`${kind} requires do`);
|
|
3298
|
+
const condition = header.replace(new RegExp(`^${kind}\\s*`), "").replace(/(?:;\s*|\s+)do\s*$/, "");
|
|
3299
|
+
const body = parseSequence(i + 1, [/^done\b/]);
|
|
3300
|
+
const done = /^done(?:\s*<\s*(.+?))?\s*;?\s*((?:\|\||&&|\||;).*)?$/.exec(lines[body.index]?.trim() ?? "");
|
|
2970
3301
|
if (!done)
|
|
2971
3302
|
throw new ShellSyntaxError(`unterminated ${kind}`);
|
|
2972
|
-
|
|
2973
|
-
|
|
3303
|
+
const compound = {
|
|
3304
|
+
type: "loop", kind, condition, body: body.nodes, input: done[1] ?? null,
|
|
3305
|
+
source: [...lines.slice(compoundStart, body.index), `done${done[1] ? ` < ${done[1]}` : ""}`].join("\n"),
|
|
3306
|
+
};
|
|
3307
|
+
const result = makeCompoundPostfix(compound, done[2] ?? "", lines, body.index + 1);
|
|
3308
|
+
nodes.push(result.node);
|
|
3309
|
+
i = result.nextLine;
|
|
2974
3310
|
continue;
|
|
2975
3311
|
}
|
|
2976
3312
|
if (/^for(?:\s|$)/.test(trimmed)) {
|
|
3313
|
+
const compoundStart = i;
|
|
2977
3314
|
let header = trimmed;
|
|
2978
|
-
|
|
3315
|
+
let bodyStart = -1;
|
|
3316
|
+
while (i < lines.length) {
|
|
3317
|
+
if (/;\s*do\s*$/.test(header)) {
|
|
3318
|
+
header = header.replace(/;\s*do\s*$/, "");
|
|
3319
|
+
bodyStart = i + 1;
|
|
3320
|
+
break;
|
|
3321
|
+
}
|
|
3322
|
+
if (lines[i + 1]?.trim() === "do") {
|
|
3323
|
+
bodyStart = i + 2;
|
|
3324
|
+
i++;
|
|
3325
|
+
break;
|
|
3326
|
+
}
|
|
3327
|
+
if (++i >= lines.length) break;
|
|
2979
3328
|
header += ` ${lines[i].trim()}`;
|
|
2980
|
-
|
|
3329
|
+
}
|
|
3330
|
+
const match = /^for\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s+in(?:\s+(.*?))?)?\s*;?$/.exec(header);
|
|
2981
3331
|
if (!match) throw new ShellSyntaxError("invalid for command");
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3332
|
+
if (bodyStart < 0) throw new ShellSyntaxError("for requires do");
|
|
3333
|
+
const body = parseSequence(bodyStart, [/^done\b/]);
|
|
3334
|
+
if (!/^done\b/.test(lines[body.index]?.trim() ?? ""))
|
|
3335
|
+
throw new ShellSyntaxError(`unterminated for: ${lines[compoundStart]?.trim()} (stopped at ${lines[body.index]?.trim()})`);
|
|
3336
|
+
const compound = {
|
|
3337
|
+
type: "for", name: match[1], words: match[2] ?? null, body: body.nodes,
|
|
3338
|
+
source: [...lines.slice(compoundStart, body.index), "done"].join("\n"),
|
|
3339
|
+
};
|
|
3340
|
+
const close = /^done\b(.*)$/.exec(lines[body.index].trim());
|
|
3341
|
+
const result = makeCompoundPostfix(compound, close?.[1] ?? "", lines, body.index + 1);
|
|
3342
|
+
nodes.push(result.node);
|
|
3343
|
+
i = result.nextLine;
|
|
2987
3344
|
continue;
|
|
2988
3345
|
}
|
|
2989
3346
|
if (/^case(?:\s|$)/.test(trimmed)) {
|
|
3347
|
+
const compoundStart = i;
|
|
2990
3348
|
let header = trimmed;
|
|
2991
|
-
while (!/\s+in
|
|
3349
|
+
while (!/\s+in(?:\s|$)/.test(header) && ++i < lines.length)
|
|
2992
3350
|
header += ` ${lines[i].trim()}`;
|
|
2993
|
-
const match = /^case\s+(.+?)\s+in
|
|
3351
|
+
const match = /^case\s+(.+?)\s+in(?:\s+(.*))?$/.exec(header);
|
|
2994
3352
|
if (!match) throw new ShellSyntaxError("case requires in");
|
|
2995
|
-
const
|
|
3353
|
+
const inlineEsac = findShellKeyword(match[2] ?? "", "esac");
|
|
3354
|
+
if (inlineEsac >= 0) {
|
|
3355
|
+
const inlineArms = match[2].slice(0, inlineEsac);
|
|
3356
|
+
const suffix = match[2].slice(inlineEsac + "esac".length);
|
|
3357
|
+
const compound = {
|
|
3358
|
+
type: "case",
|
|
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]] : [];
|
|
2996
3369
|
i++;
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
const
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
bodyLines.push(current);
|
|
3012
|
-
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--;
|
|
3013
3384
|
}
|
|
3014
|
-
|
|
3385
|
+
bodyLines.push(lines[i++]);
|
|
3015
3386
|
}
|
|
3016
|
-
if (!/^esac\
|
|
3017
|
-
|
|
3018
|
-
|
|
3387
|
+
if (!/^esac\b/.test(lines[i]?.trim() ?? ""))
|
|
3388
|
+
throw new ShellSyntaxError(`unterminated case: ${trimmed} (at ${lines[i]?.trim() ?? "EOF"})`);
|
|
3389
|
+
const compound = {
|
|
3390
|
+
type: "case", word: match[1], arms: parseCaseArms(bodyLines.join("\n")),
|
|
3391
|
+
source: [...lines.slice(compoundStart, i), "esac"].join("\n"),
|
|
3392
|
+
};
|
|
3393
|
+
const close = /^esac\b(.*)$/.exec(lines[i].trim());
|
|
3394
|
+
const result = makeCompoundPostfix(compound, close?.[1] ?? "", lines, i + 1);
|
|
3395
|
+
nodes.push(result.node);
|
|
3396
|
+
i = result.nextLine;
|
|
3019
3397
|
continue;
|
|
3020
3398
|
}
|
|
3021
|
-
const grown =
|
|
3399
|
+
const grown = growLogicalCommand(lines, i);
|
|
3022
3400
|
nodes.push({ type: "command", source: grown.source });
|
|
3023
3401
|
i = grown.next;
|
|
3024
3402
|
}
|
|
@@ -3069,6 +3447,61 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3069
3447
|
};
|
|
3070
3448
|
for (const node of nodes) {
|
|
3071
3449
|
if (state.exitRequested) break;
|
|
3450
|
+
if (node.type === "compoundPostfix") {
|
|
3451
|
+
if (node.operator === "|") {
|
|
3452
|
+
const link = new TransformStream();
|
|
3453
|
+
const leftPromise = runExternal(
|
|
3454
|
+
pipelineCompoundArgv(node.compound.source),
|
|
3455
|
+
pipelineBuiltinState(state),
|
|
3456
|
+
options.input ?? null,
|
|
3457
|
+
false,
|
|
3458
|
+
true,
|
|
3459
|
+
link.writable,
|
|
3460
|
+
{ pipelineKillSignal: "SIGTERM" },
|
|
3461
|
+
);
|
|
3462
|
+
const rightState = {
|
|
3463
|
+
...state,
|
|
3464
|
+
env: { ...state.env },
|
|
3465
|
+
aliases: { ...state.aliases },
|
|
3466
|
+
functions: { ...state.functions },
|
|
3467
|
+
readonly: new Set(state.readonly),
|
|
3468
|
+
directoryHistory: [...state.directoryHistory],
|
|
3469
|
+
tabs: [...state.tabs],
|
|
3470
|
+
exitRequested: false,
|
|
3471
|
+
exitStatus: 0,
|
|
3472
|
+
};
|
|
3473
|
+
const right = await runExternal(
|
|
3474
|
+
pipelineCompoundArgv(node.command),
|
|
3475
|
+
pipelineBuiltinState(rightState),
|
|
3476
|
+
link.readable,
|
|
3477
|
+
true,
|
|
3478
|
+
true,
|
|
3479
|
+
);
|
|
3480
|
+
const left = await leftPromise;
|
|
3481
|
+
execution = result(right.status, right.stdout, concatBytes([left.stderr, right.stderr]));
|
|
3482
|
+
} else {
|
|
3483
|
+
execution = await executeNodeList([node.compound], state, { capture });
|
|
3484
|
+
append(execution);
|
|
3485
|
+
const shouldRun = node.operator === "&&"
|
|
3486
|
+
? execution.status === 0
|
|
3487
|
+
: node.operator === "||"
|
|
3488
|
+
? execution.status !== 0
|
|
3489
|
+
: true;
|
|
3490
|
+
if (shouldRun) {
|
|
3491
|
+
execution = await execute(node.command, state, { capture });
|
|
3492
|
+
append(execution);
|
|
3493
|
+
}
|
|
3494
|
+
state.lastStatus = execution.status;
|
|
3495
|
+
continue;
|
|
3496
|
+
}
|
|
3497
|
+
state.lastStatus = execution.status;
|
|
3498
|
+
if (capture) append(execution);
|
|
3499
|
+
else {
|
|
3500
|
+
if (execution.stdout.byteLength) stdout.push(execution.stdout);
|
|
3501
|
+
if (execution.stderr.byteLength) stderr.push(execution.stderr);
|
|
3502
|
+
}
|
|
3503
|
+
continue;
|
|
3504
|
+
}
|
|
3072
3505
|
if (node.type === "function") {
|
|
3073
3506
|
state.functions[node.name] = node.body;
|
|
3074
3507
|
continue;
|
|
@@ -3152,29 +3585,39 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3152
3585
|
const parsed = parse(`__case ${node.word}`)[0]?.pipeline[0];
|
|
3153
3586
|
const value = (await expandWord(parsed.words[1], state, { single: true }))[0] ?? "";
|
|
3154
3587
|
execution = result();
|
|
3588
|
+
let fallThrough = false;
|
|
3155
3589
|
for (const arm of node.arms) {
|
|
3156
|
-
let matches =
|
|
3157
|
-
for (
|
|
3158
|
-
|
|
3159
|
-
pattern = stripExpansionMarkers(await expandText(pattern, state));
|
|
3160
|
-
if (globPatternRegex(pattern, true, true, true).test(value)) { matches = true; break; }
|
|
3590
|
+
let matches = fallThrough;
|
|
3591
|
+
if (!matches) for (const pattern of arm.patterns) {
|
|
3592
|
+
if ((await casePatternRegex(pattern, state)).test(value)) { matches = true; break; }
|
|
3161
3593
|
}
|
|
3162
3594
|
if (!matches) continue;
|
|
3163
3595
|
execution = await executeNodeList(arm.body, state, { capture });
|
|
3164
3596
|
append(execution);
|
|
3597
|
+
if (arm.terminator === ";&") { fallThrough = true; continue; }
|
|
3598
|
+
if (arm.terminator === ";|") { fallThrough = false; continue; }
|
|
3165
3599
|
break;
|
|
3166
3600
|
}
|
|
3167
3601
|
state.lastStatus = execution.status;
|
|
3168
3602
|
continue;
|
|
3169
3603
|
}
|
|
3170
3604
|
let selected = null;
|
|
3605
|
+
let selectedThen = "";
|
|
3171
3606
|
for (const branch of node.branches) {
|
|
3172
3607
|
let condition = branch.condition.trim();
|
|
3173
3608
|
let negate = false;
|
|
3174
3609
|
while (/^!\s+/.test(condition)) { negate = !negate; condition = condition.replace(/^!\s+/, ""); }
|
|
3175
3610
|
const checked = await execute(condition, state, { capture, simple: true });
|
|
3176
3611
|
append(checked);
|
|
3177
|
-
if ((checked.status === 0) !== negate) {
|
|
3612
|
+
if ((checked.status === 0) !== negate) {
|
|
3613
|
+
selected = branch.body;
|
|
3614
|
+
selectedThen = branch.thenCommand ?? "";
|
|
3615
|
+
break;
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
if (selectedThen) {
|
|
3619
|
+
const initial = await execute(selectedThen, state, { capture });
|
|
3620
|
+
append(initial);
|
|
3178
3621
|
}
|
|
3179
3622
|
execution = selected
|
|
3180
3623
|
? await executeNodeList(selected, state, { capture })
|
|
@@ -3210,15 +3653,22 @@ async function runExternal(
|
|
|
3210
3653
|
cmd: spawnArgv,
|
|
3211
3654
|
cwd: nativePath(state.cwd),
|
|
3212
3655
|
env: state.env,
|
|
3213
|
-
stdin: input === null ? "inherit" : input,
|
|
3214
|
-
stdout: captureStdout || stdoutSink ? "pipe" : "inherit",
|
|
3215
|
-
stderr: captureStderr || runtimeOptions.stderrSink ? "pipe" : "inherit",
|
|
3656
|
+
stdin: runtimeOptions.background ? "ignore" : input === null ? "inherit" : input,
|
|
3657
|
+
stdout: runtimeOptions.background ? "inherit" : captureStdout || stdoutSink ? "pipe" : "inherit",
|
|
3658
|
+
stderr: runtimeOptions.background ? "inherit" : captureStderr || runtimeOptions.stderrSink ? "pipe" : "inherit",
|
|
3216
3659
|
onExit(proc, exitCode, signalCode, error) {
|
|
3217
3660
|
runtimeOptions.onExit?.(proc, exitCode, signalCode, error);
|
|
3218
3661
|
},
|
|
3219
3662
|
};
|
|
3220
3663
|
try {
|
|
3221
3664
|
const proc = Bun.spawn(options);
|
|
3665
|
+
if (runtimeOptions.background) {
|
|
3666
|
+
if (!Array.isArray(Bun.sha.procs)) Bun.sha.procs = [];
|
|
3667
|
+
Bun.sha.procs.push(proc);
|
|
3668
|
+
proc.unref();
|
|
3669
|
+
const index = Bun.sha.procs.length - 1;
|
|
3670
|
+
return result(0, "", `[${index + 1}] ${proc.pid} Bun.sha.procs[${index}]\n`);
|
|
3671
|
+
}
|
|
3222
3672
|
const pipelineKillSignal = runtimeOptions.pipelineKillSignal ?? "SIGPIPE";
|
|
3223
3673
|
// A downstream pipeline stage that closes early (for example `head -n 3`)
|
|
3224
3674
|
// leaves nothing reading the other end of `stdoutSink`/`stderrSink` (a
|
|
@@ -3286,6 +3736,85 @@ function pipelineBuiltinArgv(argv) {
|
|
|
3286
3736
|
: [...BUN_RUNTIME_COMMAND, BUNMSH_ENTRY, "-cc", ...argv];
|
|
3287
3737
|
}
|
|
3288
3738
|
|
|
3739
|
+
function pipelineCompoundArgv(source) {
|
|
3740
|
+
return IS_COMPILED
|
|
3741
|
+
? [...EXECUTABLE_COMMAND, "-c", source]
|
|
3742
|
+
: [...BUN_RUNTIME_COMMAND, BUNMSH_ENTRY, "-c", source];
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
function leadingSubshell(source) {
|
|
3746
|
+
const leading = source.length - source.trimStart().length;
|
|
3747
|
+
if (source[leading] !== "(") return null;
|
|
3748
|
+
let depth = 0, quote = null;
|
|
3749
|
+
for (let i = leading; i < source.length; i++) {
|
|
3750
|
+
const ch = source[i];
|
|
3751
|
+
if (quote === "'") { if (ch === "'") quote = null; continue; }
|
|
3752
|
+
if (quote === '"') {
|
|
3753
|
+
if (ch === "\\" && /[\\"$`\n]/.test(source[i + 1] ?? "")) { i++; continue; }
|
|
3754
|
+
if (ch === '"') quote = null;
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3757
|
+
if (ch === "\\") { i++; continue; }
|
|
3758
|
+
if (ch === "'" || ch === '"') { quote = ch; continue; }
|
|
3759
|
+
if (ch === "`") {
|
|
3760
|
+
try { i = substitutionEnd(source, i) - 1; } catch { return null; }
|
|
3761
|
+
continue;
|
|
3762
|
+
}
|
|
3763
|
+
if (ch === "$" && source[i + 1] === "(") {
|
|
3764
|
+
try { i = substitutionEnd(source, i) - 1; } catch { return null; }
|
|
3765
|
+
continue;
|
|
3766
|
+
}
|
|
3767
|
+
if (ch === "(") depth++;
|
|
3768
|
+
else if (ch === ")" && --depth === 0)
|
|
3769
|
+
return { body: source.slice(leading + 1, i), suffix: source.slice(i + 1) };
|
|
3770
|
+
}
|
|
3771
|
+
return null;
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
function rewriteSubshells(source, state) {
|
|
3775
|
+
let output = "", quote = null;
|
|
3776
|
+
for (let i = 0; i < source.length;) {
|
|
3777
|
+
const ch = source[i];
|
|
3778
|
+
if (quote === "'") {
|
|
3779
|
+
output += ch;
|
|
3780
|
+
if (ch === "'") quote = null;
|
|
3781
|
+
i++;
|
|
3782
|
+
continue;
|
|
3783
|
+
}
|
|
3784
|
+
if (quote === '"') {
|
|
3785
|
+
output += ch;
|
|
3786
|
+
if (ch === "\\" && /[\\"$`\n]/.test(source[i + 1] ?? "")) output += source[++i];
|
|
3787
|
+
else if (ch === '"') quote = null;
|
|
3788
|
+
i++;
|
|
3789
|
+
continue;
|
|
3790
|
+
}
|
|
3791
|
+
if (ch === "\\") { output += source.slice(i, i + 2); i += 2; continue; }
|
|
3792
|
+
if (ch === "'" || ch === '"') { quote = ch; output += ch; i++; continue; }
|
|
3793
|
+
if (ch === "`") {
|
|
3794
|
+
try { const end = substitutionEnd(source, i); output += source.slice(i, end); i = end; continue; }
|
|
3795
|
+
catch { output += ch; i++; continue; }
|
|
3796
|
+
}
|
|
3797
|
+
if (ch === "$" && source[i + 1] === "(") {
|
|
3798
|
+
try { const end = substitutionEnd(source, i); output += source.slice(i, end); i = end; continue; }
|
|
3799
|
+
catch { output += ch; i++; continue; }
|
|
3800
|
+
}
|
|
3801
|
+
const boundary = i === 0 || /[\s;|&]/.test(source[i - 1]);
|
|
3802
|
+
if (ch === "(" && boundary) {
|
|
3803
|
+
const group = leadingSubshell(source.slice(i));
|
|
3804
|
+
if (group) {
|
|
3805
|
+
const argv = pipelineCompoundArgv(group.body);
|
|
3806
|
+
const serialized = pipelineBuiltinState(state).env[PIPELINE_STATE_ENV];
|
|
3807
|
+
output += `BUNMSH_INTERNAL_PIPELINE_STATE=${quoteShellWord(serialized)} ${argv.map(quoteShellWord).join(" ")}`;
|
|
3808
|
+
i += group.body.length + 2;
|
|
3809
|
+
continue;
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
output += ch;
|
|
3813
|
+
i++;
|
|
3814
|
+
}
|
|
3815
|
+
return output;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3289
3818
|
function pipelineBuiltinState(state) {
|
|
3290
3819
|
return {
|
|
3291
3820
|
...state,
|
|
@@ -3379,6 +3908,15 @@ async function runCommandArgv(
|
|
|
3379
3908
|
if(argv[0] === "&")
|
|
3380
3909
|
commandArgv = argv.slice(1) ;
|
|
3381
3910
|
}
|
|
3911
|
+
|
|
3912
|
+
if (options.background && (
|
|
3913
|
+
options.pipelineStage ||
|
|
3914
|
+
["command", "builtin", "__builtin", "time", "yes"].includes(commandArgv[0]) ||
|
|
3915
|
+
Object.hasOwn(state.functions, commandArgv[0]) ||
|
|
3916
|
+
Object.hasOwn(builtins, commandArgv[0]) ||
|
|
3917
|
+
(!commandArgv[0].includes("/") && Object.hasOwn(fallbackBuiltins, commandArgv[0]) &&
|
|
3918
|
+
!findExecutable(commandArgv[0], state))
|
|
3919
|
+
)) return result(2, "", "bunmsh: &: only a single external command is supported\n");
|
|
3382
3920
|
|
|
3383
3921
|
let commandState = state;
|
|
3384
3922
|
if (options.pipelineStage && runsInPipelineSubprocess(commandArgv, commandState))
|
|
@@ -3585,6 +4123,7 @@ async function runCommand(command, state, options = {}) {
|
|
|
3585
4123
|
for (const redirect of command.redirects) {
|
|
3586
4124
|
if (redirect.op === "1>&2") { stdoutToStderr = true; continue; }
|
|
3587
4125
|
if (redirect.op === "2>&1") { stderrToStdout = true; continue; }
|
|
4126
|
+
if (/^\d+>&\d+$/.test(redirect.op)) continue;
|
|
3588
4127
|
if (redirect.op === "<<" || redirect.op === "<<-") {
|
|
3589
4128
|
const heredoc = redirect.heredoc ?? { body: "", expand: false };
|
|
3590
4129
|
const content = heredoc.expand
|
|
@@ -3674,6 +4213,7 @@ async function runCommand(command, state, options = {}) {
|
|
|
3674
4213
|
stderrSink: stderrRedirect,
|
|
3675
4214
|
onSpawn: options.onSpawn,
|
|
3676
4215
|
onExit: options.onExit,
|
|
4216
|
+
background: Boolean(options.background),
|
|
3677
4217
|
},
|
|
3678
4218
|
);
|
|
3679
4219
|
|
|
@@ -3690,10 +4230,15 @@ async function runCommand(command, state, options = {}) {
|
|
|
3690
4230
|
}
|
|
3691
4231
|
|
|
3692
4232
|
async function runPipeline(pipeline, state, options = {}) {
|
|
4233
|
+
if (options.background && (pipeline.length !== 1 || pipeline[0].redirects.length !== 0))
|
|
4234
|
+
return result(2, "", "bunmsh: &: only a single external command without redirections is supported\n");
|
|
3693
4235
|
if (pipeline.length === 1)
|
|
3694
4236
|
return runCommand(pipeline[0], state, {
|
|
4237
|
+
input: options.input ?? null,
|
|
4238
|
+
stdoutSink: options.stdoutSink ?? null,
|
|
3695
4239
|
captureStdout: Boolean(options.capture),
|
|
3696
4240
|
captureStderr: Boolean(options.capture),
|
|
4241
|
+
background: Boolean(options.background),
|
|
3697
4242
|
});
|
|
3698
4243
|
|
|
3699
4244
|
const links = Array.from(
|
|
@@ -3726,7 +4271,7 @@ async function runPipeline(pipeline, state, options = {}) {
|
|
|
3726
4271
|
const stages = pipeline.map((command, i) => {
|
|
3727
4272
|
const last = i === pipeline.length - 1;
|
|
3728
4273
|
return runCommand(command, state, {
|
|
3729
|
-
input: i === 0 ? null : links[i - 1].readable,
|
|
4274
|
+
input: i === 0 ? options.input ?? null : links[i - 1].readable,
|
|
3730
4275
|
captureStdout: last && Boolean(options.capture),
|
|
3731
4276
|
captureStderr: Boolean(options.capture),
|
|
3732
4277
|
stdoutSink: last ? null : links[i].writable,
|
|
@@ -3774,6 +4319,47 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3774
4319
|
return execution;
|
|
3775
4320
|
}
|
|
3776
4321
|
const rawSource = source.trimStart();
|
|
4322
|
+
const group = leadingSubshell(source);
|
|
4323
|
+
if (group) {
|
|
4324
|
+
let suffix = group.suffix;
|
|
4325
|
+
let discardStdout = false, discardStderr = false;
|
|
4326
|
+
while (true) {
|
|
4327
|
+
const redirect = /^\s*(?:(\d+)?(>>?|<)\s*([^\s;&|]+)|(\d+>&\d+))/.exec(suffix);
|
|
4328
|
+
if (!redirect) break;
|
|
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);
|
|
4360
|
+
}
|
|
4361
|
+
return execution;
|
|
4362
|
+
}
|
|
3777
4363
|
if (isJavaScriptMode(rawSource)) {
|
|
3778
4364
|
let execution;
|
|
3779
4365
|
const previousCwd = process.cwd();
|
|
@@ -3844,6 +4430,7 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3844
4430
|
try {
|
|
3845
4431
|
execution = await executeNodeList(parseCompoundScript(source), state, {
|
|
3846
4432
|
capture: Boolean(io.capture),
|
|
4433
|
+
input: io.input ?? null,
|
|
3847
4434
|
});
|
|
3848
4435
|
}
|
|
3849
4436
|
catch (error) {
|
|
@@ -3858,6 +4445,12 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3858
4445
|
return execution;
|
|
3859
4446
|
}
|
|
3860
4447
|
|
|
4448
|
+
const withSubshells = rewriteSubshells(source, state);
|
|
4449
|
+
if (withSubshells !== source) {
|
|
4450
|
+
source = withSubshells;
|
|
4451
|
+
return execute(source, state, io);
|
|
4452
|
+
}
|
|
4453
|
+
|
|
3861
4454
|
let jobs;
|
|
3862
4455
|
try {
|
|
3863
4456
|
jobs = parse(source);
|
|
@@ -3876,7 +4469,12 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3876
4469
|
for (const job of jobs) {
|
|
3877
4470
|
if (job.connector === "&&" && execution.status !== 0) continue;
|
|
3878
4471
|
if (job.connector === "||" && execution.status === 0) continue;
|
|
3879
|
-
execution = await runPipeline(job.pipeline, state, {
|
|
4472
|
+
execution = await runPipeline(job.pipeline, state, {
|
|
4473
|
+
capture: io.capture,
|
|
4474
|
+
background: job.background,
|
|
4475
|
+
input: io.input ?? null,
|
|
4476
|
+
stdoutSink: io.stdoutSink ?? null,
|
|
4477
|
+
});
|
|
3880
4478
|
if (job.negate) execution.status = execution.status === 0 ? 1 : 0;
|
|
3881
4479
|
state.lastStatus = execution.status;
|
|
3882
4480
|
if (execution.stdout.byteLength) {
|