typescript-virtual-container 1.4.3 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/.vscode/settings.json +2 -1
  2. package/README.md +137 -19
  3. package/benchmark-results.txt +21 -21
  4. package/builds/self-standalone.js +322 -266
  5. package/builds/self-standalone.js.map +4 -4
  6. package/builds/standalone-wo-sftp.js +268 -212
  7. package/builds/standalone-wo-sftp.js.map +4 -4
  8. package/builds/standalone.js +268 -212
  9. package/builds/standalone.js.map +4 -4
  10. package/builds/web-full-api.min.js +5 -5
  11. package/builds/web-full-api.min.js.map +3 -3
  12. package/builds/web.min.js +5 -5
  13. package/builds/web.min.js.map +3 -3
  14. package/bun.lock +101 -6
  15. package/dist/Honeypot/index.js +1 -0
  16. package/dist/Honeypot/index.js.map +1 -0
  17. package/dist/SSHClient/index.js +1 -0
  18. package/dist/SSHClient/index.js.map +1 -0
  19. package/dist/SSHMimic/exec.js +1 -0
  20. package/dist/SSHMimic/exec.js.map +1 -0
  21. package/dist/SSHMimic/executor.js +1 -0
  22. package/dist/SSHMimic/executor.js.map +1 -0
  23. package/dist/SSHMimic/hostKey.js +1 -0
  24. package/dist/SSHMimic/hostKey.js.map +1 -0
  25. package/dist/SSHMimic/index.js +1 -0
  26. package/dist/SSHMimic/index.js.map +1 -0
  27. package/dist/SSHMimic/loginBanner.js +1 -0
  28. package/dist/SSHMimic/loginBanner.js.map +1 -0
  29. package/dist/SSHMimic/loginFormat.js +1 -0
  30. package/dist/SSHMimic/loginFormat.js.map +1 -0
  31. package/dist/SSHMimic/prompt.js +1 -0
  32. package/dist/SSHMimic/prompt.js.map +1 -0
  33. package/dist/SSHMimic/sftp.js +1 -0
  34. package/dist/SSHMimic/sftp.js.map +1 -0
  35. package/dist/VirtualFileSystem/binaryPack.js +1 -0
  36. package/dist/VirtualFileSystem/binaryPack.js.map +1 -0
  37. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  38. package/dist/VirtualFileSystem/index.js +1 -0
  39. package/dist/VirtualFileSystem/index.js.map +1 -0
  40. package/dist/VirtualFileSystem/internalTypes.js +1 -0
  41. package/dist/VirtualFileSystem/internalTypes.js.map +1 -0
  42. package/dist/VirtualFileSystem/path.js +1 -0
  43. package/dist/VirtualFileSystem/path.js.map +1 -0
  44. package/dist/VirtualPackageManager/index.js +1 -0
  45. package/dist/VirtualPackageManager/index.js.map +1 -0
  46. package/dist/VirtualShell/index.js +1 -0
  47. package/dist/VirtualShell/index.js.map +1 -0
  48. package/dist/VirtualShell/shell.js +1 -0
  49. package/dist/VirtualShell/shell.js.map +1 -0
  50. package/dist/VirtualShell/shellParser.d.ts.map +1 -1
  51. package/dist/VirtualShell/shellParser.js +3 -1
  52. package/dist/VirtualShell/shellParser.js.map +1 -0
  53. package/dist/VirtualUserManager/index.js +1 -0
  54. package/dist/VirtualUserManager/index.js.map +1 -0
  55. package/dist/commands/adduser.js +1 -0
  56. package/dist/commands/adduser.js.map +1 -0
  57. package/dist/commands/alias.js +1 -0
  58. package/dist/commands/alias.js.map +1 -0
  59. package/dist/commands/apt.js +1 -0
  60. package/dist/commands/apt.js.map +1 -0
  61. package/dist/commands/awk.d.ts.map +1 -1
  62. package/dist/commands/awk.js +2 -2
  63. package/dist/commands/awk.js.map +1 -0
  64. package/dist/commands/base64.js +1 -0
  65. package/dist/commands/base64.js.map +1 -0
  66. package/dist/commands/cat.js +1 -0
  67. package/dist/commands/cat.js.map +1 -0
  68. package/dist/commands/cd.js +3 -2
  69. package/dist/commands/cd.js.map +1 -0
  70. package/dist/commands/chmod.js +1 -0
  71. package/dist/commands/chmod.js.map +1 -0
  72. package/dist/commands/clear.js +1 -0
  73. package/dist/commands/clear.js.map +1 -0
  74. package/dist/commands/command-helpers.js +1 -0
  75. package/dist/commands/command-helpers.js.map +1 -0
  76. package/dist/commands/cp.js +1 -0
  77. package/dist/commands/cp.js.map +1 -0
  78. package/dist/commands/curl.js +1 -0
  79. package/dist/commands/curl.js.map +1 -0
  80. package/dist/commands/cut.js +1 -0
  81. package/dist/commands/cut.js.map +1 -0
  82. package/dist/commands/date.js +1 -0
  83. package/dist/commands/date.js.map +1 -0
  84. package/dist/commands/declare.js +1 -0
  85. package/dist/commands/declare.js.map +1 -0
  86. package/dist/commands/deluser.js +1 -0
  87. package/dist/commands/deluser.js.map +1 -0
  88. package/dist/commands/df.js +1 -0
  89. package/dist/commands/df.js.map +1 -0
  90. package/dist/commands/diff.js +1 -0
  91. package/dist/commands/diff.js.map +1 -0
  92. package/dist/commands/dpkg.js +1 -0
  93. package/dist/commands/dpkg.js.map +1 -0
  94. package/dist/commands/du.js +1 -0
  95. package/dist/commands/du.js.map +1 -0
  96. package/dist/commands/echo.js +1 -0
  97. package/dist/commands/echo.js.map +1 -0
  98. package/dist/commands/env.js +1 -0
  99. package/dist/commands/env.js.map +1 -0
  100. package/dist/commands/exit.js +1 -0
  101. package/dist/commands/exit.js.map +1 -0
  102. package/dist/commands/export.js +1 -0
  103. package/dist/commands/export.js.map +1 -0
  104. package/dist/commands/find.js +1 -0
  105. package/dist/commands/find.js.map +1 -0
  106. package/dist/commands/free.js +1 -0
  107. package/dist/commands/free.js.map +1 -0
  108. package/dist/commands/grep.js +1 -0
  109. package/dist/commands/grep.js.map +1 -0
  110. package/dist/commands/groups.js +1 -0
  111. package/dist/commands/groups.js.map +1 -0
  112. package/dist/commands/gzip.js +1 -0
  113. package/dist/commands/gzip.js.map +1 -0
  114. package/dist/commands/head.js +1 -0
  115. package/dist/commands/head.js.map +1 -0
  116. package/dist/commands/help.js +1 -0
  117. package/dist/commands/help.js.map +1 -0
  118. package/dist/commands/helpers.d.ts.map +1 -1
  119. package/dist/commands/helpers.js +4 -0
  120. package/dist/commands/helpers.js.map +1 -0
  121. package/dist/commands/history.js +1 -0
  122. package/dist/commands/history.js.map +1 -0
  123. package/dist/commands/hostname.js +1 -0
  124. package/dist/commands/hostname.js.map +1 -0
  125. package/dist/commands/htop.js +1 -0
  126. package/dist/commands/htop.js.map +1 -0
  127. package/dist/commands/id.js +1 -0
  128. package/dist/commands/id.js.map +1 -0
  129. package/dist/commands/index.js +1 -0
  130. package/dist/commands/index.js.map +1 -0
  131. package/dist/commands/kill.js +1 -0
  132. package/dist/commands/kill.js.map +1 -0
  133. package/dist/commands/ln.js +1 -0
  134. package/dist/commands/ln.js.map +1 -0
  135. package/dist/commands/ls.d.ts.map +1 -1
  136. package/dist/commands/ls.js +171 -37
  137. package/dist/commands/ls.js.map +1 -0
  138. package/dist/commands/lsb-release.js +1 -0
  139. package/dist/commands/lsb-release.js.map +1 -0
  140. package/dist/commands/man.js +1 -0
  141. package/dist/commands/man.js.map +1 -0
  142. package/dist/commands/mkdir.js +1 -0
  143. package/dist/commands/mkdir.js.map +1 -0
  144. package/dist/commands/mv.js +1 -0
  145. package/dist/commands/mv.js.map +1 -0
  146. package/dist/commands/nano.js +1 -0
  147. package/dist/commands/nano.js.map +1 -0
  148. package/dist/commands/neofetch.js +1 -0
  149. package/dist/commands/neofetch.js.map +1 -0
  150. package/dist/commands/node.js +1 -0
  151. package/dist/commands/node.js.map +1 -0
  152. package/dist/commands/npm.js +1 -0
  153. package/dist/commands/npm.js.map +1 -0
  154. package/dist/commands/passwd.js +1 -0
  155. package/dist/commands/passwd.js.map +1 -0
  156. package/dist/commands/ping.js +1 -0
  157. package/dist/commands/ping.js.map +1 -0
  158. package/dist/commands/printf.js +1 -0
  159. package/dist/commands/printf.js.map +1 -0
  160. package/dist/commands/ps.js +1 -0
  161. package/dist/commands/ps.js.map +1 -0
  162. package/dist/commands/pwd.js +1 -0
  163. package/dist/commands/pwd.js.map +1 -0
  164. package/dist/commands/python.js +1 -0
  165. package/dist/commands/python.js.map +1 -0
  166. package/dist/commands/read.js +1 -0
  167. package/dist/commands/read.js.map +1 -0
  168. package/dist/commands/registry.js +1 -0
  169. package/dist/commands/registry.js.map +1 -0
  170. package/dist/commands/rm.js +1 -0
  171. package/dist/commands/rm.js.map +1 -0
  172. package/dist/commands/runtime.d.ts.map +1 -1
  173. package/dist/commands/runtime.js +37 -0
  174. package/dist/commands/runtime.js.map +1 -0
  175. package/dist/commands/sed.js +1 -0
  176. package/dist/commands/sed.js.map +1 -0
  177. package/dist/commands/seq.js +1 -0
  178. package/dist/commands/seq.js.map +1 -0
  179. package/dist/commands/set.js +1 -0
  180. package/dist/commands/set.js.map +1 -0
  181. package/dist/commands/sh.d.ts.map +1 -1
  182. package/dist/commands/sh.js +9 -4
  183. package/dist/commands/sh.js.map +1 -0
  184. package/dist/commands/shift.js +1 -0
  185. package/dist/commands/shift.js.map +1 -0
  186. package/dist/commands/sleep.js +1 -0
  187. package/dist/commands/sleep.js.map +1 -0
  188. package/dist/commands/sort.js +1 -0
  189. package/dist/commands/sort.js.map +1 -0
  190. package/dist/commands/source.js +1 -0
  191. package/dist/commands/source.js.map +1 -0
  192. package/dist/commands/stat.js +1 -0
  193. package/dist/commands/stat.js.map +1 -0
  194. package/dist/commands/su.js +1 -0
  195. package/dist/commands/su.js.map +1 -0
  196. package/dist/commands/sudo.js +1 -0
  197. package/dist/commands/sudo.js.map +1 -0
  198. package/dist/commands/tail.js +1 -0
  199. package/dist/commands/tail.js.map +1 -0
  200. package/dist/commands/tar.js +1 -0
  201. package/dist/commands/tar.js.map +1 -0
  202. package/dist/commands/tee.js +1 -0
  203. package/dist/commands/tee.js.map +1 -0
  204. package/dist/commands/test.d.ts.map +1 -1
  205. package/dist/commands/test.js +1 -0
  206. package/dist/commands/test.js.map +1 -0
  207. package/dist/commands/touch.js +1 -0
  208. package/dist/commands/touch.js.map +1 -0
  209. package/dist/commands/tr.js +1 -0
  210. package/dist/commands/tr.js.map +1 -0
  211. package/dist/commands/tree.js +1 -0
  212. package/dist/commands/tree.js.map +1 -0
  213. package/dist/commands/true.js +1 -0
  214. package/dist/commands/true.js.map +1 -0
  215. package/dist/commands/type.js +1 -0
  216. package/dist/commands/type.js.map +1 -0
  217. package/dist/commands/uname.js +1 -0
  218. package/dist/commands/uname.js.map +1 -0
  219. package/dist/commands/uniq.js +1 -0
  220. package/dist/commands/uniq.js.map +1 -0
  221. package/dist/commands/unset.js +1 -0
  222. package/dist/commands/unset.js.map +1 -0
  223. package/dist/commands/uptime.js +1 -0
  224. package/dist/commands/uptime.js.map +1 -0
  225. package/dist/commands/wc.js +2 -1
  226. package/dist/commands/wc.js.map +1 -0
  227. package/dist/commands/wget.js +1 -0
  228. package/dist/commands/wget.js.map +1 -0
  229. package/dist/commands/which.js +1 -0
  230. package/dist/commands/which.js.map +1 -0
  231. package/dist/commands/who.js +1 -0
  232. package/dist/commands/who.js.map +1 -0
  233. package/dist/commands/whoami.js +1 -0
  234. package/dist/commands/whoami.js.map +1 -0
  235. package/dist/commands/xargs.js +1 -0
  236. package/dist/commands/xargs.js.map +1 -0
  237. package/dist/index.js +1 -0
  238. package/dist/index.js.map +1 -0
  239. package/dist/modules/linuxRootfs.d.ts +35 -17
  240. package/dist/modules/linuxRootfs.d.ts.map +1 -1
  241. package/dist/modules/linuxRootfs.js +332 -152
  242. package/dist/modules/linuxRootfs.js.map +1 -0
  243. package/dist/modules/neofetch.js +1 -0
  244. package/dist/modules/neofetch.js.map +1 -0
  245. package/dist/modules/shellInteractive.js +1 -0
  246. package/dist/modules/shellInteractive.js.map +1 -0
  247. package/dist/modules/shellRuntime.js +1 -0
  248. package/dist/modules/shellRuntime.js.map +1 -0
  249. package/dist/self-standalone.js +1 -0
  250. package/dist/self-standalone.js.map +1 -0
  251. package/dist/standalone-wo-sftp.js +1 -0
  252. package/dist/standalone-wo-sftp.js.map +1 -0
  253. package/dist/standalone.js +1 -0
  254. package/dist/standalone.js.map +1 -0
  255. package/dist/types/commands.d.ts +1 -1
  256. package/dist/types/commands.d.ts.map +1 -1
  257. package/dist/types/commands.js +1 -0
  258. package/dist/types/commands.js.map +1 -0
  259. package/dist/types/pipeline.js +1 -0
  260. package/dist/types/pipeline.js.map +1 -0
  261. package/dist/types/streams.js +1 -0
  262. package/dist/types/streams.js.map +1 -0
  263. package/dist/types/vfs.js +1 -0
  264. package/dist/types/vfs.js.map +1 -0
  265. package/dist/utils/expand.d.ts +2 -2
  266. package/dist/utils/expand.d.ts.map +1 -1
  267. package/dist/utils/expand.js +336 -124
  268. package/dist/utils/expand.js.map +1 -0
  269. package/dist/utils/perfLogger.js +1 -0
  270. package/dist/utils/perfLogger.js.map +1 -0
  271. package/dist/utils/tokenize.js +1 -0
  272. package/dist/utils/tokenize.js.map +1 -0
  273. package/dist/utils/vfsDiff.js +1 -0
  274. package/dist/utils/vfsDiff.js.map +1 -0
  275. package/dist/web-api.js +1 -0
  276. package/dist/web-api.js.map +1 -0
  277. package/dist/web-full.js +1 -0
  278. package/dist/web-full.js.map +1 -0
  279. package/dist/web.js +1 -0
  280. package/dist/web.js.map +1 -0
  281. package/docs/.nojekyll +1 -0
  282. package/docs/assets/hierarchy.js +1 -0
  283. package/docs/assets/highlight.css +162 -0
  284. package/docs/assets/icons.js +18 -0
  285. package/docs/assets/icons.svg +1 -0
  286. package/docs/assets/main.js +60 -0
  287. package/docs/assets/navigation.js +1 -0
  288. package/docs/assets/search.js +1 -0
  289. package/docs/assets/style.css +1633 -0
  290. package/docs/classes/HoneyPot.html +31 -0
  291. package/docs/classes/SshClient.html +66 -0
  292. package/docs/classes/VirtualFileSystem.html +262 -0
  293. package/docs/classes/VirtualPackageManager.html +63 -0
  294. package/docs/classes/VirtualSftpServer.html +169 -0
  295. package/docs/classes/VirtualShell.html +265 -0
  296. package/docs/classes/VirtualSshServer.html +177 -0
  297. package/docs/classes/VirtualUserManager.html +276 -0
  298. package/docs/docs/.nojekyll +1 -0
  299. package/docs/docs/assets/hierarchy.js +1 -0
  300. package/docs/docs/assets/highlight.css +162 -0
  301. package/docs/docs/assets/icons.js +18 -0
  302. package/docs/docs/assets/icons.svg +1 -0
  303. package/docs/docs/assets/main.js +60 -0
  304. package/docs/docs/assets/navigation.js +1 -0
  305. package/docs/docs/assets/search.js +1 -0
  306. package/docs/docs/assets/style.css +1633 -0
  307. package/docs/docs/hierarchy.html +1 -0
  308. package/docs/docs/index.html +1842 -0
  309. package/docs/docs/media/LICENSE +21 -0
  310. package/docs/docs/modules.html +1 -0
  311. package/docs/functions/assertDiff.html +6 -0
  312. package/docs/functions/diffSnapshots.html +7 -0
  313. package/docs/functions/formatDiff.html +6 -0
  314. package/docs/functions/getArg.html +13 -0
  315. package/docs/functions/getFlag.html +15 -0
  316. package/docs/functions/ifFlag.html +11 -0
  317. package/docs/hierarchy.html +1 -0
  318. package/docs/index.html +1842 -0
  319. package/docs/interfaces/AuditLogEntry.html +6 -0
  320. package/docs/interfaces/CommandContext.html +22 -0
  321. package/docs/interfaces/CommandResult.html +26 -0
  322. package/docs/interfaces/ExecStream.html +11 -0
  323. package/docs/interfaces/HoneyPotStats.html +14 -0
  324. package/docs/interfaces/InstalledPackage.html +20 -0
  325. package/docs/interfaces/NanoEditorSession.html +8 -0
  326. package/docs/interfaces/PackageDefinition.html +30 -0
  327. package/docs/interfaces/PackageFile.html +8 -0
  328. package/docs/interfaces/RemoveOptions.html +4 -0
  329. package/docs/interfaces/ShellEnv.html +6 -0
  330. package/docs/interfaces/ShellModule.html +14 -0
  331. package/docs/interfaces/ShellProperties.html +14 -0
  332. package/docs/interfaces/ShellStream.html +11 -0
  333. package/docs/interfaces/SudoChallenge.html +24 -0
  334. package/docs/interfaces/VfsBaseNode.html +12 -0
  335. package/docs/interfaces/VfsDiff.html +10 -0
  336. package/docs/interfaces/VfsDiffEntry.html +6 -0
  337. package/docs/interfaces/VfsDiffModified.html +10 -0
  338. package/docs/interfaces/VfsDirectoryNode.html +15 -0
  339. package/docs/interfaces/VfsFileNode.html +17 -0
  340. package/docs/interfaces/VfsOptions.html +12 -0
  341. package/docs/interfaces/VfsSnapshot.html +3 -0
  342. package/docs/interfaces/VfsSnapshotBaseNode.html +8 -0
  343. package/docs/interfaces/VfsSnapshotDirectoryNode.html +10 -0
  344. package/docs/interfaces/VfsSnapshotFileNode.html +12 -0
  345. package/docs/interfaces/WriteFileOptions.html +6 -0
  346. package/docs/media/LICENSE +21 -0
  347. package/docs/modules.html +1 -0
  348. package/docs/types/CommandMode.html +2 -0
  349. package/docs/types/CommandOutcome.html +2 -0
  350. package/docs/types/VfsNodeStats.html +2 -0
  351. package/docs/types/VfsNodeType.html +2 -0
  352. package/docs/types/VfsPersistenceMode.html +5 -0
  353. package/docs/types/VfsSnapshotNode.html +2 -0
  354. package/examples/web.min.js +5 -5
  355. package/package.json +7 -4
  356. package/src/VirtualFileSystem/index.ts +11 -9
  357. package/src/VirtualShell/shellParser.ts +3 -2
  358. package/src/bun.d.ts +1 -0
  359. package/src/commands/awk.ts +1 -2
  360. package/src/commands/cd.ts +2 -2
  361. package/src/commands/helpers.ts +3 -0
  362. package/src/commands/ls.ts +210 -41
  363. package/src/commands/runtime.ts +56 -3
  364. package/src/commands/sh.ts +7 -4
  365. package/src/commands/test.ts +4 -2
  366. package/src/commands/wc.ts +1 -1
  367. package/src/modules/linuxRootfs.ts +420 -231
  368. package/src/types/commands.ts +1 -1
  369. package/src/utils/expand.ts +256 -76
  370. package/tests/command-helpers.test.ts +80 -0
  371. package/tests/commands-admin-net.test.ts +441 -0
  372. package/tests/commands-advanced.test.ts +456 -0
  373. package/tests/commands-core.test.ts +562 -0
  374. package/tests/commands-missing.test.ts +570 -0
  375. package/tests/commands-specific-units.test.ts +327 -0
  376. package/tests/commands-text-sys.test.ts +445 -0
  377. package/tests/expand.test.ts +170 -0
  378. package/tests/helpers.test.ts +75 -0
  379. package/tests/test-helper.ts +79 -0
  380. package/tsconfig.json +3 -0
  381. package/typedoc.json +8 -0
  382. package/tests/bun-test-shim.ts +0 -9
@@ -60,7 +60,7 @@ export interface SudoChallenge {
60
60
  * Returns a `CommandResult` written to the terminal, or `null` to show
61
61
  * another prompt (pass `nextPrompt` to change the prompt text).
62
62
  */
63
- onPassword?: (input: string, shell: import("../VirtualShell").VirtualShell) => Promise<{
63
+ onPassword?: (input: string, shell: VirtualShell) => Promise<{
64
64
  result: CommandResult | null;
65
65
  nextPrompt?: string;
66
66
  }>;
@@ -20,35 +20,152 @@
20
20
 
21
21
  // ─── arithmetic evaluator ────────────────────────────────────────────────────
22
22
 
23
+ type ArithToken =
24
+ | { type: "number"; value: number }
25
+ | { type: "plus" | "minus" | "mul" | "div" | "mod" | "pow" | "lparen" | "rparen" };
26
+
27
+ function tokenizeArith(expr: string, env: Record<string, string>): ArithToken[] {
28
+ const tokens: ArithToken[] = [];
29
+ let i = 0;
30
+ while (i < expr.length) {
31
+ const ch = expr[i]!;
32
+ if (/\s/.test(ch)) {
33
+ i++;
34
+ continue;
35
+ }
36
+ if (ch === "+") { tokens.push({ type: "plus" }); i++; continue; }
37
+ if (ch === "-") { tokens.push({ type: "minus" }); i++; continue; }
38
+ if (ch === "*") {
39
+ if (expr[i + 1] === "*") { tokens.push({ type: "pow" }); i += 2; continue; }
40
+ tokens.push({ type: "mul" });
41
+ i++;
42
+ continue;
43
+ }
44
+ if (ch === "/") { tokens.push({ type: "div" }); i++; continue; }
45
+ if (ch === "%") { tokens.push({ type: "mod" }); i++; continue; }
46
+ if (ch === "(") { tokens.push({ type: "lparen" }); i++; continue; }
47
+ if (ch === ")") { tokens.push({ type: "rparen" }); i++; continue; }
48
+ if (/\d/.test(ch)) {
49
+ let j = i + 1;
50
+ while (j < expr.length && /\d/.test(expr[j]!)) j++;
51
+ tokens.push({ type: "number", value: Number(expr.slice(i, j)) });
52
+ i = j;
53
+ continue;
54
+ }
55
+ if (/[A-Za-z_]/.test(ch)) {
56
+ let j = i + 1;
57
+ while (j < expr.length && /[A-Za-z0-9_]/.test(expr[j]!)) j++;
58
+ const name = expr.slice(i, j);
59
+ const raw = env[name];
60
+ const value = raw === undefined || raw === "" ? 0 : Number(raw);
61
+ tokens.push({ type: "number", value: Number.isFinite(value) ? value : 0 });
62
+ i = j;
63
+ continue;
64
+ }
65
+ return [];
66
+ }
67
+ return tokens;
68
+ }
69
+
23
70
  /**
24
- * Evaluate a simple integer arithmetic expression.
71
+ * Evaluate a simple integer arithmetic expression with a bounded parser.
25
72
  * Supports: + - * / % ** unary- ( )
26
- * Variables are resolved from `env` before evaluation.
73
+ * Variables are resolved from `env`.
27
74
  * Returns NaN on syntax error.
28
75
  */
29
76
  export function evalArith(expr: string, env: Record<string, string>): number {
30
- // Substitute variable names before evaluating
31
- const substituted = expr.replace(
32
- /\b([A-Za-z_][A-Za-z0-9_]*)\b/g,
33
- (_, name) => {
34
- const val = env[name];
35
- return val !== undefined && val !== "" ? val : "0";
36
- },
37
- );
38
-
39
- // Whitelist: only digits, operators, spaces, parens
40
- if (!/^[\d\s+\-*/%()^!&|<>=,. ]+$/.test(substituted)) return NaN;
77
+ const trimmed = expr.trim();
78
+ if (trimmed.length === 0 || trimmed.length > 1024) return NaN;
79
+ const tokens = tokenizeArith(trimmed, env);
80
+ if (tokens.length === 0) return NaN;
41
81
 
42
- try {
43
- // Use Function constructor for safe subset (no identifiers remain)
44
- // eslint-disable-next-line no-new-func
45
- const result = Function(
46
- `"use strict"; return (${substituted.replace(/\*\*/g, "**")});`,
47
- )();
48
- return typeof result === "number" ? Math.trunc(result) : NaN;
49
- } catch {
82
+ let index = 0;
83
+
84
+ const peek = () => tokens[index];
85
+ const consume = () => tokens[index++];
86
+
87
+ const parsePrimary = (): number => {
88
+ const token = consume();
89
+ if (!token) return NaN;
90
+ if (token.type === "number") return token.value;
91
+ if (token.type === "lparen") {
92
+ const value = parseExpression();
93
+ if (tokens[index]?.type !== "rparen") return NaN;
94
+ index++;
95
+ return value;
96
+ }
50
97
  return NaN;
51
- }
98
+ };
99
+
100
+ const parseUnary = (): number => {
101
+ const token = peek();
102
+ if (token?.type === "plus") {
103
+ consume();
104
+ return parseUnary();
105
+ }
106
+ if (token?.type === "minus") {
107
+ consume();
108
+ return -parseUnary();
109
+ }
110
+ return parsePrimary();
111
+ };
112
+
113
+ const parsePower = (): number => {
114
+ let left = parseUnary();
115
+ while (peek()?.type === "pow") {
116
+ consume();
117
+ const right = parseUnary();
118
+ left = left ** right;
119
+ }
120
+ return left;
121
+ };
122
+
123
+ const parseTerm = (): number => {
124
+ let left = parsePower();
125
+ while (true) {
126
+ const token = peek();
127
+ if (token?.type === "mul") {
128
+ consume();
129
+ left *= parsePower();
130
+ continue;
131
+ }
132
+ if (token?.type === "div") {
133
+ consume();
134
+ const right = parsePower();
135
+ left = right === 0 ? NaN : left / right;
136
+ continue;
137
+ }
138
+ if (token?.type === "mod") {
139
+ consume();
140
+ const right = parsePower();
141
+ left = right === 0 ? NaN : left % right;
142
+ continue;
143
+ }
144
+ return left;
145
+ }
146
+ };
147
+
148
+ const parseExpression = (): number => {
149
+ let left = parseTerm();
150
+ while (true) {
151
+ const token = peek();
152
+ if (token?.type === "plus") {
153
+ consume();
154
+ left += parseTerm();
155
+ continue;
156
+ }
157
+ if (token?.type === "minus") {
158
+ consume();
159
+ left -= parseTerm();
160
+ continue;
161
+ }
162
+ return left;
163
+ }
164
+ };
165
+
166
+ const result = parseExpression();
167
+ if (!Number.isFinite(result) || index !== tokens.length) return NaN;
168
+ return Math.trunc(result);
52
169
  }
53
170
 
54
171
  // ─── synchronous expansion ───────────────────────────────────────────────────
@@ -105,67 +222,121 @@ function outsideSingleQuotes(
105
222
  * Returns a single-element array when no brace expansion applies.
106
223
  */
107
224
  export function expandBraces(token: string): string[] {
108
- // Find the first { not preceded by $
109
- let depth = 0;
110
- let start = -1;
111
- for (let i = 0; i < token.length; i++) {
112
- const ch = token[i]!;
113
- if (ch === "{" && token[i - 1] !== "$") {
114
- if (depth === 0) start = i;
115
- depth++;
116
- } else if (ch === "}") {
117
- depth--;
118
- if (depth === 0 && start !== -1) {
119
- const prefix = token.slice(0, start);
120
- const inner = token.slice(start + 1, i);
121
- const suffix = token.slice(i + 1);
122
-
123
- // Range: {1..5} or {a..e}
124
- const rangeMatch = inner.match(/^(-?\d+)\.\.(-?\d+)(?:\.\.-?(\d+))?$/) ||
125
- inner.match(/^([a-z])\.\.([a-z])$/);
126
- if (rangeMatch) {
127
- const items: string[] = [];
128
- if (/\d/.test(rangeMatch[1]!)) {
129
- const from = parseInt(rangeMatch[1]!, 10);
130
- const to = parseInt(rangeMatch[2]!, 10);
131
- const step = rangeMatch[3] ? parseInt(rangeMatch[3], 10) : 1;
132
- const inc = from <= to ? step : -step;
133
- for (let n = from; from <= to ? n <= to : n >= to; n += inc) {
134
- items.push(String(n));
225
+ const MaxBraceDepth = 8;
226
+ const MaxBraceExpansions = 256;
227
+
228
+ function expandBracesInternal(value: string, depth: number): string[] {
229
+ if (depth > MaxBraceDepth) return [value];
230
+ // Find the first { not preceded by $
231
+ let braceDepth = 0;
232
+ let start = -1;
233
+ for (let i = 0; i < value.length; i++) {
234
+ const ch = value[i]!;
235
+ if (ch === "{" && value[i - 1] !== "$") {
236
+ if (braceDepth === 0) start = i;
237
+ braceDepth++;
238
+ } else if (ch === "}") {
239
+ braceDepth--;
240
+ if (braceDepth === 0 && start !== -1) {
241
+ const prefix = value.slice(0, start);
242
+ const inner = value.slice(start + 1, i);
243
+ const suffix = value.slice(i + 1);
244
+
245
+ // Range: {1..5} or {a..e}
246
+ const rangeMatch = inner.match(/^(-?\d+)\.\.(-?\d+)(?:\.\.-?(\d+))?$/) ||
247
+ inner.match(/^([a-z])\.\.([a-z])$/);
248
+ if (rangeMatch) {
249
+ const items: string[] = [];
250
+ if (/\d/.test(rangeMatch[1]!)) {
251
+ const from = parseInt(rangeMatch[1]!, 10);
252
+ const to = parseInt(rangeMatch[2]!, 10);
253
+ const step = rangeMatch[3] ? parseInt(rangeMatch[3], 10) : 1;
254
+ const inc = from <= to ? step : -step;
255
+ for (let n = from; from <= to ? n <= to : n >= to; n += inc) {
256
+ items.push(String(n));
257
+ }
258
+ } else {
259
+ const from = rangeMatch[1]!.charCodeAt(0);
260
+ const to = rangeMatch[2]!.charCodeAt(0);
261
+ const inc = from <= to ? 1 : -1;
262
+ for (let c = from; from <= to ? c <= to : c >= to; c += inc) {
263
+ items.push(String.fromCharCode(c));
264
+ }
135
265
  }
136
- } else {
137
- const from = rangeMatch[1]!.charCodeAt(0);
138
- const to = rangeMatch[2]!.charCodeAt(0);
139
- const inc = from <= to ? 1 : -1;
140
- for (let c = from; from <= to ? c <= to : c >= to; c += inc) {
141
- items.push(String.fromCharCode(c));
266
+
267
+ const expanded = items.map((v) => `${prefix}${v}${suffix}`);
268
+ const output: string[] = [];
269
+ for (const item of expanded) {
270
+ output.push(...expandBracesInternal(item, depth + 1));
271
+ if (output.length > MaxBraceExpansions) return [value];
142
272
  }
273
+ return output;
143
274
  }
144
- const expanded = items.map((v) => `${prefix}${v}${suffix}`);
145
- return expanded.flatMap(expandBraces);
146
- }
147
275
 
148
- // Comma list: {a,b,c} — split respecting nested braces
149
- const parts: string[] = [];
150
- let cur = "";
151
- let d2 = 0;
152
- for (const ch2 of inner) {
153
- if (ch2 === "{") { d2++; cur += ch2; }
154
- else if (ch2 === "}") { d2--; cur += ch2; }
155
- else if (ch2 === "," && d2 === 0) { parts.push(cur); cur = ""; }
156
- else { cur += ch2; }
276
+ // Comma list: {a,b,c} — split respecting nested braces
277
+ const parts: string[] = [];
278
+ let cur = "";
279
+ let innerDepth = 0;
280
+ for (const ch2 of inner) {
281
+ if (ch2 === "{") { innerDepth++; cur += ch2; }
282
+ else if (ch2 === "}") { innerDepth--; cur += ch2; }
283
+ else if (ch2 === "," && innerDepth === 0) { parts.push(cur); cur = ""; }
284
+ else { cur += ch2; }
285
+ }
286
+ parts.push(cur);
287
+
288
+ if (parts.length > 1) {
289
+ const output: string[] = [];
290
+ for (const part of parts) {
291
+ output.push(...expandBracesInternal(`${prefix}${part}${suffix}`, depth + 1));
292
+ if (output.length > MaxBraceExpansions) return [value];
293
+ }
294
+ return output;
295
+ }
296
+ break;
157
297
  }
158
- parts.push(cur);
298
+ }
299
+ }
300
+ return [value];
301
+ }
302
+
303
+ return expandBracesInternal(token, 0);
304
+ }
159
305
 
160
- if (parts.length > 1) {
161
- const expanded = parts.map((p) => `${prefix}${p}${suffix}`);
162
- return expanded.flatMap(expandBraces);
306
+ function expandArithmeticChunks(input: string, env: Record<string, string>): string {
307
+ let result = "";
308
+ let index = 0;
309
+ while (index < input.length) {
310
+ if (input[index] === "$" && input[index + 1] === "(" && input[index + 2] === "(") {
311
+ let scan = index + 3;
312
+ let depth = 0;
313
+ while (scan < input.length) {
314
+ const ch = input[scan]!;
315
+ if (ch === "(") {
316
+ depth++;
317
+ } else if (ch === ")") {
318
+ if (depth > 0) {
319
+ depth--;
320
+ } else if (input[scan + 1] === ")") {
321
+ const expr = input.slice(index + 3, scan);
322
+ const value = evalArith(expr, env);
323
+ result += Number.isNaN(value) ? "0" : String(value);
324
+ index = scan + 2;
325
+ break;
326
+ }
163
327
  }
328
+ scan++;
329
+ }
330
+ if (scan >= input.length) {
331
+ result += input.slice(index);
164
332
  break;
165
333
  }
334
+ continue;
166
335
  }
336
+ result += input[index]!
337
+ index++;
167
338
  }
168
- return [token];
339
+ return result;
169
340
  }
170
341
 
171
342
  export function expandSync(
@@ -191,10 +362,7 @@ export function expandSync(
191
362
  s = s.replace(/\$#/g, "0");
192
363
 
193
364
  // $(( arithmetic )) — must come before ${ and $VAR to avoid conflicts
194
- s = s.replace(/\$\(\(([^)]+(?:\([^)]*\)[^)]*)*)\)\)/g, (_, expr) => {
195
- const result = evalArith(expr, env);
196
- return Number.isNaN(result) ? "0" : String(result);
197
- });
365
+ s = expandArithmeticChunks(s, env);
198
366
 
199
367
  // ${#VAR} — string length
200
368
  s = s.replace(/\$\{#([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) =>
@@ -254,6 +422,14 @@ export async function expandAsync(
254
422
  lastExit: number,
255
423
  runCmd: (cmd: string) => Promise<string>,
256
424
  ): Promise<string> {
425
+ const depthKey = "__shellExpandDepth";
426
+ const maxDepth = 8;
427
+ const currentDepth = Number(env[depthKey] ?? "0");
428
+ if (currentDepth >= maxDepth) {
429
+ return expandSync(input, env, lastExit);
430
+ }
431
+ env[depthKey] = String(currentDepth + 1);
432
+ try {
257
433
  // $(cmd) substitution — skip content inside single quotes
258
434
  if (input.includes("$(")) {
259
435
  let result = "";
@@ -308,4 +484,8 @@ export async function expandAsync(
308
484
  }
309
485
 
310
486
  return expandSync(input, env, lastExit);
487
+ } finally {
488
+ if (currentDepth <= 0) delete env[depthKey];
489
+ else env[depthKey] = String(currentDepth);
490
+ }
311
491
  }
@@ -8,6 +8,21 @@ describe("command-helpers", () => {
8
8
  expect(ifFlag(["docs"], ["-l", "--long"])).toBe(false);
9
9
  });
10
10
 
11
+ test("ifFlag with multiple flags checks all", () => {
12
+ expect(ifFlag(["-l", "-h"], ["-l", "--long"])).toBe(true);
13
+ expect(ifFlag(["-h"], ["-l", "--long"])).toBe(false);
14
+ expect(ifFlag(["-l"], ["-l", "--long"])).toBe(true);
15
+ });
16
+
17
+ test("ifFlag empty args returns false", () => {
18
+ expect(ifFlag([], ["-l", "--long"])).toBe(false);
19
+ });
20
+
21
+ test("ifFlag single flag array", () => {
22
+ expect(ifFlag(["-n"], "-n")).toBe(true);
23
+ expect(ifFlag(["-m"], "-n")).toBe(false);
24
+ });
25
+
11
26
  test("getFlag returns value for adjacent and inline forms", () => {
12
27
  expect(getFlag(["-u", "root", "id"], ["-u", "--user"])).toBe("root");
13
28
  expect(getFlag(["--user=alice", "id"], ["-u", "--user"])).toBe("alice");
@@ -16,6 +31,26 @@ describe("command-helpers", () => {
16
31
  expect(getFlag(["pwd"], ["-u", "--user"])).toBeUndefined();
17
32
  });
18
33
 
34
+ test("getFlag with multiple flag aliases", () => {
35
+ expect(getFlag(["-u", "john"], ["-u", "--user", "-U"])).toBe("john");
36
+ expect(getFlag(["--user=mary"], ["-u", "--user"])).toBe("mary");
37
+ expect(getFlag(["-U", "admin"], ["-u", "--user", "-U"])).toBe("admin");
38
+ });
39
+
40
+ test("getFlag inline with equals", () => {
41
+ expect(getFlag(["--option=value"], "--option")).toBe("value");
42
+ expect(getFlag(["--opt=123"], "--opt")).toBe("123");
43
+ });
44
+
45
+ test("getFlag first occurrence", () => {
46
+ expect(getFlag(["-u", "first", "-u", "second"], "-u")).toBe("first");
47
+ });
48
+
49
+ test("getFlag with boolean flag", () => {
50
+ expect(getFlag(["-v"], "-v")).toBe(true);
51
+ expect(getFlag(["-v", "file.txt"], "-v")).toBe("file.txt");
52
+ });
53
+
19
54
  test("getArg skips bool and value flags", () => {
20
55
  const args = ["-i", "-u", "root", "sh", "-c", "whoami"];
21
56
  const options = { flags: ["-i"], flagsWithValue: ["-u"] };
@@ -26,6 +61,29 @@ describe("command-helpers", () => {
26
61
  expect(getArg(args, 3, options)).toBeUndefined();
27
62
  });
28
63
 
64
+ test("getArg with no flags", () => {
65
+ const args = ["file1", "file2", "file3"];
66
+ const options = { flags: [], flagsWithValue: [] };
67
+
68
+ expect(getArg(args, 0, options)).toBe("file1");
69
+ expect(getArg(args, 1, options)).toBe("file2");
70
+ expect(getArg(args, 2, options)).toBe("file3");
71
+ });
72
+
73
+ test("getArg empty args", () => {
74
+ const args: string[] = [];
75
+ const options = { flags: ["-n"] };
76
+
77
+ expect(getArg(args, 0, options)).toBeUndefined();
78
+ });
79
+
80
+ test("getArg skips values for flagsWithValue", () => {
81
+ const args = ["-d", ":", "-f", "1", "input.txt"];
82
+ const options = { flagsWithValue: ["-d", "-f"] };
83
+
84
+ expect(getArg(args, 0, options)).toBe("input.txt");
85
+ });
86
+
29
87
  test("getArg keeps tokens after -- as positional", () => {
30
88
  const args = ["-n", "--", "-n", "hello"];
31
89
  const options = { flags: ["-n"] };
@@ -33,4 +91,26 @@ describe("command-helpers", () => {
33
91
  expect(getArg(args, 0, options)).toBe("-n");
34
92
  expect(getArg(args, 1, options)).toBe("hello");
35
93
  });
94
+
95
+ test("getArg -- blocks all flag processing", () => {
96
+ const args = ["-a", "--", "-a", "-b", "-c"];
97
+ const options = { flags: ["-a", "-b", "-c"] };
98
+
99
+ expect(getArg(args, 0, options)).toBe("-a");
100
+ expect(getArg(args, 1, options)).toBe("-b");
101
+ expect(getArg(args, 2, options)).toBe("-c");
102
+ });
103
+
104
+ test("getArg with mixed flags and positionals", () => {
105
+ const args = ["-v", "file1", "-o", "output.txt", "file2"];
106
+ const options = { flags: ["-v"], flagsWithValue: ["-o"] };
107
+
108
+ expect(getArg(args, 0, options)).toBe("file1");
109
+ expect(getArg(args, 1, options)).toBe("file2");
110
+ });
111
+
112
+ test("getArg at end of args", () => {
113
+ const args = ["a", "b", "c"];
114
+ expect(getArg(args, 5, {})).toBeUndefined();
115
+ });
36
116
  });