typescript-virtual-container 1.5.4 → 1.5.5
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/README.md +18 -526
- package/dist/.tsbuildinfo +1 -1
- package/dist/VirtualShell/shell.js +158 -11
- package/dist/commands/bc.d.ts +2 -0
- package/dist/commands/bc.js +28 -0
- package/dist/commands/{ifconfig.d.ts → ip.d.ts} +1 -1
- package/dist/commands/{ifconfig.js → ip.js} +3 -3
- package/dist/commands/jobs.d.ts +4 -0
- package/dist/commands/jobs.js +27 -0
- package/dist/commands/registry.js +9 -3
- package/dist/commands/runtime.js +2 -1
- package/dist/commands/set.js +20 -0
- package/dist/commands/sh.js +69 -1
- package/dist/utils/expand.js +3 -1
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
12
12
|
let historyIndex = null;
|
|
13
13
|
let historyDraft = "";
|
|
14
14
|
let cwd = userHome(authUser);
|
|
15
|
+
let pendingHeredoc = null;
|
|
15
16
|
const shellEnv = makeDefaultEnv(authUser, hostname);
|
|
16
17
|
let nanoSession = null;
|
|
17
18
|
let pendingSudo = null;
|
|
@@ -22,23 +23,33 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
22
23
|
};
|
|
23
24
|
const commandNames = Array.from(new Set(getCommandNames())).sort();
|
|
24
25
|
console.log(`[${sessionId}] Shell started for user '${authUser}' at ${remoteAddress}`);
|
|
25
|
-
//
|
|
26
|
+
// Source login/rc files at startup
|
|
26
27
|
void (async () => {
|
|
27
|
-
const
|
|
28
|
-
|
|
28
|
+
const sourceFile = async (filePath, isEnvFile = false) => {
|
|
29
|
+
if (!shell.vfs.exists(filePath))
|
|
30
|
+
return;
|
|
29
31
|
try {
|
|
30
|
-
const
|
|
31
|
-
for (const line of
|
|
32
|
+
const content = shell.vfs.readFile(filePath);
|
|
33
|
+
for (const line of content.split("\n")) {
|
|
32
34
|
const l = line.trim();
|
|
33
35
|
if (!l || l.startsWith("#"))
|
|
34
36
|
continue;
|
|
35
|
-
|
|
37
|
+
if (isEnvFile) {
|
|
38
|
+
// /etc/environment: KEY=VALUE pairs only, no shell syntax
|
|
39
|
+
const m = l.match(/^([A-Za-z_][A-Za-z0-9_]*)=["']?(.+?)["']?\s*$/);
|
|
40
|
+
if (m)
|
|
41
|
+
shellEnv.vars[m[1]] = m[2];
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, shellEnv);
|
|
45
|
+
}
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
|
-
catch {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
48
|
+
catch { /* ignore */ }
|
|
49
|
+
};
|
|
50
|
+
await sourceFile("/etc/environment", true);
|
|
51
|
+
await sourceFile(`${userHome(authUser)}/.profile`);
|
|
52
|
+
await sourceFile(`${userHome(authUser)}/.bashrc`);
|
|
42
53
|
})();
|
|
43
54
|
function renderLine() {
|
|
44
55
|
const prompt = buildCurrentPrompt();
|
|
@@ -283,6 +294,53 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
283
294
|
nanoSession.process.stdin.write(chunk);
|
|
284
295
|
return;
|
|
285
296
|
}
|
|
297
|
+
if (pendingHeredoc) {
|
|
298
|
+
const hd = pendingHeredoc;
|
|
299
|
+
const input = chunk.toString("utf8");
|
|
300
|
+
for (let i = 0; i < input.length; i++) {
|
|
301
|
+
const ch = input[i];
|
|
302
|
+
if (ch === "") {
|
|
303
|
+
pendingHeredoc = null;
|
|
304
|
+
stream.write("^C\r\n");
|
|
305
|
+
renderLine();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (ch === "" || ch === "\b") {
|
|
309
|
+
lineBuffer = lineBuffer.slice(0, -1);
|
|
310
|
+
renderLine();
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (ch === "\r" || ch === "\n") {
|
|
314
|
+
const typedLine = lineBuffer;
|
|
315
|
+
lineBuffer = "";
|
|
316
|
+
cursorPos = 0;
|
|
317
|
+
stream.write("\r\n");
|
|
318
|
+
if (typedLine === hd.delimiter) {
|
|
319
|
+
const stdin = hd.lines.join("\n");
|
|
320
|
+
const cmd = hd.cmdBefore;
|
|
321
|
+
pendingHeredoc = null;
|
|
322
|
+
pushHistory(`${cmd} << ${hd.delimiter}`);
|
|
323
|
+
const result = await Promise.resolve(runCommand(cmd, authUser, hostname, "shell", cwd, shell, stdin, shellEnv));
|
|
324
|
+
if (result.stdout)
|
|
325
|
+
stream.write(`${toTtyLines(result.stdout)}\r\n`);
|
|
326
|
+
if (result.stderr)
|
|
327
|
+
stream.write(`${toTtyLines(result.stderr)}\r\n`);
|
|
328
|
+
if (result.nextCwd)
|
|
329
|
+
cwd = result.nextCwd;
|
|
330
|
+
renderLine();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
hd.lines.push(typedLine);
|
|
334
|
+
stream.write("> ");
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (ch >= " " || ch === "\t") {
|
|
338
|
+
lineBuffer += ch;
|
|
339
|
+
stream.write(ch);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
286
344
|
if (pendingSudo) {
|
|
287
345
|
const input = chunk.toString("utf8");
|
|
288
346
|
for (let i = 0; i < input.length; i += 1) {
|
|
@@ -407,6 +465,47 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
407
465
|
}
|
|
408
466
|
continue;
|
|
409
467
|
}
|
|
468
|
+
// Home: \x1b[1~ or \x1b[H
|
|
469
|
+
if (third === "1" && fourth === "~") {
|
|
470
|
+
i += 3;
|
|
471
|
+
cursorPos = 0;
|
|
472
|
+
renderLine();
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (third === "H") {
|
|
476
|
+
i += 2;
|
|
477
|
+
cursorPos = 0;
|
|
478
|
+
renderLine();
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
// End: \x1b[4~ or \x1b[F
|
|
482
|
+
if (third === "4" && fourth === "~") {
|
|
483
|
+
i += 3;
|
|
484
|
+
cursorPos = lineBuffer.length;
|
|
485
|
+
renderLine();
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (third === "F") {
|
|
489
|
+
i += 2;
|
|
490
|
+
cursorPos = lineBuffer.length;
|
|
491
|
+
renderLine();
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Home/End via \x1bO sequences (some terminals)
|
|
496
|
+
if (next === "O" && third) {
|
|
497
|
+
if (third === "H") {
|
|
498
|
+
i += 2;
|
|
499
|
+
cursorPos = 0;
|
|
500
|
+
renderLine();
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (third === "F") {
|
|
504
|
+
i += 2;
|
|
505
|
+
cursorPos = lineBuffer.length;
|
|
506
|
+
renderLine();
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
410
509
|
}
|
|
411
510
|
}
|
|
412
511
|
if (ch === "\u0003") {
|
|
@@ -418,13 +517,61 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
418
517
|
renderLine();
|
|
419
518
|
continue;
|
|
420
519
|
}
|
|
520
|
+
if (ch === "\u0001") {
|
|
521
|
+
cursorPos = 0;
|
|
522
|
+
renderLine();
|
|
523
|
+
continue;
|
|
524
|
+
} // Ctrl+A
|
|
525
|
+
if (ch === "\u0005") {
|
|
526
|
+
cursorPos = lineBuffer.length;
|
|
527
|
+
renderLine();
|
|
528
|
+
continue;
|
|
529
|
+
} // Ctrl+E
|
|
530
|
+
if (ch === "\u000b") {
|
|
531
|
+
lineBuffer = lineBuffer.slice(0, cursorPos);
|
|
532
|
+
renderLine();
|
|
533
|
+
continue;
|
|
534
|
+
} // Ctrl+K
|
|
535
|
+
if (ch === "\u0015") {
|
|
536
|
+
lineBuffer = lineBuffer.slice(cursorPos);
|
|
537
|
+
cursorPos = 0;
|
|
538
|
+
renderLine();
|
|
539
|
+
continue;
|
|
540
|
+
} // Ctrl+U
|
|
541
|
+
if (ch === "\u0017") { // Ctrl+W — kill word backward
|
|
542
|
+
let wStart = cursorPos;
|
|
543
|
+
while (wStart > 0 && lineBuffer[wStart - 1] === " ")
|
|
544
|
+
wStart--;
|
|
545
|
+
while (wStart > 0 && lineBuffer[wStart - 1] !== " ")
|
|
546
|
+
wStart--;
|
|
547
|
+
lineBuffer = lineBuffer.slice(0, wStart) + lineBuffer.slice(cursorPos);
|
|
548
|
+
cursorPos = wStart;
|
|
549
|
+
renderLine();
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
421
552
|
if (ch === "\r" || ch === "\n") {
|
|
422
|
-
|
|
553
|
+
let line = lineBuffer.trim();
|
|
423
554
|
lineBuffer = "";
|
|
424
555
|
cursorPos = 0;
|
|
425
556
|
historyIndex = null;
|
|
426
557
|
historyDraft = "";
|
|
427
558
|
stream.write("\r\n");
|
|
559
|
+
// !! history expansion
|
|
560
|
+
if (line === "!!" || line.startsWith("!! ") || /\s!!$/.test(line) || / !! /.test(line)) {
|
|
561
|
+
const lastCmd = history.length > 0 ? history[history.length - 1] : "";
|
|
562
|
+
line = line === "!!" ? lastCmd : line.replace(/!!/g, lastCmd);
|
|
563
|
+
}
|
|
564
|
+
else if (/(?:^|\s)!!/.test(line)) {
|
|
565
|
+
const lastCmd = history.length > 0 ? history[history.length - 1] : "";
|
|
566
|
+
line = line.replace(/!!/g, lastCmd);
|
|
567
|
+
}
|
|
568
|
+
// Heredoc detection: cmd << DELIM
|
|
569
|
+
const heredocMatch = line.match(/^(.*?)\s*<<-?\s*['"`]?([A-Za-z_][A-Za-z0-9_]*)['"`]?\s*$/);
|
|
570
|
+
if (heredocMatch && line.length > 0) {
|
|
571
|
+
pendingHeredoc = { delimiter: heredocMatch[2], lines: [], cmdBefore: heredocMatch[1].trim() || "cat" };
|
|
572
|
+
stream.write("> ");
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
428
575
|
if (line.length > 0) {
|
|
429
576
|
const result = await Promise.resolve(runCommand(line, authUser, hostname, "shell", cwd, shell, undefined, shellEnv));
|
|
430
577
|
pushHistory(line);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { evalArith } from "../utils/expand";
|
|
2
|
+
export const bcCommand = {
|
|
3
|
+
name: "bc",
|
|
4
|
+
description: "Arbitrary precision calculator language",
|
|
5
|
+
category: "system",
|
|
6
|
+
params: ["[expression]"],
|
|
7
|
+
run: ({ args, stdin }) => {
|
|
8
|
+
const input = (stdin ?? args.join(" ")).trim();
|
|
9
|
+
if (!input)
|
|
10
|
+
return { stdout: "", exitCode: 0 };
|
|
11
|
+
const results = [];
|
|
12
|
+
for (const line of input.split("\n")) {
|
|
13
|
+
const expr = line.trim();
|
|
14
|
+
if (!expr || expr.startsWith("#"))
|
|
15
|
+
continue;
|
|
16
|
+
// Strip trailing semicolons
|
|
17
|
+
const cleaned = expr.replace(/;+$/, "").trim();
|
|
18
|
+
const val = evalArith(cleaned, {});
|
|
19
|
+
if (!Number.isNaN(val)) {
|
|
20
|
+
results.push(String(val));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
return { stderr: `bc: syntax error on line: ${expr}`, exitCode: 1 };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { stdout: results.join("\n"), exitCode: 0 };
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -21,8 +21,8 @@ const LINK_OUTPUT = `1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state
|
|
|
21
21
|
* @category network
|
|
22
22
|
* @params ["<object> <command>"]
|
|
23
23
|
*/
|
|
24
|
-
export const
|
|
25
|
-
name: "
|
|
24
|
+
export const ipCommand = {
|
|
25
|
+
name: "ip",
|
|
26
26
|
description: "Show/manipulate routing, network devices, interfaces",
|
|
27
27
|
category: "network",
|
|
28
28
|
params: ["<object> <command>"],
|
|
@@ -30,7 +30,7 @@ export const ifconfigCommand = {
|
|
|
30
30
|
const obj = args[0]?.toLowerCase();
|
|
31
31
|
const cmd = args[1]?.toLowerCase() ?? "show";
|
|
32
32
|
if (!obj) {
|
|
33
|
-
return { stderr: "Usage:
|
|
33
|
+
return { stderr: "Usage: ip [ OPTIONS ] OBJECT { COMMAND | help }\nOBJECT := { link | addr | route | neigh }", exitCode: 1 };
|
|
34
34
|
}
|
|
35
35
|
if (obj === "addr" || obj === "address" || obj === "a") {
|
|
36
36
|
return { stdout: ADDR_OUTPUT, exitCode: 0 };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const jobsCommand = {
|
|
2
|
+
name: "jobs",
|
|
3
|
+
description: "List active jobs",
|
|
4
|
+
category: "shell",
|
|
5
|
+
params: [],
|
|
6
|
+
run: () => ({ stdout: "", exitCode: 0 }),
|
|
7
|
+
};
|
|
8
|
+
export const bgCommand = {
|
|
9
|
+
name: "bg",
|
|
10
|
+
description: "Resume a suspended job in the background",
|
|
11
|
+
category: "shell",
|
|
12
|
+
params: ["[%jobspec]"],
|
|
13
|
+
run: ({ args }) => ({
|
|
14
|
+
stderr: `bg: ${args[0] ?? "%1"}: no such job`,
|
|
15
|
+
exitCode: 1,
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
export const fgCommand = {
|
|
19
|
+
name: "fg",
|
|
20
|
+
description: "Resume a suspended job in the foreground",
|
|
21
|
+
category: "shell",
|
|
22
|
+
params: ["[%jobspec]"],
|
|
23
|
+
run: ({ args }) => ({
|
|
24
|
+
stderr: `fg: ${args[0] ?? "%1"}: no such job`,
|
|
25
|
+
exitCode: 1,
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
@@ -4,6 +4,7 @@ import { aptCacheCommand, aptCommand } from "./apt";
|
|
|
4
4
|
import { awkCommand } from "./awk";
|
|
5
5
|
import { base64Command } from "./base64";
|
|
6
6
|
import { basenameCommand, dirnameCommand } from "./basename";
|
|
7
|
+
import { bcCommand } from "./bc";
|
|
7
8
|
import { catCommand } from "./cat";
|
|
8
9
|
import { cdCommand } from "./cd";
|
|
9
10
|
import { chmodCommand } from "./chmod";
|
|
@@ -35,7 +36,8 @@ import { historyCommand } from "./history";
|
|
|
35
36
|
import { hostnameCommand } from "./hostname";
|
|
36
37
|
import { htopCommand } from "./htop";
|
|
37
38
|
import { idCommand } from "./id";
|
|
38
|
-
import {
|
|
39
|
+
import { ipCommand } from "./ip";
|
|
40
|
+
import { bgCommand, fgCommand, jobsCommand } from "./jobs";
|
|
39
41
|
import { killCommand } from "./kill";
|
|
40
42
|
import { dmesgCommand, lastCommand } from "./last";
|
|
41
43
|
import { lnCommand, readlinkCommand } from "./ln";
|
|
@@ -81,13 +83,13 @@ import { unameCommand } from "./uname";
|
|
|
81
83
|
import { uniqCommand } from "./uniq";
|
|
82
84
|
import { unsetCommand } from "./unset";
|
|
83
85
|
import { uptimeCommand } from "./uptime";
|
|
86
|
+
import { wCommand } from "./w";
|
|
84
87
|
import { wcCommand } from "./wc";
|
|
85
88
|
import { wgetCommand } from "./wget";
|
|
86
89
|
import { whichCommand } from "./which";
|
|
87
90
|
import { whoCommand } from "./who";
|
|
88
91
|
import { whoamiCommand } from "./whoami";
|
|
89
92
|
import { xargsCommand } from "./xargs";
|
|
90
|
-
import { wCommand } from "./w";
|
|
91
93
|
const BASE_COMMANDS = [
|
|
92
94
|
// Navigation
|
|
93
95
|
pwdCommand,
|
|
@@ -159,7 +161,7 @@ const BASE_COMMANDS = [
|
|
|
159
161
|
sttyCommand,
|
|
160
162
|
lastCommand,
|
|
161
163
|
dmesgCommand,
|
|
162
|
-
|
|
164
|
+
ipCommand,
|
|
163
165
|
yesCommand,
|
|
164
166
|
fortuneCommand,
|
|
165
167
|
cowsayCommand,
|
|
@@ -184,6 +186,10 @@ const BASE_COMMANDS = [
|
|
|
184
186
|
dpkgCommand,
|
|
185
187
|
dpkgQueryCommand,
|
|
186
188
|
// Shell (extended)
|
|
189
|
+
jobsCommand,
|
|
190
|
+
bgCommand,
|
|
191
|
+
fgCommand,
|
|
192
|
+
bcCommand,
|
|
187
193
|
whichCommand,
|
|
188
194
|
typeCommand,
|
|
189
195
|
manCommand,
|
package/dist/commands/runtime.js
CHANGED
|
@@ -15,10 +15,11 @@ export function makeDefaultEnv(authUser, hostname) {
|
|
|
15
15
|
HOME: userHome(authUser),
|
|
16
16
|
USER: authUser,
|
|
17
17
|
LOGNAME: authUser,
|
|
18
|
-
SHELL: "/bin/
|
|
18
|
+
SHELL: "/bin/bash",
|
|
19
19
|
TERM: "xterm-256color",
|
|
20
20
|
HOSTNAME: hostname,
|
|
21
21
|
PS1: "\\u@\\h:\\w\\$ ",
|
|
22
|
+
"0": "/bin/bash",
|
|
22
23
|
},
|
|
23
24
|
lastExitCode: 0,
|
|
24
25
|
};
|
package/dist/commands/set.js
CHANGED
|
@@ -11,11 +11,31 @@ export const setCommand = {
|
|
|
11
11
|
run: ({ args, env }) => {
|
|
12
12
|
if (args.length === 0) {
|
|
13
13
|
const out = Object.entries(env.vars)
|
|
14
|
+
.filter(([k]) => !k.startsWith("__"))
|
|
14
15
|
.map(([k, v]) => `${k}=${v}`)
|
|
15
16
|
.join("\n");
|
|
16
17
|
return { stdout: out, exitCode: 0 };
|
|
17
18
|
}
|
|
18
19
|
for (const arg of args) {
|
|
20
|
+
const flagMatch = arg.match(/^([+-])([a-zA-Z]+)$/);
|
|
21
|
+
if (flagMatch) {
|
|
22
|
+
const on = flagMatch[1] === "-";
|
|
23
|
+
for (const flag of flagMatch[2]) {
|
|
24
|
+
if (flag === "e") {
|
|
25
|
+
if (on)
|
|
26
|
+
env.vars.__errexit = "1";
|
|
27
|
+
else
|
|
28
|
+
delete env.vars.__errexit;
|
|
29
|
+
}
|
|
30
|
+
if (flag === "x") {
|
|
31
|
+
if (on)
|
|
32
|
+
env.vars.__xtrace = "1";
|
|
33
|
+
else
|
|
34
|
+
delete env.vars.__xtrace;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
19
39
|
if (arg.includes("=")) {
|
|
20
40
|
const eq = arg.indexOf("=");
|
|
21
41
|
env.vars[arg.slice(0, eq)] = arg.slice(eq + 1);
|
package/dist/commands/sh.js
CHANGED
|
@@ -125,6 +125,43 @@ function parseBlocks(lines) {
|
|
|
125
125
|
}
|
|
126
126
|
blocks.push({ type: "while", cond, body });
|
|
127
127
|
}
|
|
128
|
+
else if (line.startsWith("case ") && line.endsWith(" in") || line.match(/^case\s+.+\s+in$/)) {
|
|
129
|
+
const caseExpr = line.replace(/^case\s+/, "").replace(/\s+in$/, "").trim();
|
|
130
|
+
const patterns = [];
|
|
131
|
+
i++;
|
|
132
|
+
while (i < lines.length && lines[i]?.trim() !== "esac") {
|
|
133
|
+
const pl = lines[i].trim();
|
|
134
|
+
if (!pl || pl === "esac") {
|
|
135
|
+
i++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// pattern) or pattern1|pattern2)
|
|
139
|
+
const patMatch = pl.match(/^(.+?)\)\s*(.*)$/);
|
|
140
|
+
if (patMatch) {
|
|
141
|
+
const pat = patMatch[1].trim();
|
|
142
|
+
const body = [];
|
|
143
|
+
if (patMatch[2]?.trim() && patMatch[2].trim() !== ";;") {
|
|
144
|
+
body.push(patMatch[2].trim());
|
|
145
|
+
}
|
|
146
|
+
i++;
|
|
147
|
+
while (i < lines.length) {
|
|
148
|
+
const bl = lines[i].trim();
|
|
149
|
+
if (bl === ";;" || bl === "esac")
|
|
150
|
+
break;
|
|
151
|
+
if (bl)
|
|
152
|
+
body.push(bl);
|
|
153
|
+
i++;
|
|
154
|
+
}
|
|
155
|
+
if (lines[i]?.trim() === ";;")
|
|
156
|
+
i++; // skip ;;
|
|
157
|
+
patterns.push({ pattern: pat, body });
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
blocks.push({ type: "case", expr: caseExpr, patterns });
|
|
164
|
+
}
|
|
128
165
|
else {
|
|
129
166
|
blocks.push({ type: "cmd", line });
|
|
130
167
|
}
|
|
@@ -189,9 +226,12 @@ async function evalCondition(cond, ctx) {
|
|
|
189
226
|
async function runBlocks(blocks, ctx) {
|
|
190
227
|
let lastResult = { exitCode: 0 };
|
|
191
228
|
let output = "";
|
|
229
|
+
let traceOutput = "";
|
|
192
230
|
for (const block of blocks) {
|
|
193
231
|
if (block.type === "cmd") {
|
|
194
232
|
const expanded = await expandVars(block.line, ctx.env.vars, ctx.env.lastExitCode, ctx);
|
|
233
|
+
if (ctx.env.vars.__xtrace)
|
|
234
|
+
traceOutput += `+ ${expanded}\n`;
|
|
195
235
|
// Bare VAR=val assignment(s) — handle before dispatching to runCommand
|
|
196
236
|
const assignRe = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)/;
|
|
197
237
|
const tokens = expanded.trim().split(/\s+/);
|
|
@@ -231,6 +271,8 @@ async function runBlocks(blocks, ctx) {
|
|
|
231
271
|
output += `${r.stdout}\n`;
|
|
232
272
|
if (r.stderr)
|
|
233
273
|
return { ...r, stdout: output.trim() };
|
|
274
|
+
if (ctx.env.vars.__errexit && (r.exitCode ?? 0) !== 0)
|
|
275
|
+
return { ...r, stdout: output.trim() };
|
|
234
276
|
lastResult = r;
|
|
235
277
|
}
|
|
236
278
|
else if (block.type === "if") {
|
|
@@ -311,8 +353,34 @@ async function runBlocks(blocks, ctx) {
|
|
|
311
353
|
iterations++;
|
|
312
354
|
}
|
|
313
355
|
}
|
|
356
|
+
else if (block.type === "case") {
|
|
357
|
+
const expanded = await expandVars(block.expr, ctx.env.vars, ctx.env.lastExitCode, ctx);
|
|
358
|
+
for (const pat of block.patterns) {
|
|
359
|
+
const alts = pat.pattern.split("|").map((p) => p.trim());
|
|
360
|
+
const matched = alts.some((p) => {
|
|
361
|
+
if (p === "*")
|
|
362
|
+
return true;
|
|
363
|
+
if (p.includes("*") || p.includes("?")) {
|
|
364
|
+
const re = new RegExp(`^${p.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
|
|
365
|
+
return re.test(expanded);
|
|
366
|
+
}
|
|
367
|
+
return p === expanded;
|
|
368
|
+
});
|
|
369
|
+
if (matched) {
|
|
370
|
+
const sub = await runBlocks(parseBlocks(pat.body), ctx);
|
|
371
|
+
if (sub.stdout)
|
|
372
|
+
output += `${sub.stdout}\n`;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const finalStdout = output.trim() || lastResult.stdout;
|
|
379
|
+
if (traceOutput) {
|
|
380
|
+
const traceStderr = (lastResult.stderr ? `${lastResult.stderr}\n` : "") + traceOutput.trim();
|
|
381
|
+
return { ...lastResult, stdout: finalStdout, stderr: traceStderr || lastResult.stderr };
|
|
314
382
|
}
|
|
315
|
-
return { ...lastResult, stdout:
|
|
383
|
+
return { ...lastResult, stdout: finalStdout };
|
|
316
384
|
}
|
|
317
385
|
/**
|
|
318
386
|
* Execute shell scripts or commands with a minimal shell interpreter.
|
package/dist/utils/expand.js
CHANGED
|
@@ -369,10 +369,12 @@ export function expandSync(input, env, lastExit = 0, home) {
|
|
|
369
369
|
let s = chunk;
|
|
370
370
|
// Tilde expansion — only at start of token or after `:` or whitespace
|
|
371
371
|
s = s.replace(/(^|[\s:])~(\/|$)/g, (_, pre, post) => `${pre}${homePath}${post}`);
|
|
372
|
-
// $? $$ $#
|
|
372
|
+
// $? $$ $# $RANDOM $LINENO
|
|
373
373
|
s = s.replace(/\$\?/g, String(lastExit));
|
|
374
374
|
s = s.replace(/\$\$/g, "1");
|
|
375
375
|
s = s.replace(/\$#/g, "0");
|
|
376
|
+
s = s.replace(/\$RANDOM\b/g, () => String(Math.floor(Math.random() * 32768)));
|
|
377
|
+
s = s.replace(/\$LINENO\b/g, "1");
|
|
376
378
|
// $(( arithmetic )) — must come before ${ and $VAR to avoid conflicts
|
|
377
379
|
s = expandArithmeticChunks(s, env);
|
|
378
380
|
// ${#VAR} — string length
|