buninu-linux 0.7.5 → 0.8.55
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/Dockerfile +5 -2
- package/NOTICE.md +110 -49
- package/README.md +439 -144
- package/fetch-npm.sh +44 -0
- package/fetch-plus-build.sh +1 -0
- package/index.js +15 -13
- package/initramfs/bin/buninu-browser +24 -0
- package/initramfs/bin/jspinyin +12 -0
- package/initramfs/bin/jspulse +2 -0
- package/initramfs/bin/tar +40 -6
- package/initramfs/bin/top +1 -7
- package/initramfs/buninu/CHANGELOG.md +4 -0
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +28 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/main.js +7 -5
- package/initramfs/buninu/apps/bunmsh/src/shell.js +584 -75
- package/initramfs/buninu/apps/bunmsh/test/shell1.test.js +64 -0
- package/initramfs/buninu/package.json +1 -1
- package/initramfs/init.js +5 -0
- package/initramfs/lib/browser-resources.js +25 -0
- package/initramfs/lib/tar-exclude.js +20 -0
- package/initramfs/lib/tar-strip.js +94 -0
- package/initramfs/lib/top-memory.js +12 -0
- package/initramfs/usr/share/doc/buninu-linux/tar.md +24 -1
- package/initramfs/usr/share/doc/buninu-linux/top.md +18 -0
- package/package.json +1 -1
- package/scripts/arch.js +5 -5
- package/scripts/arch.sh +5 -5
- package/scripts/fetch-npm.js +51 -0
- package/scripts/fetch.js +1 -0
- package/scripts/js-tools.js +3 -0
- package/scripts/npm-packages.md +18 -0
- package/scripts/pack-initramfs.sh +1 -0
- package/scripts/pack.js +2 -0
- package/scripts/stage-npm.js +57 -0
- package/scripts/stage-npm.sh +62 -0
- package/test/fetch-npm.test.js +92 -0
- package/test/host/Dockerfile +6 -4
- package/test/host/boot-test.exp +2 -2
- package/test/host/host-test.sh +2 -2
- package/test/host/in-container.sh +4 -4
- package/test/js-tools-selection.test.js +19 -0
- package/test/npm-launchers.test.js +59 -0
- package/test/stage-npm.test.js +130 -0
- package/test/tar-strip.test.js +218 -0
- package/test/top-memory.test.js +23 -0
- package/initramfs/buninu/apps/bunmsh/test/shell.test.js +0 -2092
|
@@ -284,23 +284,22 @@ export function tokenize(source, options = {}) {
|
|
|
284
284
|
continue;
|
|
285
285
|
}
|
|
286
286
|
const two = source.slice(i, i + 2);
|
|
287
|
-
const fdDup = source.slice(i
|
|
287
|
+
const fdDup = /^(\d+>&\d+)/.exec(source.slice(i))?.[1] ?? "";
|
|
288
288
|
const operatorAhead =
|
|
289
289
|
["|", ";", "<", ">"].includes(ch) ||
|
|
290
290
|
two === "&&" ||
|
|
291
291
|
two === "||" ||
|
|
292
292
|
two === "2>" ||
|
|
293
|
-
fdDup
|
|
294
|
-
fdDup === "2>&1";
|
|
293
|
+
Boolean(fdDup);
|
|
295
294
|
if (current && operatorAhead) {
|
|
296
295
|
finishWord();
|
|
297
296
|
continue;
|
|
298
297
|
}
|
|
299
298
|
// A descriptor duplication begins with the shorter `2>` redirect token,
|
|
300
299
|
// so it must win before ordinary file redirects are considered.
|
|
301
|
-
if (!current &&
|
|
300
|
+
if (!current && fdDup) {
|
|
302
301
|
tokens.push({ type: "op", value: fdDup, offset: i });
|
|
303
|
-
i +=
|
|
302
|
+
i += fdDup.length;
|
|
304
303
|
atWordBoundary = true;
|
|
305
304
|
continue;
|
|
306
305
|
}
|
|
@@ -363,7 +362,7 @@ export function tokenize(source, options = {}) {
|
|
|
363
362
|
}
|
|
364
363
|
if (source[i] === "\\" && i + 1 < source.length) {
|
|
365
364
|
const next = source[i + 1];
|
|
366
|
-
if (next === "$" || next === '"' || next === "\\") {
|
|
365
|
+
if (next === "$" || next === "`" || next === '"' || next === "\\") {
|
|
367
366
|
text += `\u0000${next}`;
|
|
368
367
|
i += 2;
|
|
369
368
|
continue;
|
|
@@ -418,8 +417,7 @@ export function tokenize(source, options = {}) {
|
|
|
418
417
|
source.slice(i, i + 2) === "&&" ||
|
|
419
418
|
source.slice(i, i + 2) === "||" ||
|
|
420
419
|
source.slice(i, i + 2) === "2>" ||
|
|
421
|
-
source.slice(i
|
|
422
|
-
source.slice(i, i + 4) === "2>&1";
|
|
420
|
+
/^\d+>&\d+/.test(source.slice(i));
|
|
423
421
|
if (isSpace(c) || c === "\n" || c === "'" || c === '"' || c === "\\" || maybeOp)
|
|
424
422
|
break;
|
|
425
423
|
text += c;
|
|
@@ -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;
|
|
@@ -618,6 +616,66 @@ function globPatternRegex(pattern, anchoredStart, anchoredEnd, greedy) {
|
|
|
618
616
|
return new RegExp(`${anchoredStart ? "^" : ""}${source}${anchoredEnd ? "$" : ""}`);
|
|
619
617
|
}
|
|
620
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
|
+
|
|
621
679
|
async function parameterExpansion(body, state) {
|
|
622
680
|
if (/^[!%][A-Za-z_][A-Za-z0-9_]*$/.test(body)) {
|
|
623
681
|
const value = parameterValue(body.slice(1), state);
|
|
@@ -2925,6 +2983,9 @@ function compoundLogicalSource(source) {
|
|
|
2925
2983
|
breaks.add(token.offset);
|
|
2926
2984
|
if (["elif", "else", "fi", "done", "esac"].includes(word))
|
|
2927
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);
|
|
2928
2989
|
if (["then", "do", "else"].includes(word) && tokens[index + 1])
|
|
2929
2990
|
breaks.add(tokens[index + 1].offset);
|
|
2930
2991
|
commandStart = ["if", "while", "until", "elif", "then", "do", "else"].includes(word);
|
|
@@ -2936,14 +2997,12 @@ function compoundLogicalSource(source) {
|
|
|
2936
2997
|
return logical;
|
|
2937
2998
|
}
|
|
2938
2999
|
|
|
2939
|
-
// A leaf command
|
|
2940
|
-
//
|
|
2941
|
-
//
|
|
2942
|
-
//
|
|
2943
|
-
|
|
2944
|
-
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) {
|
|
2945
3005
|
const line = lines[index];
|
|
2946
|
-
if (!line.includes("<<")) return { source: line, next: index + 1 };
|
|
2947
3006
|
let end = index;
|
|
2948
3007
|
let candidate = line;
|
|
2949
3008
|
while (true) {
|
|
@@ -2952,7 +3011,7 @@ function growHeredocLine(lines, index) {
|
|
|
2952
3011
|
return { source: candidate, next: end + 1 };
|
|
2953
3012
|
} catch (error) {
|
|
2954
3013
|
if (!(error instanceof ShellSyntaxError) ||
|
|
2955
|
-
|
|
3014
|
+
!/^(?:unterminated (?:here-document|single quote|double quote|command substitution|substitution))/.test(error.message))
|
|
2956
3015
|
throw error;
|
|
2957
3016
|
end++;
|
|
2958
3017
|
if (end >= lines.length) throw error;
|
|
@@ -2961,7 +3020,199 @@ function growHeredocLine(lines, index) {
|
|
|
2961
3020
|
}
|
|
2962
3021
|
}
|
|
2963
3022
|
|
|
2964
|
-
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) {
|
|
2965
3216
|
const lines = compoundLogicalSource(source.replace(/\\\r?\n/g, "")).split(/\r?\n/);
|
|
2966
3217
|
const parseSequence = (start, stops = []) => {
|
|
2967
3218
|
const nodes = [];
|
|
@@ -2989,102 +3240,163 @@ function parseCompoundScript(source) {
|
|
|
2989
3240
|
}
|
|
2990
3241
|
|
|
2991
3242
|
if (/^if(?:\s|$)/.test(trimmed)) {
|
|
3243
|
+
const compoundStart = i;
|
|
2992
3244
|
const branches = [];
|
|
2993
3245
|
let header = trimmed;
|
|
2994
|
-
while (
|
|
3246
|
+
while (!/(?:;\s*|\s+)then(?:\s+.*)?$/.test(header) && ++i < lines.length)
|
|
2995
3247
|
header += ` ${lines[i].trim()}`;
|
|
2996
|
-
|
|
2997
|
-
|
|
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() ?? "";
|
|
2998
3252
|
i++;
|
|
2999
3253
|
while (true) {
|
|
3000
|
-
const body = parseSequence(i, [/^elif(?:\s|$)/, /^else\s*;?$/, /^fi\
|
|
3001
|
-
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 });
|
|
3002
3256
|
i = body.index;
|
|
3003
3257
|
const stop = lines[i]?.trim() ?? "";
|
|
3004
3258
|
if (/^elif(?:\s|$)/.test(stop)) {
|
|
3005
3259
|
header = stop;
|
|
3006
|
-
while (
|
|
3260
|
+
while (!/(?:;\s*|\s+)then(?:\s+.*)?$/.test(header) && ++i < lines.length)
|
|
3007
3261
|
header += ` ${lines[i].trim()}`;
|
|
3008
|
-
|
|
3009
|
-
|
|
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() ?? "";
|
|
3010
3266
|
i++;
|
|
3011
3267
|
continue;
|
|
3012
3268
|
}
|
|
3013
3269
|
let otherwise = [];
|
|
3014
3270
|
if (/^else\s*;?$/.test(stop)) {
|
|
3015
|
-
const body = parseSequence(i + 1, [/^fi\
|
|
3271
|
+
const body = parseSequence(i + 1, [/^fi\b/]);
|
|
3016
3272
|
otherwise = body.nodes;
|
|
3017
3273
|
i = body.index;
|
|
3018
3274
|
}
|
|
3019
|
-
if (!/^fi\
|
|
3275
|
+
if (!/^fi\b/.test(lines[i]?.trim() ?? ""))
|
|
3020
3276
|
throw new ShellSyntaxError("unterminated if");
|
|
3021
|
-
|
|
3022
|
-
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;
|
|
3023
3287
|
break;
|
|
3024
3288
|
}
|
|
3025
3289
|
continue;
|
|
3026
3290
|
}
|
|
3027
3291
|
if (/^(?:while|until)(?:\s|$)/.test(trimmed)) {
|
|
3292
|
+
const compoundStart = i;
|
|
3028
3293
|
const kind = trimmed.startsWith("until") ? "until" : "while";
|
|
3029
3294
|
let header = trimmed;
|
|
3030
|
-
while (
|
|
3295
|
+
while (!/(?:;\s*|\s+)do\s*$/.test(header) && ++i < lines.length)
|
|
3031
3296
|
header += ` ${lines[i].trim()}`;
|
|
3032
|
-
if (
|
|
3033
|
-
const condition = header.replace(new RegExp(`^${kind}\\s*`), "").replace(
|
|
3034
|
-
const body = parseSequence(i + 1, [/^done
|
|
3035
|
-
const done = /^done(?:\s*<\s*(.+?))?\s
|
|
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() ?? "");
|
|
3036
3301
|
if (!done)
|
|
3037
3302
|
throw new ShellSyntaxError(`unterminated ${kind}`);
|
|
3038
|
-
|
|
3039
|
-
|
|
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;
|
|
3040
3310
|
continue;
|
|
3041
3311
|
}
|
|
3042
3312
|
if (/^for(?:\s|$)/.test(trimmed)) {
|
|
3313
|
+
const compoundStart = i;
|
|
3043
3314
|
let header = trimmed;
|
|
3044
|
-
|
|
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;
|
|
3045
3328
|
header += ` ${lines[i].trim()}`;
|
|
3046
|
-
|
|
3329
|
+
}
|
|
3330
|
+
const match = /^for\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s+in(?:\s+(.*?))?)?\s*;?$/.exec(header);
|
|
3047
3331
|
if (!match) throw new ShellSyntaxError("invalid for command");
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
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;
|
|
3053
3344
|
continue;
|
|
3054
3345
|
}
|
|
3055
3346
|
if (/^case(?:\s|$)/.test(trimmed)) {
|
|
3347
|
+
const compoundStart = i;
|
|
3056
3348
|
let header = trimmed;
|
|
3057
|
-
while (!/\s+in
|
|
3349
|
+
while (!/\s+in(?:\s|$)/.test(header) && ++i < lines.length)
|
|
3058
3350
|
header += ` ${lines[i].trim()}`;
|
|
3059
|
-
const match = /^case\s+(.+?)\s+in
|
|
3351
|
+
const match = /^case\s+(.+?)\s+in(?:\s+(.*))?$/.exec(header);
|
|
3060
3352
|
if (!match) throw new ShellSyntaxError("case requires in");
|
|
3061
|
-
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]] : [];
|
|
3062
3369
|
i++;
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
const
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
bodyLines.push(current);
|
|
3078
|
-
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--;
|
|
3079
3384
|
}
|
|
3080
|
-
|
|
3385
|
+
bodyLines.push(lines[i++]);
|
|
3081
3386
|
}
|
|
3082
|
-
if (!/^esac\
|
|
3083
|
-
|
|
3084
|
-
|
|
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;
|
|
3085
3397
|
continue;
|
|
3086
3398
|
}
|
|
3087
|
-
const grown =
|
|
3399
|
+
const grown = growLogicalCommand(lines, i);
|
|
3088
3400
|
nodes.push({ type: "command", source: grown.source });
|
|
3089
3401
|
i = grown.next;
|
|
3090
3402
|
}
|
|
@@ -3135,6 +3447,61 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3135
3447
|
};
|
|
3136
3448
|
for (const node of nodes) {
|
|
3137
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
|
+
}
|
|
3138
3505
|
if (node.type === "function") {
|
|
3139
3506
|
state.functions[node.name] = node.body;
|
|
3140
3507
|
continue;
|
|
@@ -3218,29 +3585,39 @@ async function executeNodeList(nodes, state, options = {}) {
|
|
|
3218
3585
|
const parsed = parse(`__case ${node.word}`)[0]?.pipeline[0];
|
|
3219
3586
|
const value = (await expandWord(parsed.words[1], state, { single: true }))[0] ?? "";
|
|
3220
3587
|
execution = result();
|
|
3588
|
+
let fallThrough = false;
|
|
3221
3589
|
for (const arm of node.arms) {
|
|
3222
|
-
let matches =
|
|
3223
|
-
for (
|
|
3224
|
-
|
|
3225
|
-
pattern = stripExpansionMarkers(await expandText(pattern, state));
|
|
3226
|
-
if (globPatternRegex(pattern, true, true, true).test(value)) { matches = true; break; }
|
|
3590
|
+
let matches = fallThrough;
|
|
3591
|
+
if (!matches) for (const pattern of arm.patterns) {
|
|
3592
|
+
if ((await casePatternRegex(pattern, state)).test(value)) { matches = true; break; }
|
|
3227
3593
|
}
|
|
3228
3594
|
if (!matches) continue;
|
|
3229
3595
|
execution = await executeNodeList(arm.body, state, { capture });
|
|
3230
3596
|
append(execution);
|
|
3597
|
+
if (arm.terminator === ";&") { fallThrough = true; continue; }
|
|
3598
|
+
if (arm.terminator === ";|") { fallThrough = false; continue; }
|
|
3231
3599
|
break;
|
|
3232
3600
|
}
|
|
3233
3601
|
state.lastStatus = execution.status;
|
|
3234
3602
|
continue;
|
|
3235
3603
|
}
|
|
3236
3604
|
let selected = null;
|
|
3605
|
+
let selectedThen = "";
|
|
3237
3606
|
for (const branch of node.branches) {
|
|
3238
3607
|
let condition = branch.condition.trim();
|
|
3239
3608
|
let negate = false;
|
|
3240
3609
|
while (/^!\s+/.test(condition)) { negate = !negate; condition = condition.replace(/^!\s+/, ""); }
|
|
3241
3610
|
const checked = await execute(condition, state, { capture, simple: true });
|
|
3242
3611
|
append(checked);
|
|
3243
|
-
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);
|
|
3244
3621
|
}
|
|
3245
3622
|
execution = selected
|
|
3246
3623
|
? await executeNodeList(selected, state, { capture })
|
|
@@ -3359,6 +3736,85 @@ function pipelineBuiltinArgv(argv) {
|
|
|
3359
3736
|
: [...BUN_RUNTIME_COMMAND, BUNMSH_ENTRY, "-cc", ...argv];
|
|
3360
3737
|
}
|
|
3361
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
|
+
|
|
3362
3818
|
function pipelineBuiltinState(state) {
|
|
3363
3819
|
return {
|
|
3364
3820
|
...state,
|
|
@@ -3667,6 +4123,7 @@ async function runCommand(command, state, options = {}) {
|
|
|
3667
4123
|
for (const redirect of command.redirects) {
|
|
3668
4124
|
if (redirect.op === "1>&2") { stdoutToStderr = true; continue; }
|
|
3669
4125
|
if (redirect.op === "2>&1") { stderrToStdout = true; continue; }
|
|
4126
|
+
if (/^\d+>&\d+$/.test(redirect.op)) continue;
|
|
3670
4127
|
if (redirect.op === "<<" || redirect.op === "<<-") {
|
|
3671
4128
|
const heredoc = redirect.heredoc ?? { body: "", expand: false };
|
|
3672
4129
|
const content = heredoc.expand
|
|
@@ -3777,6 +4234,8 @@ async function runPipeline(pipeline, state, options = {}) {
|
|
|
3777
4234
|
return result(2, "", "bunmsh: &: only a single external command without redirections is supported\n");
|
|
3778
4235
|
if (pipeline.length === 1)
|
|
3779
4236
|
return runCommand(pipeline[0], state, {
|
|
4237
|
+
input: options.input ?? null,
|
|
4238
|
+
stdoutSink: options.stdoutSink ?? null,
|
|
3780
4239
|
captureStdout: Boolean(options.capture),
|
|
3781
4240
|
captureStderr: Boolean(options.capture),
|
|
3782
4241
|
background: Boolean(options.background),
|
|
@@ -3812,7 +4271,7 @@ async function runPipeline(pipeline, state, options = {}) {
|
|
|
3812
4271
|
const stages = pipeline.map((command, i) => {
|
|
3813
4272
|
const last = i === pipeline.length - 1;
|
|
3814
4273
|
return runCommand(command, state, {
|
|
3815
|
-
input: i === 0 ? null : links[i - 1].readable,
|
|
4274
|
+
input: i === 0 ? options.input ?? null : links[i - 1].readable,
|
|
3816
4275
|
captureStdout: last && Boolean(options.capture),
|
|
3817
4276
|
captureStderr: Boolean(options.capture),
|
|
3818
4277
|
stdoutSink: last ? null : links[i].writable,
|
|
@@ -3860,6 +4319,47 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3860
4319
|
return execution;
|
|
3861
4320
|
}
|
|
3862
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
|
+
}
|
|
3863
4363
|
if (isJavaScriptMode(rawSource)) {
|
|
3864
4364
|
let execution;
|
|
3865
4365
|
const previousCwd = process.cwd();
|
|
@@ -3930,6 +4430,7 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3930
4430
|
try {
|
|
3931
4431
|
execution = await executeNodeList(parseCompoundScript(source), state, {
|
|
3932
4432
|
capture: Boolean(io.capture),
|
|
4433
|
+
input: io.input ?? null,
|
|
3933
4434
|
});
|
|
3934
4435
|
}
|
|
3935
4436
|
catch (error) {
|
|
@@ -3944,6 +4445,12 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3944
4445
|
return execution;
|
|
3945
4446
|
}
|
|
3946
4447
|
|
|
4448
|
+
const withSubshells = rewriteSubshells(source, state);
|
|
4449
|
+
if (withSubshells !== source) {
|
|
4450
|
+
source = withSubshells;
|
|
4451
|
+
return execute(source, state, io);
|
|
4452
|
+
}
|
|
4453
|
+
|
|
3947
4454
|
let jobs;
|
|
3948
4455
|
try {
|
|
3949
4456
|
jobs = parse(source);
|
|
@@ -3965,6 +4472,8 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3965
4472
|
execution = await runPipeline(job.pipeline, state, {
|
|
3966
4473
|
capture: io.capture,
|
|
3967
4474
|
background: job.background,
|
|
4475
|
+
input: io.input ?? null,
|
|
4476
|
+
stdoutSink: io.stdoutSink ?? null,
|
|
3968
4477
|
});
|
|
3969
4478
|
if (job.negate) execution.status = execution.status === 0 ? 1 : 0;
|
|
3970
4479
|
state.lastStatus = execution.status;
|