typescript-virtual-container 1.5.4 → 1.5.6

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 (40) hide show
  1. package/README.md +107 -541
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/VirtualShell/shell.js +158 -11
  4. package/dist/commands/awk.d.ts +6 -11
  5. package/dist/commands/awk.js +462 -109
  6. package/dist/commands/bc.d.ts +2 -0
  7. package/dist/commands/bc.js +28 -0
  8. package/dist/commands/bzip2.d.ts +11 -0
  9. package/dist/commands/bzip2.js +91 -0
  10. package/dist/commands/find.d.ts +2 -2
  11. package/dist/commands/find.js +212 -37
  12. package/dist/commands/{ifconfig.d.ts → ip.d.ts} +1 -1
  13. package/dist/commands/{ifconfig.js → ip.js} +3 -3
  14. package/dist/commands/jobs.d.ts +4 -0
  15. package/dist/commands/jobs.js +27 -0
  16. package/dist/commands/lsof.d.ts +6 -0
  17. package/dist/commands/lsof.js +30 -0
  18. package/dist/commands/perl.d.ts +6 -0
  19. package/dist/commands/perl.js +76 -0
  20. package/dist/commands/registry.js +22 -3
  21. package/dist/commands/runtime.js +2 -1
  22. package/dist/commands/sed.d.ts +2 -2
  23. package/dist/commands/sed.js +216 -34
  24. package/dist/commands/set.js +20 -0
  25. package/dist/commands/sh.js +111 -1
  26. package/dist/commands/strace.d.ts +6 -0
  27. package/dist/commands/strace.js +26 -0
  28. package/dist/commands/tar.d.ts +2 -1
  29. package/dist/commands/tar.js +138 -52
  30. package/dist/commands/zip.d.ts +11 -0
  31. package/dist/commands/zip.js +232 -0
  32. package/dist/modules/linuxRootfs.js +1 -1
  33. package/dist/utils/expand.js +67 -1
  34. package/package.json +5 -4
  35. package/dist/self-standalone.d.ts +0 -1
  36. package/dist/self-standalone.js +0 -444
  37. package/dist/standalone-wo-sftp.d.ts +0 -1
  38. package/dist/standalone-wo-sftp.js +0 -30
  39. package/dist/standalone.d.ts +0 -1
  40. package/dist/standalone.js +0 -61
@@ -0,0 +1,11 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * Compress files using bzip2 (VFS-internal gzip with BZ2 marker — round-trips within VM).
4
+ * @category archive
5
+ */
6
+ export declare const bzip2Command: ShellModule;
7
+ /**
8
+ * Decompress bzip2 files (VFS-internal format).
9
+ * @category archive
10
+ */
11
+ export declare const bunzip2Command: ShellModule;
@@ -0,0 +1,91 @@
1
+ import { gunzipSync, gzipSync } from "fflate";
2
+ import { resolvePath } from "./helpers";
3
+ // BZ2 magic bytes: "BZh" — we store gzip data after this marker so our tools
4
+ // can round-trip. Real bzip2 files won't be decompressable this way, but files
5
+ // created and extracted within this VM will work correctly.
6
+ const BZ2_MAGIC = Buffer.from("BZhVFS\0");
7
+ function vfsBzip2(data) {
8
+ const gz = Buffer.from(gzipSync(data));
9
+ return Buffer.concat([BZ2_MAGIC, gz]);
10
+ }
11
+ function vfsBunzip2(data) {
12
+ if (!data.subarray(0, BZ2_MAGIC.length).equals(BZ2_MAGIC))
13
+ return null;
14
+ try {
15
+ return Buffer.from(gunzipSync(data.subarray(BZ2_MAGIC.length)));
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ /**
22
+ * Compress files using bzip2 (VFS-internal gzip with BZ2 marker — round-trips within VM).
23
+ * @category archive
24
+ */
25
+ export const bzip2Command = {
26
+ name: "bzip2",
27
+ description: "Compress files using Burrows-Wheeler algorithm",
28
+ category: "archive",
29
+ params: ["[-k] [-d] <file>"],
30
+ run: ({ authUser, shell, cwd, args }) => {
31
+ const keepOrig = args.includes("-k") || args.includes("--keep");
32
+ const decompress = args.includes("-d") || args.includes("--decompress");
33
+ const file = args.find((a) => !a.startsWith("-"));
34
+ if (!file)
35
+ return { stderr: "bzip2: no file specified", exitCode: 1 };
36
+ const p = resolvePath(cwd, file);
37
+ if (!shell.vfs.exists(p))
38
+ return { stderr: `bzip2: ${file}: No such file or directory`, exitCode: 1 };
39
+ if (decompress) {
40
+ if (!file.endsWith(".bz2"))
41
+ return { stderr: `bzip2: ${file}: unknown suffix -- ignored`, exitCode: 1 };
42
+ const raw = shell.vfs.readFileRaw(p);
43
+ const result = vfsBunzip2(raw);
44
+ if (!result)
45
+ return { stderr: `bzip2: ${file}: data integrity error`, exitCode: 2 };
46
+ const dest = p.slice(0, -4);
47
+ shell.writeFileAsUser(authUser, dest, result);
48
+ if (!keepOrig)
49
+ shell.vfs.remove(p);
50
+ return { exitCode: 0 };
51
+ }
52
+ if (file.endsWith(".bz2"))
53
+ return { stderr: `bzip2: ${file}: already has .bz2 suffix -- unchanged`, exitCode: 1 };
54
+ const raw = shell.vfs.readFileRaw(p);
55
+ shell.vfs.writeFile(`${p}.bz2`, vfsBzip2(raw));
56
+ if (!keepOrig)
57
+ shell.vfs.remove(p);
58
+ return { exitCode: 0 };
59
+ },
60
+ };
61
+ /**
62
+ * Decompress bzip2 files (VFS-internal format).
63
+ * @category archive
64
+ */
65
+ export const bunzip2Command = {
66
+ name: "bunzip2",
67
+ description: "Decompress bzip2 files",
68
+ category: "archive",
69
+ aliases: ["bzcat"],
70
+ params: ["[-k] <file>"],
71
+ run: ({ authUser, shell, cwd, args }) => {
72
+ const keepOrig = args.includes("-k") || args.includes("--keep");
73
+ const file = args.find((a) => !a.startsWith("-"));
74
+ if (!file)
75
+ return { stderr: "bunzip2: no file specified", exitCode: 1 };
76
+ const p = resolvePath(cwd, file);
77
+ if (!shell.vfs.exists(p))
78
+ return { stderr: `bunzip2: ${file}: No such file or directory`, exitCode: 1 };
79
+ if (!file.endsWith(".bz2"))
80
+ return { stderr: `bunzip2: ${file}: unknown suffix -- ignored`, exitCode: 1 };
81
+ const raw = shell.vfs.readFileRaw(p);
82
+ const result = vfsBunzip2(raw);
83
+ if (!result)
84
+ return { stderr: `bunzip2: ${file}: data integrity error`, exitCode: 2 };
85
+ const dest = p.slice(0, -4);
86
+ shell.writeFileAsUser(authUser, dest, result);
87
+ if (!keepOrig)
88
+ shell.vfs.remove(p);
89
+ return { exitCode: 0 };
90
+ },
91
+ };
@@ -1,7 +1,7 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  /**
3
- * Find files and directories by name and type with minimal pattern support.
3
+ * Find files and directories with filtering, -exec, -maxdepth, -not, -o, -a.
4
4
  * @category files
5
- * @params ["[path] [-name <pattern>] [-type f|d]"]
5
+ * @params ["[path] [expression...]"]
6
6
  */
7
7
  export declare const findCommand: ShellModule;
@@ -1,55 +1,230 @@
1
- import { getFlag } from "./command-helpers";
2
1
  import { assertPathAccess, resolvePath } from "./helpers";
2
+ import { runCommand } from "./runtime";
3
3
  /**
4
- * Find files and directories by name and type with minimal pattern support.
4
+ * Find files and directories with filtering, -exec, -maxdepth, -not, -o, -a.
5
5
  * @category files
6
- * @params ["[path] [-name <pattern>] [-type f|d]"]
6
+ * @params ["[path] [expression...]"]
7
7
  */
8
8
  export const findCommand = {
9
9
  name: "find",
10
10
  description: "Search for files",
11
11
  category: "files",
12
- params: ["[path] [-name <pattern>] [-type f|d]"],
13
- run: ({ authUser, shell, cwd, args }) => {
14
- const namePattern = getFlag(args, ["-name"]);
15
- const typeFilter = getFlag(args, ["-type"]);
16
- const positionals = args.filter((a) => !a.startsWith("-") && a !== namePattern && a !== typeFilter);
17
- const rootArg = positionals[0] ?? ".";
18
- const rootPath = resolvePath(cwd, rootArg);
19
- try {
20
- assertPathAccess(authUser, rootPath, "find");
21
- if (!shell.vfs.exists(rootPath)) {
22
- return {
23
- stderr: `find: ${rootArg}: No such file or directory`,
24
- exitCode: 1,
25
- };
12
+ params: ["[path] [expression...]"],
13
+ run: async ({ authUser, shell, cwd, args, env, hostname, mode }) => {
14
+ // Collect root paths (positional args before first - option)
15
+ const roots = [];
16
+ let i = 0;
17
+ while (i < args.length && !args[i].startsWith("-") && args[i] !== "!" && args[i] !== "(") {
18
+ roots.push(args[i]);
19
+ i++;
20
+ }
21
+ if (roots.length === 0)
22
+ roots.push(".");
23
+ const exprArgs = args.slice(i);
24
+ let maxDepth = Infinity;
25
+ let minDepth = 0;
26
+ const execCmds = [];
27
+ function parseExpr(tokens, pos) {
28
+ return parseOr(tokens, pos);
29
+ }
30
+ function parseOr(tokens, pos) {
31
+ let [left, p] = parseAnd(tokens, pos);
32
+ while (tokens[p] === "-o" || tokens[p] === "-or") {
33
+ p++;
34
+ const [right, np] = parseAnd(tokens, p);
35
+ left = { type: "or", left, right };
36
+ p = np;
26
37
  }
38
+ return [left, p];
27
39
  }
28
- catch (err) {
29
- const msg = err instanceof Error ? err.message : String(err);
30
- return { stderr: `find: ${msg}`, exitCode: 1 };
40
+ function parseAnd(tokens, pos) {
41
+ let [left, p] = parseNot(tokens, pos);
42
+ while (p < tokens.length && tokens[p] !== "-o" && tokens[p] !== "-or" && tokens[p] !== ")") {
43
+ if (tokens[p] === "-a" || tokens[p] === "-and")
44
+ p++;
45
+ if (p >= tokens.length || tokens[p] === "-o" || tokens[p] === ")")
46
+ break;
47
+ const [right, np] = parseNot(tokens, p);
48
+ left = { type: "and", left, right };
49
+ p = np;
50
+ }
51
+ return [left, p];
52
+ }
53
+ function parseNot(tokens, pos) {
54
+ if (tokens[pos] === "!" || tokens[pos] === "-not") {
55
+ const [pred, np] = parsePrimary(tokens, pos + 1);
56
+ return [{ type: "not", pred }, np];
57
+ }
58
+ return parsePrimary(tokens, pos);
59
+ }
60
+ function parsePrimary(tokens, pos) {
61
+ const tok = tokens[pos];
62
+ if (!tok)
63
+ return [{ type: "true" }, pos];
64
+ if (tok === "(") {
65
+ const [pred, np] = parseExpr(tokens, pos + 1);
66
+ const closePos = tokens[np] === ")" ? np + 1 : np;
67
+ return [pred, closePos];
68
+ }
69
+ if (tok === "-name")
70
+ return [{ type: "name", pat: tokens[pos + 1] ?? "*", ignoreCase: false }, pos + 2];
71
+ if (tok === "-iname")
72
+ return [{ type: "name", pat: tokens[pos + 1] ?? "*", ignoreCase: true }, pos + 2];
73
+ if (tok === "-type")
74
+ return [{ type: "type", t: tokens[pos + 1] ?? "f" }, pos + 2];
75
+ if (tok === "-maxdepth") {
76
+ maxDepth = parseInt(tokens[pos + 1] ?? "0", 10);
77
+ return [{ type: "true" }, pos + 2];
78
+ }
79
+ if (tok === "-mindepth") {
80
+ minDepth = parseInt(tokens[pos + 1] ?? "0", 10);
81
+ return [{ type: "true" }, pos + 2];
82
+ }
83
+ if (tok === "-empty")
84
+ return [{ type: "empty" }, pos + 1];
85
+ if (tok === "-print" || tok === "-print0")
86
+ return [{ type: "print" }, pos + 1];
87
+ if (tok === "-true")
88
+ return [{ type: "true" }, pos + 1];
89
+ if (tok === "-false")
90
+ return [{ type: "false" }, pos + 1];
91
+ if (tok === "-size") {
92
+ const raw = tokens[pos + 1] ?? "0";
93
+ const unit = raw.slice(-1);
94
+ const n = parseInt(raw, 10);
95
+ return [{ type: "size", n, unit }, pos + 2];
96
+ }
97
+ if (tok === "-exec" || tok === "-execdir") {
98
+ const useDir = tok === "-execdir";
99
+ const cmd = [];
100
+ let j = pos + 1;
101
+ while (j < tokens.length && tokens[j] !== ";") {
102
+ cmd.push(tokens[j]);
103
+ j++;
104
+ }
105
+ execCmds.push({ cmd, useDir });
106
+ return [{ type: "exec", cmd, useDir }, j + 1];
107
+ }
108
+ // Unknown predicate — skip
109
+ return [{ type: "true" }, pos + 1];
110
+ }
111
+ const pred = exprArgs.length > 0 ? parseExpr(exprArgs, 0)[0] : { type: "true" };
112
+ function globToRegex(pat, flags = "") {
113
+ const esc = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
114
+ return new RegExp(`^${esc}$`, flags);
115
+ }
116
+ function matchPred(p, fullPath, depth) {
117
+ switch (p.type) {
118
+ case "true": return true;
119
+ case "false": return false;
120
+ case "not": return !matchPred(p.pred, fullPath, depth);
121
+ case "and": return matchPred(p.left, fullPath, depth) && matchPred(p.right, fullPath, depth);
122
+ case "or": return matchPred(p.left, fullPath, depth) || matchPred(p.right, fullPath, depth);
123
+ case "name": {
124
+ const base = fullPath.split("/").pop() ?? "";
125
+ return globToRegex(p.pat, p.ignoreCase ? "i" : "").test(base);
126
+ }
127
+ case "type": {
128
+ try {
129
+ const st = shell.vfs.stat(fullPath);
130
+ if (p.t === "f")
131
+ return st.type === "file";
132
+ if (p.t === "d")
133
+ return st.type === "directory";
134
+ if (p.t === "l")
135
+ return false; // VFS has no symlink type
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ return false;
141
+ }
142
+ case "empty": {
143
+ try {
144
+ const st = shell.vfs.stat(fullPath);
145
+ if (st.type === "directory")
146
+ return shell.vfs.list(fullPath).length === 0;
147
+ return shell.vfs.readFile(fullPath).length === 0;
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ }
153
+ case "size": {
154
+ try {
155
+ const content = shell.vfs.readFile(fullPath);
156
+ const bytes = content.length;
157
+ const unit = p.unit;
158
+ let fileSize = bytes;
159
+ if (unit === "k" || unit === "K")
160
+ fileSize = Math.ceil(bytes / 1024);
161
+ else if (unit === "M")
162
+ fileSize = Math.ceil(bytes / (1024 * 1024));
163
+ else if (unit === "c")
164
+ fileSize = bytes;
165
+ return fileSize === p.n;
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ }
171
+ case "print": return true;
172
+ case "exec": return true; // handled separately
173
+ default: return true;
174
+ }
31
175
  }
32
- const nameRegex = namePattern
33
- ? new RegExp(`^${namePattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".")}$`)
34
- : null;
35
176
  const results = [];
36
- const walk = (currentPath, display) => {
37
- const stat = shell.vfs.stat(currentPath);
38
- const matchesType = !typeFilter ||
39
- (typeFilter === "f" && stat.type === "file") ||
40
- (typeFilter === "d" && stat.type === "directory");
41
- const matchesName = !nameRegex || nameRegex.test(currentPath.split("/").pop() ?? "");
42
- if (matchesType && matchesName)
177
+ function walk(currentPath, display, depth) {
178
+ if (depth > maxDepth)
179
+ return;
180
+ try {
181
+ assertPathAccess(authUser, currentPath, "find");
182
+ }
183
+ catch {
184
+ return;
185
+ }
186
+ if (depth >= minDepth && matchPred(pred, currentPath, depth)) {
43
187
  results.push(display);
44
- if (stat.type === "directory") {
188
+ }
189
+ let stat;
190
+ try {
191
+ stat = shell.vfs.stat(currentPath);
192
+ }
193
+ catch {
194
+ return;
195
+ }
196
+ if (stat.type === "directory" && depth < maxDepth) {
45
197
  for (const entry of shell.vfs.list(currentPath)) {
46
- const full = `${currentPath}/${entry}`;
47
- const disp = `${display}/${entry}`;
48
- walk(full, disp);
198
+ walk(`${currentPath}/${entry}`, `${display}/${entry}`, depth + 1);
49
199
  }
50
200
  }
51
- };
52
- walk(rootPath, rootArg);
53
- return { stdout: results.join("\n"), exitCode: 0 };
201
+ }
202
+ for (const root of roots) {
203
+ const rootPath = resolvePath(cwd, root);
204
+ if (!shell.vfs.exists(rootPath)) {
205
+ return { stderr: `find: '${root}': No such file or directory`, exitCode: 1 };
206
+ }
207
+ walk(rootPath, root === "." ? "." : root, 0);
208
+ }
209
+ // Execute -exec commands
210
+ if (execCmds.length > 0 && results.length > 0) {
211
+ const execOutputs = [];
212
+ for (const { cmd } of execCmds) {
213
+ for (const filePath of results) {
214
+ const expanded = cmd.map((t) => t === "{}" ? filePath : t);
215
+ const cmdStr = expanded.map((t) => (t.includes(" ") ? `"${t}"` : t)).join(" ");
216
+ const r = await runCommand(cmdStr, authUser, hostname, mode, cwd, shell, undefined, env);
217
+ if (r.stdout)
218
+ execOutputs.push(r.stdout.replace(/\n$/, ""));
219
+ if (r.stderr)
220
+ execOutputs.push(r.stderr.replace(/\n$/, ""));
221
+ }
222
+ }
223
+ if (execOutputs.length > 0) {
224
+ return { stdout: `${execOutputs.join("\n")}\n`, exitCode: 0 };
225
+ }
226
+ return { exitCode: 0 };
227
+ }
228
+ return { stdout: results.join("\n") + (results.length > 0 ? "\n" : ""), exitCode: 0 };
54
229
  },
55
230
  };
@@ -4,4 +4,4 @@ import type { ShellModule } from "../types/commands";
4
4
  * @category network
5
5
  * @params ["<object> <command>"]
6
6
  */
7
- export declare const ifconfigCommand: ShellModule;
7
+ export declare const ipCommand: ShellModule;
@@ -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 ifconfigCommand = {
25
- name: "ifconfig",
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: ifconfig [interface]", exitCode: 1 };
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,4 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ export declare const jobsCommand: ShellModule;
3
+ export declare const bgCommand: ShellModule;
4
+ export declare const fgCommand: ShellModule;
@@ -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
+ };
@@ -0,0 +1,6 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * List open files (stub — returns simulated process file descriptors).
4
+ * @category system
5
+ */
6
+ export declare const lsofCommand: ShellModule;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * List open files (stub — returns simulated process file descriptors).
3
+ * @category system
4
+ */
5
+ export const lsofCommand = {
6
+ name: "lsof",
7
+ description: "List open files",
8
+ category: "system",
9
+ params: ["[-p <pid>] [-u <user>] [-i [addr]]"],
10
+ run: ({ authUser, args }) => {
11
+ const iNet = args.includes("-i");
12
+ if (iNet) {
13
+ const header = "COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME";
14
+ const rows = [
15
+ `sshd 1234 root 3u IPv4 12345 0t0 TCP *:22 (LISTEN)`,
16
+ `nginx 567 root 6u IPv4 23456 0t0 TCP *:80 (LISTEN)`,
17
+ ];
18
+ return { stdout: `${header}\n${rows.join("\n")}`, exitCode: 0 };
19
+ }
20
+ const header = "COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME";
21
+ const rows = [
22
+ `bash 1001 ${authUser} cwd DIR 8,1 4096 2 /home/${authUser}`,
23
+ `bash 1001 ${authUser} txt REG 8,1 1183448 23 /bin/bash`,
24
+ `bash 1001 ${authUser} 0u CHR 136,0 0t0 3 /dev/pts/0`,
25
+ `bash 1001 ${authUser} 1u CHR 136,0 0t0 3 /dev/pts/0`,
26
+ `bash 1001 ${authUser} 2u CHR 136,0 0t0 3 /dev/pts/0`,
27
+ ];
28
+ return { stdout: `${header}\n${rows.join("\n")}`, exitCode: 0 };
29
+ },
30
+ };
@@ -0,0 +1,6 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * Perl interpreter stub — supports -e one-liners with print, say, basic regex, and -p/-n/-i.
4
+ * @category scripting
5
+ */
6
+ export declare const perlCommand: ShellModule;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Perl interpreter stub — supports -e one-liners with print, say, basic regex, and -p/-n/-i.
3
+ * @category scripting
4
+ */
5
+ export const perlCommand = {
6
+ name: "perl",
7
+ description: "Practical Extraction and Report Language",
8
+ category: "scripting",
9
+ params: ["[-e <expr>] [-p] [-n] [-i] [file]"],
10
+ run: ({ args, stdin }) => {
11
+ const eIdx = args.indexOf("-e");
12
+ const code = eIdx !== -1 ? args[eIdx + 1] : undefined;
13
+ const printLoop = args.includes("-p");
14
+ const noAutoprint = args.includes("-n");
15
+ const hasLoop = printLoop || noAutoprint;
16
+ if (!code) {
17
+ return { stderr: "perl: no code specified (only -e one-liners supported)", exitCode: 1 };
18
+ }
19
+ const input = stdin ?? "";
20
+ const lines = input.split("\n");
21
+ if (lines[lines.length - 1] === "")
22
+ lines.pop();
23
+ const out = [];
24
+ if (hasLoop) {
25
+ for (let li = 0; li < lines.length; li++) {
26
+ let line = lines[li];
27
+ // $_ = line, $. = line number
28
+ let processed = code
29
+ .replace(/\$_/g, JSON.stringify(line))
30
+ .replace(/\$\./g, String(li + 1));
31
+ // s/pat/rep/[g] substitution on $_
32
+ const sMatch = processed.match(/^s([^a-zA-Z0-9])(.*?)\1(.*?)\1([gi]*)$/);
33
+ if (sMatch) {
34
+ const flags = sMatch[4] ?? "";
35
+ try {
36
+ const re = new RegExp(sMatch[2], flags.includes("i") ? (flags.includes("g") ? "gi" : "i") : flags.includes("g") ? "g" : "");
37
+ line = line.replace(re, sMatch[3]);
38
+ }
39
+ catch { /* ignore */ }
40
+ if (printLoop)
41
+ out.push(line);
42
+ continue;
43
+ }
44
+ // print / say
45
+ const printM = processed.match(/^(?:print|say)\s+(?:STDOUT\s+)?(?:"([^"]*)"|'([^']*)'|(.+))(?:\s*;)?$/);
46
+ if (printM) {
47
+ const val = (printM[1] ?? printM[2] ?? printM[3] ?? "")
48
+ .replace(/\\n/g, "\n").replace(/\\t/g, "\t");
49
+ out.push(code.startsWith("say") ? val : val.replace(/\n$/, ""));
50
+ if (printLoop)
51
+ out.push(line);
52
+ continue;
53
+ }
54
+ if (printLoop)
55
+ out.push(line);
56
+ }
57
+ }
58
+ else {
59
+ // Single expression
60
+ const printM = code.match(/^(?:print|say)\s+(?:STDOUT\s+)?(?:"([^"]*)"|'([^']*)'|(.*))(?:\s*;)?$/);
61
+ if (printM) {
62
+ const val = (printM[1] ?? printM[2] ?? printM[3] ?? "")
63
+ .replace(/\\n/g, "\n").replace(/\\t/g, "\t");
64
+ out.push(val);
65
+ }
66
+ else {
67
+ // Version string
68
+ if (code.trim() === 'print $]' || code.includes("version")) {
69
+ out.push("5.036001");
70
+ }
71
+ }
72
+ }
73
+ const result = out.join("\n");
74
+ return { stdout: result + (result && !result.endsWith("\n") ? "\n" : ""), exitCode: 0 };
75
+ },
76
+ };
@@ -4,6 +4,12 @@ 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";
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";
7
13
  import { catCommand } from "./cat";
8
14
  import { cdCommand } from "./cd";
9
15
  import { chmodCommand } from "./chmod";
@@ -35,7 +41,8 @@ import { historyCommand } from "./history";
35
41
  import { hostnameCommand } from "./hostname";
36
42
  import { htopCommand } from "./htop";
37
43
  import { idCommand } from "./id";
38
- import { ifconfigCommand } from "./ifconfig";
44
+ import { ipCommand } from "./ip";
45
+ import { bgCommand, fgCommand, jobsCommand } from "./jobs";
39
46
  import { killCommand } from "./kill";
40
47
  import { dmesgCommand, lastCommand } from "./last";
41
48
  import { lnCommand, readlinkCommand } from "./ln";
@@ -81,13 +88,13 @@ import { unameCommand } from "./uname";
81
88
  import { uniqCommand } from "./uniq";
82
89
  import { unsetCommand } from "./unset";
83
90
  import { uptimeCommand } from "./uptime";
91
+ import { wCommand } from "./w";
84
92
  import { wcCommand } from "./wc";
85
93
  import { wgetCommand } from "./wget";
86
94
  import { whichCommand } from "./which";
87
95
  import { whoCommand } from "./who";
88
96
  import { whoamiCommand } from "./whoami";
89
97
  import { xargsCommand } from "./xargs";
90
- import { wCommand } from "./w";
91
98
  const BASE_COMMANDS = [
92
99
  // Navigation
93
100
  pwdCommand,
@@ -125,6 +132,10 @@ const BASE_COMMANDS = [
125
132
  tarCommand,
126
133
  gzipCommand,
127
134
  gunzipCommand,
135
+ zipCommand,
136
+ unzipCommand,
137
+ bzip2Command,
138
+ bunzip2Command,
128
139
  base64Command,
129
140
  // System info
130
141
  whoamiCommand,
@@ -159,7 +170,7 @@ const BASE_COMMANDS = [
159
170
  sttyCommand,
160
171
  lastCommand,
161
172
  dmesgCommand,
162
- ifconfigCommand,
173
+ ipCommand,
163
174
  yesCommand,
164
175
  fortuneCommand,
165
176
  cowsayCommand,
@@ -184,6 +195,10 @@ const BASE_COMMANDS = [
184
195
  dpkgCommand,
185
196
  dpkgQueryCommand,
186
197
  // Shell (extended)
198
+ jobsCommand,
199
+ bgCommand,
200
+ fgCommand,
201
+ bcCommand,
187
202
  whichCommand,
188
203
  typeCommand,
189
204
  manCommand,
@@ -208,6 +223,10 @@ const BASE_COMMANDS = [
208
223
  uptimeCommand,
209
224
  freeCommand,
210
225
  lsbReleaseCommand,
226
+ lsofCommand,
227
+ straceCommand,
228
+ // Scripting
229
+ perlCommand,
211
230
  ];
212
231
  const customCommands = [];
213
232
  const commandRegistry = new Map();
@@ -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/sh",
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
  };