@taliesinb/mash 0.0.0-stage → 0.1.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.
Files changed (203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -2
  3. package/bin/mash.ts +253 -0
  4. package/dist/bin/mash.d.ts +39 -0
  5. package/dist/bin/mash.d.ts.map +1 -0
  6. package/dist/bin/mash.js +281 -0
  7. package/dist/bin/mash.js.map +1 -0
  8. package/dist/src/analysis/analyze.d.ts +97 -0
  9. package/dist/src/analysis/analyze.d.ts.map +1 -0
  10. package/dist/src/analysis/analyze.js +1406 -0
  11. package/dist/src/analysis/analyze.js.map +1 -0
  12. package/dist/src/analysis/fs.d.ts +39 -0
  13. package/dist/src/analysis/fs.d.ts.map +1 -0
  14. package/dist/src/analysis/fs.js +117 -0
  15. package/dist/src/analysis/fs.js.map +1 -0
  16. package/dist/src/analysis/outputs.d.ts +16 -0
  17. package/dist/src/analysis/outputs.d.ts.map +1 -0
  18. package/dist/src/analysis/outputs.js +277 -0
  19. package/dist/src/analysis/outputs.js.map +1 -0
  20. package/dist/src/analysis/value.d.ts +76 -0
  21. package/dist/src/analysis/value.d.ts.map +1 -0
  22. package/dist/src/analysis/value.js +248 -0
  23. package/dist/src/analysis/value.js.map +1 -0
  24. package/dist/src/behaviors/defs/core.d.ts +15 -0
  25. package/dist/src/behaviors/defs/core.d.ts.map +1 -0
  26. package/dist/src/behaviors/defs/core.js +1084 -0
  27. package/dist/src/behaviors/defs/core.js.map +1 -0
  28. package/dist/src/behaviors/defs/devtools.d.ts +9 -0
  29. package/dist/src/behaviors/defs/devtools.d.ts.map +1 -0
  30. package/dist/src/behaviors/defs/devtools.js +432 -0
  31. package/dist/src/behaviors/defs/devtools.js.map +1 -0
  32. package/dist/src/behaviors/defs/macos.d.ts +12 -0
  33. package/dist/src/behaviors/defs/macos.d.ts.map +1 -0
  34. package/dist/src/behaviors/defs/macos.js +454 -0
  35. package/dist/src/behaviors/defs/macos.js.map +1 -0
  36. package/dist/src/behaviors/defs/tools.d.ts +9 -0
  37. package/dist/src/behaviors/defs/tools.d.ts.map +1 -0
  38. package/dist/src/behaviors/defs/tools.js +639 -0
  39. package/dist/src/behaviors/defs/tools.js.map +1 -0
  40. package/dist/src/behaviors/dsl.d.ts +95 -0
  41. package/dist/src/behaviors/dsl.d.ts.map +1 -0
  42. package/dist/src/behaviors/dsl.js +267 -0
  43. package/dist/src/behaviors/dsl.js.map +1 -0
  44. package/dist/src/behaviors/index.d.ts +12 -0
  45. package/dist/src/behaviors/index.d.ts.map +1 -0
  46. package/dist/src/behaviors/index.js +18 -0
  47. package/dist/src/behaviors/index.js.map +1 -0
  48. package/dist/src/behaviors/registry.d.ts +45 -0
  49. package/dist/src/behaviors/registry.d.ts.map +1 -0
  50. package/dist/src/behaviors/registry.js +266 -0
  51. package/dist/src/behaviors/registry.js.map +1 -0
  52. package/dist/src/behaviors/roles.d.ts +29 -0
  53. package/dist/src/behaviors/roles.d.ts.map +1 -0
  54. package/dist/src/behaviors/roles.js +198 -0
  55. package/dist/src/behaviors/roles.js.map +1 -0
  56. package/dist/src/behaviors/types.d.ts +120 -0
  57. package/dist/src/behaviors/types.d.ts.map +1 -0
  58. package/dist/src/behaviors/types.js +12 -0
  59. package/dist/src/behaviors/types.js.map +1 -0
  60. package/dist/src/index.d.ts +27 -0
  61. package/dist/src/index.d.ts.map +1 -0
  62. package/dist/src/index.js +24 -0
  63. package/dist/src/index.js.map +1 -0
  64. package/dist/src/integrations/deepsea.d.ts +121 -0
  65. package/dist/src/integrations/deepsea.d.ts.map +1 -0
  66. package/dist/src/integrations/deepsea.js +199 -0
  67. package/dist/src/integrations/deepsea.js.map +1 -0
  68. package/dist/src/machine.d.ts +139 -0
  69. package/dist/src/machine.d.ts.map +1 -0
  70. package/dist/src/machine.js +400 -0
  71. package/dist/src/machine.js.map +1 -0
  72. package/dist/src/parser/ast.d.ts +369 -0
  73. package/dist/src/parser/ast.d.ts.map +1 -0
  74. package/dist/src/parser/ast.js +17 -0
  75. package/dist/src/parser/ast.js.map +1 -0
  76. package/dist/src/parser/parser.d.ts +48 -0
  77. package/dist/src/parser/parser.d.ts.map +1 -0
  78. package/dist/src/parser/parser.js +1950 -0
  79. package/dist/src/parser/parser.js.map +1 -0
  80. package/dist/src/policy/effects.d.ts +86 -0
  81. package/dist/src/policy/effects.d.ts.map +1 -0
  82. package/dist/src/policy/effects.js +53 -0
  83. package/dist/src/policy/effects.js.map +1 -0
  84. package/dist/src/policy/policy.d.ts +130 -0
  85. package/dist/src/policy/policy.d.ts.map +1 -0
  86. package/dist/src/policy/policy.js +532 -0
  87. package/dist/src/policy/policy.js.map +1 -0
  88. package/dist/src/printer/doc.d.ts +151 -0
  89. package/dist/src/printer/doc.d.ts.map +1 -0
  90. package/dist/src/printer/doc.js +409 -0
  91. package/dist/src/printer/doc.js.map +1 -0
  92. package/dist/src/printer/highlight.d.ts +53 -0
  93. package/dist/src/printer/highlight.d.ts.map +1 -0
  94. package/dist/src/printer/highlight.js +339 -0
  95. package/dist/src/printer/highlight.js.map +1 -0
  96. package/dist/src/printer/html.d.ts +72 -0
  97. package/dist/src/printer/html.d.ts.map +1 -0
  98. package/dist/src/printer/html.js +423 -0
  99. package/dist/src/printer/html.js.map +1 -0
  100. package/dist/src/printer/print.d.ts +63 -0
  101. package/dist/src/printer/print.d.ts.map +1 -0
  102. package/dist/src/printer/print.js +484 -0
  103. package/dist/src/printer/print.js.map +1 -0
  104. package/dist/src/printer/summary.d.ts +50 -0
  105. package/dist/src/printer/summary.d.ts.map +1 -0
  106. package/dist/src/printer/summary.js +621 -0
  107. package/dist/src/printer/summary.js.map +1 -0
  108. package/dist/src/trace/events.d.ts +183 -0
  109. package/dist/src/trace/events.d.ts.map +1 -0
  110. package/dist/src/trace/events.js +9 -0
  111. package/dist/src/trace/events.js.map +1 -0
  112. package/dist/src/trace/progress.d.ts +58 -0
  113. package/dist/src/trace/progress.d.ts.map +1 -0
  114. package/dist/src/trace/progress.js +83 -0
  115. package/dist/src/trace/progress.js.map +1 -0
  116. package/dist/src/vm/arith.d.ts +29 -0
  117. package/dist/src/vm/arith.d.ts.map +1 -0
  118. package/dist/src/vm/arith.js +258 -0
  119. package/dist/src/vm/arith.js.map +1 -0
  120. package/dist/src/vm/builtins.d.ts +31 -0
  121. package/dist/src/vm/builtins.d.ts.map +1 -0
  122. package/dist/src/vm/builtins.js +1773 -0
  123. package/dist/src/vm/builtins.js.map +1 -0
  124. package/dist/src/vm/emulate.d.ts +75 -0
  125. package/dist/src/vm/emulate.d.ts.map +1 -0
  126. package/dist/src/vm/emulate.js +380 -0
  127. package/dist/src/vm/emulate.js.map +1 -0
  128. package/dist/src/vm/expand.d.ts +68 -0
  129. package/dist/src/vm/expand.d.ts.map +1 -0
  130. package/dist/src/vm/expand.js +744 -0
  131. package/dist/src/vm/expand.js.map +1 -0
  132. package/dist/src/vm/external.d.ts +34 -0
  133. package/dist/src/vm/external.d.ts.map +1 -0
  134. package/dist/src/vm/external.js +228 -0
  135. package/dist/src/vm/external.js.map +1 -0
  136. package/dist/src/vm/fifo.d.ts +66 -0
  137. package/dist/src/vm/fifo.d.ts.map +1 -0
  138. package/dist/src/vm/fifo.js +232 -0
  139. package/dist/src/vm/fifo.js.map +1 -0
  140. package/dist/src/vm/interp.d.ts +209 -0
  141. package/dist/src/vm/interp.d.ts.map +1 -0
  142. package/dist/src/vm/interp.js +1641 -0
  143. package/dist/src/vm/interp.js.map +1 -0
  144. package/dist/src/vm/io.d.ts +180 -0
  145. package/dist/src/vm/io.d.ts.map +1 -0
  146. package/dist/src/vm/io.js +502 -0
  147. package/dist/src/vm/io.js.map +1 -0
  148. package/dist/src/vm/pattern.d.ts +46 -0
  149. package/dist/src/vm/pattern.d.ts.map +1 -0
  150. package/dist/src/vm/pattern.js +498 -0
  151. package/dist/src/vm/pattern.js.map +1 -0
  152. package/dist/src/vm/proc.d.ts +94 -0
  153. package/dist/src/vm/proc.d.ts.map +1 -0
  154. package/dist/src/vm/proc.js +130 -0
  155. package/dist/src/vm/proc.js.map +1 -0
  156. package/dist/src/vm/state.d.ts +104 -0
  157. package/dist/src/vm/state.d.ts.map +1 -0
  158. package/dist/src/vm/state.js +233 -0
  159. package/dist/src/vm/state.js.map +1 -0
  160. package/dist/src/vm/test.d.ts +11 -0
  161. package/dist/src/vm/test.d.ts.map +1 -0
  162. package/dist/src/vm/test.js +277 -0
  163. package/dist/src/vm/test.js.map +1 -0
  164. package/package.json +63 -4
  165. package/src/analysis/analyze.ts +1395 -0
  166. package/src/analysis/fs.ts +121 -0
  167. package/src/analysis/outputs.ts +242 -0
  168. package/src/analysis/value.ts +264 -0
  169. package/src/behaviors/defs/core.ts +970 -0
  170. package/src/behaviors/defs/devtools.ts +400 -0
  171. package/src/behaviors/defs/macos.ts +426 -0
  172. package/src/behaviors/defs/tools.ts +596 -0
  173. package/src/behaviors/dsl.ts +293 -0
  174. package/src/behaviors/index.ts +19 -0
  175. package/src/behaviors/registry.ts +261 -0
  176. package/src/behaviors/roles.ts +187 -0
  177. package/src/behaviors/types.ts +127 -0
  178. package/src/index.ts +27 -0
  179. package/src/integrations/deepsea.ts +259 -0
  180. package/src/machine.ts +483 -0
  181. package/src/parser/ast.ts +472 -0
  182. package/src/parser/parser.ts +1826 -0
  183. package/src/policy/effects.ts +102 -0
  184. package/src/policy/policy.ts +578 -0
  185. package/src/printer/doc.ts +454 -0
  186. package/src/printer/highlight.ts +283 -0
  187. package/src/printer/html.ts +441 -0
  188. package/src/printer/print.ts +528 -0
  189. package/src/printer/summary.ts +601 -0
  190. package/src/trace/events.ts +102 -0
  191. package/src/trace/progress.ts +119 -0
  192. package/src/vm/arith.ts +236 -0
  193. package/src/vm/builtins.ts +1590 -0
  194. package/src/vm/emulate.ts +337 -0
  195. package/src/vm/expand.ts +721 -0
  196. package/src/vm/external.ts +265 -0
  197. package/src/vm/fifo.ts +226 -0
  198. package/src/vm/interp.ts +1623 -0
  199. package/src/vm/io.ts +570 -0
  200. package/src/vm/pattern.ts +477 -0
  201. package/src/vm/proc.ts +147 -0
  202. package/src/vm/state.ts +267 -0
  203. package/src/vm/test.ts +253 -0
@@ -0,0 +1,1641 @@
1
+ /**
2
+ * The tree-walking interpreter. Every command node emits a `step` event
3
+ * before it runs; simple commands are expanded, resolved, assessed against
4
+ * the policy (through {@link Runtime.assess}, which consults behavior
5
+ * definitions and hooks), and only then have their redirections opened and
6
+ * run. Pipelines run their stages concurrently over in-memory pipes; external
7
+ * commands are real child processes whose stdio is pumped through the VM's
8
+ * channels so it can be observed.
9
+ */
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import { parse, ParseError, literalOf } from "../parser/parser.js";
13
+ import { known } from "../analysis/value.js";
14
+ import { ArithError, evaluateArith } from "./arith.js";
15
+ import { BUILTINS } from "./builtins.js";
16
+ import { Expander, varElements } from "./expand.js";
17
+ import { runExternal } from "./external.js";
18
+ import { emulateFind, emulateXargs } from "./emulate.js";
19
+ import { BrokenPipeError, Capture, ConstInput, FdTable, FsError, HostFile, NullFile, Pipe, } from "./io.js";
20
+ import { matchPattern } from "./pattern.js";
21
+ import { AbortRun, CommandError, DiscardError, ExitSignal, FatalError, LoopSignal, Proc, ReturnSignal } from "./proc.js";
22
+ import { initShellVars, newVar, ShellState } from "./state.js";
23
+ import { evalCond, evalTest } from "./test.js";
24
+ export class Interpreter {
25
+ rt;
26
+ expander;
27
+ constructor(rt) {
28
+ this.rt = rt;
29
+ this.expander = new Expander({
30
+ commandSubst: (node, proc) => this.commandSubst(node, proc),
31
+ processSubst: (node, proc) => this.processSubst(node, proc),
32
+ arith: (expr, proc) => this.arith(expr, proc),
33
+ assign: (proc, name, value) => this.assignScalar(proc, name, value),
34
+ dynamic: (proc, name) => this.dynamicVar(proc, name),
35
+ shellPid: rt.shellPid,
36
+ });
37
+ }
38
+ // -------------------------------------------------------------------------
39
+ // Entry points
40
+ // -------------------------------------------------------------------------
41
+ /** Run a parsed script in `proc` (the top-level shell). Returns the exit status. */
42
+ async runTop(script, proc) {
43
+ let status = 0;
44
+ try {
45
+ status = await this.execTopLevel(script, proc);
46
+ }
47
+ catch (e) {
48
+ status = this.handleTermination(e, proc);
49
+ }
50
+ status = await this.runExitTrap(proc, status);
51
+ return status;
52
+ }
53
+ /**
54
+ * Execute a script's top-level statements like bash reads them: an
55
+ * expansion error discards only the current top-level command, and a
56
+ * syntax error further down (partial parse) is reported when reached.
57
+ */
58
+ async execTopLevel(script, proc) {
59
+ let status = proc.state.lastStatus;
60
+ let discardedLine = -1;
61
+ for (const stmt of script.body) {
62
+ // bash reads one line (a newline-terminated list) at a time; a discard drops the rest of it
63
+ if (this.rt.line(stmt) === discardedLine)
64
+ continue;
65
+ try {
66
+ status = await this.execStmt(stmt, proc);
67
+ }
68
+ catch (e) {
69
+ if (!(e instanceof DiscardError))
70
+ throw e;
71
+ this.errorMessage(proc, e.message);
72
+ status = e.status;
73
+ proc.state.lastStatus = status;
74
+ discardedLine = this.rt.line(stmt);
75
+ }
76
+ }
77
+ if (script.error) {
78
+ this.errorMessage(proc, `syntax error: ${script.error.message}`);
79
+ throw new ExitSignal(2);
80
+ }
81
+ return status;
82
+ }
83
+ handleTermination(e, proc) {
84
+ if (e instanceof ExitSignal)
85
+ return e.status;
86
+ if (e instanceof FatalError || e instanceof DiscardError) {
87
+ this.errorMessage(proc, e.message);
88
+ return e.status;
89
+ }
90
+ if (e instanceof ReturnSignal)
91
+ return e.status;
92
+ if (e instanceof LoopSignal)
93
+ return 0;
94
+ if (e instanceof BrokenPipeError)
95
+ return 141;
96
+ throw e;
97
+ }
98
+ async runExitTrap(proc, status) {
99
+ const trap = proc.state.traps.get('EXIT');
100
+ if (trap === undefined || trap === '')
101
+ return status;
102
+ proc.state.traps.delete('EXIT');
103
+ proc.state.lastStatus = status;
104
+ try {
105
+ const script = this.rt.parse(trap, 'trap EXIT');
106
+ await this.execList(script.body, proc);
107
+ }
108
+ catch (e) {
109
+ if (e instanceof ExitSignal)
110
+ return e.status;
111
+ if (e instanceof AbortRun)
112
+ throw e;
113
+ if (!(e instanceof ParseError || e instanceof FatalError || e instanceof DiscardError || e instanceof ReturnSignal))
114
+ throw e;
115
+ if (e instanceof FatalError || e instanceof DiscardError)
116
+ this.errorMessage(proc, e.message);
117
+ }
118
+ return status;
119
+ }
120
+ // -------------------------------------------------------------------------
121
+ // Output helpers
122
+ // -------------------------------------------------------------------------
123
+ async write(proc, fd, text) {
124
+ const f = proc.fds.get(fd);
125
+ if (!f || !f.writable) {
126
+ if (fd === 1)
127
+ throw new CommandError('write error: Bad file descriptor');
128
+ return;
129
+ }
130
+ await f.write(text);
131
+ }
132
+ errorMessage(proc, message) {
133
+ const f = proc.fds.get(2);
134
+ this.rt.emit({ type: 'error', message, pid: proc.pid });
135
+ if (f?.writable)
136
+ f.write(`mash: ${message}\n`).catch(() => { });
137
+ }
138
+ // -------------------------------------------------------------------------
139
+ // Lists and statements
140
+ // -------------------------------------------------------------------------
141
+ async execList(stmts, proc) {
142
+ let status = proc.state.lastStatus;
143
+ for (const stmt of stmts) {
144
+ status = await this.execStmt(stmt, proc);
145
+ }
146
+ return status;
147
+ }
148
+ async execStmt(stmt, proc) {
149
+ if (this.rt.signal.aborted)
150
+ throw new AbortRun('aborted', 130);
151
+ if (proc.state.options.noexec)
152
+ return 0;
153
+ if (stmt.background) {
154
+ this.startBackground(stmt, proc);
155
+ proc.state.lastStatus = 0;
156
+ return 0;
157
+ }
158
+ const status = await this.execCommand(stmt.command, proc);
159
+ proc.state.lastStatus = status;
160
+ return status;
161
+ }
162
+ startBackground(stmt, proc) {
163
+ const pid = this.rt.nextPid();
164
+ const child = proc.subshell(pid);
165
+ // background jobs do not read the terminal: stdin is /dev/null unless redirected
166
+ const stdin = proc.fds.get(0);
167
+ if (stdin && (stdin.channel.kind === 'host'))
168
+ child.fds.set(0, new NullFile(this.rt.channel('null', '/dev/null'), this.rt.listeners));
169
+ const ac = new AbortController();
170
+ child.jobSignal = ac.signal;
171
+ child.jobChildren = new Set();
172
+ const children = child.jobChildren;
173
+ const job = {
174
+ pid,
175
+ nodeId: stmt.id,
176
+ status: null,
177
+ done: Promise.resolve(0),
178
+ kill: (sig = 'SIGTERM') => {
179
+ ac.abort();
180
+ for (const c of children)
181
+ c.kill(sig);
182
+ },
183
+ };
184
+ this.rt.emit({ type: 'job:start', jobPid: pid, nodeId: stmt.id });
185
+ job.done = this.runSubshell(child, () => this.execCommand(stmt.command, child), 'background', stmt.id).then((s) => {
186
+ job.status = s;
187
+ this.rt.emit({ type: 'job:end', jobPid: pid, status: s });
188
+ return s;
189
+ });
190
+ proc.jobs.set(pid, job);
191
+ this.rt.registerJob(job);
192
+ proc.state.lastBgPid = pid;
193
+ }
194
+ /** Run `fn` as the body of subshell `child`, handling exit, traps and fd cleanup. */
195
+ async runSubshell(child, fn, reason, nodeId) {
196
+ this.rt.emit({ type: 'subshell:start', pid: child.pid, parentPid: child.parentPid, nodeId, reason });
197
+ let status;
198
+ try {
199
+ try {
200
+ status = await fn();
201
+ }
202
+ catch (e) {
203
+ if (e instanceof AbortRun)
204
+ throw e;
205
+ status = this.handleTermination(e, child);
206
+ // bash 5: an unbound variable or ${x:?} ends a ( … ) subshell with status 1
207
+ // (the top-level shell still exits with 127; bash 3.2 uses 127 for both)
208
+ if (reason === 'subshell' && e instanceof FatalError && e.status === 127)
209
+ status = 1;
210
+ }
211
+ status = await this.runExitTrap(child, status);
212
+ }
213
+ finally {
214
+ child.fds.closeAll();
215
+ }
216
+ this.rt.emit({ type: 'subshell:end', pid: child.pid, status });
217
+ return status;
218
+ }
219
+ inErrTrap = false;
220
+ /** After a failing command outside a condition: run the ERR trap, then apply `set -e`. */
221
+ async checkErrexit(status, proc) {
222
+ if (status === 0 || proc.errexitSuppressed > 0)
223
+ return;
224
+ const trap = proc.state.traps.get('ERR');
225
+ if (trap && !this.inErrTrap) {
226
+ this.inErrTrap = true;
227
+ try {
228
+ proc.state.lastStatus = status;
229
+ await this.execList(this.rt.parse(trap, 'trap ERR').body, proc.derive({ suppress: true }));
230
+ }
231
+ finally {
232
+ this.inErrTrap = false;
233
+ }
234
+ proc.state.lastStatus = status;
235
+ }
236
+ if (proc.state.options.errexit)
237
+ throw new ExitSignal(status);
238
+ }
239
+ step(node, proc) {
240
+ if (this.rt.signal.aborted)
241
+ throw new AbortRun('aborted', 130);
242
+ if (proc.jobSignal?.aborted)
243
+ throw new ExitSignal(143);
244
+ proc.line = this.rt.line(node);
245
+ this.rt.emit({ type: 'step', nodeId: node.id, span: node.span, line: proc.line, node: node.type, pid: proc.pid, depth: proc.depth });
246
+ }
247
+ stepCount = 0;
248
+ async execCommand(cmd, proc) {
249
+ // In-process loops only await microtasks while pipes have room; yield to
250
+ // the event loop regularly so child I/O (and the host) is never starved.
251
+ if (++this.stepCount % 64 === 0)
252
+ await new Promise((r) => setImmediate(r));
253
+ this.step(cmd, proc);
254
+ switch (cmd.type) {
255
+ case 'SimpleCommand': {
256
+ const s = await this.execSimple(cmd, proc);
257
+ // a lone command is a pipeline of one: PIPESTATUS=($?)
258
+ this.setPipestatus(proc, [s]);
259
+ await this.checkErrexit(s, proc);
260
+ return s;
261
+ }
262
+ case 'Pipeline': {
263
+ const s = await this.execPipeline(cmd, proc);
264
+ if (!cmd.negated)
265
+ await this.checkErrexit(s, proc);
266
+ return s;
267
+ }
268
+ case 'AndOr':
269
+ return this.execAndOr(cmd, proc);
270
+ case 'FunctionDef':
271
+ proc.state.functions.set(cmd.name, { def: cmd });
272
+ return 0;
273
+ default:
274
+ return this.withCompoundRedirects(cmd, proc, async (p) => {
275
+ const s = await this.execCompound(cmd, p);
276
+ if (cmd.type === 'Subshell' || cmd.type === 'ArithCommand' || cmd.type === 'CondCommand') {
277
+ this.setPipestatus(p, [s]);
278
+ await this.checkErrexit(s, p);
279
+ }
280
+ return s;
281
+ });
282
+ }
283
+ }
284
+ async execCompound(cmd, proc) {
285
+ switch (cmd.type) {
286
+ case 'Group':
287
+ return this.execList(cmd.body, proc);
288
+ case 'Subshell': {
289
+ const child = proc.subshell(this.rt.nextPid());
290
+ return this.runSubshell(child, () => this.execList(cmd.body, child), 'subshell', cmd.id);
291
+ }
292
+ case 'If':
293
+ return this.execIf(cmd, proc);
294
+ case 'Loop':
295
+ return this.execLoop(cmd, proc);
296
+ case 'For':
297
+ return this.execFor(cmd, proc);
298
+ case 'ArithFor':
299
+ return this.execArithFor(cmd, proc);
300
+ case 'Select':
301
+ return this.execSelect(cmd, proc);
302
+ case 'Case':
303
+ return this.execCase(cmd, proc);
304
+ case 'ArithCommand': {
305
+ try {
306
+ const v = await this.arith(cmd.expr, proc);
307
+ return v !== 0n ? 0 : 1;
308
+ }
309
+ catch (e) {
310
+ if (e instanceof ArithError) {
311
+ this.errorMessage(proc, `((: ${e.message}`);
312
+ return 1;
313
+ }
314
+ throw e;
315
+ }
316
+ }
317
+ case 'CondCommand': {
318
+ try {
319
+ return (await evalCond(cmd.expr, proc, this)) ? 0 : 1;
320
+ }
321
+ catch (e) {
322
+ if (e instanceof CommandError) {
323
+ this.errorMessage(proc, e.message);
324
+ return 2;
325
+ }
326
+ throw e;
327
+ }
328
+ }
329
+ }
330
+ }
331
+ async execAndOr(cmd, proc) {
332
+ const suppressed = proc.derive({ suppress: true });
333
+ let status = await this.execCommand(cmd.items[0], cmd.items.length > 1 ? suppressed : proc);
334
+ for (let i = 1; i < cmd.items.length; i++) {
335
+ const op = cmd.ops[i - 1];
336
+ proc.state.lastStatus = status;
337
+ if ((op === '&&' && status !== 0) || (op === '||' && status === 0))
338
+ continue;
339
+ const last = i === cmd.items.length - 1;
340
+ status = await this.execCommand(cmd.items[i], last ? proc : suppressed);
341
+ }
342
+ return status;
343
+ }
344
+ async execIf(cmd, proc) {
345
+ const cond = proc.derive({ suppress: true });
346
+ for (let i = 0; i < cmd.clauses.length; i++) {
347
+ const c = cmd.clauses[i];
348
+ const s = await this.execList(c.cond, cond);
349
+ if (s === 0) {
350
+ this.rt.emit({ type: 'branch', nodeId: cmd.id, taken: i, pid: proc.pid });
351
+ return this.execList(c.body, proc);
352
+ }
353
+ }
354
+ if (cmd.elseBody) {
355
+ this.rt.emit({ type: 'branch', nodeId: cmd.id, taken: 'else', pid: proc.pid });
356
+ return this.execList(cmd.elseBody, proc);
357
+ }
358
+ this.rt.emit({ type: 'branch', nodeId: cmd.id, taken: 'none', pid: proc.pid });
359
+ return 0;
360
+ }
361
+ /** Run a loop body; returns 'break' / 'continue' / status. */
362
+ async loopBody(body, proc) {
363
+ try {
364
+ return { status: await this.execList(body, proc) };
365
+ }
366
+ catch (e) {
367
+ if (e instanceof LoopSignal) {
368
+ if (e.levels > 1) {
369
+ e.levels--;
370
+ throw e;
371
+ }
372
+ return { status: 0, ctl: e.kind };
373
+ }
374
+ throw e;
375
+ }
376
+ }
377
+ async execLoop(cmd, proc) {
378
+ let status = 0;
379
+ let iteration = 0;
380
+ const inner = proc.derive({ loop: true });
381
+ const cond = inner.derive({ suppress: true });
382
+ for (;;) {
383
+ let c;
384
+ try {
385
+ c = await this.execList(cmd.cond, cond);
386
+ }
387
+ catch (e) {
388
+ if (e instanceof LoopSignal && e.levels <= 1) {
389
+ if (e.kind === 'break')
390
+ break;
391
+ continue;
392
+ }
393
+ if (e instanceof LoopSignal)
394
+ e.levels--;
395
+ throw e;
396
+ }
397
+ if ((cmd.kind === 'while') !== (c === 0))
398
+ break;
399
+ iteration++;
400
+ this.rt.emit({ type: 'loop:iteration', nodeId: cmd.id, iteration, pid: proc.pid });
401
+ const r = await this.loopBody(cmd.body, inner);
402
+ status = r.status;
403
+ if (r.ctl === 'break')
404
+ break;
405
+ }
406
+ return status;
407
+ }
408
+ async execFor(cmd, proc) {
409
+ const items = cmd.items === null ? [...proc.state.positional] : await this.expandFieldsChecked(cmd.items, proc);
410
+ let status = 0;
411
+ let iteration = 0;
412
+ const inner = proc.derive({ loop: true });
413
+ for (const item of items) {
414
+ iteration++;
415
+ this.assignScalar(proc, cmd.variable, item);
416
+ this.rt.emit({ type: 'loop:iteration', nodeId: cmd.id, iteration, variable: cmd.variable, value: item, pid: proc.pid });
417
+ const r = await this.loopBody(cmd.body, inner);
418
+ status = r.status;
419
+ if (r.ctl === 'break')
420
+ break;
421
+ }
422
+ return status;
423
+ }
424
+ async execSelect(cmd, proc) {
425
+ // Non-interactive: print the menu to stderr, read choices from stdin.
426
+ const items = cmd.items === null ? [...proc.state.positional] : await this.expandFieldsChecked(cmd.items, proc);
427
+ let status = 0;
428
+ const inner = proc.derive({ loop: true });
429
+ for (;;) {
430
+ await this.write(proc, 2, items.map((it, i) => `${i + 1}) ${it}\n`).join('') + (proc.state.getString('PS3') ?? '#? '));
431
+ const stdin = proc.fds.get(0);
432
+ const line = stdin ? await stdin.readUntil(10) : null;
433
+ if (line === null)
434
+ return status;
435
+ const reply = new TextDecoder().decode(line).replace(/\n$/, '');
436
+ this.assignScalar(proc, 'REPLY', reply);
437
+ const n = Number(reply);
438
+ this.assignScalar(proc, cmd.variable, Number.isInteger(n) && n >= 1 && n <= items.length ? items[n - 1] : '');
439
+ const r = await this.loopBody(cmd.body, inner);
440
+ status = r.status;
441
+ if (r.ctl === 'break')
442
+ break;
443
+ }
444
+ return status;
445
+ }
446
+ async execArithFor(cmd, proc) {
447
+ const ev = async (e) => {
448
+ try {
449
+ return await this.arith(e, proc);
450
+ }
451
+ catch (err) {
452
+ if (err instanceof ArithError)
453
+ throw new DiscardError(err.message);
454
+ throw err;
455
+ }
456
+ };
457
+ let status = 0;
458
+ let iteration = 0;
459
+ const inner = proc.derive({ loop: true });
460
+ await ev(cmd.init);
461
+ for (;;) {
462
+ if (cmd.cond && (await ev(cmd.cond)) === 0n)
463
+ break;
464
+ iteration++;
465
+ this.rt.emit({ type: 'loop:iteration', nodeId: cmd.id, iteration, pid: proc.pid });
466
+ const r = await this.loopBody(cmd.body, inner);
467
+ status = r.status;
468
+ if (r.ctl === 'break')
469
+ break;
470
+ await ev(cmd.step);
471
+ }
472
+ return status;
473
+ }
474
+ async execCase(cmd, proc) {
475
+ const word = await this.expander.expandString(cmd.word, proc);
476
+ const nocase = proc.state.shopts.has('nocasematch');
477
+ let status = 0;
478
+ let fallthrough = false;
479
+ for (let i = 0; i < cmd.items.length; i++) {
480
+ const item = cmd.items[i];
481
+ let matched = fallthrough;
482
+ if (!matched) {
483
+ for (const p of item.patterns) {
484
+ const pat = await this.expander.expandPattern(p, proc);
485
+ if (matchPattern(word, pat, { nocase })) {
486
+ matched = true;
487
+ break;
488
+ }
489
+ }
490
+ }
491
+ if (!matched)
492
+ continue;
493
+ this.rt.emit({ type: 'branch', nodeId: cmd.id, taken: i, pid: proc.pid });
494
+ status = await this.execList(item.body, proc);
495
+ if (item.terminator === ';;')
496
+ return status;
497
+ fallthrough = item.terminator === ';&';
498
+ }
499
+ if (!fallthrough)
500
+ this.rt.emit({ type: 'branch', nodeId: cmd.id, taken: 'none', pid: proc.pid });
501
+ return status;
502
+ }
503
+ // -------------------------------------------------------------------------
504
+ // Pipelines
505
+ // -------------------------------------------------------------------------
506
+ async execPipeline(cmd, proc) {
507
+ const start = Date.now();
508
+ const n = cmd.stages.length;
509
+ const stageProc = cmd.negated ? proc.derive({ suppress: true }) : proc;
510
+ let statuses;
511
+ if (n === 1) {
512
+ statuses = [await this.execCommand(cmd.stages[0], stageProc)];
513
+ }
514
+ else {
515
+ const pipes = [];
516
+ for (let i = 0; i < n - 1; i++) {
517
+ const label = `${this.rt.text(cmd.stages[i])} | ${this.rt.text(cmd.stages[i + 1])}`;
518
+ const ch = this.rt.channel('pipe', label, { nodeId: cmd.id });
519
+ pipes.push(new Pipe(ch, this.rt.listeners));
520
+ }
521
+ const runs = cmd.stages.map((stage, i) => {
522
+ const child = stageProc.subshell(this.rt.nextPid());
523
+ if (i > 0)
524
+ child.fds.set(0, pipes[i - 1].readEnd.retain());
525
+ if (i < n - 1) {
526
+ child.fds.set(1, pipes[i].writeEnd.retain());
527
+ if (cmd.pipeStderr[i])
528
+ child.fds.dup(1, 2);
529
+ }
530
+ return this.runSubshell(child, () => this.execCommand(stage, child), 'pipeline', stage.id);
531
+ });
532
+ for (const p of pipes) {
533
+ p.readEnd.release();
534
+ p.writeEnd.release();
535
+ }
536
+ statuses = await Promise.all(runs);
537
+ }
538
+ this.setPipestatus(proc, statuses);
539
+ let status = statuses[statuses.length - 1];
540
+ if (proc.state.options.pipefail) {
541
+ for (let i = statuses.length - 1; i >= 0; i--) {
542
+ if (statuses[i] !== 0) {
543
+ status = statuses[i];
544
+ break;
545
+ }
546
+ }
547
+ }
548
+ if (cmd.timed) {
549
+ const secs = (Date.now() - start) / 1000;
550
+ await this.write(proc, 2, `\nreal\t${Math.floor(secs / 60)}m${(secs % 60).toFixed(3)}s\nuser\t0m0.000s\nsys\t0m0.000s\n`).catch(() => { });
551
+ }
552
+ if (cmd.negated)
553
+ status = status === 0 ? 1 : 0;
554
+ return status;
555
+ }
556
+ setPipestatus(proc, statuses) {
557
+ const values = new Map();
558
+ statuses.forEach((s, i) => values.set(i, String(s)));
559
+ proc.state.globals.set('PIPESTATUS', newVar({ kind: 'indexed', values }));
560
+ }
561
+ // -------------------------------------------------------------------------
562
+ // Simple commands
563
+ // -------------------------------------------------------------------------
564
+ async expandFieldsChecked(words, proc) {
565
+ return this.expander.expandFields(words, proc);
566
+ }
567
+ async execSimple(cmd, proc) {
568
+ const mark = proc.procSubstFds.length;
569
+ try {
570
+ return await this.execSimpleInner(cmd, proc);
571
+ }
572
+ catch (e) {
573
+ if (e instanceof CommandError) {
574
+ this.errorMessage(proc, e.message);
575
+ return e.status;
576
+ }
577
+ throw e;
578
+ }
579
+ finally {
580
+ this.closeProcSubsts(proc, mark);
581
+ }
582
+ }
583
+ closeProcSubsts(proc, mark) {
584
+ while (proc.procSubstFds.length > mark) {
585
+ const { fds, fd } = proc.procSubstFds.pop();
586
+ fds.close(fd);
587
+ }
588
+ }
589
+ /** `expand_aliases`: re-parse the command with the alias text substituted for its first word. */
590
+ async expandAlias(cmd, proc) {
591
+ const st = proc.state;
592
+ if (!st.shopts.has('expand_aliases') || st.aliases.size === 0 || st.aliasDepth > 16)
593
+ return null;
594
+ const first = cmd.words[0];
595
+ if (!first || first.type !== 'Word')
596
+ return null;
597
+ const name = literalOf(first);
598
+ if (name === null || !st.aliases.has(name))
599
+ return null;
600
+ const full = this.rt.text(cmd);
601
+ const offset = cmd.span.start;
602
+ const text = full.slice(0, first.span.start - offset) + st.aliases.get(name) + full.slice(first.span.end - offset);
603
+ const script = this.rt.parse(text, `alias ${name}`);
604
+ st.aliasDepth++;
605
+ try {
606
+ return await this.execList(script.body, proc);
607
+ }
608
+ finally {
609
+ st.aliasDepth--;
610
+ }
611
+ }
612
+ async execSimpleInner(cmd, proc) {
613
+ proc.lastSubstStatus = null;
614
+ const aliased = await this.expandAlias(cmd, proc);
615
+ if (aliased !== null)
616
+ return aliased;
617
+ // 1. Expand words (Assign nodes after declaration builtins are evaluated by the builtin).
618
+ const argv = [];
619
+ const declAssigns = [];
620
+ for (const w of cmd.words) {
621
+ if (w.type === 'Assign') {
622
+ declAssigns.push({ index: argv.length, assign: w });
623
+ argv.push(`\u0000assign:${declAssigns.length - 1}`);
624
+ }
625
+ else {
626
+ argv.push(...(await this.expander.expandFields([w], proc)));
627
+ }
628
+ }
629
+ // 2. Assignment-only statement
630
+ if (argv.length === 0) {
631
+ for (const a of cmd.assigns) {
632
+ try {
633
+ await this.performAssign(a, proc, {});
634
+ }
635
+ catch (e) {
636
+ if (e instanceof CommandError && /readonly variable/.test(e.message))
637
+ throw new FatalError(e.message, 1);
638
+ throw e;
639
+ }
640
+ }
641
+ const redirStatus = await this.redirectOnly(cmd, proc);
642
+ if (redirStatus !== 0)
643
+ return redirStatus;
644
+ if (proc.state.options.xtrace && cmd.assigns.length > 0) {
645
+ await this.xtrace(proc, cmd.assigns.map((a) => `${a.name}=${this.rt.text(a.value ?? a)}`));
646
+ }
647
+ return proc.lastSubstStatus ?? 0;
648
+ }
649
+ // 3. Prefix assignments (temporary for functions/builtins, environment for externals)
650
+ const prefix = [];
651
+ for (const a of cmd.assigns) {
652
+ if (a.value?.type === 'ArrayLiteral')
653
+ continue;
654
+ const v = a.value ? await this.expander.expandString(a.value, proc, { tildeAssign: true }) : '';
655
+ prefix.push({ name: a.name, value: v });
656
+ }
657
+ // 4. Resolve
658
+ const name = argv[0];
659
+ let kind;
660
+ let exePath;
661
+ const fn = proc.state.functions.get(name);
662
+ const tempPath = prefix.find((p) => p.name === 'PATH')?.value;
663
+ if (fn)
664
+ kind = 'function';
665
+ else if (BUILTINS.has(name))
666
+ kind = 'builtin';
667
+ else {
668
+ exePath = resolveCommand(name, proc.state, tempPath);
669
+ kind = exePath ? 'external' : 'not-found';
670
+ }
671
+ // 5. Redirect targets
672
+ const redirects = await this.resolveRedirects(cmd.redirs, proc);
673
+ const redirectEffects = redirectEffectsOf(redirects);
674
+ // 6. Assess (behavior + policy + hooks)
675
+ const execId = this.rt.nextExecId();
676
+ const env = this.childEnv(proc, prefix);
677
+ const realArgv = this.realArgv(argv, declAssigns);
678
+ const stdinFile = redirects.find((r) => r.fd === 0);
679
+ const stdinKind = stdinFile ? (stdinFile.path ? 'file' : 'pipe') : kindOfStdin(proc.fds.get(0));
680
+ const assessment = await this.rt.assess({
681
+ execId, node: cmd, argv: realArgv, kind, path: exePath, cwd: proc.state.cwd, env, redirectEffects, stdinKind, pid: proc.pid,
682
+ });
683
+ if (assessment.action === 'abort')
684
+ throw new AbortRun(assessment.reason, assessment.status);
685
+ if (assessment.action === 'deny') {
686
+ this.errorMessage(proc, `${name}: ${assessment.reason}`);
687
+ return assessment.status;
688
+ }
689
+ if (proc.state.options.xtrace)
690
+ await this.xtrace(proc, [...prefix.map((p) => `${p.name}=${p.value}`), ...realArgv]);
691
+ this.rt.emit({
692
+ type: 'command:start', execId, nodeId: cmd.id, span: cmd.span, argv: realArgv, kind, path: exePath, cwd: proc.state.cwd, pid: proc.pid,
693
+ redirects: redirects.map(redirectInfo), line: proc.line,
694
+ });
695
+ const started = Date.now();
696
+ let status;
697
+ // 7. Open redirections into a command-local fd table
698
+ const fds = proc.fds.clone();
699
+ try {
700
+ try {
701
+ await this.openRedirects(redirects, fds, proc);
702
+ }
703
+ catch (e) {
704
+ if (e instanceof FsError || e instanceof CommandError) {
705
+ this.errorMessage(proc, e.message);
706
+ status = 1;
707
+ this.rt.emit({ type: 'command:end', execId, nodeId: cmd.id, status, durationMs: Date.now() - started });
708
+ return status;
709
+ }
710
+ throw e;
711
+ }
712
+ const cproc = proc.derive({ fds });
713
+ if (assessment.action === 'simulate') {
714
+ if (assessment.stdout)
715
+ await this.write(cproc, 1, assessment.stdout).catch(() => { });
716
+ if (assessment.stderr)
717
+ await this.write(cproc, 2, assessment.stderr).catch(() => { });
718
+ status = assessment.status;
719
+ }
720
+ else {
721
+ status = await this.dispatch(kind, name, argv, declAssigns, prefix, exePath, env, cmd, cproc, proc, assessment.readsStdin, execId);
722
+ }
723
+ }
724
+ catch (e) {
725
+ if (e instanceof BrokenPipeError) {
726
+ // SIGPIPE: an in-process write to a pipe nobody reads kills this (sub)shell, as in bash
727
+ this.rt.emit({ type: 'command:end', execId, nodeId: cmd.id, status: 141, durationMs: Date.now() - started });
728
+ throw new ExitSignal(141);
729
+ }
730
+ else {
731
+ this.rt.emit({ type: 'command:end', execId, nodeId: cmd.id, status: e instanceof ExitSignal || e instanceof ReturnSignal ? e.status : 1, durationMs: Date.now() - started });
732
+ throw e;
733
+ }
734
+ }
735
+ finally {
736
+ fds.closeAll();
737
+ }
738
+ this.rt.emit({ type: 'command:end', execId, nodeId: cmd.id, status, durationMs: Date.now() - started });
739
+ await this.rt.afterCommand(execId, status);
740
+ this.setUnderscore(proc, realArgv);
741
+ return status;
742
+ }
743
+ setUnderscore(proc, argv) {
744
+ const v = proc.state.globals.get('_');
745
+ const last = argv[argv.length - 1] ?? '';
746
+ if (v)
747
+ v.value = { kind: 'string', value: last };
748
+ else
749
+ proc.state.globals.set('_', newVar({ kind: 'string', value: last }));
750
+ }
751
+ realArgv(argv, declAssigns) {
752
+ if (declAssigns.length === 0)
753
+ return argv;
754
+ return argv.map((a) => {
755
+ const m = /^\u0000assign:(\d+)$/.exec(a);
756
+ if (!m)
757
+ return a;
758
+ const as = declAssigns[Number(m[1])].assign;
759
+ return this.rt.text(as);
760
+ });
761
+ }
762
+ async dispatch(kind, name, argv, declAssigns, prefix, exePath, env, cmd, cproc, base, readsStdin, execId) {
763
+ switch (kind) {
764
+ case 'function': {
765
+ const entry = cproc.state.functions.get(name);
766
+ return this.withTempVars(cproc, prefix, () => this.callFunction(entry.def, argv, cproc));
767
+ }
768
+ case 'builtin': {
769
+ const run = () => this.runBuiltin(name, argv, declAssigns, cproc, base, cmd);
770
+ if (prefix.length === 0)
771
+ return run();
772
+ return this.withTempVars(cproc, prefix, run);
773
+ }
774
+ case 'external': {
775
+ if (this.rt.inlineScripts) {
776
+ const plan = inlineShellPlan(exePath, argv, cproc.state.cwd);
777
+ if (plan)
778
+ return this.runInlineScript(plan, env, cproc);
779
+ }
780
+ if (this.rt.emulate) {
781
+ const base = path.basename(exePath);
782
+ const req = { execId, path: exePath, argv, env, proc: cproc, node: cmd };
783
+ const emulated = base === 'xargs' ? await emulateXargs(this, req) : base === 'find' ? await emulateFind(this, req) : null;
784
+ if (emulated !== null)
785
+ return emulated;
786
+ }
787
+ return runExternal(this, {
788
+ execId, path: exePath, argv, env, cwd: cproc.state.cwd, proc: cproc, readsStdin,
789
+ });
790
+ }
791
+ case 'not-found': {
792
+ const p = name.includes('/') ? path.resolve(cproc.state.cwd, name) : null;
793
+ if (p && fs.existsSync(p)) {
794
+ this.errorMessage(cproc, `${name}: ${fs.statSync(p).isDirectory() ? 'Is a directory' : 'Permission denied'}`);
795
+ return 126;
796
+ }
797
+ this.errorMessage(cproc, `${name}: ${p ? 'No such file or directory' : 'command not found'}`);
798
+ return 127;
799
+ }
800
+ default:
801
+ return 0;
802
+ }
803
+ }
804
+ /**
805
+ * Execute a nested shell script (`bash -c ...`, `bash file`, `./script.sh`)
806
+ * inside the VM as a fresh shell process: only exported variables carry
807
+ * over, functions and options do not. Its commands are traced and
808
+ * policy-checked individually instead of disappearing into a child bash.
809
+ */
810
+ async runInlineScript(plan, env, proc) {
811
+ const script = this.rt.parse(plan.source, plan.origin, { partial: true });
812
+ const state = new ShellState(proc.state.cwd);
813
+ for (const [k, v] of Object.entries(env)) {
814
+ const fm = /^BASH_FUNC_(.+)%%$/.exec(k);
815
+ if (fm) {
816
+ const def = proc.state.functions.get(fm[1]);
817
+ if (def?.exported)
818
+ state.functions.set(fm[1], def);
819
+ continue;
820
+ }
821
+ state.globals.set(k, newVar({ kind: 'string', value: v }, { exported: true }));
822
+ }
823
+ state.globals.set('SHLVL', newVar({ kind: 'string', value: String(Number(env.SHLVL ?? '1') + 1) }, { exported: true }));
824
+ initShellVars(state);
825
+ for (const [k, v] of Object.entries(plan.options))
826
+ state.options[k] = v;
827
+ state.positional = plan.args;
828
+ state.arg0 = plan.arg0;
829
+ const child = new Proc(state, proc.fds.clone(), this.rt.nextPid(), proc.pid, 0, new Map(), true);
830
+ child.jobSignal = proc.jobSignal;
831
+ child.jobChildren = proc.jobChildren;
832
+ return this.runSubshell(child, () => this.execTopLevel(script, child), 'script', script.id);
833
+ }
834
+ /**
835
+ * Run an already-expanded argv (for `command`, `builtin`, `exec`): resolve,
836
+ * assess against the policy, and dispatch.
837
+ */
838
+ async runArgv(argv, proc, node, opts) {
839
+ const name = argv[0];
840
+ let kind;
841
+ let exePath;
842
+ if (!opts.externalOnly && !opts.skipFunctions && !opts.builtinOnly && proc.state.functions.has(name))
843
+ kind = 'function';
844
+ else if (!opts.externalOnly && BUILTINS.has(name))
845
+ kind = 'builtin';
846
+ else if (opts.builtinOnly)
847
+ kind = 'not-found';
848
+ else {
849
+ exePath = resolveCommand(name, proc.state);
850
+ kind = exePath ? 'external' : 'not-found';
851
+ }
852
+ const execId = this.rt.nextExecId();
853
+ const env = this.childEnv(proc, []);
854
+ const assessment = await this.rt.assess({
855
+ execId, node, argv, kind, path: exePath, cwd: proc.state.cwd, env, redirectEffects: [], stdinKind: kindOfStdin(proc.fds.get(0)), pid: proc.pid,
856
+ });
857
+ if (assessment.action === 'abort')
858
+ throw new AbortRun(assessment.reason, assessment.status);
859
+ if (assessment.action === 'deny') {
860
+ this.errorMessage(proc, `${name}: ${assessment.reason}`);
861
+ return assessment.status;
862
+ }
863
+ this.rt.emit({ type: 'command:start', execId, nodeId: node.id, span: node.span, argv, kind, path: exePath, cwd: proc.state.cwd, pid: proc.pid, redirects: [] });
864
+ const started = Date.now();
865
+ let status;
866
+ if (assessment.action === 'simulate') {
867
+ if (assessment.stdout)
868
+ await this.write(proc, 1, assessment.stdout).catch(() => { });
869
+ if (assessment.stderr)
870
+ await this.write(proc, 2, assessment.stderr).catch(() => { });
871
+ status = assessment.status;
872
+ }
873
+ else {
874
+ status = await this.dispatch(kind, name, argv, [], [], exePath, env, node, proc, proc, assessment.readsStdin, execId);
875
+ }
876
+ this.rt.emit({ type: 'command:end', execId, nodeId: node.id, status, durationMs: Date.now() - started });
877
+ await this.rt.afterCommand(execId, status);
878
+ return status;
879
+ }
880
+ async withTempVars(proc, vars, fn) {
881
+ if (vars.length === 0)
882
+ return fn();
883
+ const scope = new Map();
884
+ for (const v of vars)
885
+ scope.set(v.name, newVar({ kind: 'string', value: v.value }, { exported: true }));
886
+ proc.state.locals.push(scope);
887
+ try {
888
+ return await fn();
889
+ }
890
+ finally {
891
+ const i = proc.state.locals.lastIndexOf(scope);
892
+ if (i >= 0)
893
+ proc.state.locals.splice(i, 1);
894
+ }
895
+ }
896
+ async callFunction(def, argv, proc) {
897
+ const st = proc.state;
898
+ const savedPositional = st.positional;
899
+ st.positional = argv.slice(1);
900
+ st.locals.push(new Map());
901
+ st.funcStack.unshift(def.name);
902
+ this.rt.emit({ type: 'function:enter', name: def.name, nodeId: def.id, pid: proc.pid, args: argv.slice(1) });
903
+ let status;
904
+ try {
905
+ status = await this.execCommand(def.body, proc);
906
+ }
907
+ catch (e) {
908
+ if (e instanceof ReturnSignal)
909
+ status = e.status;
910
+ else
911
+ throw e;
912
+ }
913
+ finally {
914
+ const ret = st.traps.get('RETURN');
915
+ if (ret) {
916
+ try {
917
+ await this.execList(this.rt.parse(ret, 'trap RETURN').body, proc);
918
+ }
919
+ catch {
920
+ // a failing RETURN trap does not change the function's status
921
+ }
922
+ }
923
+ st.locals.pop();
924
+ st.positional = savedPositional;
925
+ st.funcStack.shift();
926
+ }
927
+ this.rt.emit({ type: 'function:exit', name: def.name, status, pid: proc.pid });
928
+ return status;
929
+ }
930
+ async runBuiltin(name, argv, declAssigns, proc, base, cmd) {
931
+ const impl = BUILTINS.get(name);
932
+ const ctx = {
933
+ interp: this,
934
+ proc,
935
+ base,
936
+ argv,
937
+ node: cmd,
938
+ assignArg: (arg) => {
939
+ const m = /^\u0000assign:(\d+)$/.exec(arg);
940
+ return m ? declAssigns[Number(m[1])].assign : null;
941
+ },
942
+ out: (text) => this.write(proc, 1, text),
943
+ err: (text) => this.write(proc, 2, text),
944
+ error: (message) => this.errorMessage(proc, `${name}: ${message}`),
945
+ };
946
+ try {
947
+ return await impl(ctx);
948
+ }
949
+ catch (e) {
950
+ if (e instanceof CommandError) {
951
+ this.errorMessage(proc, `${name}: ${e.message}`);
952
+ return e.status;
953
+ }
954
+ throw e;
955
+ }
956
+ }
957
+ async redirectOnly(cmd, proc) {
958
+ if (cmd.redirs.length === 0)
959
+ return 0;
960
+ const redirects = await this.resolveRedirects(cmd.redirs, proc);
961
+ const a = await this.rt.assessRedirects(redirectEffectsOf(redirects), cmd, proc);
962
+ if (a.action === 'abort')
963
+ throw new AbortRun(a.reason, a.status);
964
+ if (a.action === 'deny') {
965
+ this.errorMessage(proc, a.reason);
966
+ return a.status;
967
+ }
968
+ const fds = proc.fds.clone();
969
+ try {
970
+ await this.openRedirects(redirects, fds, proc);
971
+ }
972
+ catch (e) {
973
+ if (e instanceof FsError || e instanceof CommandError) {
974
+ this.errorMessage(proc, e.message);
975
+ return 1;
976
+ }
977
+ throw e;
978
+ }
979
+ finally {
980
+ fds.closeAll();
981
+ }
982
+ return 0;
983
+ }
984
+ async xtrace(proc, words) {
985
+ const ps4 = proc.state.getString('PS4') ?? '+ ';
986
+ const { shellQuote } = await import("./expand.js");
987
+ await this.write(proc, 2, `${ps4}${words.map((w) => (w === '' ? "''" : shellQuote(w))).join(' ')}\n`).catch(() => { });
988
+ }
989
+ childEnv(proc, prefix) {
990
+ const env = proc.state.environment();
991
+ for (const [name, f] of proc.state.functions) {
992
+ if (f.exported)
993
+ env[`BASH_FUNC_${name}%%`] = `() ${this.rt.text(f.def.body)}`;
994
+ }
995
+ for (const p of prefix)
996
+ env[p.name] = p.value;
997
+ return env;
998
+ }
999
+ // -------------------------------------------------------------------------
1000
+ // Assignments
1001
+ // -------------------------------------------------------------------------
1002
+ assignScalar(proc, name, value, opts = {}) {
1003
+ const st = proc.state;
1004
+ const v = opts.local ? st.declareLocal(name) : st.ensure(name);
1005
+ if (v.readonly)
1006
+ throw new CommandError(`${name}: readonly variable`);
1007
+ value = this.applyAttrs(v, value, proc);
1008
+ if (v.value?.kind === 'indexed')
1009
+ v.value.values.set(0, value);
1010
+ else if (v.value?.kind === 'assoc')
1011
+ v.value.values.set('0', value);
1012
+ else
1013
+ v.value = { kind: 'string', value };
1014
+ this.rt.emit({ type: 'assign', name, value, pid: proc.pid, nodeId: 0 });
1015
+ if (name === 'PWD' && path.isAbsolute(value)) {
1016
+ // keep the VM cwd in sync only through cd
1017
+ }
1018
+ }
1019
+ /** `name[index]=value` from an already-expanded subscript (`printf -v 'a[1]'`). */
1020
+ async assignElement(proc, name, index, value) {
1021
+ const v = proc.state.ensure(name);
1022
+ if (v.readonly)
1023
+ throw new CommandError(`${name}: readonly variable`);
1024
+ if (v.value?.kind === 'assoc' || v.declaredKind === 'assoc') {
1025
+ if (v.value?.kind !== 'assoc')
1026
+ v.value = { kind: 'assoc', values: new Map() };
1027
+ v.value.values.set(index, this.applyAttrs(v, value, proc));
1028
+ }
1029
+ else {
1030
+ let idx = Number(await this.arithText(index, proc));
1031
+ if (v.value === null || v.value.kind !== 'indexed') {
1032
+ const values = new Map();
1033
+ if (v.value?.kind === 'string')
1034
+ values.set(0, v.value.value);
1035
+ v.value = { kind: 'indexed', values };
1036
+ }
1037
+ if (idx < 0)
1038
+ idx = (v.value.values.size === 0 ? 0 : Math.max(...v.value.values.keys()) + 1) + idx;
1039
+ v.value.values.set(idx, this.applyAttrs(v, value, proc));
1040
+ }
1041
+ this.rt.emit({ type: 'assign', name: `${name}[…]`, value, pid: proc.pid, nodeId: 0 });
1042
+ }
1043
+ applyAttrs(v, value, proc) {
1044
+ if (v.integer) {
1045
+ try {
1046
+ const env = this.arithEnv(proc);
1047
+ // synchronous evaluation of text
1048
+ value = String(evaluateArithTextSync(value, env));
1049
+ }
1050
+ catch {
1051
+ value = '0';
1052
+ }
1053
+ }
1054
+ if (v.upper)
1055
+ value = value.toUpperCase();
1056
+ if (v.lower)
1057
+ value = value.toLowerCase();
1058
+ return value;
1059
+ }
1060
+ /**
1061
+ * Expand an assignment's value now and return an equivalent assignment with
1062
+ * literal parts (so `local x=1 y=$x` sees the old `$x`, as bash expands
1063
+ * all arguments before the builtin runs).
1064
+ */
1065
+ async freezeAssign(a, proc) {
1066
+ const lit = (value, like) => ({
1067
+ type: 'Word', id: like.id, span: like.span, parts: [{ type: 'SglQuoted', value, ansi: false, id: like.id, span: like.span }],
1068
+ });
1069
+ const index = a.index ? lit(await this.expander.expandString(a.index, proc), a.index) : null;
1070
+ if (a.value === null)
1071
+ return { ...a, index };
1072
+ if (a.value.type === 'Word')
1073
+ return { ...a, index, value: lit(await this.expander.expandString(a.value, proc, { tildeAssign: true }), a.value) };
1074
+ const elems = [];
1075
+ for (const el of a.value.elems) {
1076
+ if (el.index)
1077
+ elems.push({ index: lit(await this.expander.expandString(el.index, proc), el.index), value: lit(await this.expander.expandString(el.value, proc), el.value) });
1078
+ else
1079
+ for (const f of await this.expander.expandFields([el.value], proc))
1080
+ elems.push({ index: null, value: lit(f, el.value) });
1081
+ }
1082
+ return { ...a, index, value: { ...a.value, elems } };
1083
+ }
1084
+ /** Perform an assignment node (prefix, bare, or declaration-builtin argument). */
1085
+ async performAssign(a, proc, opts) {
1086
+ const st = proc.state;
1087
+ const v = opts.local ? st.declareLocal(a.name) : st.ensure(a.name);
1088
+ if (v.readonly)
1089
+ throw new CommandError(`${a.name}: readonly variable`);
1090
+ if (opts.exported)
1091
+ v.exported = true;
1092
+ if (opts.declareKind)
1093
+ v.declaredKind = opts.declareKind;
1094
+ if (a.value?.type === 'ArrayLiteral') {
1095
+ const isAssoc = opts.declareKind === 'assoc' || v.declaredKind === 'assoc' || v.value?.kind === 'assoc';
1096
+ if (isAssoc) {
1097
+ const values = a.append && v.value?.kind === 'assoc' ? v.value.values : new Map();
1098
+ for (const el of a.value.elems) {
1099
+ if (!el.index) {
1100
+ // bash 5.1+: `declare -A m=(k v k2 v2)` key/value pairs
1101
+ continue;
1102
+ }
1103
+ const key = await this.expander.expandString(el.index, proc);
1104
+ values.set(key, this.applyAttrs(v, await this.expander.expandString(el.value, proc), proc));
1105
+ }
1106
+ const pairs = a.value.elems.filter((e) => !e.index);
1107
+ for (let i = 0; i + 1 < pairs.length; i += 2) {
1108
+ values.set(await this.expander.expandString(pairs[i].value, proc), await this.expander.expandString(pairs[i + 1].value, proc));
1109
+ }
1110
+ v.value = { kind: 'assoc', values };
1111
+ }
1112
+ else {
1113
+ const values = a.append && v.value?.kind === 'indexed' ? v.value.values : a.append && v.value?.kind === 'string' ? new Map([[0, v.value.value]]) : new Map();
1114
+ let next = values.size === 0 ? 0 : Math.max(...values.keys()) + 1;
1115
+ for (const el of a.value.elems) {
1116
+ if (el.index) {
1117
+ const idxText = await this.expander.expandString(el.index, proc);
1118
+ next = Number(await this.arithText(idxText, proc));
1119
+ values.set(next, this.applyAttrs(v, await this.expander.expandString(el.value, proc), proc));
1120
+ next++;
1121
+ }
1122
+ else {
1123
+ for (const f of await this.expander.expandFields([el.value], proc)) {
1124
+ values.set(next++, this.applyAttrs(v, f, proc));
1125
+ }
1126
+ }
1127
+ }
1128
+ v.value = { kind: 'indexed', values };
1129
+ }
1130
+ this.rt.emit({ type: 'assign', name: a.name, value: `(${varElements(v).join(' ')})`, pid: proc.pid, nodeId: a.id });
1131
+ return;
1132
+ }
1133
+ let value = a.value ? await this.expander.expandString(a.value, proc, { tildeAssign: true }) : '';
1134
+ if (a.index) {
1135
+ const isAssoc = v.value?.kind === 'assoc' || v.declaredKind === 'assoc';
1136
+ if (isAssoc) {
1137
+ const key = await this.expander.expandString(a.index, proc);
1138
+ if (v.value?.kind !== 'assoc')
1139
+ v.value = { kind: 'assoc', values: new Map() };
1140
+ const old = v.value.values.get(key) ?? '';
1141
+ v.value.values.set(key, this.applyAttrs(v, a.append ? old + value : value, proc));
1142
+ }
1143
+ else {
1144
+ const idxText = await this.expander.expandString(a.index, proc);
1145
+ let idx = Number(await this.arithText(idxText, proc));
1146
+ if (v.value === null || v.value.kind !== 'indexed') {
1147
+ const values = new Map();
1148
+ if (v.value?.kind === 'string')
1149
+ values.set(0, v.value.value);
1150
+ v.value = { kind: 'indexed', values };
1151
+ }
1152
+ if (idx < 0)
1153
+ idx = (v.value.values.size === 0 ? 0 : Math.max(...v.value.values.keys()) + 1) + idx;
1154
+ const old = v.value.values.get(idx) ?? '';
1155
+ v.value.values.set(idx, this.applyAttrs(v, a.append ? old + value : value, proc));
1156
+ }
1157
+ this.rt.emit({ type: 'assign', name: `${a.name}[…]`, value, pid: proc.pid, nodeId: a.id });
1158
+ return;
1159
+ }
1160
+ if (a.append) {
1161
+ if (v.integer) {
1162
+ const old = st.getString(a.name) ?? '0';
1163
+ value = String(BigInt(this.applyAttrs(v, old, proc)) + BigInt(this.applyAttrs(v, value, proc)));
1164
+ }
1165
+ else if (v.value?.kind === 'indexed') {
1166
+ const values = v.value.values;
1167
+ values.set(values.size === 0 ? 0 : Math.max(...values.keys()) + 1, value);
1168
+ return;
1169
+ }
1170
+ else {
1171
+ value = (st.getString(a.name) ?? '') + value;
1172
+ }
1173
+ }
1174
+ if (a.value === null && opts.declareKind === undefined && (opts.local || opts.exported)) {
1175
+ // `local x` / `export x` without value: declare only
1176
+ }
1177
+ value = this.applyAttrs(v, value, proc);
1178
+ if (v.value?.kind === 'indexed')
1179
+ v.value.values.set(0, value);
1180
+ else if (v.value?.kind === 'assoc')
1181
+ v.value.values.set('0', value);
1182
+ else
1183
+ v.value = { kind: 'string', value };
1184
+ this.rt.emit({ type: 'assign', name: a.name, value, pid: proc.pid, nodeId: a.id });
1185
+ }
1186
+ // -------------------------------------------------------------------------
1187
+ // Arithmetic
1188
+ // -------------------------------------------------------------------------
1189
+ arithEnv(proc) {
1190
+ const st = proc.state;
1191
+ return {
1192
+ get: (name, index) => {
1193
+ if (index === null) {
1194
+ if (/^[0-9]+$/.test(name))
1195
+ return st.positional[Number(name) - 1];
1196
+ return this.dynamicVar(proc, name) ?? st.getString(name);
1197
+ }
1198
+ const v = st.lookup(name);
1199
+ if (!v || v.value === null)
1200
+ return undefined;
1201
+ if (v.value.kind === 'indexed')
1202
+ return v.value.values.get(Number(index));
1203
+ if (v.value.kind === 'assoc')
1204
+ return v.value.values.get(index);
1205
+ return index === '0' ? v.value.value : undefined;
1206
+ },
1207
+ isAssoc: (name) => {
1208
+ const v = st.lookup(name);
1209
+ return v?.value?.kind === 'assoc' || v?.declaredKind === 'assoc';
1210
+ },
1211
+ set: (name, index, value) => {
1212
+ const v = st.ensure(name);
1213
+ if (v.readonly)
1214
+ throw new ArithError(`${name}: readonly variable`);
1215
+ const s = String(value);
1216
+ if (index === null) {
1217
+ if (v.value?.kind === 'indexed')
1218
+ v.value.values.set(0, s);
1219
+ else
1220
+ v.value = { kind: 'string', value: s };
1221
+ }
1222
+ else if (v.value?.kind === 'assoc' || v.declaredKind === 'assoc') {
1223
+ if (v.value?.kind !== 'assoc')
1224
+ v.value = { kind: 'assoc', values: new Map() };
1225
+ v.value.values.set(index, s);
1226
+ }
1227
+ else {
1228
+ if (v.value?.kind !== 'indexed') {
1229
+ const values = new Map();
1230
+ if (v.value?.kind === 'string')
1231
+ values.set(0, v.value.value);
1232
+ v.value = { kind: 'indexed', values };
1233
+ }
1234
+ v.value.values.set(Number(index), s);
1235
+ }
1236
+ this.rt.emit({ type: 'assign', name, value: s, pid: proc.pid, nodeId: 0 });
1237
+ },
1238
+ expandWord: (word) => this.expander.expandString(word, proc),
1239
+ nounset: st.options.nounset,
1240
+ };
1241
+ }
1242
+ async arith(expr, proc) {
1243
+ return evaluateArith(expr, this.arithEnv(proc));
1244
+ }
1245
+ async arithText(text, proc) {
1246
+ const { evaluateArithText } = await import("./arith.js");
1247
+ try {
1248
+ return evaluateArithText(text, this.arithEnv(proc));
1249
+ }
1250
+ catch (e) {
1251
+ if (e instanceof ArithError)
1252
+ throw new DiscardError(e.message);
1253
+ throw e;
1254
+ }
1255
+ }
1256
+ // -------------------------------------------------------------------------
1257
+ // Dynamic variables
1258
+ // -------------------------------------------------------------------------
1259
+ startTime = Date.now();
1260
+ dynamicVar(proc, name) {
1261
+ switch (name) {
1262
+ case 'RANDOM': return String(Math.floor(Math.random() * 32768));
1263
+ case 'SECONDS': return String(Math.floor((Date.now() - this.startTime) / 1000));
1264
+ case 'EPOCHSECONDS': return String(Math.floor(Date.now() / 1000));
1265
+ case 'EPOCHREALTIME': return (Date.now() / 1000).toFixed(6);
1266
+ case 'LINENO': return String(proc.line);
1267
+ case 'BASHPID': return String(proc.pid);
1268
+ case 'BASH_SUBSHELL': return String(proc.depth);
1269
+ case 'PPID': return String(process.pid);
1270
+ case 'FUNCNAME': return proc.state.funcStack[0];
1271
+ case 'UID': return String(process.getuid?.() ?? 0);
1272
+ case 'EUID': return String(process.geteuid?.() ?? 0);
1273
+ case 'BASH': return '/bin/bash';
1274
+ default: return undefined;
1275
+ }
1276
+ }
1277
+ // -------------------------------------------------------------------------
1278
+ // Substitutions
1279
+ // -------------------------------------------------------------------------
1280
+ async commandSubst(node, proc) {
1281
+ // $(< file)
1282
+ if (node.body.length === 1 && node.body[0].command.type === 'SimpleCommand') {
1283
+ const c = node.body[0].command;
1284
+ if (c.words.length === 0 && c.assigns.length === 0 && c.redirs.length === 1 && c.redirs[0].op === '<') {
1285
+ const target = await this.expander.expandString(c.redirs[0].target, proc);
1286
+ const abs = path.resolve(proc.state.cwd, target);
1287
+ const a = await this.rt.assessRedirects([{ kind: 'read', path: known(abs), source: '$(<)' }], c, proc);
1288
+ if (a.action === 'abort')
1289
+ throw new AbortRun(a.reason, a.status);
1290
+ if (a.action === 'deny') {
1291
+ this.errorMessage(proc, a.reason);
1292
+ proc.lastSubstStatus = a.status;
1293
+ return '';
1294
+ }
1295
+ try {
1296
+ const text = fs.readFileSync(abs, 'utf8');
1297
+ proc.lastSubstStatus = 0;
1298
+ return text.replace(/\n+$/, '');
1299
+ }
1300
+ catch (e) {
1301
+ this.errorMessage(proc, `${target}: ${e.code === 'ENOENT' ? 'No such file or directory' : String(e)}`);
1302
+ proc.lastSubstStatus = 1;
1303
+ return '';
1304
+ }
1305
+ }
1306
+ }
1307
+ const child = proc.subshell(this.rt.nextPid());
1308
+ if (!proc.state.shopts.has('inherit_errexit'))
1309
+ child.state.options.errexit = false;
1310
+ const capture = new Capture(this.rt.channel('capture', `$(${this.rt.text(node).replace(/^\$\(|\)$/g, '').replace(/^`|`$/g, '')})`, { nodeId: node.id }), this.rt.listeners);
1311
+ child.fds.set(1, capture);
1312
+ // keep a reference so closeAll does not discard the bytes before we read them
1313
+ capture.retain();
1314
+ const status = await this.runSubshell(child, () => this.execList(node.body, child), 'cmdsubst', node.id);
1315
+ proc.lastSubstStatus = status;
1316
+ proc.state.lastStatus = status;
1317
+ const text = capture.text();
1318
+ capture.release();
1319
+ return text.replace(/\n+$/, '');
1320
+ }
1321
+ async processSubst(node, proc) {
1322
+ const ch = this.rt.channel('procsubst', `${node.direction}(${this.rt.text(node).slice(2, -1)})`, { nodeId: node.id });
1323
+ const pipe = new Pipe(ch, this.rt.listeners);
1324
+ const child = proc.subshell(this.rt.nextPid());
1325
+ let fd = 63;
1326
+ while (proc.fds.get(fd) && fd > 10)
1327
+ fd--;
1328
+ if (node.direction === '<') {
1329
+ child.fds.set(1, pipe.writeEnd);
1330
+ proc.fds.set(fd, pipe.readEnd);
1331
+ }
1332
+ else {
1333
+ child.fds.set(0, pipe.readEnd);
1334
+ proc.fds.set(fd, pipe.writeEnd);
1335
+ }
1336
+ proc.procSubstFds.push({ fds: proc.fds, fd });
1337
+ this.rt.track(this.runSubshell(child, () => this.execList(node.body, child), 'procsubst', node.id));
1338
+ return `/dev/fd/${fd}`;
1339
+ }
1340
+ // -------------------------------------------------------------------------
1341
+ // Redirections
1342
+ // -------------------------------------------------------------------------
1343
+ async withCompoundRedirects(cmd, proc, fn) {
1344
+ if (cmd.redirs.length === 0)
1345
+ return fn(proc);
1346
+ const mark = proc.procSubstFds.length;
1347
+ const redirects = await this.resolveRedirects(cmd.redirs, proc);
1348
+ const a = await this.rt.assessRedirects(redirectEffectsOf(redirects), cmd, proc);
1349
+ if (a.action === 'abort')
1350
+ throw new AbortRun(a.reason, a.status);
1351
+ if (a.action === 'deny') {
1352
+ this.errorMessage(proc, a.reason);
1353
+ this.closeProcSubsts(proc, mark);
1354
+ return a.status;
1355
+ }
1356
+ const fds = proc.fds.clone();
1357
+ try {
1358
+ try {
1359
+ await this.openRedirects(redirects, fds, proc);
1360
+ }
1361
+ catch (e) {
1362
+ if (e instanceof FsError || e instanceof CommandError) {
1363
+ this.errorMessage(proc, e.message);
1364
+ return 1;
1365
+ }
1366
+ throw e;
1367
+ }
1368
+ this.closeProcSubsts(proc, mark);
1369
+ return await fn(proc.derive({ fds }));
1370
+ }
1371
+ finally {
1372
+ fds.closeAll();
1373
+ this.closeProcSubsts(proc, mark);
1374
+ }
1375
+ }
1376
+ async resolveRedirects(redirs, proc) {
1377
+ const out = [];
1378
+ for (const r of redirs) {
1379
+ const defaultFd = r.op.startsWith('<') ? 0 : 1;
1380
+ const fd = r.fd ?? defaultFd;
1381
+ if (r.op === '<<' || r.op === '<<-') {
1382
+ const body = r.heredoc ? (r.heredoc.quoted ? r.heredoc.body.parts[0]?.value ?? '' : await this.expander.expandHeredoc(r.heredoc.body, proc)) : '';
1383
+ out.push({ node: r, fd, op: r.op, content: body, fdVar: r.fdVar ?? undefined });
1384
+ continue;
1385
+ }
1386
+ if (r.op === '<<<') {
1387
+ const s = await this.expander.expandString(r.target, proc);
1388
+ out.push({ node: r, fd, op: r.op, content: s + '\n', fdVar: r.fdVar ?? undefined });
1389
+ continue;
1390
+ }
1391
+ const fields = await this.expander.expandFields([r.target], proc);
1392
+ if (fields.length !== 1 && !(r.op === '<&' || r.op === '>&'))
1393
+ throw new CommandError(`${this.rt.text(r.target)}: ambiguous redirect`);
1394
+ const targetText = fields[0] ?? '';
1395
+ if (r.op === '<&' || r.op === '>&') {
1396
+ if (targetText === '-') {
1397
+ out.push({ node: r, fd, op: r.op, close: true, fdVar: r.fdVar ?? undefined });
1398
+ continue;
1399
+ }
1400
+ const m = /^(\d+)(-?)$/.exec(targetText);
1401
+ if (m) {
1402
+ out.push({ node: r, fd, op: r.op, dupFd: Number(m[1]), close: m[2] === '-', fdVar: r.fdVar ?? undefined });
1403
+ continue;
1404
+ }
1405
+ if (r.op === '>&' && r.fd === null) {
1406
+ // `>& file` == `&> file`
1407
+ out.push({ node: r, fd: 1, op: '&>', path: path.resolve(proc.state.cwd, targetText) });
1408
+ continue;
1409
+ }
1410
+ throw new CommandError(`${targetText}: ambiguous redirect`);
1411
+ }
1412
+ const special = specialPath(targetText);
1413
+ out.push({ node: r, fd: r.op === '&>' || r.op === '&>>' ? 1 : fd, op: r.op, path: special ? undefined : path.resolve(proc.state.cwd, targetText), special, fdVar: r.fdVar ?? undefined });
1414
+ }
1415
+ return out;
1416
+ }
1417
+ async openRedirects(redirects, fds, proc) {
1418
+ for (const r of redirects) {
1419
+ let fd = r.fd;
1420
+ if (r.fdVar) {
1421
+ if (r.close && r.dupFd === undefined) {
1422
+ const n = Number(proc.state.getString(r.fdVar));
1423
+ if (Number.isInteger(n))
1424
+ fds.close(n);
1425
+ continue;
1426
+ }
1427
+ fd = fds.freeFd(10);
1428
+ this.assignScalar(proc, r.fdVar, String(fd));
1429
+ }
1430
+ if (r.close && r.dupFd === undefined) {
1431
+ fds.close(fd);
1432
+ continue;
1433
+ }
1434
+ if (r.dupFd !== undefined) {
1435
+ if (!fds.dup(r.dupFd, fd))
1436
+ throw new CommandError(`${r.dupFd}: Bad file descriptor`);
1437
+ if (r.close)
1438
+ fds.close(r.dupFd);
1439
+ continue;
1440
+ }
1441
+ if (r.content !== undefined) {
1442
+ fds.set(fd, new ConstInput(this.rt.channel('heredoc', r.op === '<<<' ? '<<<' : `<<${r.node.heredoc?.delimiter ?? ''}`, { nodeId: r.node.id }), r.content, this.rt.listeners));
1443
+ continue;
1444
+ }
1445
+ let file;
1446
+ if (r.special === 'null')
1447
+ file = new NullFile(this.rt.channel('null', '/dev/null'), this.rt.listeners);
1448
+ else if (r.special && typeof r.special === 'object') {
1449
+ const src = fds.get(r.special.dup) ?? proc.fds.get(r.special.dup);
1450
+ if (!src)
1451
+ throw new CommandError(`/dev/fd/${r.special.dup}: Bad file descriptor`);
1452
+ file = src.retain();
1453
+ }
1454
+ else {
1455
+ const mode = r.op === '<' ? 'read' : r.op === '>>' || r.op === '&>>' ? 'append' : r.op === '<>' ? 'readwrite' : 'write';
1456
+ const noclobber = proc.state.options.noclobber && (r.op === '>' || r.op === '&>') && !isNonRegular(r.path);
1457
+ file = await HostFile.open(this.rt.channel('file', `${r.op} ${r.path}`, { path: r.path, nodeId: r.node.id }), r.path, mode, this.rt.listeners, noclobber, proc.state.umask);
1458
+ }
1459
+ fds.set(fd, file);
1460
+ if (r.op === '&>' || r.op === '&>>')
1461
+ fds.dup(1, 2);
1462
+ }
1463
+ }
1464
+ }
1465
+ const INLINE_SHELLS = new Set(['bash', 'sh']);
1466
+ /**
1467
+ * Decide whether an external invocation is a shell script mash can run
1468
+ * itself: `bash [-euxo opt] -c SCRIPT [arg0 args]`, `bash [opts] FILE args`,
1469
+ * or an executable file whose shebang names bash/sh.
1470
+ */
1471
+ function inlineShellPlan(exePath, argv, cwd) {
1472
+ const base = path.basename(exePath);
1473
+ let args = argv.slice(1);
1474
+ if (!INLINE_SHELLS.has(base)) {
1475
+ // `./script.sh` with a bash/sh shebang
1476
+ let head;
1477
+ try {
1478
+ const fd = fs.openSync(exePath, 'r');
1479
+ const buf = Buffer.alloc(128);
1480
+ const n = fs.readSync(fd, buf, 0, 128, 0);
1481
+ fs.closeSync(fd);
1482
+ head = buf.subarray(0, n).toString('utf8');
1483
+ }
1484
+ catch {
1485
+ return null;
1486
+ }
1487
+ const m = /^#![ \t]*(\S+)(?:[ \t]+(\S+))?/.exec(head.split("\n")[0]);
1488
+ if (!m) {
1489
+ // no shebang: the kernel returns ENOEXEC and bash runs the file as a shell script
1490
+ if (head.startsWith('#!') || head.includes('\0') || /[\x00-\x08\x0e-\x1f]/.test(head.slice(0, 64)) || head.startsWith('\x7fELF') || head.startsWith('\xca\xfe') || head.startsWith('\xcf\xfa'))
1491
+ return null;
1492
+ args = [exePath, ...args];
1493
+ return inlineFromArgs(args, base, cwd);
1494
+ }
1495
+ const interp = path.basename(m[1]) === 'env' ? m[2] : path.basename(m[1]);
1496
+ if (!interp || !INLINE_SHELLS.has(interp))
1497
+ return null;
1498
+ if (m[2] && path.basename(m[1]) !== 'env' && m[2] !== '-e')
1499
+ return null;
1500
+ args = [exePath, ...args];
1501
+ if (m[2] === '-e')
1502
+ args.unshift('-e');
1503
+ }
1504
+ return inlineFromArgs(args, base, cwd);
1505
+ }
1506
+ function inlineFromArgs(args, base, cwd) {
1507
+ const options = {};
1508
+ const flagMap = { e: 'errexit', u: 'nounset', x: 'xtrace', f: 'noglob', v: 'verbose', C: 'noclobber', a: 'allexport' };
1509
+ let i = 0;
1510
+ let command;
1511
+ for (; i < args.length; i++) {
1512
+ const a = args[i];
1513
+ if (a === '-c') {
1514
+ command = args[i + 1];
1515
+ i += 2;
1516
+ break;
1517
+ }
1518
+ if (a === '-o' || a === '+o') {
1519
+ const name = args[++i];
1520
+ if (name === 'pipefail' || name === 'errexit' || name === 'nounset' || name === 'xtrace' || name === 'noglob')
1521
+ options[name] = a === '-o';
1522
+ else
1523
+ return null;
1524
+ continue;
1525
+ }
1526
+ if (a === '--') {
1527
+ i++;
1528
+ break;
1529
+ }
1530
+ if (/^[-+][euxfvCa]+$/.test(a)) {
1531
+ for (const f of a.slice(1))
1532
+ options[flagMap[f]] = a[0] === '-';
1533
+ continue;
1534
+ }
1535
+ if (a.startsWith('-'))
1536
+ return null; // interactive, login, --rcfile, ... : spawn a real shell
1537
+ break;
1538
+ }
1539
+ if (command !== undefined) {
1540
+ return { source: command, origin: 'bash -c', arg0: args[i] ?? base, args: args.slice(i + 1), options };
1541
+ }
1542
+ const file = args[i];
1543
+ if (file === undefined)
1544
+ return null; // reads the script from stdin
1545
+ const abs = path.resolve(cwd, file);
1546
+ let source;
1547
+ try {
1548
+ source = fs.readFileSync(abs, 'utf8');
1549
+ }
1550
+ catch {
1551
+ return null;
1552
+ }
1553
+ return { source, origin: abs, arg0: file, args: args.slice(i + 1), options };
1554
+ }
1555
+ import { evaluateArithText as evaluateArithTextSync } from "./arith.js";
1556
+ function isNonRegular(p) {
1557
+ try {
1558
+ return !fs.statSync(p).isFile();
1559
+ }
1560
+ catch {
1561
+ return false;
1562
+ }
1563
+ }
1564
+ function specialPath(p) {
1565
+ if (p === '/dev/null')
1566
+ return 'null';
1567
+ if (p === '/dev/stdin')
1568
+ return { dup: 0 };
1569
+ if (p === '/dev/stdout')
1570
+ return { dup: 1 };
1571
+ if (p === '/dev/stderr')
1572
+ return { dup: 2 };
1573
+ const m = /^\/dev\/fd\/(\d+)$/.exec(p);
1574
+ if (m)
1575
+ return { dup: Number(m[1]) };
1576
+ return undefined;
1577
+ }
1578
+ function redirectEffectsOf(redirects) {
1579
+ const effects = [];
1580
+ for (const r of redirects) {
1581
+ if (!r.path)
1582
+ continue;
1583
+ if (r.op === '<')
1584
+ effects.push({ kind: 'read', path: known(r.path), source: 'redirect' });
1585
+ else if (r.op === '<>') {
1586
+ effects.push({ kind: 'read', path: known(r.path), source: 'redirect' });
1587
+ effects.push({ kind: 'write', path: known(r.path), source: 'redirect' });
1588
+ }
1589
+ else
1590
+ effects.push({ kind: 'write', path: known(r.path), append: r.op === '>>' || r.op === '&>>', source: 'redirect' });
1591
+ }
1592
+ return effects;
1593
+ }
1594
+ function redirectInfo(r) {
1595
+ return { fd: r.fd, op: r.op, path: r.path ?? (r.special === 'null' ? '/dev/null' : undefined), dupFd: r.dupFd };
1596
+ }
1597
+ function kindOfStdin(f) {
1598
+ if (!f)
1599
+ return 'null';
1600
+ switch (f.channel.kind) {
1601
+ case 'null': return 'null';
1602
+ case 'file':
1603
+ case 'heredoc': return 'file';
1604
+ case 'host': return 'terminal';
1605
+ default: return 'pipe';
1606
+ }
1607
+ }
1608
+ /** PATH lookup (with the shell's hash table). */
1609
+ export function resolveCommand(name, state, pathOverride) {
1610
+ const isExec = (p) => {
1611
+ try {
1612
+ const st = fs.statSync(p);
1613
+ if (!st.isFile())
1614
+ return false;
1615
+ fs.accessSync(p, fs.constants.X_OK);
1616
+ return true;
1617
+ }
1618
+ catch {
1619
+ return false;
1620
+ }
1621
+ };
1622
+ if (name.includes('/')) {
1623
+ const p = path.resolve(state.cwd, name);
1624
+ return isExec(p) ? p : undefined;
1625
+ }
1626
+ const cached = pathOverride === undefined ? state.hash.get(name) : undefined;
1627
+ if (cached && isExec(cached))
1628
+ return cached;
1629
+ const PATH = pathOverride ?? state.getString('PATH') ?? '/usr/bin:/bin';
1630
+ for (const dir of PATH.split(':')) {
1631
+ const p = path.resolve(state.cwd, dir === '' ? '.' : dir, name);
1632
+ if (isExec(p)) {
1633
+ if (pathOverride === undefined)
1634
+ state.hash.set(name, p);
1635
+ return p;
1636
+ }
1637
+ }
1638
+ return undefined;
1639
+ }
1640
+ export { literalOf, evalTest };
1641
+ //# sourceMappingURL=interp.js.map