typescript-virtual-container 1.5.5 → 1.5.7

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.
Files changed (64) hide show
  1. package/README.md +117 -35
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/SSHMimic/index.d.ts +5 -1
  4. package/dist/SSHMimic/index.js +27 -3
  5. package/dist/SSHMimic/scp.d.ts +34 -0
  6. package/dist/SSHMimic/scp.js +285 -0
  7. package/dist/SSHMimic/sftp.d.ts +53 -3
  8. package/dist/SSHMimic/sftp.js +9 -3
  9. package/dist/VirtualFileSystem/binaryPack.d.ts +7 -0
  10. package/dist/VirtualFileSystem/binaryPack.js +37 -1
  11. package/dist/VirtualFileSystem/index.d.ts +7 -0
  12. package/dist/VirtualFileSystem/index.js +67 -27
  13. package/dist/VirtualFileSystem/internalTypes.d.ts +2 -0
  14. package/dist/VirtualFileSystem/path.d.ts +5 -0
  15. package/dist/VirtualFileSystem/path.js +24 -11
  16. package/dist/VirtualPackageManager/index.d.ts +4 -2
  17. package/dist/VirtualPackageManager/index.js +24 -4
  18. package/dist/VirtualShell/index.d.ts +4 -0
  19. package/dist/VirtualShell/index.js +1 -7
  20. package/dist/VirtualShell/shell.js +40 -10
  21. package/dist/VirtualShell/shellParser.js +1 -22
  22. package/dist/commands/awk.d.ts +6 -11
  23. package/dist/commands/awk.js +462 -109
  24. package/dist/commands/bzip2.d.ts +11 -0
  25. package/dist/commands/bzip2.js +91 -0
  26. package/dist/commands/exit.js +1 -1
  27. package/dist/commands/find.d.ts +2 -2
  28. package/dist/commands/find.js +209 -37
  29. package/dist/commands/helpers.d.ts +0 -20
  30. package/dist/commands/helpers.js +0 -97
  31. package/dist/commands/lsof.d.ts +6 -0
  32. package/dist/commands/lsof.js +30 -0
  33. package/dist/commands/perl.d.ts +6 -0
  34. package/dist/commands/perl.js +76 -0
  35. package/dist/commands/python.js +5 -2
  36. package/dist/commands/registry.js +19 -1
  37. package/dist/commands/runtime.js +65 -87
  38. package/dist/commands/sed.d.ts +2 -2
  39. package/dist/commands/sed.js +216 -34
  40. package/dist/commands/sh.js +42 -0
  41. package/dist/commands/strace.d.ts +6 -0
  42. package/dist/commands/strace.js +26 -0
  43. package/dist/commands/tar.d.ts +2 -1
  44. package/dist/commands/tar.js +138 -52
  45. package/dist/commands/test.js +2 -2
  46. package/dist/commands/zip.d.ts +11 -0
  47. package/dist/commands/zip.js +232 -0
  48. package/dist/modules/linuxRootfs.js +1 -4
  49. package/dist/modules/neofetch.js +2 -2
  50. package/dist/types/commands.d.ts +4 -0
  51. package/dist/utils/argv.d.ts +6 -0
  52. package/dist/utils/argv.js +32 -0
  53. package/dist/utils/expand.d.ts +5 -2
  54. package/dist/utils/expand.js +112 -45
  55. package/dist/utils/glob.d.ts +6 -0
  56. package/dist/utils/glob.js +34 -0
  57. package/dist/utils/tokenize.js +13 -13
  58. package/package.json +9 -7
  59. package/dist/self-standalone.d.ts +0 -1
  60. package/dist/self-standalone.js +0 -444
  61. package/dist/standalone-wo-sftp.d.ts +0 -1
  62. package/dist/standalone-wo-sftp.js +0 -30
  63. package/dist/standalone.d.ts +0 -1
  64. package/dist/standalone.js +0 -61
@@ -5,6 +5,11 @@ import { awkCommand } from "./awk";
5
5
  import { base64Command } from "./base64";
6
6
  import { basenameCommand, dirnameCommand } from "./basename";
7
7
  import { bcCommand } from "./bc";
8
+ import { bunzip2Command, bzip2Command } from "./bzip2";
9
+ import { lsofCommand } from "./lsof";
10
+ import { perlCommand } from "./perl";
11
+ import { straceCommand } from "./strace";
12
+ import { unzipCommand, zipCommand } from "./zip";
8
13
  import { catCommand } from "./cat";
9
14
  import { cdCommand } from "./cd";
10
15
  import { chmodCommand } from "./chmod";
@@ -127,6 +132,10 @@ const BASE_COMMANDS = [
127
132
  tarCommand,
128
133
  gzipCommand,
129
134
  gunzipCommand,
135
+ zipCommand,
136
+ unzipCommand,
137
+ bzip2Command,
138
+ bunzip2Command,
130
139
  base64Command,
131
140
  // System info
132
141
  whoamiCommand,
@@ -214,6 +223,10 @@ const BASE_COMMANDS = [
214
223
  uptimeCommand,
215
224
  freeCommand,
216
225
  lsbReleaseCommand,
226
+ lsofCommand,
227
+ straceCommand,
228
+ // Scripting
229
+ perlCommand,
217
230
  ];
218
231
  const customCommands = [];
219
232
  const commandRegistry = new Map();
@@ -242,7 +255,12 @@ export function registerCommand(module) {
242
255
  throw new Error("Command names must be non-empty and contain no spaces");
243
256
  }
244
257
  customCommands.push(normalized);
245
- buildCache();
258
+ // Incremental insert — avoids full Map rebuild for every registerCommand call
259
+ commandRegistry.set(normalized.name, normalized);
260
+ for (const alias of normalized.aliases ?? [])
261
+ commandRegistry.set(alias, normalized);
262
+ // Invalidate sorted names cache; rebuilt lazily on next getCommandNames()
263
+ cachedCommandNames = null;
246
264
  }
247
265
  export function createCustomCommand(name, params, run) {
248
266
  return { name, params, run };
@@ -4,6 +4,16 @@ import { parseScript } from "../VirtualShell/shellParser";
4
4
  import { expandAsync, expandBraces, expandGlob } from "../utils/expand";
5
5
  import { tokenizeCommand } from "../utils/tokenize";
6
6
  import { resolveModule } from "./registry";
7
+ // Module-level compiled regexes — avoids recompilation on every runCommand call
8
+ const ASSIGN_RE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
9
+ const RE_FOR = /\bfor\s+\w+\s+in\b/;
10
+ const RE_WHILE = /\bwhile\s+/;
11
+ const RE_IF = /\bif\s+/;
12
+ const RE_FUNC_BRACE = /\w+\s*\(\s*\)\s*\{/;
13
+ const RE_FUNC_KW = /\bfunction\s+\w+/;
14
+ const RE_ARITH = /\(\(\s*.+\s*\)\)/;
15
+ const RE_PIPE = /(?<![|&])[|](?![|])/;
16
+ const RE_OPERATORS = /[><;&]|\|\|/;
7
17
  /** Returns the home directory path for a given user. Root lives at /root. */
8
18
  export function userHome(authUser) {
9
19
  return authUser === "root" ? "/root" : `/home/${authUser}`;
@@ -43,7 +53,13 @@ function resolveVfsBinary(name, env, shell, authUser) {
43
53
  return null;
44
54
  }
45
55
  }
46
- const pathDirs = (env.vars.PATH ?? "/usr/local/bin:/usr/bin:/bin").split(":");
56
+ const rawPath = env.vars.PATH ?? "/usr/local/bin:/usr/bin:/bin";
57
+ // Cache split PATH on the env object to avoid re-splitting on every binary lookup
58
+ if (!env._pathDirs || env._pathRaw !== rawPath) {
59
+ env._pathRaw = rawPath;
60
+ env._pathDirs = rawPath.split(":");
61
+ }
62
+ const pathDirs = env._pathDirs;
47
63
  for (const dir of pathDirs) {
48
64
  if ((dir === "/sbin" || dir === "/usr/sbin") && authUser !== "root")
49
65
  continue;
@@ -63,6 +79,35 @@ function resolveVfsBinary(name, env, shell, authUser) {
63
79
  return null;
64
80
  }
65
81
  const MAX_CALL_DEPTH = 8;
82
+ /** Run a VFS stub file as a command, handling `exec builtin <name>` and `sh -c` stubs. */
83
+ async function runVfsStub(vfsBinary, cmdName, args, rawInput, authUser, hostname, mode, cwd, shell, env, stdin) {
84
+ const stubContent = shell.vfs.readFile(vfsBinary);
85
+ const builtinMatch = stubContent.match(/exec\s+builtin\s+(\S+)/);
86
+ if (builtinMatch) {
87
+ const builtinMod = resolveModule(builtinMatch[1]);
88
+ if (builtinMod) {
89
+ return builtinMod.run({
90
+ authUser, hostname,
91
+ activeSessions: shell.users.listActiveSessions(),
92
+ rawInput, mode, args, stdin, cwd, shell, env,
93
+ });
94
+ }
95
+ // Guard: missing builtin — stop here to avoid sh -c infinite loop
96
+ return { stderr: `${cmdName}: exec builtin '${builtinMatch[1]}' not found`, exitCode: 127 };
97
+ }
98
+ const shMod = resolveModule("sh");
99
+ if (shMod) {
100
+ return shMod.run({
101
+ authUser, hostname,
102
+ activeSessions: shell.users.listActiveSessions(),
103
+ rawInput: `sh -c ${JSON.stringify(stubContent)}`,
104
+ mode,
105
+ args: ["-c", stubContent, "--", ...args],
106
+ stdin, cwd, shell, env,
107
+ });
108
+ }
109
+ return { stderr: `${cmdName}: command not found`, exitCode: 127 };
110
+ }
66
111
  let _callDepth = 0;
67
112
  export async function runCommandDirect(name, args, authUser, hostname, mode, cwd, shell, stdin, env) {
68
113
  // Anti-loop guard: track call depth via env to avoid infinite recursion
@@ -81,7 +126,7 @@ export async function runCommandDirect(name, args, authUser, hostname, mode, cwd
81
126
  }
82
127
  }
83
128
  async function _runCommandDirectInner(name, args, authUser, hostname, mode, cwd, shell, stdin, env) {
84
- const assignRe = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
129
+ const assignRe = ASSIGN_RE;
85
130
  const invocation = [name, ...args];
86
131
  let assignCount = 0;
87
132
  while (assignCount < invocation.length && assignRe.test(invocation[assignCount])) {
@@ -119,42 +164,7 @@ async function _runCommandDirectInner(name, args, authUser, hostname, mode, cwd,
119
164
  if (!mod) {
120
165
  const vfsBinary = resolveVfsBinary(name, env, shell, authUser);
121
166
  if (vfsBinary) {
122
- const stubContent = shell.vfs.readFile(vfsBinary);
123
- const builtinMatch = stubContent.match(/exec\s+builtin\s+(\S+)/);
124
- if (builtinMatch) {
125
- const builtinMod = resolveModule(builtinMatch[1]);
126
- if (builtinMod) {
127
- return await builtinMod.run({
128
- authUser,
129
- hostname,
130
- activeSessions: shell.users.listActiveSessions(),
131
- rawInput: [name, ...args].join(" "),
132
- mode,
133
- args,
134
- stdin,
135
- cwd,
136
- shell,
137
- env,
138
- });
139
- }
140
- // builtin not found — stop here, don't fall through to sh -c (avoids infinite loop)
141
- return { stderr: `${name}: exec builtin '${builtinMatch[1]}' not found`, exitCode: 127 };
142
- }
143
- const shMod = resolveModule("sh");
144
- if (shMod) {
145
- return await shMod.run({
146
- authUser,
147
- hostname,
148
- activeSessions: shell.users.listActiveSessions(),
149
- rawInput: `sh -c ${JSON.stringify(stubContent)}`,
150
- mode,
151
- args: ["-c", stubContent, "--", ...args],
152
- stdin,
153
- cwd,
154
- shell,
155
- env,
156
- });
157
- }
167
+ return runVfsStub(vfsBinary, name, args, [name, ...args].join(" "), authUser, hostname, mode, cwd, shell, env, stdin);
158
168
  }
159
169
  return { stderr: `${name}: command not found`, exitCode: 127 };
160
170
  }
@@ -220,18 +230,13 @@ export async function runCommand(rawInput, authUser, hostname, mode, cwd, shell,
220
230
  ? trimmed.replace(rawFirstWord, aliasVal)
221
231
  : trimmed;
222
232
  // Detect sh-syntax constructs that must be handled by the sh interpreter
223
- const isShScript = /\bfor\s+\w+\s+in\b/.test(aliasExpanded) ||
224
- /\bwhile\s+/.test(aliasExpanded) ||
225
- /\bif\s+/.test(aliasExpanded) ||
226
- /\w+\s*\(\s*\)\s*\{/.test(aliasExpanded) ||
227
- /\bfunction\s+\w+/.test(aliasExpanded) ||
228
- /\(\(\s*.+\s*\)\)/.test(aliasExpanded);
229
- const hasOperators = /(?<![|&])[|](?![|])/.test(aliasExpanded) ||
230
- aliasExpanded.includes(">") ||
231
- aliasExpanded.includes("<") ||
232
- aliasExpanded.includes("&&") ||
233
- aliasExpanded.includes("||") ||
234
- aliasExpanded.includes(";");
233
+ const isShScript = RE_FOR.test(aliasExpanded) ||
234
+ RE_WHILE.test(aliasExpanded) ||
235
+ RE_IF.test(aliasExpanded) ||
236
+ RE_FUNC_BRACE.test(aliasExpanded) ||
237
+ RE_FUNC_KW.test(aliasExpanded) ||
238
+ RE_ARITH.test(aliasExpanded);
239
+ const hasOperators = RE_PIPE.test(aliasExpanded) || RE_OPERATORS.test(aliasExpanded);
235
240
  if ((isShScript && rawFirstWord !== "sh" && rawFirstWord !== "bash") || hasOperators) {
236
241
  // sh-syntax: route through sh interpreter to handle for/while/functions
237
242
  if (isShScript && rawFirstWord !== "sh" && rawFirstWord !== "bash") {
@@ -267,52 +272,25 @@ export async function runCommand(rawInput, authUser, hostname, mode, cwd, shell,
267
272
  const parts = tokenizeCommand(expanded.trim());
268
273
  if (parts.length === 0)
269
274
  return { exitCode: 0 };
270
- const assignRe = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
275
+ const assignRe = ASSIGN_RE;
271
276
  if (assignRe.test(parts[0])) {
272
277
  return runCommandDirect(parts[0], parts.slice(1), authUser, hostname, mode, cwd, shell, stdin, shellEnv);
273
278
  }
274
279
  const commandName = parts[0]?.toLowerCase() ?? "";
275
280
  // Apply brace expansion to each arg token
276
- const args = parts.slice(1).flatMap(expandBraces).flatMap(token => expandGlob(token, cwd, shell.vfs));
281
+ const rawArgs = parts.slice(1);
282
+ const args = [];
283
+ for (const token of rawArgs) {
284
+ for (const brace of expandBraces(token)) {
285
+ for (const glob of expandGlob(brace, cwd, shell.vfs))
286
+ args.push(glob);
287
+ }
288
+ }
277
289
  const mod = resolveModule(commandName);
278
290
  if (!mod) {
279
291
  const vfsBinary = resolveVfsBinary(commandName, shellEnv, shell, authUser);
280
292
  if (vfsBinary) {
281
- const stubContent = shell.vfs.readFile(vfsBinary);
282
- const builtinMatch = stubContent.match(/exec\s+builtin\s+(\S+)/);
283
- if (builtinMatch) {
284
- const builtinName = builtinMatch[1];
285
- const builtinMod = resolveModule(builtinName);
286
- if (builtinMod) {
287
- return await builtinMod.run({
288
- authUser,
289
- hostname,
290
- activeSessions: shell.users.listActiveSessions(),
291
- rawInput: [commandName, ...args].join(" "),
292
- mode,
293
- args,
294
- stdin,
295
- cwd,
296
- shell,
297
- env: shellEnv,
298
- });
299
- }
300
- }
301
- const shMod = resolveModule("sh");
302
- if (shMod) {
303
- return await shMod.run({
304
- authUser,
305
- hostname,
306
- activeSessions: shell.users.listActiveSessions(),
307
- rawInput: `sh -c ${JSON.stringify(stubContent)}`,
308
- mode,
309
- args: ["-c", stubContent, "--", ...args],
310
- stdin,
311
- cwd,
312
- shell,
313
- env: shellEnv,
314
- });
315
- }
293
+ return runVfsStub(vfsBinary, commandName, args, expanded, authUser, hostname, mode, cwd, shell, shellEnv, stdin);
316
294
  }
317
295
  return { stderr: `${commandName}: command not found`, exitCode: 127 };
318
296
  }
@@ -1,7 +1,7 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  /**
3
- * Stream editor for filtering and transforming text lines.
3
+ * Stream editor supports s/pat/rep/[gI], d, p, q, =, addresses (N, /re/, N,M, /re/,/re/, $).
4
4
  * @category text
5
- * @params ["-e <expr> [file]", "s/pattern/replace/[g]"]
5
+ * @params ["[-n] [-e <expr>] [file]"]
6
6
  */
7
7
  export declare const sedCommand: ShellModule;
@@ -1,22 +1,74 @@
1
- import { getFlag, ifFlag } from "./command-helpers";
1
+ import { ifFlag } from "./command-helpers";
2
2
  import { resolvePath } from "./helpers";
3
3
  /**
4
- * Stream editor for filtering and transforming text lines.
4
+ * Stream editor supports s/pat/rep/[gI], d, p, q, =, addresses (N, /re/, N,M, /re/,/re/, $).
5
5
  * @category text
6
- * @params ["-e <expr> [file]", "s/pattern/replace/[g]"]
6
+ * @params ["[-n] [-e <expr>] [file]"]
7
7
  */
8
8
  export const sedCommand = {
9
9
  name: "sed",
10
10
  description: "Stream editor for filtering and transforming text",
11
11
  category: "text",
12
- params: ["-e <expr> [file]", "s/pattern/replace/[g]"],
12
+ params: ["[-n] [-e <expr>] [file]"],
13
13
  run: ({ authUser, shell, cwd, args, stdin }) => {
14
14
  const inPlace = ifFlag(args, ["-i"]);
15
- const expr = getFlag(args, ["-e"]) ??
16
- args.find((a) => !a.startsWith("-"));
17
- const fileArg = args.filter((a) => !a.startsWith("-") && a !== expr).pop();
18
- if (!expr)
15
+ const suppressAuto = ifFlag(args, ["-n"]);
16
+ // Collect all -e expressions and the first non-flag positional
17
+ const exprs = [];
18
+ let fileArg;
19
+ let i = 0;
20
+ while (i < args.length) {
21
+ const a = args[i];
22
+ if (a === "-e" || a === "--expression") {
23
+ i++;
24
+ if (args[i])
25
+ exprs.push(args[i]);
26
+ i++;
27
+ }
28
+ else if (a === "-n" || a === "-i") {
29
+ i++;
30
+ }
31
+ else if (a.startsWith("-e")) {
32
+ exprs.push(a.slice(2));
33
+ i++;
34
+ }
35
+ else if (!a.startsWith("-")) {
36
+ if (exprs.length === 0)
37
+ exprs.push(a);
38
+ else
39
+ fileArg = a;
40
+ i++;
41
+ }
42
+ else {
43
+ i++;
44
+ }
45
+ }
46
+ // If only one positional collected as expr and no file yet, check for file after
47
+ // Re-parse: first non-flag that follows all -e is the file
48
+ if (exprs.length === 0)
19
49
  return { stderr: "sed: no expression", exitCode: 1 };
50
+ // Re-check: if exprs[0] was set from positional, remaining positionals are files
51
+ {
52
+ let foundExprFromFlag = false;
53
+ let j = 0;
54
+ while (j < args.length) {
55
+ const a = args[j];
56
+ if (a === "-e" || a === "--expression") {
57
+ foundExprFromFlag = true;
58
+ j += 2;
59
+ }
60
+ else if (a.startsWith("-e")) {
61
+ foundExprFromFlag = true;
62
+ j++;
63
+ }
64
+ else
65
+ j++;
66
+ }
67
+ if (!foundExprFromFlag) {
68
+ // expr is first positional, file is second
69
+ fileArg = args.filter((a) => !a.startsWith("-")).slice(1)[0];
70
+ }
71
+ }
20
72
  let content = stdin ?? "";
21
73
  if (fileArg) {
22
74
  const p = resolvePath(cwd, fileArg);
@@ -24,32 +76,162 @@ export const sedCommand = {
24
76
  content = shell.vfs.readFile(p);
25
77
  }
26
78
  catch {
27
- return {
28
- stderr: `sed: ${fileArg}: No such file or directory`,
29
- exitCode: 1,
30
- };
31
- }
32
- }
33
- // Parse s/from/to/[g]
34
- const sMatch = expr.match(/^s([^a-zA-Z0-9])(.+?)\1(.*?)\1([gi]*)$/);
35
- if (!sMatch)
36
- return { stderr: `sed: unrecognized command: ${expr}`, exitCode: 1 };
37
- const [, , from, to, flags] = sMatch;
38
- const regexFlags = (flags ?? "").includes("i")
39
- ? "gi"
40
- : (flags ?? "").includes("g")
41
- ? "g"
42
- : "";
43
- let regex;
44
- try {
45
- regex = new RegExp(from, regexFlags || "");
46
- }
47
- catch (_e) {
48
- return { stderr: `sed: invalid regex: ${from}`, exitCode: 1 };
49
- }
50
- const result = (flags ?? "").includes("g") || regexFlags.includes("g")
51
- ? content.replace(regex, to ?? "")
52
- : content.replace(regex, to ?? "");
79
+ return { stderr: `sed: ${fileArg}: No such file or directory`, exitCode: 1 };
80
+ }
81
+ }
82
+ function parseAddr(s) {
83
+ if (!s)
84
+ return [undefined, s];
85
+ if (s[0] === "$")
86
+ return [{ type: "last" }, s.slice(1)];
87
+ if (/^\d/.test(s)) {
88
+ const m = s.match(/^(\d+)(.*)/s);
89
+ if (m)
90
+ return [{ type: "line", n: parseInt(m[1], 10) }, m[2]];
91
+ }
92
+ if (s[0] === "/") {
93
+ const end = s.indexOf("/", 1);
94
+ if (end !== -1) {
95
+ try {
96
+ const re = new RegExp(s.slice(1, end));
97
+ return [{ type: "regex", re }, s.slice(end + 1)];
98
+ }
99
+ catch { /* bad regex */ }
100
+ }
101
+ }
102
+ return [undefined, s];
103
+ }
104
+ function parseInstrs(expr) {
105
+ const instrs = [];
106
+ // Split on unquoted semicolons or newlines
107
+ const parts = expr.split(/\n|(?<=^|[^\\]);/);
108
+ for (const raw of parts) {
109
+ const part = raw.trim();
110
+ if (!part || part.startsWith("#"))
111
+ continue;
112
+ let rest = part;
113
+ const [addr1, after1] = parseAddr(rest);
114
+ rest = after1.trim();
115
+ let addr2;
116
+ if (rest[0] === ",") {
117
+ rest = rest.slice(1).trim();
118
+ const [a2, after2] = parseAddr(rest);
119
+ addr2 = a2;
120
+ rest = after2.trim();
121
+ }
122
+ const op = rest[0];
123
+ if (!op)
124
+ continue;
125
+ if (op === "s") {
126
+ // s/from/to/flags
127
+ const delim = rest[1] ?? "/";
128
+ const sRe = new RegExp(`^s${re(delim)}((?:[^${re(delim)}\\\\]|\\\\.)*)${re(delim)}((?:[^${re(delim)}\\\\]|\\\\.)*)${re(delim)}([gGiIp]*)$`);
129
+ const m = rest.match(sRe);
130
+ if (!m) {
131
+ instrs.push({ op: "d", addr1, addr2 });
132
+ continue;
133
+ } // bad expr, skip
134
+ const flags = m[3] ?? "";
135
+ let from;
136
+ try {
137
+ from = new RegExp(m[1], flags.includes("i") || flags.includes("I") ? "i" : "");
138
+ }
139
+ catch {
140
+ continue;
141
+ }
142
+ instrs.push({ op: "s", addr1, addr2, from, to: m[2], global: flags.includes("g") || flags.includes("G"), print: flags.includes("p") });
143
+ }
144
+ else if (op === "d") {
145
+ instrs.push({ op: "d", addr1, addr2 });
146
+ }
147
+ else if (op === "p") {
148
+ instrs.push({ op: "p", addr1, addr2 });
149
+ }
150
+ else if (op === "q") {
151
+ instrs.push({ op: "q", addr1 });
152
+ }
153
+ else if (op === "=") {
154
+ instrs.push({ op: "=", addr1, addr2 });
155
+ }
156
+ }
157
+ return instrs;
158
+ }
159
+ function re(c) {
160
+ return c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
161
+ }
162
+ const allInstrs = exprs.flatMap(parseInstrs);
163
+ const lines = content.split("\n");
164
+ // Remove trailing empty string from trailing newline
165
+ if (lines[lines.length - 1] === "")
166
+ lines.pop();
167
+ const total = lines.length;
168
+ function matchesAddr(addr, lineNo, line) {
169
+ if (!addr)
170
+ return true;
171
+ if (addr.type === "line")
172
+ return lineNo === addr.n;
173
+ if (addr.type === "last")
174
+ return lineNo === total;
175
+ return addr.re.test(line);
176
+ }
177
+ function inRange(instr, lineNo, line, rangeActive) {
178
+ const { addr1, addr2 } = instr;
179
+ if (!addr1)
180
+ return true;
181
+ if (!addr2)
182
+ return matchesAddr(addr1, lineNo, line);
183
+ // Two-address range
184
+ let active = rangeActive.get(instr) ?? false;
185
+ if (!active && matchesAddr(addr1, lineNo, line)) {
186
+ active = true;
187
+ rangeActive.set(instr, true);
188
+ }
189
+ if (active && matchesAddr(addr2, lineNo, line)) {
190
+ rangeActive.set(instr, false);
191
+ return true;
192
+ }
193
+ if (active)
194
+ return true;
195
+ return false;
196
+ }
197
+ const out = [];
198
+ const rangeActive = new Map();
199
+ let quit = false;
200
+ for (let li = 0; li < lines.length && !quit; li++) {
201
+ let line = lines[li];
202
+ const lineNo = li + 1;
203
+ let deleted = false;
204
+ for (const instr of allInstrs) {
205
+ if (!inRange(instr, lineNo, line, rangeActive))
206
+ continue;
207
+ if (instr.op === "d") {
208
+ deleted = true;
209
+ break;
210
+ }
211
+ if (instr.op === "p") {
212
+ out.push(line);
213
+ }
214
+ if (instr.op === "=") {
215
+ out.push(String(lineNo));
216
+ }
217
+ if (instr.op === "q") {
218
+ quit = true;
219
+ }
220
+ if (instr.op === "s") {
221
+ const replaced = instr.global
222
+ ? line.replace(new RegExp(instr.from.source, instr.from.flags.includes("i") ? "gi" : "g"), instr.to)
223
+ : line.replace(instr.from, instr.to);
224
+ if (replaced !== line) {
225
+ line = replaced;
226
+ if (instr.print && suppressAuto)
227
+ out.push(line);
228
+ }
229
+ }
230
+ }
231
+ if (!deleted && !suppressAuto)
232
+ out.push(line);
233
+ }
234
+ const result = out.join("\n") + (out.length > 0 ? "\n" : "");
53
235
  if (inPlace && fileArg) {
54
236
  const p = resolvePath(cwd, fileArg);
55
237
  shell.writeFileAsUser(authUser, p, result);
@@ -125,6 +125,32 @@ function parseBlocks(lines) {
125
125
  }
126
126
  blocks.push({ type: "while", cond, body });
127
127
  }
128
+ else if (line.startsWith("until ")) {
129
+ const cond = line
130
+ .replace(/^until\s+/, "")
131
+ .replace(/;\s*do\s*$/, "")
132
+ .trim();
133
+ const body = [];
134
+ i++;
135
+ while (i < lines.length && lines[i]?.trim() !== "done") {
136
+ const l = lines[i].trim().replace(/^do\s+/, "");
137
+ if (l && l !== "do")
138
+ body.push(l);
139
+ i++;
140
+ }
141
+ blocks.push({ type: "until", cond, body });
142
+ }
143
+ else if (/^[A-Za-z_][A-Za-z0-9_]*=\s*\(/.test(line)) {
144
+ // Array assignment: arr=(elem1 elem2 ...)
145
+ const arrMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=\s*\(([^)]*)\)$/);
146
+ if (arrMatch) {
147
+ const elems = arrMatch[2].trim().split(/\s+/).filter(Boolean);
148
+ blocks.push({ type: "array", name: arrMatch[1], elements: elems });
149
+ }
150
+ else {
151
+ blocks.push({ type: "cmd", line });
152
+ }
153
+ }
128
154
  else if (line.startsWith("case ") && line.endsWith(" in") || line.match(/^case\s+.+\s+in$/)) {
129
155
  const caseExpr = line.replace(/^case\s+/, "").replace(/\s+in$/, "").trim();
130
156
  const patterns = [];
@@ -353,6 +379,22 @@ async function runBlocks(blocks, ctx) {
353
379
  iterations++;
354
380
  }
355
381
  }
382
+ else if (block.type === "until") {
383
+ let iterations = 0;
384
+ while (iterations < 1000 && !(await evalCondition(block.cond, ctx))) {
385
+ const sub = await runBlocks(parseBlocks(block.body), ctx);
386
+ if (sub.stdout)
387
+ output += `${sub.stdout}\n`;
388
+ if (sub.closeSession)
389
+ return sub;
390
+ iterations++;
391
+ }
392
+ }
393
+ else if (block.type === "array") {
394
+ // Store array: arr[0]=e0, arr[1]=e1, ..., arr=space-joined (for ${arr[@]})
395
+ block.elements.forEach((el, idx) => { ctx.env.vars[`${block.name}[${idx}]`] = el; });
396
+ ctx.env.vars[block.name] = block.elements.join(" ");
397
+ }
356
398
  else if (block.type === "case") {
357
399
  const expanded = await expandVars(block.expr, ctx.env.vars, ctx.env.lastExitCode, ctx);
358
400
  for (const pat of block.patterns) {
@@ -0,0 +1,6 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * Trace system calls and signals (stub — runs command, emits fake strace output).
4
+ * @category system
5
+ */
6
+ export declare const straceCommand: ShellModule;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Trace system calls and signals (stub — runs command, emits fake strace output).
3
+ * @category system
4
+ */
5
+ export const straceCommand = {
6
+ name: "strace",
7
+ description: "Trace system calls and signals",
8
+ category: "system",
9
+ params: ["[-e <expr>] [-o <file>] <command> [args]"],
10
+ run: ({ args }) => {
11
+ const cmd = args.find((a) => !a.startsWith("-"));
12
+ if (!cmd)
13
+ return { stderr: "strace: must have PROG [ARGS] or -p PID", exitCode: 1 };
14
+ const _pid = Math.floor(Math.random() * 30000) + 1000;
15
+ const lines = [
16
+ `execve("/usr/bin/${cmd}", ["${cmd}"${args.slice(1).map((a) => `, "${a}"`).join("")}], 0x... /* ... vars */) = 0`,
17
+ `brk(NULL) = 0x${(Math.random() * 0xfffff | 0).toString(16)}000`,
18
+ `access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)`,
19
+ `openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3`,
20
+ `fstat(3, {st_mode=S_IFREG|0644, st_size=...}) = 0`,
21
+ `close(3) = 0`,
22
+ `+++ exited with 0 +++`,
23
+ ];
24
+ return { stderr: lines.join("\n"), exitCode: 0 };
25
+ },
26
+ };
@@ -1,6 +1,7 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  /**
3
- * Archive or extract files with tar and optional gzip compression.
3
+ * Archive or extract files with tar writes real POSIX ustar binary format.
4
+ * Supports -c/-x/-t, -z (gzip), -j (bzip2 stub), -v (verbose), -f.
4
5
  * @category archive
5
6
  * @params ["[-czf|-xzf|-tf] <archive> [files...]"]
6
7
  */