buninu 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +86 -5
- package/apps/bunmsh/CHANGELOG.md +176 -0
- package/apps/bunmsh/LICENSE-MICRO +58 -0
- package/apps/bunmsh/README.md +243 -2
- package/apps/bunmsh/help/README.md +292 -0
- package/apps/bunmsh/help/curl.md +181 -0
- package/apps/bunmsh/help/kill.md +16 -0
- package/apps/bunmsh/help/pspa.md +38 -0
- package/apps/bunmsh/help/pspac.md +57 -0
- package/apps/bunmsh/package.json +6 -2
- package/apps/bunmsh/src/completion.js +116 -0
- package/apps/bunmsh/src/curl.js +1191 -0
- package/apps/bunmsh/src/main.js +74 -0
- package/apps/bunmsh/src/shell.js +346 -5
- package/apps/bunmsh/test/curl.test.js +335 -0
- package/apps/bunmsh/test/reference.test.js +127 -1
- package/apps/bunmsh/test/shell.test.js +316 -2
- package/package.json +1 -1
package/apps/bunmsh/src/main.js
CHANGED
|
@@ -7,16 +7,21 @@ import {
|
|
|
7
7
|
execute,
|
|
8
8
|
executeArgv,
|
|
9
9
|
executeBunShellFallback,
|
|
10
|
+
isJavaScriptMode,
|
|
10
11
|
needsMoreInput,
|
|
11
12
|
pipelineChildState,
|
|
12
13
|
} from "./shell.js";
|
|
13
14
|
import {
|
|
14
15
|
CommandIndex,
|
|
15
16
|
FileIndex,
|
|
17
|
+
VariableIndex,
|
|
16
18
|
completionContext,
|
|
17
19
|
fitGhost,
|
|
18
20
|
historyGhost,
|
|
21
|
+
javascriptContext,
|
|
19
22
|
nextGhostChunk,
|
|
23
|
+
variableCompletion,
|
|
24
|
+
variableContext,
|
|
20
25
|
} from "./completion.js";
|
|
21
26
|
import { readAssetText } from "../single-exe/assetsHelper.js";
|
|
22
27
|
import { buildEarlyExit, stringifyNonPrimitiveDefineValues } from "../single-exe/compiled.js";
|
|
@@ -166,6 +171,41 @@ async function interactive(state) {
|
|
|
166
171
|
let mouseTracking = terminal && state.mouseTracking;
|
|
167
172
|
const commandIndex = new CommandIndex(builtinNames());
|
|
168
173
|
const fileIndex = new FileIndex();
|
|
174
|
+
const variableIndex = new VariableIndex();
|
|
175
|
+
// A `$` being typed outranks the word it sits in: `$ED` is a variable
|
|
176
|
+
// name, not a command or a file, wherever on the line it appears. Single
|
|
177
|
+
// quotes are the exception, since nothing expands inside them.
|
|
178
|
+
const variableMatch = (line, cursor, context) => {
|
|
179
|
+
if (context.quote === "'") return null;
|
|
180
|
+
const variable = variableContext(line, cursor);
|
|
181
|
+
return variable ? { variable } : null;
|
|
182
|
+
};
|
|
183
|
+
// JavaScript mode is decided on the whole source the evaluator will see,
|
|
184
|
+
// which on a continued line is what is pending plus what is being typed.
|
|
185
|
+
// `pending` is declared further down, next to the loop that fills it; it
|
|
186
|
+
// is always initialised by the time a keystroke can reach this.
|
|
187
|
+
const javascriptLine = (line) => isJavaScriptMode(pending ? `${pending}\n${line}` : line);
|
|
188
|
+
// What a JavaScript line offers: a shell variable name after `$.`, a path
|
|
189
|
+
// inside a string literal, and nothing where neither applies — shell
|
|
190
|
+
// word-splitting would only produce noise there.
|
|
191
|
+
const javascriptMatches = (line, cursor) => {
|
|
192
|
+
const js = javascriptContext(line, cursor);
|
|
193
|
+
if (js?.kind === "variable")
|
|
194
|
+
return {
|
|
195
|
+
js,
|
|
196
|
+
names: variableIndex.matches(state, js.prefix).map((name) => variableCompletion(js, name)),
|
|
197
|
+
first: variableIndex.first(state, js.prefix),
|
|
198
|
+
complete: (name) => variableCompletion(js, name),
|
|
199
|
+
};
|
|
200
|
+
if (js?.kind === "path")
|
|
201
|
+
return {
|
|
202
|
+
js,
|
|
203
|
+
names: fileIndex.matches(js.prefix, state),
|
|
204
|
+
first: fileIndex.first(js.prefix, state),
|
|
205
|
+
complete: (name) => name,
|
|
206
|
+
};
|
|
207
|
+
return null;
|
|
208
|
+
};
|
|
169
209
|
state.history = await importedHistory(state.env);
|
|
170
210
|
const history = state.history;
|
|
171
211
|
// Marks everything just loaded (including whatever another session has
|
|
@@ -347,7 +387,17 @@ async function interactive(state) {
|
|
|
347
387
|
}, onPaste) : process.stdin;
|
|
348
388
|
const completer = (line) => {
|
|
349
389
|
commandIndex.refreshIfChanged(state);
|
|
390
|
+
if (javascriptLine(line)) {
|
|
391
|
+
const found = javascriptMatches(line, line.length);
|
|
392
|
+
return found?.names.length ? [found.names, found.js.text] : [[], line];
|
|
393
|
+
}
|
|
350
394
|
const context = completionContext(line);
|
|
395
|
+
const found = variableMatch(line, line.length, context);
|
|
396
|
+
if (found) {
|
|
397
|
+
const names = variableIndex.matches(state, found.variable.prefix);
|
|
398
|
+
if (names.length)
|
|
399
|
+
return [names.map((name) => variableCompletion(found.variable, name)), found.variable.text];
|
|
400
|
+
}
|
|
351
401
|
if (context.command) {
|
|
352
402
|
return [
|
|
353
403
|
context.prefix.includes("/")
|
|
@@ -460,15 +510,39 @@ async function interactive(state) {
|
|
|
460
510
|
let renderTask = null;
|
|
461
511
|
const currentGhost = () => {
|
|
462
512
|
if (readline.cursor !== readline.line.length) return null;
|
|
513
|
+
if (javascriptLine(readline.line)) {
|
|
514
|
+
const found = javascriptMatches(readline.line, readline.cursor);
|
|
515
|
+
// An empty name matches everything, so it ghosts nothing; Tab still
|
|
516
|
+
// lists it all, the way it does for an empty word in shell text.
|
|
517
|
+
if (found?.js.prefix && found.first)
|
|
518
|
+
return found.complete(found.first).slice(found.js.text.length);
|
|
519
|
+
return historyGhost(history, readline.line);
|
|
520
|
+
}
|
|
463
521
|
const context = completionContext(readline.line, readline.cursor);
|
|
522
|
+
const found = variableMatch(readline.line, readline.cursor, context);
|
|
523
|
+
// An empty name would match every variable there is, so a bare `$`
|
|
524
|
+
// ghosts nothing; Tab still lists them all, the way it does for files.
|
|
525
|
+
const variableGhost = () => {
|
|
526
|
+
if (!found?.variable.prefix) return null;
|
|
527
|
+
const name = variableIndex.first(state, found.variable.prefix);
|
|
528
|
+
return name
|
|
529
|
+
? variableCompletion(found.variable, name).slice(found.variable.text.length)
|
|
530
|
+
: null;
|
|
531
|
+
};
|
|
464
532
|
if (context.command) {
|
|
533
|
+
const ghost = variableGhost();
|
|
534
|
+
if (ghost) return ghost;
|
|
465
535
|
const match = context.prefix.includes("/")
|
|
466
536
|
? fileIndex.first(context.prefix, state)
|
|
467
537
|
: commandIndex.first(context.prefix);
|
|
468
538
|
return match ? match.slice(context.prefix.length) : null;
|
|
469
539
|
}
|
|
540
|
+
// History still wins where it applies: it completes the whole line,
|
|
541
|
+
// which reaches further than one variable name.
|
|
470
542
|
const fromHistory = historyGhost(history, readline.line);
|
|
471
543
|
if (fromHistory) return fromHistory;
|
|
544
|
+
const ghost = variableGhost();
|
|
545
|
+
if (ghost) return ghost;
|
|
472
546
|
const match = fileIndex.first(context.prefix, state);
|
|
473
547
|
return match ? match.slice(context.prefix.length) : null;
|
|
474
548
|
};
|
package/apps/bunmsh/src/shell.js
CHANGED
|
@@ -44,13 +44,14 @@ import { findIsRegularBuiltin, runFind } from "./find.js";
|
|
|
44
44
|
import { main as startFileServer, parseServeArguments, waitForInterrupt } from "../serve.js";
|
|
45
45
|
import { main as catFancy } from "./cat-fancy.js";
|
|
46
46
|
import { parseCatOperands } from "./cat-operands.js";
|
|
47
|
+
import { runCurl } from "./curl.js";
|
|
47
48
|
|
|
48
49
|
const encoder = new TextEncoder();
|
|
49
50
|
const decoder = new TextDecoder();
|
|
50
51
|
const DEFAULT_ALIASES = {
|
|
51
52
|
ls: ["ls", "--color=auto"],
|
|
52
|
-
diff: ["diff", "--color=auto"],
|
|
53
53
|
grep: ["grep", "--color=auto"],
|
|
54
|
+
diff: ["diff", "--color"],
|
|
54
55
|
};
|
|
55
56
|
const DEFAULT_COMMAND_PATH = process.platform === "win32"
|
|
56
57
|
? (environmentValue(process.env, "PATH") ?? "")
|
|
@@ -1574,6 +1575,19 @@ const builtins = {
|
|
|
1574
1575
|
let status = 0, stderr = "";
|
|
1575
1576
|
for (; i < argv.length; i++) {
|
|
1576
1577
|
const pid = Number(argv[i]);
|
|
1578
|
+
// Signal 0 asks whether a PID can be signalled without touching it,
|
|
1579
|
+
// which taskkill has no way to express — and which process.kill does
|
|
1580
|
+
// answer on Windows. Everything else goes through taskkill there.
|
|
1581
|
+
if (process.platform === "win32" && signal !== 0) {
|
|
1582
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1583
|
+
status = 1;
|
|
1584
|
+
stderr += `bunmsh: kill: ${argv[i]}: arguments must be process ids\n`;
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
const failure = await runWindowsKill(pid);
|
|
1588
|
+
if (failure) { status = 1; stderr += `bunmsh: kill: ${argv[i]}: ${failure}\n`; }
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1577
1591
|
try { process.kill(pid, signal); }
|
|
1578
1592
|
catch (error) { status = 1; stderr += `bunmsh: kill: ${argv[i]}: ${error.message}\n`; }
|
|
1579
1593
|
}
|
|
@@ -1907,6 +1921,38 @@ async function runTrFallback(argv, _state, input) {
|
|
|
1907
1921
|
return result(0, output);
|
|
1908
1922
|
}
|
|
1909
1923
|
|
|
1924
|
+
// `curl` only ever runs when the system has none, so it inherits the
|
|
1925
|
+
// streaming rules the other fallbacks use: write straight through when the
|
|
1926
|
+
// bytes are headed for the terminal or a pipeline stage, buffer when the
|
|
1927
|
+
// caller is capturing them.
|
|
1928
|
+
async function runCurlFallback(argv, state, input, context = {}) {
|
|
1929
|
+
const streamStdout = state.pipelineChild ||
|
|
1930
|
+
(!context.captureStdout && !context.options?.stdoutSink);
|
|
1931
|
+
const streamStderr = state.pipelineChild || !context.captureStderr;
|
|
1932
|
+
const stdout = [];
|
|
1933
|
+
const stderr = [];
|
|
1934
|
+
const io = {
|
|
1935
|
+
cwd: nativePath(state.cwd),
|
|
1936
|
+
env: state.env,
|
|
1937
|
+
stdoutIsTTY: streamStdout && Boolean(process.stdout.isTTY),
|
|
1938
|
+
readStdin: () => readFallbackInput(input),
|
|
1939
|
+
async writeStdout(chunk) {
|
|
1940
|
+
const data = bytes(chunk);
|
|
1941
|
+
if (streamStdout) await writeStream(process.stdout, data); else stdout.push(data);
|
|
1942
|
+
},
|
|
1943
|
+
async writeStderr(text) {
|
|
1944
|
+
const data = bytes(text);
|
|
1945
|
+
if (streamStderr) await writeStream(process.stderr, data); else stderr.push(data);
|
|
1946
|
+
},
|
|
1947
|
+
};
|
|
1948
|
+
try {
|
|
1949
|
+
const status = await runCurl(argv, io);
|
|
1950
|
+
return result(status, concatBytes(stdout), concatBytes(stderr));
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
return result(2, concatBytes(stdout), concatBytes([...stderr, bytes(`curl: ${error.message}\n`)]));
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1910
1956
|
async function runTeeFallback(argv, state, input) {
|
|
1911
1957
|
let append = false, i = 1;
|
|
1912
1958
|
if (argv[i] === "-a") append = true, i++;
|
|
@@ -2342,6 +2388,282 @@ export function runUnameFallback(argv, system = {}) {
|
|
|
2342
2388
|
return result(0, `${[..."snrvmp"].filter((flag) => flags.has(flag)).map((flag) => values[flag]).join(" ")}\n`);
|
|
2343
2389
|
}
|
|
2344
2390
|
|
|
2391
|
+
// Windows has no `ps`, so the same two columns come out of Win32_Process.
|
|
2392
|
+
// bun-taskmgr reads `Select-Object ProcessId, CommandLine`, but that is
|
|
2393
|
+
// formatted as a console table, which truncates a long command line with an
|
|
2394
|
+
// ellipsis — exactly the part pspa exists to show. Building the line inside
|
|
2395
|
+
// PowerShell instead keeps it whole, and falls back to the image name for a
|
|
2396
|
+
// system process whose CommandLine is null.
|
|
2397
|
+
const PSPA_WINDOWS_QUERY = "Get-CimInstance Win32_Process | ForEach-Object { " +
|
|
2398
|
+
"$_.ProcessId.ToString() + ' ' + $(if ($_.CommandLine) { $_.CommandLine } else { $_.Name }) }";
|
|
2399
|
+
|
|
2400
|
+
export function parseWindowsProcessList(text) {
|
|
2401
|
+
const rows = [];
|
|
2402
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2403
|
+
const match = /^\s*(\d+) ?(.*)$/.exec(line);
|
|
2404
|
+
// A command line containing a newline arrives as continuation lines with
|
|
2405
|
+
// no PID of their own. There is nothing to attribute them to, so they are
|
|
2406
|
+
// dropped rather than guessed at.
|
|
2407
|
+
if (match) rows.push({ pid: Number(match[1]), args: match[2].trim() });
|
|
2408
|
+
}
|
|
2409
|
+
return rows.sort((first, second) => first.pid - second.pid);
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// Laid out like `ps -eo pid,args`, so both platforms print the same shape:
|
|
2413
|
+
// a right-aligned PID column at least as wide as procps' own, then the
|
|
2414
|
+
// command line.
|
|
2415
|
+
export function formatProcessTable(rows) {
|
|
2416
|
+
const width = rows.reduce((widest, row) => Math.max(widest, String(row.pid).length), 5);
|
|
2417
|
+
const lines = [`${"PID".padStart(width)} COMMAND`];
|
|
2418
|
+
for (const row of rows)
|
|
2419
|
+
lines.push(`${String(row.pid).padStart(width)} ${row.args}`.trimEnd());
|
|
2420
|
+
return `${lines.join("\n")}\n`;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
// Windows has no signals to deliver. libuv turns SIGTERM, SIGINT, and
|
|
2424
|
+
// SIGKILL into an unconditional TerminateProcess, refuses every other name
|
|
2425
|
+
// with ENOSYS, and in no case touches the target's children. `taskkill`
|
|
2426
|
+
// reaches all of it, and `/T /F` is the form bun-taskmgr verified on
|
|
2427
|
+
// Windows 11, so that is what a terminating signal becomes here.
|
|
2428
|
+
export function windowsKillCommand(pid) {
|
|
2429
|
+
return ["taskkill", "/PID", String(pid), "/T", "/F"];
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// taskkill reports its own failures in prose ("ERROR: The process "123" not
|
|
2433
|
+
// found."). Strip its prefix so the line reads as one of ours.
|
|
2434
|
+
export function taskkillFailure(status, stdout, stderr) {
|
|
2435
|
+
if (status === 0) return null;
|
|
2436
|
+
const message = (stderr.trim() || stdout.trim()).replace(/^ERROR:\s*/i, "");
|
|
2437
|
+
return message || `taskkill exited with status ${status}`;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
async function runWindowsKill(pid) {
|
|
2441
|
+
let proc;
|
|
2442
|
+
try {
|
|
2443
|
+
proc = Bun.spawn({
|
|
2444
|
+
cmd: windowsKillCommand(pid),
|
|
2445
|
+
stdin: "ignore",
|
|
2446
|
+
stdout: "pipe",
|
|
2447
|
+
stderr: "pipe",
|
|
2448
|
+
});
|
|
2449
|
+
} catch (error) { return `taskkill: ${error.message}`; }
|
|
2450
|
+
const [status, stdout, stderr] = await Promise.all([
|
|
2451
|
+
proc.exited,
|
|
2452
|
+
new Response(proc.stdout).text(),
|
|
2453
|
+
new Response(proc.stderr).text(),
|
|
2454
|
+
]);
|
|
2455
|
+
return taskkillFailure(status, stdout, stderr);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// `pspa` answers the question a shell with no process viewer keeps running
|
|
2459
|
+
// into: which PID belongs to which command line. POSIX hands that to
|
|
2460
|
+
// `ps -eo pid,args` and passes the output straight through — reading it over
|
|
2461
|
+
// a pipe is also what stops procps from cutting long command lines at the
|
|
2462
|
+
// terminal width, which it does when its stdout is a terminal.
|
|
2463
|
+
// Rows out of `ps`, for the callers that render the listing themselves
|
|
2464
|
+
// rather than passing it through.
|
|
2465
|
+
export function parsePosixProcessList(text) {
|
|
2466
|
+
const rows = [];
|
|
2467
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2468
|
+
if (/^\s*PID\s+COMMAND\s*$/.test(line)) continue;
|
|
2469
|
+
const match = /^\s*(\d+)\s+(.*)$/.exec(line);
|
|
2470
|
+
if (match) rows.push({ pid: Number(match[1]), args: match[2].trimEnd() });
|
|
2471
|
+
}
|
|
2472
|
+
return rows;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
async function queryProcessList(name, state) {
|
|
2476
|
+
const windows = process.platform === "win32";
|
|
2477
|
+
const command = windows
|
|
2478
|
+
? ["powershell", "-NoProfile", "-Command", PSPA_WINDOWS_QUERY]
|
|
2479
|
+
: ["ps", "-eo", "pid,args"];
|
|
2480
|
+
let proc;
|
|
2481
|
+
try {
|
|
2482
|
+
proc = Bun.spawn({
|
|
2483
|
+
cmd: command,
|
|
2484
|
+
cwd: nativePath(state.cwd),
|
|
2485
|
+
env: state.env,
|
|
2486
|
+
stdin: "ignore",
|
|
2487
|
+
stdout: "pipe",
|
|
2488
|
+
stderr: "pipe",
|
|
2489
|
+
});
|
|
2490
|
+
} catch (error) {
|
|
2491
|
+
return { failure: result(127, "", `bunmsh: ${name}: ${command[0]}: ${error.message}\n`) };
|
|
2492
|
+
}
|
|
2493
|
+
const [status, stdout, stderr] = await Promise.all([
|
|
2494
|
+
proc.exited,
|
|
2495
|
+
new Response(proc.stdout).arrayBuffer(),
|
|
2496
|
+
new Response(proc.stderr).text(),
|
|
2497
|
+
]);
|
|
2498
|
+
if (status !== 0) {
|
|
2499
|
+
return {
|
|
2500
|
+
failure: result(status, "",
|
|
2501
|
+
stderr || `bunmsh: ${name}: ${command[0]}: exited with status ${status}\n`),
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
return { windows, output: new Uint8Array(stdout), stderr };
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
async function runPspaFallback(argv, state) {
|
|
2508
|
+
if (argv.length > 1)
|
|
2509
|
+
return result(2, "", `bunmsh: pspa: unexpected operand: ${argv[1]}\n`);
|
|
2510
|
+
const query = await queryProcessList("pspa", state);
|
|
2511
|
+
if (query.failure) return query.failure;
|
|
2512
|
+
return query.windows
|
|
2513
|
+
? result(0, formatProcessTable(parseWindowsProcessList(decoder.decode(query.output))), query.stderr)
|
|
2514
|
+
: result(0, query.output, query.stderr);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// The COMMAND column of a process listing is a shell command line, so it is
|
|
2518
|
+
// highlighted as one. The rules are micro's `syntax/sh.yaml` and the colours
|
|
2519
|
+
// are its `colorschemes/monokai.micro` colour-links, which is also the
|
|
2520
|
+
// palette catfancy already uses — so a command line reads the same here, in
|
|
2521
|
+
// a previewed script, and in the editor.
|
|
2522
|
+
export const SH_COLORS = {
|
|
2523
|
+
comment: Bun.color("#75715E", "ansi-16m"),
|
|
2524
|
+
identifier: Bun.color("#66D9EF", "ansi-16m"),
|
|
2525
|
+
constant: Bun.color("#AE81FF", "ansi-16m"),
|
|
2526
|
+
string: Bun.color("#E6DB74", "ansi-16m"),
|
|
2527
|
+
statement: Bun.color("#F92672", "ansi-16m"),
|
|
2528
|
+
type: Bun.color("#66D9EF", "ansi-16m"),
|
|
2529
|
+
special: Bun.color("#A6E22E", "ansi-16m"),
|
|
2530
|
+
path: "\x1b[2m",
|
|
2531
|
+
header: "\x1b[1m",
|
|
2532
|
+
reset: "\x1b[0m",
|
|
2533
|
+
};
|
|
2534
|
+
|
|
2535
|
+
// sh.yaml's rules, in its order. A later rule paints over an earlier one,
|
|
2536
|
+
// which is how micro resolves them too: that is why the flag rule beats the
|
|
2537
|
+
// command names it overlaps, and why `$var` ends up an identifier rather
|
|
2538
|
+
// than the `special` its `$` matched first.
|
|
2539
|
+
const SH_RULES = [
|
|
2540
|
+
{ name: "constant", pattern: /\b[0-9]+\b/g },
|
|
2541
|
+
{ name: "statement", pattern: /\b(break|case|continue|do|done|elif|else|esac|exec|exit|fi|for|function|if|in|return|select|then|trap|until|wait|while)\b/g },
|
|
2542
|
+
{ name: "special", pattern: /[`$<>!=&~^{}();\][]+/g },
|
|
2543
|
+
{ name: "type", pattern: /\b(cd|command|echo|eval|export|getopts|let|local|read|set|shift|time|umask|unset)\b/g },
|
|
2544
|
+
{ name: "type", pattern: /\b((g|ig)?awk|bash|dash|find|getopt|\w{0,4}grep|kill|killall|\w{0,4}less|make|pkill|sed|sh|tar)\b/g },
|
|
2545
|
+
{ name: "type", pattern: /\b(base64|basename|cat|chcon|chgrp|chmod|chown|chroot|cksum|comm|cp|csplit|cut|date|dd|df|dir|dircolors|dirname|du|env|expand|expr|factor|false|fmt|fold|head|hostid|id|install|join|link|ln|logname|ls|md5sum|mkdir|mkfifo|mknod|mktemp|mv|nice|nl|nohup|nproc|numfmt|od|paste|pathchk|pinky|pr|printenv|printf|ptx|pwd|readlink|realpath|rm|rmdir|runcon|seq|(sha1|sha224|sha256|sha384|sha512)sum|shred|shuf|sleep|sort|split|stat|stdbuf|stty|sum|sync|tac|tail|tee|test|time|timeout|touch|tr|true|truncate|tsort|tty|uname|unexpand|uniq|unlink|users|vdir|wc|who|whoami|yes)\b/g },
|
|
2546
|
+
{ name: "statement", pattern: /(\s|^)(--?[A-Za-z0-9][\w-]*)/g, group: 2 },
|
|
2547
|
+
{ name: "identifier", pattern: /\$\{[\w:!%&=+#~@*^$?, .\-/[\]]+\}/g },
|
|
2548
|
+
{ name: "identifier", pattern: /\$([0-9!#@*$?-]|[A-Za-z_]\w*)/g },
|
|
2549
|
+
];
|
|
2550
|
+
|
|
2551
|
+
// Quoted strings and comments are regions in sh.yaml, not patterns: they
|
|
2552
|
+
// swallow whatever is inside them. Finding their spans first is what keeps
|
|
2553
|
+
// a `#` inside a quoted argument from turning the rest of the line into a
|
|
2554
|
+
// comment, and a quote inside a comment from opening a string.
|
|
2555
|
+
function shellRegions(text) {
|
|
2556
|
+
const regions = [];
|
|
2557
|
+
for (let i = 0; i < text.length; i++) {
|
|
2558
|
+
const ch = text[i];
|
|
2559
|
+
if (ch === '"' || ch === "'") {
|
|
2560
|
+
let end = i + 1;
|
|
2561
|
+
while (end < text.length) {
|
|
2562
|
+
if (text[end] === "\\") { end += 2; continue; }
|
|
2563
|
+
if (text[end] === ch) { end++; break; }
|
|
2564
|
+
end++;
|
|
2565
|
+
}
|
|
2566
|
+
end = Math.min(end, text.length);
|
|
2567
|
+
regions.push({ start: i, end, name: "string" });
|
|
2568
|
+
i = end - 1;
|
|
2569
|
+
continue;
|
|
2570
|
+
}
|
|
2571
|
+
if (ch === "#" && (i === 0 || /\s/.test(text[i - 1]))) {
|
|
2572
|
+
regions.push({ start: i, end: text.length, name: "comment" });
|
|
2573
|
+
break;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
return regions;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
// `base` paints runs before any rule runs, so a rule matching inside one
|
|
2580
|
+
// still wins — the same layering micro applies between a scheme's default
|
|
2581
|
+
// and its rules.
|
|
2582
|
+
export function highlightShellCommand(text, base = []) {
|
|
2583
|
+
const painted = new Array(text.length).fill(null);
|
|
2584
|
+
for (const run of base)
|
|
2585
|
+
for (let i = run.start; i < Math.min(run.end, text.length); i++) painted[i] = run.name;
|
|
2586
|
+
const regions = shellRegions(text);
|
|
2587
|
+
const inRegion = new Array(text.length).fill(false);
|
|
2588
|
+
for (const region of regions)
|
|
2589
|
+
for (let i = region.start; i < region.end; i++) inRegion[i] = true;
|
|
2590
|
+
for (const rule of SH_RULES) {
|
|
2591
|
+
for (const match of text.matchAll(rule.pattern)) {
|
|
2592
|
+
const value = rule.group ? match[rule.group] : match[0];
|
|
2593
|
+
if (!value) continue;
|
|
2594
|
+
const start = match.index + (rule.group ? match[0].indexOf(value) : 0);
|
|
2595
|
+
for (let i = start; i < start + value.length; i++)
|
|
2596
|
+
if (!inRegion[i]) painted[i] = rule.name;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
for (const region of regions)
|
|
2600
|
+
for (let i = region.start; i < region.end; i++) painted[i] = region.name;
|
|
2601
|
+
|
|
2602
|
+
let output = "";
|
|
2603
|
+
for (let i = 0; i < text.length;) {
|
|
2604
|
+
const name = painted[i];
|
|
2605
|
+
let end = i;
|
|
2606
|
+
while (end < text.length && painted[end] === name) end++;
|
|
2607
|
+
const chunk = text.slice(i, end);
|
|
2608
|
+
output += name ? `${SH_COLORS[name]}${chunk}${SH_COLORS.reset}` : chunk;
|
|
2609
|
+
i = end;
|
|
2610
|
+
}
|
|
2611
|
+
return output;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// Two additions to what sh.yaml describes, because a process listing is not
|
|
2615
|
+
// a script. The directory in front of the program is dimmed: that is the
|
|
2616
|
+
// part an absolute path buries the answer in, and `ps` prints a lot of it.
|
|
2617
|
+
// And the program's own name is painted `type` whether or not it is one of
|
|
2618
|
+
// the names sh.yaml lists — the file has to guess a command from a word
|
|
2619
|
+
// list, while here the first token is known to be one.
|
|
2620
|
+
// Both are base layers, so a rule still paints over them, and a quoted
|
|
2621
|
+
// program keeps the string colour a quoted string gets everywhere else.
|
|
2622
|
+
// A kernel thread — `[kworker/0:1]`, one bracketed token with no program,
|
|
2623
|
+
// path, or arguments in it — recedes whole instead of being taken apart.
|
|
2624
|
+
export function colorProcessCommand(args) {
|
|
2625
|
+
if (!args) return "";
|
|
2626
|
+
if (/^\[.+\]$/.test(args)) return `${SH_COLORS.path}${args}${SH_COLORS.reset}`;
|
|
2627
|
+
const end = args.indexOf(" ");
|
|
2628
|
+
const program = end < 0 ? args.length : end;
|
|
2629
|
+
const separator = Math.max(
|
|
2630
|
+
args.lastIndexOf("/", program - 1),
|
|
2631
|
+
args.lastIndexOf("\\", program - 1),
|
|
2632
|
+
);
|
|
2633
|
+
const base = separator < 0
|
|
2634
|
+
? [{ start: 0, end: program, name: "type" }]
|
|
2635
|
+
: [
|
|
2636
|
+
{ start: 0, end: separator + 1, name: "path" },
|
|
2637
|
+
{ start: separator + 1, end: program, name: "type" },
|
|
2638
|
+
];
|
|
2639
|
+
return highlightShellCommand(args, base);
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
export function colorProcessTable(rows) {
|
|
2643
|
+
// A PID is a number, and sh.yaml paints a number `constant`.
|
|
2644
|
+
const { constant: PID, header: HEADER, reset: RESET } = SH_COLORS;
|
|
2645
|
+
const width = rows.reduce((widest, row) => Math.max(widest, String(row.pid).length), 5);
|
|
2646
|
+
const lines = [`${HEADER}${"PID".padStart(width)} COMMAND${RESET}`];
|
|
2647
|
+
// Nothing but colour separates this from formatProcessTable's output, so
|
|
2648
|
+
// a row with no command line keeps the same trimmed tail that one has.
|
|
2649
|
+
for (const row of rows)
|
|
2650
|
+
lines.push(`${PID}${String(row.pid).padStart(width)}${RESET}` +
|
|
2651
|
+
(row.args ? ` ${colorProcessCommand(row.args)}` : ""));
|
|
2652
|
+
return `${lines.join("\n")}\n`;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// `pspac` is `pspa` coloured. Like catfancy it always colours rather than
|
|
2656
|
+
// sniffing for a terminal, so the plain listing stays one command away.
|
|
2657
|
+
async function runPspacFallback(argv, state) {
|
|
2658
|
+
if (argv.length > 1)
|
|
2659
|
+
return result(2, "", `bunmsh: pspac: unexpected operand: ${argv[1]}\n`);
|
|
2660
|
+
const query = await queryProcessList("pspac", state);
|
|
2661
|
+
if (query.failure) return query.failure;
|
|
2662
|
+
const text = decoder.decode(query.output);
|
|
2663
|
+
const rows = query.windows ? parseWindowsProcessList(text) : parsePosixProcessList(text);
|
|
2664
|
+
return result(0, colorProcessTable(rows), query.stderr);
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2345
2667
|
const fallbackBuiltins = {
|
|
2346
2668
|
basename: async (argv) => {
|
|
2347
2669
|
const i = argv[1] === "--" ? 2 : 1;
|
|
@@ -2377,6 +2699,7 @@ const fallbackBuiltins = {
|
|
|
2377
2699
|
return result(output.status, output.stdout, output.stderr);
|
|
2378
2700
|
},
|
|
2379
2701
|
cp: runBunShellCpFallback,
|
|
2702
|
+
curl: runCurlFallback,
|
|
2380
2703
|
head: runHeadFallback,
|
|
2381
2704
|
tail: (argv, state, input) => runTextFilter(argv, state, input, "tail"),
|
|
2382
2705
|
wc: runWcFallback,
|
|
@@ -2396,6 +2719,8 @@ const fallbackBuiltins = {
|
|
|
2396
2719
|
ln: async (argv, state) => runLnFallback(argv, state),
|
|
2397
2720
|
chmod: async (argv, state) => runChmodFallback(argv, state),
|
|
2398
2721
|
uname: async (argv) => runUnameFallback(argv),
|
|
2722
|
+
pspa: runPspaFallback,
|
|
2723
|
+
pspac: runPspacFallback,
|
|
2399
2724
|
bunmsh: runBunmshFallback,
|
|
2400
2725
|
bun: runBunFallback,
|
|
2401
2726
|
serve: async (argv, state) => {
|
|
@@ -2675,13 +3000,26 @@ function parseCompoundScript(source) {
|
|
|
2675
3000
|
// ShellSyntaxError whose message starts with "unterminated". An interactive
|
|
2676
3001
|
// front end can use this, mirroring mksh's PS2 continuation prompt, to tell
|
|
2677
3002
|
// "still typing this command" apart from a real syntax error.
|
|
3003
|
+
export function isJavaScriptMode(source) {
|
|
3004
|
+
return source.trimStart().startsWith("Bun.");
|
|
3005
|
+
}
|
|
3006
|
+
|
|
2678
3007
|
export function needsMoreInput(source) {
|
|
2679
3008
|
source = normalizeSource(source);
|
|
2680
3009
|
try {
|
|
2681
3010
|
if (hasCompoundSyntax(source)) parseCompoundScript(source);
|
|
2682
3011
|
else tokenize(source, { strict: true });
|
|
2683
3012
|
} catch (error) {
|
|
2684
|
-
|
|
3013
|
+
if (!(error instanceof ShellSyntaxError && error.message.startsWith("unterminated")))
|
|
3014
|
+
return false;
|
|
3015
|
+
// JavaScript has no line-continuation backslash outside a string
|
|
3016
|
+
// literal: there it is a syntax error, not a request for another line,
|
|
3017
|
+
// so asking for one only leads the typing somewhere that cannot parse.
|
|
3018
|
+
// An unterminated quote still continues, because that is JavaScript's
|
|
3019
|
+
// own line continuation and the evaluator does accept it.
|
|
3020
|
+
if (isJavaScriptMode(source) && error.message === "unterminated line continuation")
|
|
3021
|
+
return false;
|
|
3022
|
+
return true;
|
|
2685
3023
|
}
|
|
2686
3024
|
return false;
|
|
2687
3025
|
}
|
|
@@ -3394,12 +3732,15 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3394
3732
|
return execution;
|
|
3395
3733
|
}
|
|
3396
3734
|
const rawSource = source.trimStart();
|
|
3397
|
-
if (rawSource
|
|
3735
|
+
if (isJavaScriptMode(rawSource)) {
|
|
3398
3736
|
let execution;
|
|
3399
3737
|
const previousCwd = process.cwd();
|
|
3400
3738
|
try {
|
|
3401
3739
|
try {
|
|
3402
3740
|
process.chdir(nativePath(state.cwd));
|
|
3741
|
+
|
|
3742
|
+
const $ = state.env
|
|
3743
|
+
|
|
3403
3744
|
const value = await eval(rawSource);
|
|
3404
3745
|
execution = result(0, value === undefined ? "" : `${formatValue(value)}\n`);
|
|
3405
3746
|
} finally {
|
|
@@ -3416,11 +3757,11 @@ export async function execute(source, state = createState(), io = {}) {
|
|
|
3416
3757
|
return execution;
|
|
3417
3758
|
}
|
|
3418
3759
|
const lines = source.match(/.*(?:\n|$)/g)?.filter(Boolean) ?? [];
|
|
3419
|
-
if (lines.some((line) => line
|
|
3760
|
+
if (lines.some((line) => isJavaScriptMode(line))) {
|
|
3420
3761
|
const parts = [];
|
|
3421
3762
|
let shellSource = "";
|
|
3422
3763
|
for (const line of lines) {
|
|
3423
|
-
if (!line
|
|
3764
|
+
if (!isJavaScriptMode(line)) {
|
|
3424
3765
|
shellSource += line;
|
|
3425
3766
|
continue;
|
|
3426
3767
|
}
|