buninu-linux 0.7.2 → 0.7.4
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/build-uki.sh +21 -7
- package/check_build.js +217 -0
- package/index.js +24 -3
- package/initramfs/buninu/CHANGELOG.md +5 -0
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +15 -0
- package/initramfs/buninu/apps/bunmsh/PORTING.md +6 -2
- package/initramfs/buninu/apps/bunmsh/help/README.md +3 -1
- package/initramfs/buninu/apps/bunmsh/help/ln.md +3 -1
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/shell.js +173 -84
- package/initramfs/buninu/apps/bunmsh/test/reference.test.js +95 -1
- package/initramfs/buninu/apps/bunmsh/test/shell1.test.js +91 -0
- package/initramfs/buninu/apps/jsgotty/LICENSES/MIT-microsoft-terminal-conpty.txt +21 -0
- package/initramfs/buninu/apps/jsgotty/LICENSES/MIT-ws.txt +20 -0
- package/initramfs/buninu/apps/jsgotty/LICENSES/NOTICE-microsoft-terminal-conpty.md +535 -0
- package/initramfs/buninu/apps/jsgotty/NOTICE +100 -0
- package/initramfs/buninu/apps/jsgotty/conpty/OpenConsole.exe +0 -0
- package/initramfs/buninu/apps/jsgotty/conpty/conpty.dll +0 -0
- package/initramfs/buninu/apps/jsgotty/gotty.js +24 -0
- package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
- package/initramfs/buninu/apps/jsgotty/package.json +6 -2
- package/initramfs/buninu/apps/jsgotty/windows-conpty.js +135 -0
- package/initramfs/buninu/package.json +1 -1
- package/package.json +1 -1
- package/scripts/cpio.js +107 -0
- package/scripts/pack-initramfs.sh +16 -0
- package/scripts/uki.js +136 -0
|
@@ -283,7 +283,6 @@ 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
287
|
const fdDup = source.slice(i, i + 4);
|
|
289
288
|
const operatorAhead =
|
|
@@ -448,6 +447,7 @@ function emptyCommand() {
|
|
|
448
447
|
}
|
|
449
448
|
|
|
450
449
|
export function parse(source) {
|
|
450
|
+
source = normalizeSource(source);
|
|
451
451
|
const tokens = tokenize(source);
|
|
452
452
|
const jobs = [];
|
|
453
453
|
let i = 0;
|
|
@@ -513,8 +513,8 @@ export function parse(source) {
|
|
|
513
513
|
throw new ShellSyntaxError("! requires a command", firstWord.offset);
|
|
514
514
|
negate = true;
|
|
515
515
|
}
|
|
516
|
-
jobs.push({ connector, pipeline, negate });
|
|
517
516
|
const separator = tokens[i]?.value;
|
|
517
|
+
jobs.push({ connector, pipeline, negate, background: false });
|
|
518
518
|
if (!separator) break;
|
|
519
519
|
if (![";", "&&", "||"].includes(separator))
|
|
520
520
|
throw new ShellSyntaxError(`unexpected operator ${separator}`, tokens[i].offset);
|
|
@@ -523,6 +523,20 @@ export function parse(source) {
|
|
|
523
523
|
skipSemicolons();
|
|
524
524
|
if (i >= tokens.length) break;
|
|
525
525
|
}
|
|
526
|
+
const lastJob = jobs.at(-1);
|
|
527
|
+
const lastCommand = lastJob?.pipeline.at(-1);
|
|
528
|
+
const lastWord = lastCommand?.words.at(-1);
|
|
529
|
+
const lastToken = tokens.findLast((token) =>
|
|
530
|
+
token.type !== "op" || token.value !== ";" || source[token.offset] !== "\n");
|
|
531
|
+
if (lastWord === lastToken &&
|
|
532
|
+
lastWord?.fragments.length === 1 &&
|
|
533
|
+
lastWord.fragments[0].quote === "none" &&
|
|
534
|
+
lastWord.fragments[0].text === "&") {
|
|
535
|
+
lastCommand.words.pop();
|
|
536
|
+
if (lastCommand.words.length === 0 && lastCommand.redirects.length === 0)
|
|
537
|
+
throw new ShellSyntaxError("expected a command", lastWord.offset);
|
|
538
|
+
lastJob.background = true;
|
|
539
|
+
}
|
|
526
540
|
return jobs;
|
|
527
541
|
}
|
|
528
542
|
|
|
@@ -1223,33 +1237,37 @@ const builtins = {
|
|
|
1223
1237
|
const format = argv[1];
|
|
1224
1238
|
let ai = 2;
|
|
1225
1239
|
let output = "";
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
i
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1240
|
+
let previous;
|
|
1241
|
+
do {
|
|
1242
|
+
previous = ai;
|
|
1243
|
+
for (let i = 0; i < format.length; i++) {
|
|
1244
|
+
if (format[i] === "\\" && i + 1 < format.length) {
|
|
1245
|
+
const parsed = parsePrintEscapes(format.slice(i, i + 2));
|
|
1246
|
+
output += parsed.text;
|
|
1247
|
+
i++;
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (format[i] !== "%") {
|
|
1251
|
+
output += format[i];
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
if (format[i + 1] === "%") {
|
|
1255
|
+
output += "%";
|
|
1256
|
+
i++;
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
const match = /^%([0-9]*)([sdi])/.exec(format.slice(i));
|
|
1260
|
+
if (!match) return result(2, "", `bunmsh: printf: unsupported format near ${format.slice(i)}\n`);
|
|
1261
|
+
const width = Number(match[1] || 0);
|
|
1262
|
+
const kind = match[2];
|
|
1263
|
+
const value = argv[ai++] ?? (kind === "s" ? "" : "0");
|
|
1264
|
+
let rendered = kind === "s" ? value : String(Number.parseInt(value, 10) || 0);
|
|
1265
|
+
if (width > rendered.length)
|
|
1266
|
+
rendered = (match[1]?.startsWith("0") ? "0" : " ").repeat(width - rendered.length) + rendered;
|
|
1267
|
+
output += rendered;
|
|
1268
|
+
i += match[0].length - 1;
|
|
1241
1269
|
}
|
|
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
|
-
}
|
|
1270
|
+
} while (ai > previous && ai < argv.length);
|
|
1253
1271
|
return result(0, output);
|
|
1254
1272
|
},
|
|
1255
1273
|
read: async (argv, state, input) => {
|
|
@@ -1821,11 +1839,19 @@ async function runHeadFallback(argv, state, input) {
|
|
|
1821
1839
|
return result(1, "", "bunmsh: head: invalid line count\n");
|
|
1822
1840
|
const operands = argv.slice(i);
|
|
1823
1841
|
if (operands.length) {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1842
|
+
const output = [];
|
|
1843
|
+
let status = 0, stderr = "";
|
|
1844
|
+
for (const operand of operands) {
|
|
1845
|
+
if (operands.length > 1)
|
|
1846
|
+
output.push(`${output.length ? "\n" : ""}==> ${operand} <==\n`);
|
|
1847
|
+
try {
|
|
1848
|
+
const data = await readFallbackFiles([operand], state, input);
|
|
1849
|
+
output.push(bytesMode
|
|
1850
|
+
? data.slice(0, count)
|
|
1851
|
+
: decoder.decode(data).split(/(?<=\n)/).slice(0, count).join(""));
|
|
1852
|
+
} catch (error) { status = 1; stderr += `bunmsh: head: ${operand}: ${error.message}\n`; }
|
|
1853
|
+
}
|
|
1854
|
+
return result(status, concatBytes(output), stderr);
|
|
1829
1855
|
}
|
|
1830
1856
|
const reader = fallbackInput(input).getReader();
|
|
1831
1857
|
const output = [];
|
|
@@ -1873,19 +1899,32 @@ async function runTextFilter(argv, state, input, kind) {
|
|
|
1873
1899
|
reverse ||= option.includes("r"); numeric ||= option.includes("n"); unique ||= option.includes("u");
|
|
1874
1900
|
}
|
|
1875
1901
|
}
|
|
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
1902
|
if (kind === "tail") {
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1903
|
+
const operands = args.length ? args : ["-"];
|
|
1904
|
+
const output = [];
|
|
1905
|
+
let status = 0, stderr = "";
|
|
1906
|
+
for (const operand of operands) {
|
|
1907
|
+
try {
|
|
1908
|
+
const text = decoder.decode(await readFallbackFiles([operand], state, input));
|
|
1909
|
+
const lines = text.split("\n");
|
|
1910
|
+
const trailing = lines.at(-1) === "";
|
|
1911
|
+
if (trailing) lines.pop();
|
|
1912
|
+
const selected = tailFromStart
|
|
1913
|
+
? lines.slice(Math.max(0, tailCount - 1))
|
|
1914
|
+
: tailCount === 0 ? [] : lines.slice(-tailCount);
|
|
1915
|
+
if (operands.length > 1 && (tailFromStart || tailCount !== 0))
|
|
1916
|
+
output.push(`${output.length ? "\n" : ""}==> ${operand === "-" ? "standard input" : operand} <==\n`);
|
|
1917
|
+
if (selected.length) output.push(`${selected.join("\n")}${trailing ? "\n" : ""}`);
|
|
1918
|
+
} catch (error) { status = 1; stderr += `bunmsh: tail: ${operand}: ${error.message}\n`; }
|
|
1919
|
+
}
|
|
1920
|
+
return result(status, output.join(""), stderr);
|
|
1888
1921
|
}
|
|
1922
|
+
const text = decoder.decode(await readFallbackFiles(args, state, input));
|
|
1923
|
+
let lines = text.split("\n");
|
|
1924
|
+
if (lines.at(-1) === "") lines.pop();
|
|
1925
|
+
lines.sort((a, b) => numeric ? Number(a) - Number(b) : a.localeCompare(b));
|
|
1926
|
+
if (unique) lines = lines.filter((line, index) => index === 0 || line !== lines[index - 1]);
|
|
1927
|
+
if (reverse) lines.reverse();
|
|
1889
1928
|
return result(0, lines.length ? `${lines.join("\n")}\n` : "");
|
|
1890
1929
|
} catch (error) { return result(1, "", `bunmsh: ${kind}: ${error.message}\n`); }
|
|
1891
1930
|
}
|
|
@@ -1894,15 +1933,27 @@ async function runWcFallback(argv, state, input) {
|
|
|
1894
1933
|
let flags = "", i = 1;
|
|
1895
1934
|
while (/^-[lwc]+$/.test(argv[i] ?? "")) flags += argv[i++].slice(1);
|
|
1896
1935
|
if (!flags) flags = "lwc";
|
|
1897
|
-
|
|
1898
|
-
|
|
1936
|
+
const operands = argv.slice(i);
|
|
1937
|
+
const files = operands.length ? operands : ["-"];
|
|
1938
|
+
const rows = [];
|
|
1939
|
+
let status = 0, stderr = "";
|
|
1940
|
+
let totalBytes = 0;
|
|
1941
|
+
for (const operand of files) try {
|
|
1942
|
+
const data = await readFallbackFiles([operand], state, input);
|
|
1899
1943
|
const text = decoder.decode(data);
|
|
1900
1944
|
const values = [];
|
|
1901
1945
|
if (flags.includes("l")) values.push((text.match(/\n/g) ?? []).length);
|
|
1902
1946
|
if (flags.includes("w")) values.push(text.trim() ? text.trim().split(/\s+/).length : 0);
|
|
1903
1947
|
if (flags.includes("c")) values.push(data.byteLength);
|
|
1904
|
-
|
|
1905
|
-
|
|
1948
|
+
totalBytes += data.byteLength;
|
|
1949
|
+
rows.push({ values, operand });
|
|
1950
|
+
} catch (error) { status = 1; stderr += `bunmsh: wc: ${operand}: ${error.message}\n`; }
|
|
1951
|
+
if (operands.length > 1 && rows.length)
|
|
1952
|
+
rows.push({ values: rows.reduce((total, row) => total.map((n, index) => n + row.values[index]),
|
|
1953
|
+
rows[0].values.map(() => 0)), operand: "total" });
|
|
1954
|
+
const width = operands.length > 1 ? String(totalBytes).length : 0;
|
|
1955
|
+
return result(status, rows.map(({ values, operand }) =>
|
|
1956
|
+
`${values.map((value) => String(value).padStart(width)).join(" ")}${operands.length ? ` ${operand}` : ""}\n`).join(""), stderr);
|
|
1906
1957
|
}
|
|
1907
1958
|
|
|
1908
1959
|
function expandTrSet(value) {
|
|
@@ -2007,11 +2058,14 @@ async function runTeeFallback(argv, state, input) {
|
|
|
2007
2058
|
|
|
2008
2059
|
async function runHashFallback(argv, state, input, algorithm) {
|
|
2009
2060
|
const operands = argv.slice(1);
|
|
2010
|
-
|
|
2011
|
-
|
|
2061
|
+
const output = [];
|
|
2062
|
+
let status = 0, stderr = "";
|
|
2063
|
+
for (const operand of operands.length ? operands : ["-"]) try {
|
|
2064
|
+
const data = await readFallbackFiles([operand], state, input);
|
|
2012
2065
|
const digest = createHash(algorithm).update(data).digest("hex");
|
|
2013
|
-
|
|
2014
|
-
} catch (error) {
|
|
2066
|
+
output.push(`${digest} ${operand}\n`);
|
|
2067
|
+
} catch (error) { status = 1; stderr += `bunmsh: ${algorithm}sum: ${operand}: ${error.message}\n`; }
|
|
2068
|
+
return result(status, output.join(""), stderr);
|
|
2015
2069
|
}
|
|
2016
2070
|
|
|
2017
2071
|
function runMktempFallback(argv, state) {
|
|
@@ -2173,6 +2227,8 @@ async function runGrepFallback(argv, state, input, context = {}) {
|
|
|
2173
2227
|
};
|
|
2174
2228
|
|
|
2175
2229
|
const operands = argv.slice(i);
|
|
2230
|
+
const implicitCwd = !operands.length && flags.includes("r");
|
|
2231
|
+
if (implicitCwd) operands.push(".");
|
|
2176
2232
|
if (operands.length) {
|
|
2177
2233
|
try {
|
|
2178
2234
|
const files = grepFiles(operands, state, flags.includes("r"));
|
|
@@ -2181,7 +2237,8 @@ async function runGrepFallback(argv, state, input, context = {}) {
|
|
|
2181
2237
|
const lines = (await Bun.file(file.path).text()).split("\n");
|
|
2182
2238
|
if (lines.at(-1) === "") lines.pop();
|
|
2183
2239
|
for (let line = 0; line < lines.length; line++)
|
|
2184
|
-
if (processLine(lines[line], line + 1,
|
|
2240
|
+
if (processLine(lines[line], line + 1,
|
|
2241
|
+
implicitCwd ? file.shown.replace(/^\.\//, "") : file.shown, showLabel)) return result(0);
|
|
2185
2242
|
}
|
|
2186
2243
|
} catch (error) { return result(2, "", `bunmsh: grep: ${error.message}\n`); }
|
|
2187
2244
|
} else {
|
|
@@ -2341,20 +2398,26 @@ function cutCharacterRanges(spec) {
|
|
|
2341
2398
|
return ranges;
|
|
2342
2399
|
}
|
|
2343
2400
|
|
|
2344
|
-
async function runCutFallback(argv,
|
|
2401
|
+
async function runCutFallback(argv, state, input) {
|
|
2345
2402
|
let spec, i = 1;
|
|
2346
2403
|
if (argv[i] === "-c") spec = argv[++i], i++;
|
|
2347
2404
|
else if (argv[i]?.startsWith("-c")) spec = argv[i++].slice(2);
|
|
2348
|
-
if (!spec
|
|
2405
|
+
if (!spec) return result(1, "", "bunmsh: cut: usage: cut -c LIST [FILE ...]\n");
|
|
2349
2406
|
try {
|
|
2350
2407
|
const ranges = cutCharacterRanges(spec);
|
|
2351
|
-
const
|
|
2352
|
-
|
|
2353
|
-
const
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2408
|
+
const output = [];
|
|
2409
|
+
let status = 0, stderr = "";
|
|
2410
|
+
const operands = argv.slice(i);
|
|
2411
|
+
for (const operand of operands.length ? operands : ["-"]) try {
|
|
2412
|
+
const text = decoder.decode(await readFallbackFiles([operand], state, input));
|
|
2413
|
+
const trailing = text.endsWith("\n");
|
|
2414
|
+
const lines = text.split("\n");
|
|
2415
|
+
if (trailing) lines.pop();
|
|
2416
|
+
output.push(lines.map((line) => [...line].filter((_ch, index) =>
|
|
2417
|
+
ranges.some(([start, end]) => index + 1 >= start && index + 1 <= end)).join("")).join("\n") +
|
|
2418
|
+
(trailing ? "\n" : ""));
|
|
2419
|
+
} catch (error) { status = 1; stderr += `bunmsh: cut: ${operand}: ${error.message}\n`; }
|
|
2420
|
+
return result(status, output.join(""), stderr);
|
|
2358
2421
|
} catch (error) { return result(1, "", `bunmsh: cut: ${error.message}\n`); }
|
|
2359
2422
|
}
|
|
2360
2423
|
|
|
@@ -2366,24 +2429,27 @@ function runLnFallback(argv, state) {
|
|
|
2366
2429
|
noTargetDirectory ||= argv[i].includes("T");
|
|
2367
2430
|
i++;
|
|
2368
2431
|
}
|
|
2369
|
-
if (argv.length - i
|
|
2370
|
-
const
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
try {
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2432
|
+
if (argv.length - i < 2) return result(1, "", "bunmsh: ln: usage: ln [-sf] target... link_name\n");
|
|
2433
|
+
const sources = argv.slice(i, -1);
|
|
2434
|
+
const destination = nativePath(argv.at(-1));
|
|
2435
|
+
const targetPath = isAbsolute(destination) ? destination : resolvePath(nativePath(state.cwd), destination);
|
|
2436
|
+
let directory = false;
|
|
2437
|
+
if (!noTargetDirectory) try { directory = statSync(targetPath).isDirectory(); } catch {}
|
|
2438
|
+
if (sources.length > 1 && !directory)
|
|
2439
|
+
return result(1, "", "bunmsh: ln: target is not a directory\n");
|
|
2440
|
+
let status = 0, stderr = "";
|
|
2441
|
+
for (const operand of sources) {
|
|
2442
|
+
const target = nativePath(operand);
|
|
2443
|
+
const link = directory ? resolvePath(targetPath, pathBasename(target)) : targetPath;
|
|
2444
|
+
try {
|
|
2445
|
+
if (force) try { unlinkSync(link); } catch {}
|
|
2446
|
+
if (symbolic) symlinkSync(target, link); else {
|
|
2447
|
+
const source = isAbsolute(target) ? target : resolvePath(nativePath(state.cwd), target);
|
|
2448
|
+
linkSync(source, link);
|
|
2449
|
+
}
|
|
2450
|
+
} catch (error) { status = 1; stderr += `bunmsh: ln: ${operand}: ${error.message}\n`; }
|
|
2451
|
+
}
|
|
2452
|
+
return result(status, "", stderr);
|
|
2387
2453
|
}
|
|
2388
2454
|
|
|
2389
2455
|
function runChmodFallback(argv, state) {
|
|
@@ -3210,15 +3276,22 @@ async function runExternal(
|
|
|
3210
3276
|
cmd: spawnArgv,
|
|
3211
3277
|
cwd: nativePath(state.cwd),
|
|
3212
3278
|
env: state.env,
|
|
3213
|
-
stdin: input === null ? "inherit" : input,
|
|
3214
|
-
stdout: captureStdout || stdoutSink ? "pipe" : "inherit",
|
|
3215
|
-
stderr: captureStderr || runtimeOptions.stderrSink ? "pipe" : "inherit",
|
|
3279
|
+
stdin: runtimeOptions.background ? "ignore" : input === null ? "inherit" : input,
|
|
3280
|
+
stdout: runtimeOptions.background ? "inherit" : captureStdout || stdoutSink ? "pipe" : "inherit",
|
|
3281
|
+
stderr: runtimeOptions.background ? "inherit" : captureStderr || runtimeOptions.stderrSink ? "pipe" : "inherit",
|
|
3216
3282
|
onExit(proc, exitCode, signalCode, error) {
|
|
3217
3283
|
runtimeOptions.onExit?.(proc, exitCode, signalCode, error);
|
|
3218
3284
|
},
|
|
3219
3285
|
};
|
|
3220
3286
|
try {
|
|
3221
3287
|
const proc = Bun.spawn(options);
|
|
3288
|
+
if (runtimeOptions.background) {
|
|
3289
|
+
if (!Array.isArray(Bun.sha.procs)) Bun.sha.procs = [];
|
|
3290
|
+
Bun.sha.procs.push(proc);
|
|
3291
|
+
proc.unref();
|
|
3292
|
+
const index = Bun.sha.procs.length - 1;
|
|
3293
|
+
return result(0, "", `[${index + 1}] ${proc.pid} Bun.sha.procs[${index}]\n`);
|
|
3294
|
+
}
|
|
3222
3295
|
const pipelineKillSignal = runtimeOptions.pipelineKillSignal ?? "SIGPIPE";
|
|
3223
3296
|
// A downstream pipeline stage that closes early (for example `head -n 3`)
|
|
3224
3297
|
// leaves nothing reading the other end of `stdoutSink`/`stderrSink` (a
|
|
@@ -3379,6 +3452,15 @@ async function runCommandArgv(
|
|
|
3379
3452
|
if(argv[0] === "&")
|
|
3380
3453
|
commandArgv = argv.slice(1) ;
|
|
3381
3454
|
}
|
|
3455
|
+
|
|
3456
|
+
if (options.background && (
|
|
3457
|
+
options.pipelineStage ||
|
|
3458
|
+
["command", "builtin", "__builtin", "time", "yes"].includes(commandArgv[0]) ||
|
|
3459
|
+
Object.hasOwn(state.functions, commandArgv[0]) ||
|
|
3460
|
+
Object.hasOwn(builtins, commandArgv[0]) ||
|
|
3461
|
+
(!commandArgv[0].includes("/") && Object.hasOwn(fallbackBuiltins, commandArgv[0]) &&
|
|
3462
|
+
!findExecutable(commandArgv[0], state))
|
|
3463
|
+
)) return result(2, "", "bunmsh: &: only a single external command is supported\n");
|
|
3382
3464
|
|
|
3383
3465
|
let commandState = state;
|
|
3384
3466
|
if (options.pipelineStage && runsInPipelineSubprocess(commandArgv, commandState))
|
|
@@ -3674,6 +3756,7 @@ async function runCommand(command, state, options = {}) {
|
|
|
3674
3756
|
stderrSink: stderrRedirect,
|
|
3675
3757
|
onSpawn: options.onSpawn,
|
|
3676
3758
|
onExit: options.onExit,
|
|
3759
|
+
background: Boolean(options.background),
|
|
3677
3760
|
},
|
|
3678
3761
|
);
|
|
3679
3762
|
|
|
@@ -3690,10 +3773,13 @@ async function runCommand(command, state, options = {}) {
|
|
|
3690
3773
|
}
|
|
3691
3774
|
|
|
3692
3775
|
async function runPipeline(pipeline, state, options = {}) {
|
|
3776
|
+
if (options.background && (pipeline.length !== 1 || pipeline[0].redirects.length !== 0))
|
|
3777
|
+
return result(2, "", "bunmsh: &: only a single external command without redirections is supported\n");
|
|
3693
3778
|
if (pipeline.length === 1)
|
|
3694
3779
|
return runCommand(pipeline[0], state, {
|
|
3695
3780
|
captureStdout: Boolean(options.capture),
|
|
3696
3781
|
captureStderr: Boolean(options.capture),
|
|
3782
|
+
background: Boolean(options.background),
|
|
3697
3783
|
});
|
|
3698
3784
|
|
|
3699
3785
|
const links = Array.from(
|
|
@@ -3876,7 +3962,10 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3876
3962
|
for (const job of jobs) {
|
|
3877
3963
|
if (job.connector === "&&" && execution.status !== 0) continue;
|
|
3878
3964
|
if (job.connector === "||" && execution.status === 0) continue;
|
|
3879
|
-
execution = await runPipeline(job.pipeline, state, {
|
|
3965
|
+
execution = await runPipeline(job.pipeline, state, {
|
|
3966
|
+
capture: io.capture,
|
|
3967
|
+
background: job.background,
|
|
3968
|
+
});
|
|
3880
3969
|
if (job.negate) execution.status = execution.status === 0 ? 1 : 0;
|
|
3881
3970
|
state.lastStatus = execution.status;
|
|
3882
3971
|
if (execution.stdout.byteLength) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { chmodSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, readlinkSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { createState, decode, execute } from "../src/shell.js";
|
|
@@ -86,6 +86,15 @@ async function expectBuiltinSedLike(args, input, cwd = root) {
|
|
|
86
86
|
expect(actual).toEqual(reference);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
async function expectBuiltinLikeSystem(name, args, input = "", cwd = root) {
|
|
90
|
+
const [actual, reference] = await Promise.all([
|
|
91
|
+
invokeArgvWithInput(
|
|
92
|
+
[process.execPath, join(root, "src/main.js"), "-cc", "builtin", name, ...args], input, cwd),
|
|
93
|
+
invokeArgvWithInput([Bun.which(name), ...args], input, cwd),
|
|
94
|
+
]);
|
|
95
|
+
expect(actual).toEqual(reference);
|
|
96
|
+
}
|
|
97
|
+
|
|
89
98
|
describe("/bin/sh reference", () => {
|
|
90
99
|
describe("quote and word expansion", () => {
|
|
91
100
|
test("removes LF and CRLF backslash continuations before tokenization", async () => {
|
|
@@ -402,6 +411,79 @@ printf ':%s' $?
|
|
|
402
411
|
});
|
|
403
412
|
});
|
|
404
413
|
|
|
414
|
+
describe("system multi-operand reference", () => {
|
|
415
|
+
test("printf repeats its format until all arguments are consumed", async () => {
|
|
416
|
+
await expectBuiltinLikeSystem("printf", ["%s=%d\n", "one", "1", "two", "2"]);
|
|
417
|
+
await expectBuiltinLikeSystem("printf", ["[%s][%s]\n", "one", "two", "three"]);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("ln links each source into a target directory", async () => {
|
|
421
|
+
const directory = mkdtempSync(join(tmpdir(), "bunmsh-ln-reference-"));
|
|
422
|
+
const actualDir = join(directory, "actual");
|
|
423
|
+
const systemDir = join(directory, "system");
|
|
424
|
+
try {
|
|
425
|
+
for (const cwd of [actualDir, systemDir]) {
|
|
426
|
+
mkdirSync(cwd);
|
|
427
|
+
mkdirSync(join(cwd, "hard"));
|
|
428
|
+
mkdirSync(join(cwd, "soft"));
|
|
429
|
+
await Bun.write(join(cwd, "one"), "first\n");
|
|
430
|
+
await Bun.write(join(cwd, "two"), "second\n");
|
|
431
|
+
}
|
|
432
|
+
for (const args of [["one", "two", "hard"], ["-s", "one", "two", "soft"]]) {
|
|
433
|
+
const [actual, reference] = await Promise.all([
|
|
434
|
+
invokeArgvWithInput([process.execPath, join(root, "src/main.js"), "-cc", "builtin", "ln", ...args], "", actualDir),
|
|
435
|
+
invokeArgvWithInput([Bun.which("ln"), ...args], "", systemDir),
|
|
436
|
+
]);
|
|
437
|
+
expect(actual).toEqual(reference);
|
|
438
|
+
}
|
|
439
|
+
for (const name of ["one", "two"]) {
|
|
440
|
+
expect(readdirSync(join(actualDir, "hard"))).toEqual(readdirSync(join(systemDir, "hard")));
|
|
441
|
+
expect(statSync(join(actualDir, "hard", name)).ino).toBe(statSync(join(actualDir, name)).ino);
|
|
442
|
+
expect(readlinkSync(join(actualDir, "soft", name)))
|
|
443
|
+
.toBe(readlinkSync(join(systemDir, "soft", name)));
|
|
444
|
+
}
|
|
445
|
+
} finally { rmSync(directory, { recursive: true, force: true }); }
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("head and tail select lines independently from each file", async () => {
|
|
449
|
+
const directory = mkdtempSync(join(tmpdir(), "bunmsh-parts-reference-"));
|
|
450
|
+
try {
|
|
451
|
+
await Bun.write(join(directory, "one"), "alpha\nbeta\n");
|
|
452
|
+
await Bun.write(join(directory, "two"), "gamma\ndelta\n");
|
|
453
|
+
for (const args of [["-n", "1", "one", "two"], ["-c", "2", "one", "two"],
|
|
454
|
+
["-n", "0", "one", "two"]])
|
|
455
|
+
await expectBuiltinLikeSystem("head", args, "", directory);
|
|
456
|
+
for (const args of [["-n", "1", "one", "two"], ["-n", "+2", "one", "two"],
|
|
457
|
+
["-n", "0", "one", "two"]])
|
|
458
|
+
await expectBuiltinLikeSystem("tail", args, "", directory);
|
|
459
|
+
} finally { rmSync(directory, { recursive: true, force: true }); }
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("wc prints each file count and a total; cut reads each file", async () => {
|
|
463
|
+
const directory = mkdtempSync(join(tmpdir(), "bunmsh-text-reference-"));
|
|
464
|
+
try {
|
|
465
|
+
await Bun.write(join(directory, "one"), "alpha beta\ngamma\n");
|
|
466
|
+
await Bun.write(join(directory, "two"), "delta\nepsilon zeta\n");
|
|
467
|
+
for (const args of [["-l", "one", "two"], ["-lwc", "one", "two"]])
|
|
468
|
+
await expectBuiltinLikeSystem("wc", args, "", directory);
|
|
469
|
+
await expectBuiltinLikeSystem("cut", ["-c", "1-3", "one", "two"], "", directory);
|
|
470
|
+
await expectBuiltinLikeSystem("cut", ["-c", "1", "one"], "", directory);
|
|
471
|
+
} finally { rmSync(directory, { recursive: true, force: true }); }
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("md5sum and sha256sum hash each file separately", async () => {
|
|
475
|
+
const directory = mkdtempSync(join(tmpdir(), "bunmsh-hash-reference-"));
|
|
476
|
+
try {
|
|
477
|
+
await Bun.write(join(directory, "one"), "alpha\n");
|
|
478
|
+
await Bun.write(join(directory, "two"), "beta\n");
|
|
479
|
+
for (const name of ["md5sum", "sha256sum"]) {
|
|
480
|
+
await expectBuiltinLikeSystem(name, ["one", "two"], "", directory);
|
|
481
|
+
await expectBuiltinLikeSystem(name, [], "stdin text\n", directory);
|
|
482
|
+
}
|
|
483
|
+
} finally { rmSync(directory, { recursive: true, force: true }); }
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
405
487
|
describe("system tail reference", () => {
|
|
406
488
|
test("supports -n +N and compact -n+N from-start modes", async () => {
|
|
407
489
|
const input = "one\ntwo\nthree\nfour\n";
|
|
@@ -450,6 +532,18 @@ describe("system grep reference", () => {
|
|
|
450
532
|
await expectBuiltinGrepLike(["-n", "needle", "tree/a.txt", "tree/nested/b.txt"], "", directory);
|
|
451
533
|
} finally { rmSync(directory, { recursive: true, force: true }); }
|
|
452
534
|
});
|
|
535
|
+
|
|
536
|
+
test("searches the current directory for -r without a path", async () => {
|
|
537
|
+
const directory = mkdtempSync(join(tmpdir(), "bunmsh-grep-cwd-reference-"));
|
|
538
|
+
try {
|
|
539
|
+
mkdirSync(join(directory, "nested"));
|
|
540
|
+
await Bun.write(join(directory, "nested", "a.txt"), "needle one\nnone\n");
|
|
541
|
+
await expectBuiltinGrepLike(["-r", "needle"], "", directory);
|
|
542
|
+
await expectBuiltinGrepLike(["-rn", "needle"], "", directory);
|
|
543
|
+
await expectBuiltinGrepLike(["-r", "missing"], "", directory);
|
|
544
|
+
await expectBuiltinGrepLike(["-rq", "needle"], "", directory);
|
|
545
|
+
} finally { rmSync(directory, { recursive: true, force: true }); }
|
|
546
|
+
});
|
|
453
547
|
});
|
|
454
548
|
|
|
455
549
|
describe("system sed reference", () => {
|
|
@@ -93,6 +93,28 @@ describe("parser", () => {
|
|
|
93
93
|
expect(parse("echo hi | tr a-z A-Z")[0].pipeline).toHaveLength(2);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
test("parses a trailing background operator", () => {
|
|
97
|
+
const [job] = parse("sleep 30 &");
|
|
98
|
+
expect(job.background).toBe(true);
|
|
99
|
+
expect(job.pipeline).toHaveLength(1);
|
|
100
|
+
const [foreground] = parse("echo before & after");
|
|
101
|
+
expect(foreground.background).toBe(false);
|
|
102
|
+
expect(foreground.pipeline[0].words[2].fragments[0].text).toBe("&");
|
|
103
|
+
expect(parse(`echo "&"`)[0].background).toBe(false);
|
|
104
|
+
expect(parse("echo \\&")[0].background).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("recognizes a trailing & from tokens, independently of comments and redirect targets", () => {
|
|
108
|
+
for (const source of ["sleep 30 & # comment", "sleep 30 & # comment &", "sleep 30 &\r\n# comment\r\n"]) {
|
|
109
|
+
const [job] = parse(source);
|
|
110
|
+
expect(job.background).toBe(true);
|
|
111
|
+
expect(job.pipeline[0].words).toHaveLength(2);
|
|
112
|
+
}
|
|
113
|
+
for (const source of ["echo & > file&", "echo & <<< input&", "echo &; # comment &", "echo hi # &"]) {
|
|
114
|
+
expect(parse(source)[0].background).toBe(false);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
96
118
|
test("a bare trailing backslash asks for one more interactive line, then joins", () => {
|
|
97
119
|
// This is what the interactive prompt loop's `pending` string looks like
|
|
98
120
|
// the instant Enter is pressed after typing "echo hi \" -- the next
|
|
@@ -926,6 +948,75 @@ fi
|
|
|
926
948
|
expect(output.status).toBe(0);
|
|
927
949
|
});
|
|
928
950
|
|
|
951
|
+
test("returns immediately and exposes a background external process through Bun.sha.procs", async () => {
|
|
952
|
+
const previous = Bun.sha.procs;
|
|
953
|
+
Bun.sha.procs = [];
|
|
954
|
+
try {
|
|
955
|
+
const started = performance.now();
|
|
956
|
+
const output = await run(`${process.execPath} -e 'await Bun.sleep(30000)' &`);
|
|
957
|
+
const first = Bun.sha.procs.at(-1);
|
|
958
|
+
const secondOutput = await run(`${process.execPath} -e 'await Bun.sleep(30000)' &`);
|
|
959
|
+
const proc = Bun.sha.procs.at(-1);
|
|
960
|
+
|
|
961
|
+
expect(performance.now() - started).toBeLessThan(1000);
|
|
962
|
+
expect(output.status).toBe(0);
|
|
963
|
+
expect(output.stderr).toBe(`[1] ${first.pid} Bun.sha.procs[0]\n`);
|
|
964
|
+
expect(secondOutput.status).toBe(0);
|
|
965
|
+
expect(secondOutput.stderr).toBe(`[2] ${proc.pid} Bun.sha.procs[1]\n`);
|
|
966
|
+
expect(Bun.sha.procs).toHaveLength(2);
|
|
967
|
+
expect(proc).not.toBe(first);
|
|
968
|
+
expect(proc.pid).toBeGreaterThan(0);
|
|
969
|
+
expect(typeof proc.kill).toBe("function");
|
|
970
|
+
first.kill();
|
|
971
|
+
proc.kill();
|
|
972
|
+
await Promise.all([first.exited, proc.exited]);
|
|
973
|
+
} finally {
|
|
974
|
+
if (previous === undefined) delete Bun.sha.procs;
|
|
975
|
+
else Bun.sha.procs = previous;
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
test("keeps the minimal background form limited to one external command", async () => {
|
|
980
|
+
const builtin = await run("echo nope &");
|
|
981
|
+
const pipeline = await run("printf x | cat &");
|
|
982
|
+
expect(builtin.status).toBe(2);
|
|
983
|
+
expect(pipeline.status).toBe(2);
|
|
984
|
+
expect(builtin.stderr).toContain("only a single external command");
|
|
985
|
+
expect(pipeline.stderr).toContain("only a single external command");
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
test("rejects background redirects before expanding, opening files, or spawning", async () => {
|
|
989
|
+
const directory = mkdtempSync(join(tmpdir(), "bunmsh-background-redirect-"));
|
|
990
|
+
const previousProcs = Bun.sha.procs;
|
|
991
|
+
const previousCount = previousProcs?.length;
|
|
992
|
+
try {
|
|
993
|
+
await Bun.write(join(directory, "input"), "input content\n");
|
|
994
|
+
await Bun.write(join(directory, "output"), "keep this content\n");
|
|
995
|
+
const command = `'${process.execPath.replaceAll("'", "'\\''")}' -e 'await Bun.write("spawned", "ran")'`;
|
|
996
|
+
const sources = [
|
|
997
|
+
...["> output", ">> output", "2> output", "2>> output", "> new-output", "< input", "< missing-input", "<<< payload", "1>&2", "2>&1"]
|
|
998
|
+
.map((redirect) => `${command} ${redirect} & # comment`),
|
|
999
|
+
`${command} <<EOF &\nbody\nEOF\n`,
|
|
1000
|
+
`${command} <<-EOF &\n\tbody\n\tEOF\n`,
|
|
1001
|
+
`${command} > $(echo expanded > expanded-marker; echo output) &`,
|
|
1002
|
+
];
|
|
1003
|
+
for (const source of sources) {
|
|
1004
|
+
const output = await run(source, { cwd: directory });
|
|
1005
|
+
expect(output.status).toBe(2);
|
|
1006
|
+
expect(output.stdout).toBe("");
|
|
1007
|
+
expect(output.stderr).toContain("without redirections");
|
|
1008
|
+
}
|
|
1009
|
+
expect(await Bun.file(join(directory, "output")).text()).toBe("keep this content\n");
|
|
1010
|
+
expect(await Bun.file(join(directory, "input")).text()).toBe("input content\n");
|
|
1011
|
+
for (const name of ["spawned", "new-output", "expanded-marker"])
|
|
1012
|
+
expect(await Bun.file(join(directory, name)).exists()).toBe(false);
|
|
1013
|
+
expect(Bun.sha.procs).toBe(previousProcs);
|
|
1014
|
+
expect(Bun.sha.procs?.length).toBe(previousCount);
|
|
1015
|
+
} finally {
|
|
1016
|
+
rmSync(directory, { recursive: true, force: true });
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
|
|
929
1020
|
test("keeps builtin state outside pipelines", async () => {
|
|
930
1021
|
const output = await run("export BUNMSH_TEST=value; print -r -- $BUNMSH_TEST");
|
|
931
1022
|
expect(output.stdout).toBe("value\n");
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|