buninu-linux 0.18.60 → 0.18.62
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/NOTICE.md +46 -46
- package/README.md +4 -4
- package/README.zh.md +4 -4
- package/initramfs/bin/tar +32 -15
- package/initramfs/bin/unzip +74 -19
- package/initramfs/buninu/CHANGELOG.md +19 -0
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +21 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/find.js +135 -56
- package/initramfs/buninu/apps/bunmsh/src/main.js +12 -1
- package/initramfs/buninu/apps/bunmsh/src/shell.js +8 -2
- package/initramfs/buninu/apps/bunmsh/test/find.test.js +103 -0
- package/initramfs/buninu/apps/bunmsh/test/interactive-typeahead.test.js +40 -0
- package/initramfs/buninu/apps/bunmsh/test/posix-bootstrap.test.js +9 -5
- package/initramfs/buninu/apps/bunmsh/test/reference.test.js +2 -5
- package/initramfs/buninu/apps/bunmsh/test/shell2.test.js +6 -5
- package/initramfs/buninu/apps/bunmsh/test/shell3.test.js +8 -7
- package/initramfs/buninu/apps/bunmsh/test/support/terminal.js +10 -0
- package/initramfs/buninu/apps/jsgotty/bunfig.toml +3 -0
- package/initramfs/buninu/apps/jsgotty/gotty.js +14 -1
- package/initramfs/buninu/apps/jsgotty/kitty-flow.js +40 -0
- package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
- package/initramfs/buninu/apps/jsgotty/package.json +1 -1
- package/initramfs/buninu/apps/jsgotty/static/js/kitty.js +32 -4
- package/initramfs/buninu/apps/jsgotty/tests/kitty-flow-browser.test.js +43 -0
- package/initramfs/buninu/apps/jsgotty/tests/kitty-flow.test.js +55 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/fps-collector.mjs +18 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/fps-debug.js +31 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/js-eval-show-fps.js +35 -0
- package/initramfs/buninu/apps/jsgotty/tests/manual/kitty-background-debug.js +24 -0
- package/initramfs/buninu/package.json +1 -1
- package/package.json +1 -1
- package/scripts/arch.js +5 -5
- package/scripts/arch.sh +5 -5
- package/scripts/fetch.js +50 -15
- package/scripts/npm-packages.md +11 -11
- package/test/archive-bounds.test.js +128 -0
- package/test/fetch-npm.test.js +1 -1
|
@@ -23,80 +23,165 @@ export function findIsRegularBuiltin(platform = process.platform) {
|
|
|
23
23
|
return platform === "win32";
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// `write`, when given, takes each result as it is found, so the caller can stream it; without it
|
|
27
|
+
// the results are gathered and returned whole, as a captured `$(find ...)` needs.
|
|
28
|
+
export async function runFind(argv, state, platform = process.platform, execute = null, write = null) {
|
|
27
29
|
let index = 1;
|
|
28
30
|
const roots = [];
|
|
29
|
-
while (index < argv.length && argv[index] !== "!" && argv[index] !== "-not" &&
|
|
31
|
+
while (index < argv.length && argv[index] !== "!" && argv[index] !== "-not" && argv[index] !== "(" && argv[index] !== ")" &&
|
|
30
32
|
!argv[index].startsWith("-")) roots.push(argv[index++]);
|
|
31
33
|
if (!roots.length) roots.push(".");
|
|
32
|
-
let minDepth = 0, maxDepth = Infinity
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
let minDepth = 0, maxDepth = Infinity;
|
|
35
|
+
// The expression, parsed the POSIX way: `!` binds tightest, then -a (also implied between two
|
|
36
|
+
// terms), then -o; parentheses group. -prune keeps find out of a directory it matched.
|
|
37
|
+
let failure = null;
|
|
38
|
+
const fail = (message) => { failure ??= message; return { kind: "false" }; };
|
|
39
|
+
const peek = () => argv[index];
|
|
40
|
+
let hasAction = false;
|
|
41
|
+
const execs = [];
|
|
42
|
+
let nesting = 0;
|
|
43
|
+
const parsePrimary = () => {
|
|
44
|
+
if (++nesting > 128) throw new Error("expression nesting is too deep");
|
|
45
|
+
try { return parseTerm(); } finally { nesting--; }
|
|
46
|
+
};
|
|
47
|
+
const parseTerm = () => {
|
|
36
48
|
const option = argv[index++];
|
|
37
|
-
if (option === "
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
if (option === "(") {
|
|
50
|
+
const inner = parseOr();
|
|
51
|
+
if (argv[index++] !== ")") return fail("bunmsh: find: missing )\n");
|
|
52
|
+
return inner;
|
|
53
|
+
}
|
|
54
|
+
if (option === "!" || option === "-not") {
|
|
55
|
+
if (index >= argv.length || [")", "-o", "-or"].includes(peek())) return fail("bunmsh: find: expected expression after !\n");
|
|
56
|
+
return { kind: "not", node: parsePrimary() };
|
|
57
|
+
}
|
|
58
|
+
if (option === "-print" || option === "-print0") { hasAction = true; return { kind: "print", nul: option === "-print0" }; }
|
|
59
|
+
if (option === "-prune") return { kind: "prune" };
|
|
60
|
+
if (option === "-true") return { kind: "true" };
|
|
61
|
+
if (option === "-false") return { kind: "false" };
|
|
40
62
|
if (option === "-exec") {
|
|
41
63
|
const command = [];
|
|
42
|
-
while (index < argv.length && argv[index] !== ";" && argv[index]
|
|
64
|
+
while (index < argv.length && argv[index] !== ";" && !(argv[index] === "+" && command.at(-1)?.includes("{}")))
|
|
43
65
|
command.push(argv[index++]);
|
|
44
66
|
const terminator = argv[index++];
|
|
45
67
|
if (!command.length || (terminator !== ";" && terminator !== "+"))
|
|
46
|
-
return
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
return fail("bunmsh: find: -exec: missing command or terminator\n");
|
|
69
|
+
if (terminator === "+" && (command.at(-1) !== "{}" ||
|
|
70
|
+
command.filter(argument => argument.includes("{}")).length !== 1))
|
|
71
|
+
return fail("bunmsh: find: -exec + requires one {} as the final argument\n");
|
|
72
|
+
hasAction = true;
|
|
73
|
+
const node = { kind: "exec", command, mode: terminator, matches: [], matchBytes: 0 };
|
|
74
|
+
execs.push(node);
|
|
75
|
+
return node;
|
|
51
76
|
}
|
|
52
77
|
const argument = argv[index++];
|
|
53
|
-
if (argument === undefined)
|
|
54
|
-
return { status: 2, stdout: "", stderr: `bunmsh: find: ${option}: missing argument\n` };
|
|
55
|
-
let predicate;
|
|
78
|
+
if (argument === undefined) return fail(`bunmsh: find: ${option}: missing argument\n`);
|
|
56
79
|
if (option === "-name" || option === "-iname") {
|
|
57
80
|
const regex = globRegex(argument, option === "-iname");
|
|
58
|
-
|
|
59
|
-
}
|
|
81
|
+
return { kind: "test", test: (entry) => regex.test(basename(entry.shown)) };
|
|
82
|
+
}
|
|
83
|
+
if (option === "-path" || option === "-ipath") {
|
|
60
84
|
const regex = globRegex(argument, option === "-ipath");
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
argument === "d" ? entry.stats.isDirectory() : entry.stats.isSymbolicLink();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
return { kind: "test", test: (entry) => regex.test(entry.shown) };
|
|
86
|
+
}
|
|
87
|
+
if (option === "-type") {
|
|
88
|
+
if (!/^[fdl]$/.test(argument)) return fail(`bunmsh: find: unsupported type: ${argument}\n`);
|
|
89
|
+
return { kind: "test", test: (entry) => argument === "f" ? entry.stats.isFile() :
|
|
90
|
+
argument === "d" ? entry.stats.isDirectory() : entry.stats.isSymbolicLink() };
|
|
91
|
+
}
|
|
92
|
+
if (option === "-mindepth" || option === "-maxdepth") {
|
|
93
|
+
if (!/^\d+$/.test(argument)) return fail(`bunmsh: find: ${option}: invalid depth: ${argument}\n`);
|
|
70
94
|
if (option === "-mindepth") minDepth = Number(argument); else maxDepth = Number(argument);
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
return { kind: "true" };
|
|
96
|
+
}
|
|
97
|
+
return fail(`bunmsh: find: unsupported option: ${option}\n`);
|
|
98
|
+
};
|
|
99
|
+
const parseAnd = () => {
|
|
100
|
+
const nodes = [parsePrimary()];
|
|
101
|
+
while (index < argv.length && ![")", "-o", "-or"].includes(peek())) {
|
|
102
|
+
if (peek() === "-a" || peek() === "-and") index++;
|
|
103
|
+
nodes.push(parsePrimary());
|
|
104
|
+
}
|
|
105
|
+
return { kind: "all", nodes };
|
|
106
|
+
};
|
|
107
|
+
function parseOr() {
|
|
108
|
+
const nodes = [parseAnd()];
|
|
109
|
+
while (peek() === "-o" || peek() === "-or") {
|
|
110
|
+
index++;
|
|
111
|
+
nodes.push(parseAnd());
|
|
112
|
+
}
|
|
113
|
+
return { kind: "any", nodes };
|
|
75
114
|
}
|
|
76
|
-
|
|
115
|
+
let expression;
|
|
116
|
+
try { expression = index < argv.length ? parseOr() : { kind: "true" }; }
|
|
117
|
+
catch (error) { return { status: 2, stdout: "", stderr: `bunmsh: find: ${error.message}\n` }; }
|
|
118
|
+
if (index < argv.length) fail(`bunmsh: find: unexpected ${argv[index]}\n`);
|
|
119
|
+
if (failure) return { status: 2, stdout: "", stderr: failure };
|
|
120
|
+
// With no action named, the whole expression is followed by -print.
|
|
121
|
+
if (!hasAction) expression = { kind: "and", left: expression, right: { kind: "print", nul: false } };
|
|
77
122
|
let status = 0, stdout = "", stderr = "";
|
|
78
123
|
const decoder = new TextDecoder();
|
|
79
|
-
const appendExecution = (execution) => {
|
|
80
|
-
|
|
124
|
+
const appendExecution = async (execution) => {
|
|
125
|
+
const text = typeof execution.stdout === "string" ? execution.stdout : decoder.decode(execution.stdout);
|
|
126
|
+
if (write && text) await write(text); else stdout += text;
|
|
81
127
|
stderr += typeof execution.stderr === "string" ? execution.stderr : decoder.decode(execution.stderr);
|
|
82
128
|
};
|
|
83
|
-
const
|
|
84
|
-
|
|
129
|
+
const emit = async (shown, nul) => {
|
|
130
|
+
const line = shown + (nul ? "\0" : "\n");
|
|
131
|
+
if (write) await write(line); else stdout += line;
|
|
132
|
+
};
|
|
133
|
+
// Keep path batches small enough for common exec argument limits, and release
|
|
134
|
+
// them during traversal instead of retaining the whole directory tree.
|
|
135
|
+
const flush = async node => {
|
|
136
|
+
if (!node.matches.length) return;
|
|
137
|
+
const matches = node.matches;
|
|
138
|
+
node.matches = [];
|
|
139
|
+
node.matchBytes = 0;
|
|
140
|
+
if (!execute) { status = 1; return; }
|
|
141
|
+
const execution = await execute([...node.command.slice(0, -1), ...matches]);
|
|
142
|
+
await appendExecution(execution);
|
|
143
|
+
if (execution.status !== 0) status = 1;
|
|
144
|
+
};
|
|
145
|
+
const evaluate = async (node, entry) => {
|
|
146
|
+
switch (node.kind) {
|
|
147
|
+
case "true": return true;
|
|
148
|
+
case "false": return false;
|
|
149
|
+
case "test": return node.test(entry);
|
|
150
|
+
case "all":
|
|
151
|
+
for (const child of node.nodes) if (!(await evaluate(child, entry))) return false;
|
|
152
|
+
return true;
|
|
153
|
+
case "any":
|
|
154
|
+
for (const child of node.nodes) if (await evaluate(child, entry)) return true;
|
|
155
|
+
return false;
|
|
156
|
+
case "not": return !(await evaluate(node.node, entry));
|
|
157
|
+
case "and": return (await evaluate(node.left, entry)) && evaluate(node.right, entry);
|
|
158
|
+
case "or": return (await evaluate(node.left, entry)) || evaluate(node.right, entry);
|
|
159
|
+
case "prune": entry.prune = true; return true;
|
|
160
|
+
case "print": await emit(entry.shown, node.nul); return true;
|
|
161
|
+
case "exec":
|
|
162
|
+
if (node.mode === "+") {
|
|
163
|
+
const size = Buffer.byteLength(entry.shown) + 9;
|
|
164
|
+
if (node.matchBytes + size > 32768) await flush(node);
|
|
165
|
+
node.matches.push(entry.shown);
|
|
166
|
+
node.matchBytes += size;
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (!execute) return false;
|
|
170
|
+
{
|
|
171
|
+
const execution = await execute(node.command.map((argument) => argument.replaceAll("{}", entry.shown)));
|
|
172
|
+
await appendExecution(execution);
|
|
173
|
+
return execution.status === 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
};
|
|
85
178
|
const visit = async (native, shown, depth) => {
|
|
86
179
|
let stats;
|
|
87
180
|
try { stats = lstatSync(native); }
|
|
88
181
|
catch (error) { status = 1; stderr += `bunmsh: find: ${shown}: ${error.message}\n`; return; }
|
|
89
|
-
const entry = { shown, stats };
|
|
90
|
-
if (depth >= minDepth
|
|
91
|
-
|
|
92
|
-
if (!actions.length || explicitPrint) emit(shown);
|
|
93
|
-
for (const action of actions) if (action.mode === ";") {
|
|
94
|
-
if (!execute) return;
|
|
95
|
-
const command = action.command.map((argument) => argument === "{}" ? shown : argument);
|
|
96
|
-
appendExecution(await execute(command));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (!stats.isDirectory() || depth >= maxDepth) return;
|
|
182
|
+
const entry = { shown, stats, prune: false };
|
|
183
|
+
if (depth >= minDepth) await evaluate(expression, entry);
|
|
184
|
+
if (entry.prune || !stats.isDirectory() || depth >= maxDepth) return;
|
|
100
185
|
let children;
|
|
101
186
|
try { children = readdirSync(native).sort(); }
|
|
102
187
|
catch (error) { status = 1; stderr += `bunmsh: find: ${shown}: ${error.message}\n`; return; }
|
|
@@ -108,12 +193,6 @@ export async function runFind(argv, state, platform = process.platform, execute
|
|
|
108
193
|
? nativePath(root, platform) : resolve(nativePath(state.cwd, platform), nativePath(root, platform));
|
|
109
194
|
await visit(native, shellPath(root), 0);
|
|
110
195
|
}
|
|
111
|
-
for (const
|
|
112
|
-
if (!execute) continue;
|
|
113
|
-
const command = action.command.flatMap((argument) => argument === "{}" ? matches : [argument]);
|
|
114
|
-
const execution = await execute(command);
|
|
115
|
-
appendExecution(execution);
|
|
116
|
-
if (execution.status !== 0) status = 1;
|
|
117
|
-
}
|
|
196
|
+
for (const node of execs) if (node.mode === "+") await flush(node);
|
|
118
197
|
return { status, stdout, stderr };
|
|
119
198
|
}
|
|
@@ -228,6 +228,10 @@ async function interactive(state) {
|
|
|
228
228
|
let savedHead = "";
|
|
229
229
|
let savedTail = "";
|
|
230
230
|
let promptAbort = null;
|
|
231
|
+
// question() consumes one line only. Resuming a paused terminal can deliver
|
|
232
|
+
// several buffered lines before the next question is installed; readline
|
|
233
|
+
// emits the remaining lines as events. Retain them in submission order.
|
|
234
|
+
const queuedLines = [];
|
|
231
235
|
// Set by onPaste (below) when a bracketed paste contains a newline, and
|
|
232
236
|
// consumed by the main loop's readline.question() catch block right after
|
|
233
237
|
// promptAbort.abort() rejects it -- see the comment there.
|
|
@@ -447,6 +451,7 @@ async function interactive(state) {
|
|
|
447
451
|
}
|
|
448
452
|
state.lastStatus = 130;
|
|
449
453
|
if (foregroundCommand || promptAbort?.signal.aborted) return;
|
|
454
|
+
queuedLines.length = 0;
|
|
450
455
|
process.stdout.write("\n");
|
|
451
456
|
promptAbort?.abort();
|
|
452
457
|
};
|
|
@@ -500,6 +505,7 @@ async function interactive(state) {
|
|
|
500
505
|
(listener) => !resizeListenersBeforeReadline.has(listener))
|
|
501
506
|
: [];
|
|
502
507
|
readline.on("SIGINT", interrupt);
|
|
508
|
+
readline.on("line", line => queuedLines.push(line));
|
|
503
509
|
closed = new Promise((resolve) => readline.once("close", () => resolve(null)));
|
|
504
510
|
};
|
|
505
511
|
setupReadline();
|
|
@@ -629,7 +635,12 @@ async function interactive(state) {
|
|
|
629
635
|
promptAbort = new AbortController();
|
|
630
636
|
let line;
|
|
631
637
|
try {
|
|
632
|
-
|
|
638
|
+
if (queuedLines.length) {
|
|
639
|
+
// Already echoed by readline when submitted; do not paint a prompt
|
|
640
|
+
// in front of the queued command's output.
|
|
641
|
+
if (!terminal) process.stdout.write(rendered.text);
|
|
642
|
+
line = queuedLines.shift();
|
|
643
|
+
} else line = await Promise.race([
|
|
633
644
|
readline.question(rendered.text, { signal: promptAbort.signal }),
|
|
634
645
|
closed,
|
|
635
646
|
]);
|
|
@@ -1920,9 +1920,15 @@ const builtins = {
|
|
|
1920
1920
|
},
|
|
1921
1921
|
};
|
|
1922
1922
|
|
|
1923
|
-
|
|
1923
|
+
// Stream only an uncaptured stdout endpoint. Pipeline children can themselves
|
|
1924
|
+
// capture output (for example in command substitutions), so their flag is not enough.
|
|
1925
|
+
const runFindBuiltin = async (argv, state, input, context = {}) => {
|
|
1926
|
+
const endpoint = state.fdBindings?.[1] ?? { channel: 1 };
|
|
1927
|
+
const streamStdout = !context.captureStdout && !context.options?.stdoutSink &&
|
|
1928
|
+
endpoint.channel === 1 && endpoint.fd === undefined && !endpoint.closed;
|
|
1924
1929
|
const output = await runFind(argv, state, process.platform, (command) =>
|
|
1925
|
-
runCommandArgv(command, state, null, true, true)
|
|
1930
|
+
runCommandArgv(command, state, null, true, true),
|
|
1931
|
+
streamStdout ? (text) => writeStream(process.stdout, bytes(text)) : null);
|
|
1926
1932
|
return result(output.status, output.stdout, output.stderr);
|
|
1927
1933
|
};
|
|
1928
1934
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {test, expect} from "bun:test";
|
|
2
|
+
import {mkdtempSync, mkdirSync, writeFileSync, rmSync} from "node:fs";
|
|
3
|
+
import {tmpdir} from "node:os";
|
|
4
|
+
import {join} from "node:path";
|
|
5
|
+
import {runFind} from "../src/find.js";
|
|
6
|
+
import {createState, execute, decode} from "../src/shell.js";
|
|
7
|
+
|
|
8
|
+
async function fixture(fn) {
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), "bunmsh-find-expression-"));
|
|
10
|
+
mkdirSync(join(cwd, "skip"));
|
|
11
|
+
for (const file of ["a.txt", "b.js", "skip/hidden.txt"]) writeFileSync(join(cwd, file), "");
|
|
12
|
+
try { await fn(cwd); } finally { rmSync(cwd, {recursive:true, force:true}); }
|
|
13
|
+
}
|
|
14
|
+
test("find groups expressions, short circuits actions, and prunes traversal", () => fixture(async cwd => {
|
|
15
|
+
const run = args => runFind(["find", ".", ...args], {cwd});
|
|
16
|
+
expect((await run(["-name","skip","-prune","-o","-type","f","-print"])).stdout).toBe("./a.txt\n./b.js\n");
|
|
17
|
+
expect((await run(["(","-name","*.txt","-o","-name","*.js",")","!","-path","./skip/*"])).stdout).toBe("./a.txt\n./b.js\n");
|
|
18
|
+
expect((await run(["-false","-a","-print","-o","-maxdepth","0","-print0"])).stdout).toBe(".\0");
|
|
19
|
+
}));
|
|
20
|
+
test("find exec supports constant commands, embedded placeholders and literal plus", () => fixture(async cwd => {
|
|
21
|
+
const calls = [];
|
|
22
|
+
const exec = async args => { calls.push(args); return {status:1,stdout:"",stderr:""}; };
|
|
23
|
+
const run = args => runFind(["find",".","-maxdepth","0",...args],{cwd},process.platform,exec);
|
|
24
|
+
expect((await run(["-exec","echo","constant",";","-o","-print"])).stdout).toBe(".\n");
|
|
25
|
+
expect(calls.pop()).toEqual(["echo","constant"]);
|
|
26
|
+
await run(["-exec","echo","prefix{}suffix","literal","+",";"]);
|
|
27
|
+
expect(calls.pop()).toEqual(["echo","prefix.suffix","literal","+"]);
|
|
28
|
+
expect((await run(["-exec","echo","{}","{}","+"])).status).toBe(2);
|
|
29
|
+
expect(calls).toHaveLength(0);
|
|
30
|
+
expect((await run(["-exec","echo","{}","+"])).status).toBe(1);
|
|
31
|
+
expect(calls.pop()).toEqual(["echo","."]);
|
|
32
|
+
}));
|
|
33
|
+
test("find rejects invalid expressions before any action", () => fixture(async cwd => {
|
|
34
|
+
for (const args of [["-print","-o"],["(","-print"],["!"],["-name","[z-a]"]]) {
|
|
35
|
+
const chunks=[];
|
|
36
|
+
const out=await runFind(["find",".",...args],{cwd},process.platform,null,text=>chunks.push(text));
|
|
37
|
+
expect(out.status).toBe(2);
|
|
38
|
+
expect(chunks).toEqual([]);
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
41
|
+
test("find awaits streamed output and leaves captured stdout empty", () => fixture(async cwd => {
|
|
42
|
+
const args=["find",".","-type","f","-print0"];
|
|
43
|
+
const expected=await runFind(args,{cwd});
|
|
44
|
+
const chunks=[];
|
|
45
|
+
const streamed=await runFind(args,{cwd},process.platform,null,async text=>{
|
|
46
|
+
await Bun.sleep(1); chunks.push(text);
|
|
47
|
+
});
|
|
48
|
+
expect(streamed).toEqual({status:0,stdout:"",stderr:""});
|
|
49
|
+
expect(chunks.join("")).toBe(expected.stdout);
|
|
50
|
+
}));
|
|
51
|
+
test("find capture remains isolated inside a pipeline child", () => fixture(async cwd => {
|
|
52
|
+
const state=createState({cwd,env:{...process.env},args:["bunmsh"]});
|
|
53
|
+
state.pipelineChild=true;
|
|
54
|
+
const out=await execute('printf "<%s>\\n" "$(builtin find . -maxdepth 0)"',state,{capture:true});
|
|
55
|
+
expect(decode(out.stdout)).toBe("<.>\n");
|
|
56
|
+
expect(out.status).toBe(0);
|
|
57
|
+
}));
|
|
58
|
+
test("find streams through pipes and obeys output redirection", () => fixture(async cwd => {
|
|
59
|
+
const state=createState({cwd,env:{...process.env},args:["bunmsh"]});
|
|
60
|
+
for (const source of [
|
|
61
|
+
"builtin find . -maxdepth 0 | cat",
|
|
62
|
+
"builtin find . -maxdepth 0 > out; cat out",
|
|
63
|
+
"builtin find . -maxdepth 0 1>&2",
|
|
64
|
+
]) {
|
|
65
|
+
const out=await execute(source,state,{capture:true});
|
|
66
|
+
expect(out.status).toBe(0);
|
|
67
|
+
expect(decode(source.includes("1>&2")?out.stderr:out.stdout)).toBe(".\n");
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
test("find handles long flat expressions and rejects excessive nesting", () => fixture(async cwd => {
|
|
71
|
+
const flat=await runFind(["find",".","-maxdepth","0",...Array(10000).fill("-true")],{cwd});
|
|
72
|
+
expect(flat).toEqual({status:0,stdout:".\n",stderr:""});
|
|
73
|
+
const nested=await runFind(["find",".",...Array(1000).fill("!"),"-true"],{cwd});
|
|
74
|
+
expect(nested.status).toBe(2);
|
|
75
|
+
}));
|
|
76
|
+
test("find batches exec paths without losing order", () => fixture(async cwd => {
|
|
77
|
+
const paths=Array(5000).fill(".");
|
|
78
|
+
const calls=[];
|
|
79
|
+
const out=await runFind(["find",...paths,"-maxdepth","0","-exec","echo","{}","+"],{cwd},process.platform,
|
|
80
|
+
async args=>{ calls.push(args.slice(1)); return {status:0,stdout:"",stderr:""}; });
|
|
81
|
+
expect(out.status).toBe(0);
|
|
82
|
+
expect(calls.length).toBeGreaterThan(1);
|
|
83
|
+
expect(calls.flat()).toEqual(paths);
|
|
84
|
+
}));
|
|
85
|
+
test("find expressions agree with the host find on the same tree", () => fixture(async cwd => {
|
|
86
|
+
const reference=Bun.which("find");
|
|
87
|
+
if (!reference) throw new Error("reference find is required for this comparison");
|
|
88
|
+
for (const args of [
|
|
89
|
+
["-name","skip","-prune","-o","-type","f","-print"],
|
|
90
|
+
["(","-name","*.txt","-o","-name","*.js",")","!","-path","./skip/*"],
|
|
91
|
+
["-false","-a","-print","-o","-maxdepth","0","-print0"],
|
|
92
|
+
["-maxdepth","0","-exec","echo","constant",";"],
|
|
93
|
+
["-maxdepth","0","-exec","echo","prefix{}suffix","literal","+",";"],
|
|
94
|
+
]) {
|
|
95
|
+
const expected=Bun.spawnSync([reference,".",...args],{cwd});
|
|
96
|
+
const actual=await runFind(["find",".",...args],{cwd},process.platform,async command=>{
|
|
97
|
+
const p=Bun.spawnSync(command,{cwd});
|
|
98
|
+
return {status:p.exitCode,stdout:p.stdout,stderr:p.stderr};
|
|
99
|
+
});
|
|
100
|
+
expect({args, status:actual.status}).toEqual({args,status:expected.exitCode});
|
|
101
|
+
expect(actual.stdout.split("\n").sort()).toEqual(expected.stdout.toString().split("\n").sort());
|
|
102
|
+
}
|
|
103
|
+
}));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {test, expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync, rmSync} from 'node:fs';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {join} from 'node:path';
|
|
5
|
+
|
|
6
|
+
for (const delay of [0, 150, null]) test(`retains multiple submitted lines during cursor query (${delay ?? 'no'} reply)`, async () => {
|
|
7
|
+
const home = mkdtempSync(join(tmpdir(), 'bunmsh-typeahead-'));
|
|
8
|
+
const timers = [];
|
|
9
|
+
let text = '', started = false, queried = false;
|
|
10
|
+
const later = (fn, ms) => timers.push(setTimeout(fn, ms));
|
|
11
|
+
const terminal = new Bun.Terminal({cols:80, rows:24, data(t, data) {
|
|
12
|
+
const chunk = data.toString(); text += chunk;
|
|
13
|
+
if (!started && text.includes('READY> ')) {
|
|
14
|
+
started = true;
|
|
15
|
+
later(() => t.write('printf "FIRST\\n"\r'), 0);
|
|
16
|
+
}
|
|
17
|
+
if (chunk.includes('\x1b[6n')) {
|
|
18
|
+
if (!queried) {
|
|
19
|
+
queried = true;
|
|
20
|
+
// One PTY chunk: readline must not discard the second completed line.
|
|
21
|
+
later(() => t.write('printf "SECOND\\n"\rprintf "THIRD\\n"\rexit\r'), 30);
|
|
22
|
+
}
|
|
23
|
+
if (delay !== null) later(() => t.write('\x1b[1;1R'), delay);
|
|
24
|
+
}
|
|
25
|
+
}});
|
|
26
|
+
const proc = Bun.spawn([process.execPath, join(import.meta.dir, '../src/main.js'), '-i'], {
|
|
27
|
+
env:{...process.env, HOME:home, BUNMSH_IMPORT_HISTORY:'off', PS1:'READY> ', BUNMSH_MOUSE:'0'}, terminal,
|
|
28
|
+
});
|
|
29
|
+
const guard = setTimeout(() => proc.kill(), 5000);
|
|
30
|
+
try {
|
|
31
|
+
expect(await proc.exited).toBe(0);
|
|
32
|
+
const lines = Bun.stripANSI(text).replaceAll('\r', '').split('\n');
|
|
33
|
+
for (const marker of ['FIRST','SECOND','THIRD']) expect(lines.filter(line => line === marker)).toHaveLength(1);
|
|
34
|
+
expect(lines.indexOf('SECOND')).toBeLessThan(lines.indexOf('THIRD'));
|
|
35
|
+
} finally {
|
|
36
|
+
clearTimeout(guard); for (const timer of timers) clearTimeout(timer);
|
|
37
|
+
if (proc.exitCode === null) {proc.kill(); await proc.exited;}
|
|
38
|
+
terminal.close(); rmSync(home, {recursive:true, force:true});
|
|
39
|
+
}
|
|
40
|
+
}, 10000);
|
|
@@ -72,9 +72,13 @@ test("redirect order snapshots the original stdout and truncates null commands",
|
|
|
72
72
|
const dir = mkdtempSync(join(tmpdir(),'bunmsh-io-order-'));
|
|
73
73
|
try {
|
|
74
74
|
const path = join(dir,'out');
|
|
75
|
-
|
|
75
|
+
// Test descriptor routing, not console formatting: FORCE_COLOR can make
|
|
76
|
+
// console.error emit ANSI codes even when its output is captured in a pipe.
|
|
77
|
+
const cmd = `'${process.execPath}' -e 'const {writeSync}=require("node:fs");writeSync(1,"out\\n");writeSync(2,"err\\n")'`;
|
|
76
78
|
let r = await run(`${cmd} 2>&1 >'${path}'`);
|
|
77
|
-
|
|
79
|
+
// Escape control characters in failures: terminal-rendered string diffs
|
|
80
|
+
// can hide CR, ANSI sequences, or NUL bytes behind an apparent newline.
|
|
81
|
+
expect(JSON.stringify(r.stdout)).toBe(JSON.stringify('err\n')); expect(r.stderr).toBe('');
|
|
78
82
|
expect(readFileSync(path,'utf8')).toBe('out\n');
|
|
79
83
|
r = await run(`${cmd} >'${path}' 2>&1`);
|
|
80
84
|
expect(r.stdout).toBe(''); expect(r.stderr).toBe('');
|
|
@@ -219,7 +223,7 @@ test("only posix mode removes ls icons and survives pipeline state transfer", as
|
|
|
219
223
|
const dir=mkdtempSync(join(tmpdir(),'bunmsh-posix-ls-'));
|
|
220
224
|
try {
|
|
221
225
|
const path=join(dir,'file'); writeFileSync(path,'x');
|
|
222
|
-
const r=await run(`ls '${path}'\nset -o posix\
|
|
226
|
+
const r=await run(`builtin ls '${path}'\nset -o posix\nbuiltin ls '${path}' | cat\nlsfancy '${path}'\nset +o posix\nbuiltin ls '${path}'`);
|
|
223
227
|
expect(r.stdout).toBe(`📄 ${path}\n${path}\n📄 ${path}\n📄 ${path}\n`); expect(r.stderr).toBe('');
|
|
224
228
|
const options=await run('set -o\nset -o posix\necho "flags=$-"\nset +o');
|
|
225
229
|
expect(options.stdout).toContain('posix off\n'); expect(options.stdout).toContain('flags=\n'); expect(options.stdout).toContain('set -o posix\n');
|
|
@@ -508,10 +512,10 @@ test("uniq supports output files and rejects unsupported options", async () => {
|
|
|
508
512
|
const dir = mkdtempSync(join(tmpdir(),'bunmsh-uniq-'));
|
|
509
513
|
try {
|
|
510
514
|
writeFileSync(join(dir,'input'),'a\na\nb');
|
|
511
|
-
const r=await run(`uniq '${dir}/input' '${dir}/output'`);
|
|
515
|
+
const r=await run(`builtin uniq '${dir}/input' '${dir}/output'`);
|
|
512
516
|
expect(r.status).toBe(0); expect(r.stdout).toBe('');
|
|
513
517
|
expect(readFileSync(join(dir,'output'),'utf8')).toBe('a\nb\n');
|
|
514
|
-
expect((await run('uniq -z')).status).toBe(1);
|
|
518
|
+
expect((await run('builtin uniq -z')).status).toBe(1);
|
|
515
519
|
} finally { rmSync(dir,{recursive:true,force:true}); }
|
|
516
520
|
});
|
|
517
521
|
test("rm force ignores empty filenames while still deleting real operands", async () => {
|
|
@@ -417,19 +417,18 @@ describe("system multi-operand reference", () => {
|
|
|
417
417
|
await expectBuiltinLikeSystem("printf", ["[%s][%s]\n", "one", "two", "three"]);
|
|
418
418
|
});
|
|
419
419
|
|
|
420
|
-
test("ln
|
|
420
|
+
test("ln symlinks each source into a target directory", async () => {
|
|
421
421
|
const directory = mkdtempSync(join(tmpdir(), "bunmsh-ln-reference-"));
|
|
422
422
|
const actualDir = join(directory, "actual");
|
|
423
423
|
const systemDir = join(directory, "system");
|
|
424
424
|
try {
|
|
425
425
|
for (const cwd of [actualDir, systemDir]) {
|
|
426
426
|
mkdirSync(cwd);
|
|
427
|
-
mkdirSync(join(cwd, "hard"));
|
|
428
427
|
mkdirSync(join(cwd, "soft"));
|
|
429
428
|
await Bun.write(join(cwd, "one"), "first\n");
|
|
430
429
|
await Bun.write(join(cwd, "two"), "second\n");
|
|
431
430
|
}
|
|
432
|
-
for (const args of [["
|
|
431
|
+
for (const args of [["-s", "one", "two", "soft"]]) {
|
|
433
432
|
const [actual, reference] = await Promise.all([
|
|
434
433
|
invokeArgvWithInput([process.execPath, join(root, "src/main.js"), "-cc", "builtin", "ln", ...args], "", actualDir),
|
|
435
434
|
invokeArgvWithInput([Bun.which("ln"), ...args], "", systemDir),
|
|
@@ -437,8 +436,6 @@ describe("system multi-operand reference", () => {
|
|
|
437
436
|
expect(actual).toEqual(reference);
|
|
438
437
|
}
|
|
439
438
|
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
439
|
expect(readlinkSync(join(actualDir, "soft", name)))
|
|
443
440
|
.toBe(readlinkSync(join(systemDir, "soft", name)));
|
|
444
441
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { answerCursorQueries } from "./support/terminal.js";
|
|
1
2
|
import { describe, expect, test } from "bun:test";
|
|
2
3
|
import { chmodSync, mkdirSync, mkdtempSync, readlinkSync, rmSync, statSync, symlinkSync, utimesSync } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
@@ -156,7 +157,7 @@ describe("CLI", () => {
|
|
|
156
157
|
const terminal = new Bun.Terminal({
|
|
157
158
|
cols: 80,
|
|
158
159
|
rows: 24,
|
|
159
|
-
data(_terminal, data) { transcript += data.toString(); },
|
|
160
|
+
data(_terminal, data) { transcript += data.toString(); answerCursorQueries(_terminal, data); },
|
|
160
161
|
});
|
|
161
162
|
const proc = Bun.spawn({
|
|
162
163
|
cmd: [process.execPath, "src/main.js", "-i"],
|
|
@@ -182,7 +183,7 @@ describe("CLI", () => {
|
|
|
182
183
|
const terminal = new Bun.Terminal({
|
|
183
184
|
cols: 80,
|
|
184
185
|
rows: 24,
|
|
185
|
-
data(_terminal, data) { transcript += data.toString(); },
|
|
186
|
+
data(_terminal, data) { transcript += data.toString(); answerCursorQueries(_terminal, data); },
|
|
186
187
|
});
|
|
187
188
|
const proc = Bun.spawn({
|
|
188
189
|
cmd: [process.execPath, "src/main.js", "-i"],
|
|
@@ -218,7 +219,7 @@ describe("CLI", () => {
|
|
|
218
219
|
const terminal = new Bun.Terminal({
|
|
219
220
|
cols: 80,
|
|
220
221
|
rows: 24,
|
|
221
|
-
data(_terminal, data) { transcript += data.toString(); },
|
|
222
|
+
data(_terminal, data) { transcript += data.toString(); answerCursorQueries(_terminal, data); },
|
|
222
223
|
});
|
|
223
224
|
const proc = Bun.spawn({
|
|
224
225
|
cmd: [process.execPath, "src/main.js", "-i"],
|
|
@@ -249,7 +250,7 @@ describe("CLI", () => {
|
|
|
249
250
|
const terminal = new Bun.Terminal({
|
|
250
251
|
cols: 80,
|
|
251
252
|
rows: 24,
|
|
252
|
-
data(_terminal, data) { transcript += data.toString(); },
|
|
253
|
+
data(_terminal, data) { transcript += data.toString(); answerCursorQueries(_terminal, data); },
|
|
253
254
|
});
|
|
254
255
|
const proc = Bun.spawn({
|
|
255
256
|
cmd: [process.execPath, "src/main.js", "-i"],
|
|
@@ -283,7 +284,7 @@ describe("CLI", () => {
|
|
|
283
284
|
const terminal = new Bun.Terminal({
|
|
284
285
|
cols: 80,
|
|
285
286
|
rows: 24,
|
|
286
|
-
data(_terminal, data) { transcript += data.toString(); },
|
|
287
|
+
data(_terminal, data) { transcript += data.toString(); answerCursorQueries(_terminal, data); },
|
|
287
288
|
});
|
|
288
289
|
const proc = Bun.spawn({
|
|
289
290
|
cmd: [process.execPath, "src/main.js", "-i"],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { answerCursorQueries } from "./support/terminal.js";
|
|
1
2
|
import { describe, expect, test } from "bun:test";
|
|
2
3
|
import { chmodSync, mkdirSync, mkdtempSync, readlinkSync, rmSync, statSync, symlinkSync, utimesSync } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
@@ -130,7 +131,7 @@ describe("CLI", () => {
|
|
|
130
131
|
const terminal = new Bun.Terminal({
|
|
131
132
|
cols: 100,
|
|
132
133
|
rows: 30,
|
|
133
|
-
data(_terminal, data) { transcript += data.toString(); },
|
|
134
|
+
data(_terminal, data) { transcript += data.toString(); answerCursorQueries(_terminal, data); },
|
|
134
135
|
});
|
|
135
136
|
try {
|
|
136
137
|
const historyPath = join(home, ".local", "share", "bunmsh", "history");
|
|
@@ -164,7 +165,7 @@ describe("CLI", () => {
|
|
|
164
165
|
const terminal = new Bun.Terminal({
|
|
165
166
|
cols: 80,
|
|
166
167
|
rows: 24,
|
|
167
|
-
data(_terminal, data) { transcript += data.toString(); },
|
|
168
|
+
data(_terminal, data) { transcript += data.toString(); answerCursorQueries(_terminal, data); },
|
|
168
169
|
});
|
|
169
170
|
const proc = Bun.spawn({
|
|
170
171
|
cmd: [Bun.which("bun") || process.argv0, "src/main.js", "-i"],
|
|
@@ -186,7 +187,7 @@ describe("CLI", () => {
|
|
|
186
187
|
const terminal = new Bun.Terminal({
|
|
187
188
|
cols: 80,
|
|
188
189
|
rows: 24,
|
|
189
|
-
data(_terminal, data) { transcript += data.toString(); },
|
|
190
|
+
data(_terminal, data) { transcript += data.toString(); answerCursorQueries(_terminal, data); },
|
|
190
191
|
});
|
|
191
192
|
const env = { ...process.env, PS1: "> " };
|
|
192
193
|
delete env.BUNMSH_MOUSE;
|
|
@@ -228,7 +229,7 @@ describe("CLI", () => {
|
|
|
228
229
|
const terminal = new Bun.Terminal({
|
|
229
230
|
cols: 80,
|
|
230
231
|
rows: 24,
|
|
231
|
-
data(_terminal, data) { transcript += data.toString(); },
|
|
232
|
+
data(_terminal, data) { transcript += data.toString(); answerCursorQueries(_terminal, data); },
|
|
232
233
|
});
|
|
233
234
|
const env = { ...process.env, PS1: "> " };
|
|
234
235
|
delete env.BUNMSH_MOUSE;
|
|
@@ -256,7 +257,7 @@ describe("CLI", () => {
|
|
|
256
257
|
const terminal = new Bun.Terminal({
|
|
257
258
|
cols: 100,
|
|
258
259
|
rows: 30,
|
|
259
|
-
data(_terminal, data) { transcript += data.toString(); },
|
|
260
|
+
data(_terminal, data) { transcript += data.toString(); answerCursorQueries(_terminal, data); },
|
|
260
261
|
});
|
|
261
262
|
const env = { ...process.env };
|
|
262
263
|
delete env.PS1;
|
|
@@ -363,7 +364,7 @@ describe("CLI", () => {
|
|
|
363
364
|
const terminal = new Bun.Terminal({
|
|
364
365
|
cols: 100,
|
|
365
366
|
rows: 30,
|
|
366
|
-
data(_terminal, data) { transcript += data.toString(); },
|
|
367
|
+
data(_terminal, data) { transcript += data.toString(); answerCursorQueries(_terminal, data); },
|
|
367
368
|
});
|
|
368
369
|
const proc = Bun.spawn({
|
|
369
370
|
cmd: [Bun.which("bun") || process.argv0, "src/main.js", "-i"],
|
|
@@ -400,7 +401,7 @@ describe("CLI", () => {
|
|
|
400
401
|
const terminal = new Bun.Terminal({
|
|
401
402
|
cols: 100,
|
|
402
403
|
rows: 30,
|
|
403
|
-
data(_terminal, data) { transcript += data.toString(); },
|
|
404
|
+
data(_terminal, data) { transcript += data.toString(); answerCursorQueries(_terminal, data); },
|
|
404
405
|
});
|
|
405
406
|
const proc = Bun.spawn({
|
|
406
407
|
cmd: [Bun.which("bun") || process.argv0, "src/main.js", "-i"],
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Bun.Terminal is a PTY, not an emulator. Interactive tests must answer DSR
|
|
2
|
+
// instead of sending subsequent commands during the shell's fallback timeout.
|
|
3
|
+
const pending = new WeakMap();
|
|
4
|
+
export function answerCursorQueries(terminal, data) {
|
|
5
|
+
const text = (pending.get(terminal) ?? '') + data.toString();
|
|
6
|
+
for (const _ of text.matchAll(/\x1b\[6n/g)) terminal.write('\x1b[1;1R');
|
|
7
|
+
const escape = text.lastIndexOf('\x1b');
|
|
8
|
+
const tail = escape < 0 ? '' : text.slice(escape);
|
|
9
|
+
pending.set(terminal, tail.length < 4 && '\x1b[6n'.startsWith(tail) ? tail : '');
|
|
10
|
+
}
|