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.
- package/README.md +117 -35
- package/dist/.tsbuildinfo +1 -1
- package/dist/SSHMimic/index.d.ts +5 -1
- package/dist/SSHMimic/index.js +27 -3
- package/dist/SSHMimic/scp.d.ts +34 -0
- package/dist/SSHMimic/scp.js +285 -0
- package/dist/SSHMimic/sftp.d.ts +53 -3
- package/dist/SSHMimic/sftp.js +9 -3
- package/dist/VirtualFileSystem/binaryPack.d.ts +7 -0
- package/dist/VirtualFileSystem/binaryPack.js +37 -1
- package/dist/VirtualFileSystem/index.d.ts +7 -0
- package/dist/VirtualFileSystem/index.js +67 -27
- package/dist/VirtualFileSystem/internalTypes.d.ts +2 -0
- package/dist/VirtualFileSystem/path.d.ts +5 -0
- package/dist/VirtualFileSystem/path.js +24 -11
- package/dist/VirtualPackageManager/index.d.ts +4 -2
- package/dist/VirtualPackageManager/index.js +24 -4
- package/dist/VirtualShell/index.d.ts +4 -0
- package/dist/VirtualShell/index.js +1 -7
- package/dist/VirtualShell/shell.js +40 -10
- package/dist/VirtualShell/shellParser.js +1 -22
- package/dist/commands/awk.d.ts +6 -11
- package/dist/commands/awk.js +462 -109
- package/dist/commands/bzip2.d.ts +11 -0
- package/dist/commands/bzip2.js +91 -0
- package/dist/commands/exit.js +1 -1
- package/dist/commands/find.d.ts +2 -2
- package/dist/commands/find.js +209 -37
- package/dist/commands/helpers.d.ts +0 -20
- package/dist/commands/helpers.js +0 -97
- package/dist/commands/lsof.d.ts +6 -0
- package/dist/commands/lsof.js +30 -0
- package/dist/commands/perl.d.ts +6 -0
- package/dist/commands/perl.js +76 -0
- package/dist/commands/python.js +5 -2
- package/dist/commands/registry.js +19 -1
- package/dist/commands/runtime.js +65 -87
- package/dist/commands/sed.d.ts +2 -2
- package/dist/commands/sed.js +216 -34
- package/dist/commands/sh.js +42 -0
- package/dist/commands/strace.d.ts +6 -0
- package/dist/commands/strace.js +26 -0
- package/dist/commands/tar.d.ts +2 -1
- package/dist/commands/tar.js +138 -52
- package/dist/commands/test.js +2 -2
- package/dist/commands/zip.d.ts +11 -0
- package/dist/commands/zip.js +232 -0
- package/dist/modules/linuxRootfs.js +1 -4
- package/dist/modules/neofetch.js +2 -2
- package/dist/types/commands.d.ts +4 -0
- package/dist/utils/argv.d.ts +6 -0
- package/dist/utils/argv.js +32 -0
- package/dist/utils/expand.d.ts +5 -2
- package/dist/utils/expand.js +112 -45
- package/dist/utils/glob.d.ts +6 -0
- package/dist/utils/glob.js +34 -0
- package/dist/utils/tokenize.js +13 -13
- package/package.json +9 -7
- package/dist/self-standalone.d.ts +0 -1
- package/dist/self-standalone.js +0 -444
- package/dist/standalone-wo-sftp.d.ts +0 -1
- package/dist/standalone-wo-sftp.js +0 -30
- package/dist/standalone.d.ts +0 -1
- 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
|
-
|
|
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 };
|
package/dist/commands/runtime.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const hasOperators =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/sed.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
2
|
/**
|
|
3
|
-
* Stream editor
|
|
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]"
|
|
5
|
+
* @params ["[-n] [-e <expr>] [file]"]
|
|
6
6
|
*/
|
|
7
7
|
export declare const sedCommand: ShellModule;
|
package/dist/commands/sed.js
CHANGED
|
@@ -1,22 +1,74 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ifFlag } from "./command-helpers";
|
|
2
2
|
import { resolvePath } from "./helpers";
|
|
3
3
|
/**
|
|
4
|
-
* Stream editor
|
|
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]"
|
|
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]"
|
|
12
|
+
params: ["[-n] [-e <expr>] [file]"],
|
|
13
13
|
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
14
14
|
const inPlace = ifFlag(args, ["-i"]);
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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);
|
package/dist/commands/sh.js
CHANGED
|
@@ -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,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
|
+
};
|
package/dist/commands/tar.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
2
|
/**
|
|
3
|
-
* Archive or extract files with tar
|
|
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
|
*/
|