typescript-virtual-container 1.3.4 → 1.4.0
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/.vscode/settings.json +0 -1
- package/README.md +674 -1504
- package/benchmark-results.txt +21 -21
- package/builds/self-standalone.js +274 -208
- package/builds/self-standalone.js.map +4 -4
- package/builds/standalone-wo-sftp.js +201 -149
- package/builds/standalone-wo-sftp.js.map +4 -4
- package/builds/standalone.js +263 -211
- package/builds/standalone.js.map +4 -4
- package/builds/web-full-api.min.js +3 -3
- package/builds/web-full-api.min.js.map +4 -4
- package/builds/web.min.js +2 -2
- package/builds/web.min.js.map +4 -4
- package/bun.lock +14 -12
- package/dist/SSHClient/index.d.ts.map +1 -1
- package/dist/SSHClient/index.js +5 -3
- package/dist/SSHMimic/executor.d.ts +1 -3
- package/dist/SSHMimic/executor.d.ts.map +1 -1
- package/dist/SSHMimic/executor.js +20 -22
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +5 -3
- package/dist/SSHMimic/sftp.d.ts.map +1 -1
- package/dist/SSHMimic/sftp.js +26 -21
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +25 -3
- package/dist/VirtualShell/shellParser.d.ts +1 -8
- package/dist/VirtualShell/shellParser.d.ts.map +1 -1
- package/dist/VirtualShell/shellParser.js +2 -81
- package/dist/VirtualUserManager/index.d.ts +7 -1
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +47 -16
- package/dist/commands/adduser.d.ts +10 -4
- package/dist/commands/adduser.d.ts.map +1 -1
- package/dist/commands/adduser.js +75 -12
- package/dist/commands/alias.d.ts +5 -0
- package/dist/commands/alias.d.ts.map +1 -1
- package/dist/commands/alias.js +5 -0
- package/dist/commands/apt.d.ts +5 -0
- package/dist/commands/apt.d.ts.map +1 -1
- package/dist/commands/apt.js +5 -0
- package/dist/commands/awk.d.ts +10 -8
- package/dist/commands/awk.d.ts.map +1 -1
- package/dist/commands/awk.js +156 -28
- package/dist/commands/cd.d.ts.map +1 -1
- package/dist/commands/cd.js +0 -3
- package/dist/commands/clear.d.ts +5 -0
- package/dist/commands/clear.d.ts.map +1 -1
- package/dist/commands/clear.js +5 -0
- package/dist/commands/command-helpers.d.ts.map +1 -1
- package/dist/commands/command-helpers.js +8 -0
- package/dist/commands/declare.d.ts +5 -0
- package/dist/commands/declare.d.ts.map +1 -1
- package/dist/commands/declare.js +5 -0
- package/dist/commands/deluser.d.ts +12 -0
- package/dist/commands/deluser.d.ts.map +1 -1
- package/dist/commands/deluser.js +72 -6
- package/dist/commands/df.d.ts +5 -0
- package/dist/commands/df.d.ts.map +1 -1
- package/dist/commands/df.js +5 -0
- package/dist/commands/du.d.ts +5 -0
- package/dist/commands/du.d.ts.map +1 -1
- package/dist/commands/du.js +5 -0
- package/dist/commands/export.d.ts +5 -0
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +5 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +22 -4
- package/dist/commands/groups.d.ts +5 -0
- package/dist/commands/groups.d.ts.map +1 -1
- package/dist/commands/groups.js +5 -0
- package/dist/commands/gzip.d.ts +5 -2
- package/dist/commands/gzip.d.ts.map +1 -1
- package/dist/commands/gzip.js +48 -28
- package/dist/commands/head.d.ts.map +1 -1
- package/dist/commands/head.js +12 -3
- package/dist/commands/htop.d.ts +5 -0
- package/dist/commands/htop.d.ts.map +1 -1
- package/dist/commands/htop.js +5 -0
- package/dist/commands/kill.d.ts +5 -0
- package/dist/commands/kill.d.ts.map +1 -1
- package/dist/commands/kill.js +5 -0
- package/dist/commands/ln.d.ts +2 -0
- package/dist/commands/ln.d.ts.map +1 -1
- package/dist/commands/ln.js +22 -0
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +15 -0
- package/dist/commands/lsb-release.d.ts +5 -0
- package/dist/commands/lsb-release.d.ts.map +1 -1
- package/dist/commands/lsb-release.js +5 -0
- package/dist/commands/mkdir.d.ts +5 -0
- package/dist/commands/mkdir.d.ts.map +1 -1
- package/dist/commands/mkdir.js +5 -0
- package/dist/commands/mv.d.ts +5 -0
- package/dist/commands/mv.d.ts.map +1 -1
- package/dist/commands/mv.js +5 -0
- package/dist/commands/nano.d.ts +5 -0
- package/dist/commands/nano.d.ts.map +1 -1
- package/dist/commands/nano.js +5 -0
- package/dist/commands/neofetch.d.ts +5 -0
- package/dist/commands/neofetch.d.ts.map +1 -1
- package/dist/commands/neofetch.js +8 -5
- package/dist/commands/passwd.d.ts +8 -0
- package/dist/commands/passwd.d.ts.map +1 -1
- package/dist/commands/passwd.js +32 -11
- package/dist/commands/ping.d.ts +5 -0
- package/dist/commands/ping.d.ts.map +1 -1
- package/dist/commands/ping.js +5 -0
- package/dist/commands/printf.d.ts +5 -0
- package/dist/commands/printf.d.ts.map +1 -1
- package/dist/commands/printf.js +43 -12
- package/dist/commands/ps.d.ts +5 -0
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +5 -0
- package/dist/commands/read.d.ts +5 -0
- package/dist/commands/read.d.ts.map +1 -1
- package/dist/commands/read.js +5 -0
- package/dist/commands/registry.d.ts.map +1 -1
- package/dist/commands/registry.js +4 -1
- package/dist/commands/rm.d.ts +5 -0
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +5 -0
- package/dist/commands/runtime.d.ts.map +1 -1
- package/dist/commands/runtime.js +1 -57
- package/dist/commands/sed.d.ts +5 -0
- package/dist/commands/sed.d.ts.map +1 -1
- package/dist/commands/sed.js +5 -0
- package/dist/commands/set.d.ts +5 -6
- package/dist/commands/set.d.ts.map +1 -1
- package/dist/commands/set.js +5 -22
- package/dist/commands/sh.d.ts +6 -0
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +6 -0
- package/dist/commands/shift.d.ts +10 -0
- package/dist/commands/shift.d.ts.map +1 -1
- package/dist/commands/shift.js +10 -0
- package/dist/commands/sleep.d.ts +5 -0
- package/dist/commands/sleep.d.ts.map +1 -1
- package/dist/commands/sleep.js +5 -0
- package/dist/commands/sort.d.ts +5 -0
- package/dist/commands/sort.d.ts.map +1 -1
- package/dist/commands/sort.js +5 -0
- package/dist/commands/source.d.ts +5 -0
- package/dist/commands/source.d.ts.map +1 -1
- package/dist/commands/source.js +5 -0
- package/dist/commands/stat.d.ts +7 -0
- package/dist/commands/stat.d.ts.map +1 -0
- package/dist/commands/stat.js +56 -0
- package/dist/commands/su.d.ts +13 -0
- package/dist/commands/su.d.ts.map +1 -1
- package/dist/commands/su.js +45 -14
- package/dist/commands/sudo.d.ts.map +1 -1
- package/dist/commands/sudo.js +5 -0
- package/dist/commands/tail.d.ts +5 -0
- package/dist/commands/tail.d.ts.map +1 -1
- package/dist/commands/tail.js +15 -3
- package/dist/commands/tar.d.ts +5 -0
- package/dist/commands/tar.d.ts.map +1 -1
- package/dist/commands/tar.js +40 -10
- package/dist/commands/tee.d.ts +5 -0
- package/dist/commands/tee.d.ts.map +1 -1
- package/dist/commands/tee.js +5 -0
- package/dist/commands/touch.d.ts +5 -0
- package/dist/commands/touch.d.ts.map +1 -1
- package/dist/commands/touch.js +5 -0
- package/dist/commands/tr.d.ts.map +1 -1
- package/dist/commands/tr.js +45 -10
- package/dist/commands/tree.d.ts +5 -0
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +5 -0
- package/dist/commands/true.d.ts +10 -0
- package/dist/commands/true.d.ts.map +1 -1
- package/dist/commands/true.js +10 -0
- package/dist/commands/type.d.ts +5 -0
- package/dist/commands/type.d.ts.map +1 -1
- package/dist/commands/type.js +5 -0
- package/dist/commands/uname.d.ts +5 -0
- package/dist/commands/uname.d.ts.map +1 -1
- package/dist/commands/uname.js +5 -0
- package/dist/commands/uniq.d.ts +5 -0
- package/dist/commands/uniq.d.ts.map +1 -1
- package/dist/commands/uniq.js +5 -0
- package/dist/commands/unset.d.ts +5 -0
- package/dist/commands/unset.d.ts.map +1 -1
- package/dist/commands/unset.js +5 -0
- package/dist/commands/uptime.d.ts +5 -0
- package/dist/commands/uptime.d.ts.map +1 -1
- package/dist/commands/uptime.js +5 -0
- package/dist/commands/wc.d.ts +5 -0
- package/dist/commands/wc.d.ts.map +1 -1
- package/dist/commands/wc.js +5 -0
- package/dist/commands/wget.d.ts +5 -0
- package/dist/commands/wget.d.ts.map +1 -1
- package/dist/commands/wget.js +5 -0
- package/dist/commands/who.d.ts +5 -0
- package/dist/commands/who.d.ts.map +1 -1
- package/dist/commands/who.js +5 -0
- package/dist/commands/whoami.d.ts +5 -0
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +5 -0
- package/dist/commands/xargs.d.ts +5 -0
- package/dist/commands/xargs.d.ts.map +1 -1
- package/dist/commands/xargs.js +5 -0
- package/dist/self-standalone.js +254 -30
- package/dist/types/commands.d.ts +36 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/utils/tokenize.d.ts +20 -0
- package/dist/utils/tokenize.d.ts.map +1 -0
- package/dist/utils/tokenize.js +74 -0
- package/examples/web.min.js +2 -2
- package/package.json +1 -1
- package/src/SSHClient/index.ts +6 -3
- package/src/SSHMimic/executor.ts +21 -44
- package/src/SSHMimic/index.ts +7 -5
- package/src/SSHMimic/sftp.ts +28 -21
- package/src/VirtualShell/shell.ts +34 -4
- package/src/VirtualShell/shellParser.ts +2 -103
- package/src/VirtualUserManager/index.ts +43 -19
- package/src/commands/adduser.ts +86 -13
- package/src/commands/alias.ts +5 -0
- package/src/commands/apt.ts +5 -0
- package/src/commands/awk.ts +154 -29
- package/src/commands/cd.ts +0 -4
- package/src/commands/clear.ts +5 -0
- package/src/commands/command-helpers.ts +9 -0
- package/src/commands/declare.ts +5 -0
- package/src/commands/deluser.ts +84 -7
- package/src/commands/df.ts +5 -0
- package/src/commands/du.ts +5 -0
- package/src/commands/export.ts +5 -0
- package/src/commands/grep.ts +21 -8
- package/src/commands/groups.ts +5 -0
- package/src/commands/gzip.ts +54 -28
- package/src/commands/head.ts +14 -4
- package/src/commands/htop.ts +5 -0
- package/src/commands/kill.ts +5 -0
- package/src/commands/ln.ts +22 -0
- package/src/commands/ls.ts +17 -0
- package/src/commands/lsb-release.ts +5 -0
- package/src/commands/mkdir.ts +5 -0
- package/src/commands/mv.ts +5 -0
- package/src/commands/nano.ts +5 -0
- package/src/commands/neofetch.ts +8 -6
- package/src/commands/passwd.ts +35 -12
- package/src/commands/ping.ts +5 -0
- package/src/commands/printf.ts +30 -13
- package/src/commands/ps.ts +5 -0
- package/src/commands/read.ts +5 -0
- package/src/commands/registry.ts +4 -1
- package/src/commands/rm.ts +5 -0
- package/src/commands/runtime.ts +1 -61
- package/src/commands/sed.ts +5 -0
- package/src/commands/set.ts +5 -24
- package/src/commands/sh.ts +9 -3
- package/src/commands/shift.ts +10 -0
- package/src/commands/sleep.ts +5 -0
- package/src/commands/sort.ts +5 -0
- package/src/commands/source.ts +5 -0
- package/src/commands/stat.ts +61 -0
- package/src/commands/su.ts +54 -16
- package/src/commands/sudo.ts +5 -0
- package/src/commands/tail.ts +17 -3
- package/src/commands/tar.ts +38 -15
- package/src/commands/tee.ts +5 -0
- package/src/commands/touch.ts +5 -0
- package/src/commands/tr.ts +54 -10
- package/src/commands/tree.ts +5 -0
- package/src/commands/true.ts +10 -0
- package/src/commands/type.ts +5 -0
- package/src/commands/uname.ts +5 -0
- package/src/commands/uniq.ts +5 -0
- package/src/commands/unset.ts +5 -0
- package/src/commands/uptime.ts +5 -0
- package/src/commands/wc.ts +5 -0
- package/src/commands/wget.ts +5 -0
- package/src/commands/who.ts +5 -0
- package/src/commands/whoami.ts +5 -0
- package/src/commands/xargs.ts +5 -0
- package/src/self-standalone.ts +316 -33
- package/src/types/commands.ts +37 -0
- package/src/utils/tokenize.ts +78 -0
- package/builds/web-iife.min.js +0 -13
- package/builds/web-iife.min.js.map +0 -7
package/src/commands/adduser.ts
CHANGED
|
@@ -1,30 +1,103 @@
|
|
|
1
|
-
import type { ShellModule } from "../types/commands";
|
|
1
|
+
import type { CommandResult, ShellModule } from "../types/commands";
|
|
2
|
+
import type { VirtualShell } from "../VirtualShell";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* Add a new user
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Add a new user interactively.
|
|
6
|
+
*
|
|
7
|
+
* Usage: `adduser <username>`
|
|
8
|
+
*
|
|
9
|
+
* Prompts for:
|
|
10
|
+
* New password: ****
|
|
11
|
+
* Retype new password: ****
|
|
12
|
+
*
|
|
13
|
+
* Mirrors the real `adduser` behaviour — password is never passed on the
|
|
14
|
+
* command line. Root-only.
|
|
8
15
|
*/
|
|
9
16
|
export const adduserCommand: ShellModule = {
|
|
10
17
|
name: "adduser",
|
|
11
18
|
description: "Add a new user",
|
|
12
19
|
category: "users",
|
|
13
|
-
params: ["<username>
|
|
14
|
-
run:
|
|
20
|
+
params: ["<username>"],
|
|
21
|
+
run: ({ authUser, shell, args }) => {
|
|
15
22
|
if (authUser !== "root") {
|
|
16
|
-
return { stderr: "adduser: permission denied", exitCode: 1 };
|
|
23
|
+
return { stderr: "adduser: permission denied\n", exitCode: 1 };
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
const
|
|
20
|
-
if (!username
|
|
26
|
+
const username = args[0];
|
|
27
|
+
if (!username) {
|
|
21
28
|
return {
|
|
22
|
-
stderr: "
|
|
29
|
+
stderr: "Usage: adduser <username>\n",
|
|
23
30
|
exitCode: 1,
|
|
24
31
|
};
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
// Reject if user already exists
|
|
35
|
+
if (shell.users.listUsers().includes(username)) {
|
|
36
|
+
return {
|
|
37
|
+
stderr: `adduser: user '${username}' already exists\n`,
|
|
38
|
+
exitCode: 1,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let newPassword = "";
|
|
43
|
+
type Step = "new" | "retype";
|
|
44
|
+
let step: Step = "new";
|
|
45
|
+
|
|
46
|
+
const onPassword = async (
|
|
47
|
+
input: string,
|
|
48
|
+
sh: VirtualShell,
|
|
49
|
+
): Promise<{ result: CommandResult | null; nextPrompt?: string }> => {
|
|
50
|
+
if (step === "new") {
|
|
51
|
+
if (input.length < 1) {
|
|
52
|
+
return {
|
|
53
|
+
result: {
|
|
54
|
+
stderr: "adduser: password cannot be empty\n",
|
|
55
|
+
exitCode: 1,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
newPassword = input;
|
|
60
|
+
step = "retype";
|
|
61
|
+
return { result: null, nextPrompt: "Retype new password: " };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// step === "retype"
|
|
65
|
+
if (input !== newPassword) {
|
|
66
|
+
return {
|
|
67
|
+
result: {
|
|
68
|
+
stderr: "adduser: passwords do not match — user not created\n",
|
|
69
|
+
exitCode: 1,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await sh.users.addUser(username, newPassword);
|
|
75
|
+
return {
|
|
76
|
+
result: {
|
|
77
|
+
stdout: `${[
|
|
78
|
+
`Adding user '${username}' ...`,
|
|
79
|
+
`Adding new group '${username}' (1001) ...`,
|
|
80
|
+
`Adding new user '${username}' (1001) with group '${username}' ...`,
|
|
81
|
+
`Creating home directory '/home/${username}' ...`,
|
|
82
|
+
`passwd: password set for '${username}'`,
|
|
83
|
+
`adduser: done.`,
|
|
84
|
+
].join("\n")}\n`,
|
|
85
|
+
exitCode: 0,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
sudoChallenge: {
|
|
92
|
+
username,
|
|
93
|
+
targetUser: username,
|
|
94
|
+
commandLine: null,
|
|
95
|
+
loginShell: false,
|
|
96
|
+
prompt: "New password: ",
|
|
97
|
+
mode: "passwd",
|
|
98
|
+
onPassword,
|
|
99
|
+
},
|
|
100
|
+
exitCode: 0,
|
|
101
|
+
};
|
|
29
102
|
},
|
|
30
103
|
};
|
package/src/commands/alias.ts
CHANGED
|
@@ -41,6 +41,11 @@ export const aliasCommand: ShellModule = {
|
|
|
41
41
|
},
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Remove shell aliases.
|
|
46
|
+
* @category shell
|
|
47
|
+
* @params ["<name...>"]
|
|
48
|
+
*/
|
|
44
49
|
export const unaliasCommand: ShellModule = {
|
|
45
50
|
name: "unalias",
|
|
46
51
|
description: "Remove alias definitions",
|
package/src/commands/apt.ts
CHANGED
|
@@ -162,6 +162,11 @@ export const aptCommand: ShellModule = {
|
|
|
162
162
|
},
|
|
163
163
|
};
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Query the package cache and retrieve package information.
|
|
167
|
+
* @category package
|
|
168
|
+
* @params ["<search|show|policy> [pkg]"]
|
|
169
|
+
*/
|
|
165
170
|
export const aptCacheCommand: ShellModule = {
|
|
166
171
|
name: "apt-cache",
|
|
167
172
|
description: "Query the package cache",
|
package/src/commands/awk.ts
CHANGED
|
@@ -1,44 +1,169 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
2
|
import { getFlag } from "./command-helpers";
|
|
3
|
+
import { resolvePath, assertPathAccess } from "./helpers";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Minimal
|
|
6
|
-
* @category text
|
|
7
|
-
* @params ["[-F <sep>] '<program>' [file]"]
|
|
6
|
+
* Minimal awk-like pattern scanner.
|
|
8
7
|
*
|
|
9
|
-
* Supported
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* Supported:
|
|
9
|
+
* - `NR==N` pattern (line number condition)
|
|
10
|
+
* - `NF` (number of fields)
|
|
11
|
+
* - `/regex/` pattern
|
|
12
|
+
* - `{ print $N, $M, ... }` action
|
|
13
|
+
* - `{ print }` / `{ print $0 }`
|
|
14
|
+
* - `BEGIN { ... }` and `END { ... }` blocks (no side effects)
|
|
15
|
+
* - `$NF` (last field)
|
|
16
|
+
* - `-F sep` field separator
|
|
14
17
|
*/
|
|
15
18
|
export const awkCommand: ShellModule = {
|
|
16
19
|
name: "awk",
|
|
17
|
-
description: "Pattern scanning and processing language
|
|
20
|
+
description: "Pattern scanning and processing language",
|
|
18
21
|
category: "text",
|
|
19
22
|
params: ["[-F <sep>] '<program>' [file]"],
|
|
20
|
-
run: ({ args, stdin }) => {
|
|
23
|
+
run: ({ authUser, args, stdin, cwd, shell }) => {
|
|
21
24
|
const sep = (getFlag(args, ["-F"]) as string | undefined) ?? " ";
|
|
22
|
-
const
|
|
25
|
+
const nonFlagArgs = args.filter((a) => !a.startsWith("-") && a !== sep);
|
|
26
|
+
const prog = nonFlagArgs[0];
|
|
27
|
+
const fileArg = nonFlagArgs[1];
|
|
28
|
+
|
|
23
29
|
if (!prog) return { stderr: "awk: no program", exitCode: 1 };
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
31
|
+
let input = stdin ?? "";
|
|
32
|
+
if (fileArg) {
|
|
33
|
+
const filePath = resolvePath(cwd, fileArg);
|
|
34
|
+
try {
|
|
35
|
+
assertPathAccess(authUser, filePath, "awk");
|
|
36
|
+
input = shell.vfs.readFile(filePath);
|
|
37
|
+
} catch {
|
|
38
|
+
return { stderr: `awk: ${fileArg}: No such file or directory`, exitCode: 1 };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const lines = input.split("\n");
|
|
43
|
+
// Remove empty last element if input ends with \n
|
|
44
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
45
|
+
|
|
46
|
+
// Parse program into clauses: [pattern, action]
|
|
47
|
+
type Clause = { pattern: string; action: string };
|
|
48
|
+
const clauses: Clause[] = [];
|
|
49
|
+
|
|
50
|
+
const progTrim = prog.trim();
|
|
51
|
+
|
|
52
|
+
// Handle single unbraced pattern (NR==2, /regex/)
|
|
53
|
+
if (!progTrim.startsWith("{") && !progTrim.includes("{")) {
|
|
54
|
+
clauses.push({ pattern: progTrim, action: "print $0" });
|
|
55
|
+
} else {
|
|
56
|
+
// Parse "pattern { action } pattern2 { action2 }"
|
|
57
|
+
const clauseRe = /([^{]*)\{([^}]*)\}/g;
|
|
58
|
+
let m2 = clauseRe.exec(progTrim);
|
|
59
|
+
while (m2 !== null) {
|
|
60
|
+
clauses.push({ pattern: m2[1]!.trim(), action: m2[2]!.trim() });
|
|
61
|
+
m2 = clauseRe.exec(progTrim);
|
|
62
|
+
}
|
|
63
|
+
if (clauses.length === 0) {
|
|
64
|
+
clauses.push({ pattern: "", action: progTrim.replace(/[{}]/g, "").trim() });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const out: string[] = [];
|
|
69
|
+
|
|
70
|
+
// BEGIN / END
|
|
71
|
+
const beginClause = clauses.find((c) => c.pattern === "BEGIN");
|
|
72
|
+
const endClause = clauses.find((c) => c.pattern === "END");
|
|
73
|
+
const mainClauses = clauses.filter((c) => c.pattern !== "BEGIN" && c.pattern !== "END");
|
|
74
|
+
|
|
75
|
+
function splitFields(line: string): string[] {
|
|
76
|
+
if (sep === " ") return line.trim().split(/\s+/).filter(Boolean);
|
|
77
|
+
return line.split(sep);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function evalAction(action: string, line: string, nr: number): void {
|
|
81
|
+
const parts = splitFields(line);
|
|
82
|
+
const nf = parts.length;
|
|
83
|
+
|
|
84
|
+
// Expand variables
|
|
85
|
+
const resolve = (expr: string): string => {
|
|
86
|
+
expr = expr.trim();
|
|
87
|
+
if (expr === "NR") return String(nr);
|
|
88
|
+
if (expr === "NF") return String(nf);
|
|
89
|
+
if (expr === "$0") return line;
|
|
90
|
+
if (expr === "$NF") return parts[nf - 1] ?? "";
|
|
91
|
+
if (/^\$\d+$/.test(expr)) return parts[parseInt(expr.slice(1), 10) - 1] ?? "";
|
|
92
|
+
// Arithmetic NR+1, NF-1
|
|
93
|
+
const arith = expr.replace(/\bNR\b/g, String(nr)).replace(/\bNF\b/g, String(nf));
|
|
94
|
+
if (/^[\d\s+\-*/()]+$/.test(arith)) {
|
|
95
|
+
// biome-ignore lint/security/noGlobalEval: safe arithmetic — input contains only digits and operators after variable substitution
|
|
96
|
+
try { return String(Function(`"use strict"; return (${arith});`)()); } catch {} }
|
|
97
|
+
return expr.replace(/"/g, "");
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const stmts = action.split(";").map((s) => s.trim()).filter(Boolean);
|
|
101
|
+
for (const stmt of stmts) {
|
|
102
|
+
if (stmt === "print" || stmt === "print $0") {
|
|
103
|
+
out.push(line);
|
|
104
|
+
} else if (stmt.startsWith("print ")) {
|
|
105
|
+
const args2 = stmt.slice(6).split(/\s*,\s*/);
|
|
106
|
+
out.push(args2.map(resolve).join("\t"));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function matchPattern(pattern: string, line: string, nr: number): boolean {
|
|
112
|
+
if (!pattern) return true;
|
|
113
|
+
if (pattern === "1") return true;
|
|
114
|
+
|
|
115
|
+
// NR==N or NR>N etc.
|
|
116
|
+
const nrCond = pattern.match(/^NR\s*([=!<>]=?|==)\s*(\d+)$/);
|
|
117
|
+
if (nrCond) {
|
|
118
|
+
const op = nrCond[1]!;
|
|
119
|
+
const val = parseInt(nrCond[2]!, 10);
|
|
120
|
+
switch (op) {
|
|
121
|
+
case "==": return nr === val;
|
|
122
|
+
case "!=": return nr !== val;
|
|
123
|
+
case ">": return nr > val;
|
|
124
|
+
case ">=": return nr >= val;
|
|
125
|
+
case "<": return nr < val;
|
|
126
|
+
case "<=": return nr <= val;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// NR%N==M
|
|
131
|
+
const nrMod = pattern.match(/^NR%(\d+)==(\d+)$/);
|
|
132
|
+
if (nrMod) {
|
|
133
|
+
return nr % parseInt(nrMod[1]!, 10) === parseInt(nrMod[2]!, 10);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// /regex/ pattern
|
|
137
|
+
if (pattern.startsWith("/") && pattern.endsWith("/")) {
|
|
138
|
+
try {
|
|
139
|
+
return new RegExp(pattern.slice(1, -1)).test(line);
|
|
140
|
+
} catch { return false; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// $N~/regex/
|
|
144
|
+
const fieldMatch = pattern.match(/^\$(\d+)~\/(.*)\/$/);
|
|
145
|
+
if (fieldMatch) {
|
|
146
|
+
const parts = splitFields(line);
|
|
147
|
+
const field = parts[parseInt(fieldMatch[1]!, 10) - 1] ?? "";
|
|
148
|
+
try { return new RegExp(fieldMatch[2]!).test(field); } catch { return false; }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (beginClause) evalAction(beginClause.action, "", 0);
|
|
155
|
+
|
|
156
|
+
for (let nr = 1; nr <= lines.length; nr++) {
|
|
157
|
+
const line = lines[nr - 1]!;
|
|
158
|
+
for (const clause of mainClauses) {
|
|
159
|
+
if (matchPattern(clause.pattern, line, nr)) {
|
|
160
|
+
evalAction(clause.action, line, nr);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (endClause) evalAction(endClause.action, "", lines.length + 1);
|
|
166
|
+
|
|
167
|
+
return { stdout: out.join("\n") + (out.length > 0 ? "\n" : ""), exitCode: 0 };
|
|
43
168
|
},
|
|
44
169
|
};
|
package/src/commands/cd.ts
CHANGED
package/src/commands/clear.ts
CHANGED
|
@@ -15,11 +15,20 @@ function matchFlagToken(
|
|
|
15
15
|
return { matched: true, inlineValue: null };
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// --flag=value style
|
|
18
19
|
const prefix = `${flag}=`;
|
|
19
20
|
if (token.startsWith(prefix)) {
|
|
20
21
|
return { matched: true, inlineValue: token.slice(prefix.length) };
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
// Short flag inline value: -f2, -d: (single char flag like -f, -d, -n)
|
|
25
|
+
// Only applies to single-char flags (-X), not long flags (--flag)
|
|
26
|
+
if (flag.length === 2 && flag.startsWith("-") && !flag.startsWith("--")) {
|
|
27
|
+
if (token.startsWith(flag) && token.length > flag.length) {
|
|
28
|
+
return { matched: true, inlineValue: token.slice(flag.length) };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
return { matched: false, inlineValue: null };
|
|
24
33
|
}
|
|
25
34
|
|
package/src/commands/declare.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
2
|
import { ifFlag } from "./command-helpers";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Declare variables and give them attributes (integer, readonly, export, array).
|
|
6
|
+
* @category shell
|
|
7
|
+
* @params ["[-i] [-r] [-x] [-a] [name[=value]...]"]
|
|
8
|
+
*/
|
|
4
9
|
export const declareCommand: ShellModule = {
|
|
5
10
|
name: "declare",
|
|
6
11
|
aliases: ["local", "typeset"],
|
package/src/commands/deluser.ts
CHANGED
|
@@ -1,21 +1,98 @@
|
|
|
1
|
-
import type { ShellModule } from "../types/commands";
|
|
1
|
+
import type { CommandResult, ShellModule } from "../types/commands";
|
|
2
|
+
import type { VirtualShell } from "../VirtualShell";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Delete a user. Root-only.
|
|
6
|
+
*
|
|
7
|
+
* Without `-f`: prompts for confirmation — the user must type the exact
|
|
8
|
+
* username to proceed.
|
|
9
|
+
*
|
|
10
|
+
* With `-f` / `--force`: deletes without confirmation.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* deluser <username>
|
|
14
|
+
* deluser -f <username>
|
|
15
|
+
*/
|
|
3
16
|
export const deluserCommand: ShellModule = {
|
|
4
17
|
name: "deluser",
|
|
5
18
|
description: "Delete a user",
|
|
6
19
|
category: "users",
|
|
7
|
-
params: ["<username>"],
|
|
20
|
+
params: ["[-f] <username>"],
|
|
8
21
|
run: async ({ authUser, args, shell }) => {
|
|
9
22
|
if (authUser !== "root") {
|
|
10
|
-
return { stderr: "deluser: permission denied", exitCode: 1 };
|
|
23
|
+
return { stderr: "deluser: permission denied\n", exitCode: 1 };
|
|
11
24
|
}
|
|
12
25
|
|
|
13
|
-
const
|
|
26
|
+
const force =
|
|
27
|
+
args.includes("-f") ||
|
|
28
|
+
args.includes("--force") ||
|
|
29
|
+
args.includes("-y");
|
|
30
|
+
const username = args.find((a) => !a.startsWith("-"));
|
|
31
|
+
|
|
14
32
|
if (!username) {
|
|
15
|
-
return {
|
|
33
|
+
return {
|
|
34
|
+
stderr: "Usage: deluser [-f] <username>\n",
|
|
35
|
+
exitCode: 1,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!shell.users.listUsers().includes(username)) {
|
|
40
|
+
return {
|
|
41
|
+
stderr: `deluser: user '${username}' does not exist\n`,
|
|
42
|
+
exitCode: 1,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (username === "root") {
|
|
47
|
+
return {
|
|
48
|
+
stderr: "deluser: cannot remove the root account\n",
|
|
49
|
+
exitCode: 1,
|
|
50
|
+
};
|
|
16
51
|
}
|
|
17
52
|
|
|
18
|
-
|
|
19
|
-
|
|
53
|
+
// Force mode — delete without confirmation
|
|
54
|
+
if (force) {
|
|
55
|
+
await shell.users.deleteUser(username);
|
|
56
|
+
return {
|
|
57
|
+
stdout: `Removing user '${username}' ...\ndeluser: done.\n`,
|
|
58
|
+
exitCode: 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Interactive confirmation
|
|
63
|
+
const onPassword = async (
|
|
64
|
+
input: string,
|
|
65
|
+
sh: VirtualShell,
|
|
66
|
+
): Promise<{ result: CommandResult | null; nextPrompt?: string }> => {
|
|
67
|
+
if (input.trim() !== username) {
|
|
68
|
+
return {
|
|
69
|
+
result: {
|
|
70
|
+
stderr: "deluser: confirmation did not match — user not deleted\n",
|
|
71
|
+
exitCode: 1,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await sh.users.deleteUser(username);
|
|
77
|
+
return {
|
|
78
|
+
result: {
|
|
79
|
+
stdout: `Removing user '${username}' ...\ndeluser: done.\n`,
|
|
80
|
+
exitCode: 0,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
sudoChallenge: {
|
|
87
|
+
username,
|
|
88
|
+
targetUser: username,
|
|
89
|
+
commandLine: null,
|
|
90
|
+
loginShell: false,
|
|
91
|
+
prompt: `Warning: deleting user '${username}'.\nType the username to confirm: `,
|
|
92
|
+
mode: "confirm",
|
|
93
|
+
onPassword,
|
|
94
|
+
},
|
|
95
|
+
exitCode: 0,
|
|
96
|
+
};
|
|
20
97
|
},
|
|
21
98
|
};
|
package/src/commands/df.ts
CHANGED
package/src/commands/du.ts
CHANGED
|
@@ -2,6 +2,11 @@ import type { ShellModule } from "../types/commands";
|
|
|
2
2
|
import { ifFlag } from "./command-helpers";
|
|
3
3
|
import { resolvePath } from "./helpers";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Estimate file and directory space usage.
|
|
7
|
+
* @category system
|
|
8
|
+
* @params ["[-h] [-s] [path]"]
|
|
9
|
+
*/
|
|
5
10
|
export const duCommand: ShellModule = {
|
|
6
11
|
name: "du",
|
|
7
12
|
description: "Estimate file space usage",
|
package/src/commands/export.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Set or display shell environment variables for child processes.
|
|
5
|
+
* @category shell
|
|
6
|
+
* @params ["[VAR=value]"]
|
|
7
|
+
*/
|
|
3
8
|
export const exportCommand: ShellModule = {
|
|
4
9
|
name: "export",
|
|
5
10
|
description: "Set shell environment variable",
|
package/src/commands/grep.ts
CHANGED
|
@@ -14,12 +14,15 @@ export const grepCommand: ShellModule = {
|
|
|
14
14
|
params: ["[-i] [-v] [-n] [-r] <pattern> [file...]"],
|
|
15
15
|
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
16
16
|
const { flags, positionals } = parseArgs(args, {
|
|
17
|
-
flags: ["-i", "-v", "-n", "-r"],
|
|
17
|
+
flags: ["-i", "-v", "-n", "-r", "-c", "-l", "-L", "-q", "--quiet", "--silent"],
|
|
18
18
|
});
|
|
19
|
-
const caseInsensitive
|
|
20
|
-
const invertMatch
|
|
21
|
-
const showLineNumbers
|
|
22
|
-
const recursive
|
|
19
|
+
const caseInsensitive = flags.has("-i");
|
|
20
|
+
const invertMatch = flags.has("-v");
|
|
21
|
+
const showLineNumbers = flags.has("-n");
|
|
22
|
+
const recursive = flags.has("-r");
|
|
23
|
+
const countOnly = flags.has("-c");
|
|
24
|
+
const filesWithMatches = flags.has("-l");
|
|
25
|
+
const quiet = flags.has("-q") || flags.has("--quiet") || flags.has("--silent");
|
|
23
26
|
const pattern = positionals[0];
|
|
24
27
|
const files = positionals.slice(1);
|
|
25
28
|
|
|
@@ -73,7 +76,10 @@ export const grepCommand: ShellModule = {
|
|
|
73
76
|
|
|
74
77
|
if (files.length === 0) {
|
|
75
78
|
if (!stdin) return { stdout: "", exitCode: 1 };
|
|
76
|
-
|
|
79
|
+
const matched = matchLines(stdin);
|
|
80
|
+
if (countOnly) return { stdout: `${matched.length}\n`, exitCode: matched.length > 0 ? 0 : 1 };
|
|
81
|
+
if (quiet) return { exitCode: matched.length > 0 ? 0 : 1 };
|
|
82
|
+
results.push(...matched);
|
|
77
83
|
} else {
|
|
78
84
|
const resolvedPaths = files.flatMap((f) => {
|
|
79
85
|
const target = resolvePath(cwd, f);
|
|
@@ -85,7 +91,14 @@ export const grepCommand: ShellModule = {
|
|
|
85
91
|
assertPathAccess(authUser, filePath, "grep");
|
|
86
92
|
const content = shell.vfs.readFile(filePath);
|
|
87
93
|
const prefix = resolvedPaths.length > 1 ? `${file}:` : "";
|
|
88
|
-
|
|
94
|
+
const matched = matchLines(content, prefix);
|
|
95
|
+
if (countOnly) {
|
|
96
|
+
results.push(resolvedPaths.length > 1 ? `${file}:${matched.length}` : String(matched.length));
|
|
97
|
+
} else if (filesWithMatches) {
|
|
98
|
+
if (matched.length > 0) results.push(file);
|
|
99
|
+
} else {
|
|
100
|
+
results.push(...matched);
|
|
101
|
+
}
|
|
89
102
|
} catch {
|
|
90
103
|
return {
|
|
91
104
|
stderr: `grep: ${file}: No such file or directory`,
|
|
@@ -96,7 +109,7 @@ export const grepCommand: ShellModule = {
|
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
return {
|
|
99
|
-
stdout: results.length > 0 ? results.join("\n") : "",
|
|
112
|
+
stdout: results.length > 0 ? `${results.join("\n")}\n` : "",
|
|
100
113
|
exitCode: results.length > 0 ? 0 : 1,
|
|
101
114
|
};
|
|
102
115
|
},
|
package/src/commands/groups.ts
CHANGED