@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,1590 @@
1
+ /**
2
+ * Shell builtins. Each receives a {@link BuiltinContext}: `proc` has the
3
+ * command's redirections applied; `base` is the shell process itself (same
4
+ * state, original fds) for builtins that change fds permanently (`exec`).
5
+ */
6
+
7
+ import * as fs from 'node:fs'
8
+ import * as os from 'node:os'
9
+ import * as path from 'node:path'
10
+ import type * as A from '../parser/ast.ts'
11
+ import { parse, ParseError } from '../parser/parser.ts'
12
+ import { known } from '../analysis/value.ts'
13
+ import { shellQuote, varElements } from './expand.ts'
14
+ import type { Interpreter } from './interp.ts'
15
+ import { resolveCommand } from './interp.ts'
16
+ import { AbortRun, CommandError, DiscardError, ExitSignal, LoopSignal, ReturnSignal, type Proc } from './proc.ts'
17
+ import { KNOWN_SHOPTS, newVar, SET_FLAGS, SET_O_NAMES, type ShellState, type Variable } from './state.ts'
18
+ import { evalTest } from './test.ts'
19
+
20
+ export interface BuiltinContext {
21
+ interp: Interpreter
22
+ proc: Proc
23
+ base: Proc
24
+ argv: string[]
25
+ node: A.SimpleCommand
26
+ /** If `arg` is a placeholder for an `Assign` node (declaration builtins), the node. */
27
+ assignArg(arg: string): A.Assign | null
28
+ out(text: string): Promise<void>
29
+ err(text: string): Promise<void>
30
+ error(message: string): void
31
+ }
32
+
33
+ export type Builtin = (ctx: BuiltinContext) => Promise<number>
34
+
35
+ export const BUILTINS = new Map<string, Builtin>()
36
+
37
+ const def = (names: string[], fn: Builtin): void => {
38
+ for (const n of names) BUILTINS.set(n, fn)
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Escapes
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /** Interpret backslash escapes (echo -e, printf %b). Returns [text, stop] where stop means \c was seen. */
46
+ export function interpretEscapes(s: string, printfStyle = false): [string, boolean] {
47
+ let out = ''
48
+ for (let i = 0; i < s.length; i++) {
49
+ const c = s[i]
50
+ if (c !== '\\' || i + 1 >= s.length) {
51
+ out += c
52
+ continue
53
+ }
54
+ const e = s[++i]
55
+ switch (e) {
56
+ case 'a': out += '\x07'; break
57
+ case 'b': out += '\b'; break
58
+ case 'c': return [out, true]
59
+ case 'e': case 'E': out += '\x1b'; break
60
+ case 'f': out += '\f'; break
61
+ case 'n': out += '\n'; break
62
+ case 'r': out += '\r'; break
63
+ case 't': out += '\t'; break
64
+ case 'v': out += '\v'; break
65
+ case '\\': out += '\\'; break
66
+ case '"': out += printfStyle ? '"' : '\\"'; break
67
+ case "'": out += printfStyle ? "'" : "\\'"; break
68
+ case 'x': {
69
+ const m = /^[0-9a-fA-F]{1,2}/.exec(s.slice(i + 1))
70
+ if (!m) {
71
+ out += '\\x'
72
+ break
73
+ }
74
+ out += String.fromCharCode(parseInt(m[0], 16))
75
+ i += m[0].length
76
+ break
77
+ }
78
+ case 'u': case 'U': {
79
+ const m = new RegExp(`^[0-9a-fA-F]{1,${e === 'u' ? 4 : 8}}`).exec(s.slice(i + 1))
80
+ if (!m) {
81
+ out += '\\' + e
82
+ break
83
+ }
84
+ out += String.fromCodePoint(parseInt(m[0], 16))
85
+ i += m[0].length
86
+ break
87
+ }
88
+ case '0': {
89
+ const m = /^[0-7]{0,3}/.exec(s.slice(i + 1))!
90
+ out += String.fromCharCode(parseInt(m[0] || '0', 8) & 0xff)
91
+ i += m[0].length
92
+ break
93
+ }
94
+ default:
95
+ if (printfStyle && /[1-7]/.test(e)) {
96
+ const m = /^[0-7]{0,2}/.exec(s.slice(i + 1))!
97
+ out += String.fromCharCode(parseInt(e + m[0], 8) & 0xff)
98
+ i += m[0].length
99
+ } else out += '\\' + e
100
+ }
101
+ }
102
+ return [out, false]
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Trivial
107
+ // ---------------------------------------------------------------------------
108
+
109
+ def([':', 'true'], async () => 0)
110
+ def(['false'], async () => 1)
111
+
112
+ def(['echo'], async (ctx) => {
113
+ const args = ctx.argv.slice(1)
114
+ let newline = true
115
+ let escapes = ctx.proc.state.shopts.has('xpg_echo')
116
+ while (args.length > 0 && /^-[neE]+$/.test(args[0])) {
117
+ for (const f of args[0].slice(1)) {
118
+ if (f === 'n') newline = false
119
+ else if (f === 'e') escapes = true
120
+ else escapes = false
121
+ }
122
+ args.shift()
123
+ }
124
+ let text = args.join(' ')
125
+ if (escapes) {
126
+ const [t, stop] = interpretEscapes(text)
127
+ if (stop) {
128
+ await ctx.out(t)
129
+ return 0
130
+ }
131
+ text = t
132
+ }
133
+ await ctx.out(newline ? text + '\n' : text)
134
+ return 0
135
+ })
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // printf
139
+ // ---------------------------------------------------------------------------
140
+
141
+ function parseNumArg(arg: string | undefined, ctx: BuiltinContext, onError: () => void): bigint {
142
+ if (arg === undefined) return 0n
143
+ if (arg === '') {
144
+ // bash 5: an empty numeric argument is an invalid number (value 0, status 1); 3.2 accepted it
145
+ ctx.error(': invalid number')
146
+ onError()
147
+ return 0n
148
+ }
149
+ if (/^['"]/.test(arg)) return BigInt(arg.length > 1 ? arg.codePointAt(1)! : 0)
150
+ const t = arg.trim()
151
+ try {
152
+ if (/^[+-]?0[xX][0-9a-fA-F]+$/.test(t)) return BigInt(t.replace(/^([+-]?)0[xX]/, '$10x').replace(/^-0x(.*)$/, '-0x$1').startsWith('-') ? -BigInt(t.replace(/^-/, '')) : BigInt(t.replace(/^\+/, '')))
153
+ if (/^[+-]?0[0-7]+$/.test(t)) return (t.startsWith('-') ? -1n : 1n) * BigInt('0o' + t.replace(/^[+-]?0/, ''))
154
+ if (/^[+-]?\d+$/.test(t)) return BigInt(t)
155
+ } catch {
156
+ // fall through
157
+ }
158
+ const m = /^([+-]?\d+)/.exec(t)
159
+ ctx.error(`${arg}: invalid number`)
160
+ onError()
161
+ return m ? BigInt(m[1]) : 0n
162
+ }
163
+
164
+ export function formatPrintf(fmt: string, args: string[], ctx: BuiltinContext): { text: string; failed: boolean } {
165
+ let out = ''
166
+ let failed = false
167
+ let ai = 0
168
+ const next = (): string | undefined => args[ai++]
169
+ const fail = (): void => {
170
+ failed = true
171
+ }
172
+ let first = true
173
+ while (first || ai < args.length) {
174
+ first = false
175
+ const startAi = ai
176
+ for (let i = 0; i < fmt.length; i++) {
177
+ const c = fmt[i]
178
+ if (c === '\\') {
179
+ // escapes in the format
180
+ let j = i + 1
181
+ const e = fmt[j]
182
+ if (e === undefined) {
183
+ out += '\\'
184
+ continue
185
+ }
186
+ let seq = '\\' + e
187
+ if (/[0-7]/.test(e)) {
188
+ const m = /^[0-7]{1,3}/.exec(fmt.slice(j))!
189
+ seq = '\\0' + m[0]
190
+ j += m[0].length - 1
191
+ } else if (e === 'x' || e === 'u' || e === 'U') {
192
+ const m = new RegExp(`^[0-9a-fA-F]{1,${e === 'x' ? 2 : e === 'u' ? 4 : 8}}`).exec(fmt.slice(j + 1))
193
+ if (m) {
194
+ seq += m[0]
195
+ j += m[0].length
196
+ }
197
+ }
198
+ if (seq === '\\c') {
199
+ out += '\\c'
200
+ i = j
201
+ continue
202
+ }
203
+ const [t] = interpretEscapes(seq, true)
204
+ out += t
205
+ i = j
206
+ continue
207
+ }
208
+ if (c !== '%') {
209
+ out += c
210
+ continue
211
+ }
212
+ if (fmt[i + 1] === '%') {
213
+ out += '%'
214
+ i++
215
+ continue
216
+ }
217
+ const m = /^%([-+ #0']*)(\*|\d+)?(?:\.(\*|\d*))?(?:hh|h|ll|l|L|q|j|z|t)?([diouxXfFeEgGcsbqaA])/.exec(fmt.slice(i))
218
+ if (!m) {
219
+ out += c
220
+ continue
221
+ }
222
+ i += m[0].length - 1
223
+ const flags = m[1]
224
+ let width = m[2] === '*' ? Number(parseNumArg(next(), ctx, fail)) : m[2] ? Number(m[2]) : undefined
225
+ let leftAlign = flags.includes('-')
226
+ if (width !== undefined && width < 0) {
227
+ leftAlign = true
228
+ width = -width
229
+ }
230
+ const precision = m[3] === undefined ? undefined : m[3] === '*' ? Number(parseNumArg(next(), ctx, fail)) : Number(m[3] || '0')
231
+ const conv = m[4]
232
+ const isFloat = 'fFeEgGaA'.includes(conv)
233
+ const pad = (s: string, numeric: boolean): string => {
234
+ if (width === undefined || s.length >= width) return s
235
+ if (leftAlign) return s.padEnd(width)
236
+ if (numeric && flags.includes('0') && (precision === undefined || isFloat)) {
237
+ const sign = /^[+ -]/.test(s) ? s[0] : ''
238
+ return sign + s.slice(sign.length).padStart(width - sign.length, '0')
239
+ }
240
+ return s.padStart(width)
241
+ }
242
+ switch (conv) {
243
+ case 's': {
244
+ let s = next() ?? ''
245
+ if (precision !== undefined) s = s.slice(0, precision)
246
+ out += pad(s, false)
247
+ break
248
+ }
249
+ case 'b': {
250
+ const [t, stop] = interpretEscapes(next() ?? '', true)
251
+ let s = t
252
+ if (precision !== undefined) s = s.slice(0, precision)
253
+ out += pad(s, false)
254
+ if (stop) return { text: out, failed }
255
+ break
256
+ }
257
+ case 'q':
258
+ out += pad(printfQuote(next() ?? ''), false)
259
+ break
260
+ case 'c': {
261
+ // bash 5 prints the first byte, so an empty argument gives a NUL (3.2: nothing)
262
+ const a = next()
263
+ out += pad(a === '' ? '\0' : (a ?? '').slice(0, 1), false)
264
+ break
265
+ }
266
+ case 'd': case 'i': {
267
+ const n = parseNumArg(next(), ctx, fail)
268
+ let s = (n < 0n ? -n : n).toString()
269
+ if (precision !== undefined) s = s.padStart(precision, '0')
270
+ s = (n < 0n ? '-' : flags.includes('+') ? '+' : flags.includes(' ') ? ' ' : '') + s
271
+ out += pad(s, true)
272
+ break
273
+ }
274
+ case 'u': case 'o': case 'x': case 'X': {
275
+ let n = parseNumArg(next(), ctx, fail)
276
+ if (n < 0n) n = BigInt.asUintN(64, n)
277
+ let s = conv === 'o' ? n.toString(8) : conv === 'u' ? n.toString() : n.toString(16)
278
+ if (conv === 'X') s = s.toUpperCase()
279
+ if (precision !== undefined) s = s.padStart(precision, '0')
280
+ if (flags.includes('#') && n !== 0n) s = (conv === 'o' ? '0' : conv === 'x' ? '0x' : conv === 'X' ? '0X' : '') + s
281
+ out += pad(s, true)
282
+ break
283
+ }
284
+ default: {
285
+ // floating point
286
+ const raw = next()
287
+ let x = raw === undefined || raw === '' ? 0 : Number(raw)
288
+ if (Number.isNaN(x)) {
289
+ ctx.error(`${raw}: invalid number`)
290
+ fail()
291
+ x = 0
292
+ }
293
+ const p = precision ?? 6
294
+ let s: string
295
+ switch (conv) {
296
+ case 'f': case 'F': s = toFixedHalfEven(x, p); break
297
+ case 'e': case 'E': s = x.toExponential(p).replace(/e([+-])(\d)$/, 'e$10$2'); break
298
+ case 'g': case 'G': {
299
+ const pp = p === 0 ? 1 : p
300
+ const exp = x === 0 ? 0 : Math.floor(Math.log10(Math.abs(x)))
301
+ if (exp < -4 || exp >= pp) s = x.toExponential(pp - 1).replace(/\.?0+e/, 'e').replace(/e([+-])(\d)$/, 'e$10$2')
302
+ else s = String(Number(x.toPrecision(pp)))
303
+ break
304
+ }
305
+ default: s = x.toString(16)
306
+ }
307
+ if (conv === 'E' || conv === 'G' || conv === 'F') s = s.toUpperCase()
308
+ if (x >= 0 && flags.includes('+')) s = '+' + s
309
+ else if (x >= 0 && flags.includes(' ')) s = ' ' + s
310
+ out += pad(s, true)
311
+ }
312
+ }
313
+ }
314
+ if (ai === startAi) break // format consumed no arguments
315
+ }
316
+ return { text: out, failed }
317
+ }
318
+
319
+ /** `printf %q`: backslash-escape shell metacharacters (bash style). */
320
+ function printfQuote(s: string): string {
321
+ if (s === '') return "''"
322
+ if (/[\x00-\x1f\x7f]/.test(s)) return shellQuote(s)
323
+ return s.replace(/[^A-Za-z0-9_@%+=:,./-]/g, (c) => '\\' + c)
324
+ }
325
+
326
+ /** C printf `%.Nf`: round the exact binary value, ties to even. */
327
+ export function toFixedHalfEven(x: number, digits: number): string {
328
+ if (!Number.isFinite(x)) return String(x)
329
+ const neg = x < 0 || Object.is(x, -0)
330
+ // exact decimal expansion of the double
331
+ const [mant, exp] = (() => {
332
+ const buf = new DataView(new ArrayBuffer(8))
333
+ buf.setFloat64(0, Math.abs(x))
334
+ const bits = buf.getBigUint64(0)
335
+ const e = Number((bits >> 52n) & 0x7ffn)
336
+ const f = bits & ((1n << 52n) - 1n)
337
+ return e === 0 ? [f, -1074] : [f | (1n << 52n), e - 1075]
338
+ })()
339
+ // value = mant * 2^exp; scaled = value * 10^digits as a rational num/den
340
+ let num = mant * 10n ** BigInt(digits)
341
+ let den = 1n
342
+ if (exp >= 0) num <<= BigInt(exp)
343
+ else den <<= BigInt(-exp)
344
+ let q = num / den
345
+ const r = num % den
346
+ if (r * 2n > den || (r * 2n === den && (q & 1n) === 1n)) q += 1n
347
+ let s = q.toString().padStart(digits + 1, '0')
348
+ if (digits > 0) s = s.slice(0, -digits) + '.' + s.slice(-digits)
349
+ return (neg && q !== 0n ? '-' : neg && x !== 0 ? '-' : '') + s
350
+ }
351
+
352
+ def(['printf'], async (ctx) => {
353
+ const args = ctx.argv.slice(1)
354
+ let varName: string | null = null
355
+ if (args[0] === '-v') {
356
+ varName = args[1] ?? null
357
+ args.splice(0, 2)
358
+ }
359
+ if (args[0] === '--') args.shift()
360
+ if (args.length === 0) {
361
+ ctx.error('usage: printf [-v var] format [arguments]')
362
+ return 2
363
+ }
364
+ const { text, failed } = formatPrintf(args[0], args.slice(1), ctx)
365
+ const element = varName === null ? null : /^([A-Za-z_]\w*)\[(.+)\]$/.exec(varName)
366
+ if (element) await ctx.interp.assignElement(ctx.proc, element[1], element[2], text)
367
+ else if (varName !== null) ctx.interp.assignScalar(ctx.proc, varName, text)
368
+ else await ctx.out(text)
369
+ return failed ? 1 : 0
370
+ })
371
+
372
+ // ---------------------------------------------------------------------------
373
+ // Directories
374
+ // ---------------------------------------------------------------------------
375
+
376
+ function changeDir(ctx: BuiltinContext, target: string, physical: boolean): number {
377
+ const st = ctx.proc.state
378
+ let abs = path.resolve(st.cwd, target)
379
+ let stat: fs.Stats
380
+ try {
381
+ stat = fs.statSync(abs)
382
+ } catch {
383
+ ctx.error(`${target}: No such file or directory`)
384
+ return 1
385
+ }
386
+ if (!stat.isDirectory()) {
387
+ ctx.error(`${target}: Not a directory`)
388
+ return 1
389
+ }
390
+ try {
391
+ fs.accessSync(abs, fs.constants.X_OK)
392
+ } catch {
393
+ ctx.error(`${target}: Permission denied`)
394
+ return 1
395
+ }
396
+ if (physical) abs = fs.realpathSync(abs)
397
+ const old = st.cwd
398
+ st.cwd = abs
399
+ ctx.interp.assignScalar(ctx.proc, 'OLDPWD', old)
400
+ ctx.interp.assignScalar(ctx.proc, 'PWD', abs)
401
+ return 0
402
+ }
403
+
404
+ def(['cd'], async (ctx) => {
405
+ const args = ctx.argv.slice(1)
406
+ let physical = false
407
+ while (args[0] === '-P' || args[0] === '-L' || args[0] === '--') {
408
+ if (args[0] === '-P') physical = true
409
+ if (args.shift() === '--') break
410
+ }
411
+ const st = ctx.proc.state
412
+ let target = args[0]
413
+ if (target === undefined) {
414
+ target = st.getString('HOME') ?? os.homedir()
415
+ } else if (target === '-') {
416
+ const old = st.getString('OLDPWD')
417
+ if (!old) {
418
+ ctx.error('OLDPWD not set')
419
+ return 1
420
+ }
421
+ target = old
422
+ const r = changeDir(ctx, target, physical)
423
+ if (r === 0) await ctx.out(st.cwd + '\n')
424
+ return r
425
+ }
426
+ return changeDir(ctx, target, physical)
427
+ })
428
+
429
+ def(['pwd'], async (ctx) => {
430
+ const physical = ctx.argv.includes('-P')
431
+ await ctx.out((physical ? fs.realpathSync(ctx.proc.state.cwd) : ctx.proc.state.cwd) + '\n')
432
+ return 0
433
+ })
434
+
435
+ def(['pushd'], async (ctx) => {
436
+ const st = ctx.proc.state
437
+ const dir = ctx.argv[1]
438
+ if (dir === undefined) {
439
+ const top = st.dirStack.shift()
440
+ if (!top) {
441
+ ctx.error('no other directory')
442
+ return 1
443
+ }
444
+ const cur = st.cwd
445
+ const r = changeDir(ctx, top, false)
446
+ if (r === 0) st.dirStack.unshift(cur)
447
+ else st.dirStack.unshift(top)
448
+ } else {
449
+ const cur = st.cwd
450
+ const r = changeDir(ctx, dir, false)
451
+ if (r !== 0) return r
452
+ st.dirStack.unshift(cur)
453
+ }
454
+ await ctx.out([st.cwd, ...st.dirStack].join(' ') + '\n')
455
+ return 0
456
+ })
457
+
458
+ def(['popd'], async (ctx) => {
459
+ const st = ctx.proc.state
460
+ const top = st.dirStack.shift()
461
+ if (top === undefined) {
462
+ ctx.error('directory stack empty')
463
+ return 1
464
+ }
465
+ const r = changeDir(ctx, top, false)
466
+ if (r !== 0) return r
467
+ await ctx.out([st.cwd, ...st.dirStack].join(' ') + '\n')
468
+ return 0
469
+ })
470
+
471
+ def(['dirs'], async (ctx) => {
472
+ const st = ctx.proc.state
473
+ if (ctx.argv.includes('-c')) {
474
+ st.dirStack = []
475
+ return 0
476
+ }
477
+ await ctx.out([st.cwd, ...st.dirStack].join(' ') + '\n')
478
+ return 0
479
+ })
480
+
481
+ // ---------------------------------------------------------------------------
482
+ // Variables
483
+ // ---------------------------------------------------------------------------
484
+
485
+ function declLine(name: string, v: Variable): string {
486
+ let flags = ''
487
+ if (v.value?.kind === 'indexed' || v.declaredKind === 'indexed') flags += 'a'
488
+ if (v.value?.kind === 'assoc' || v.declaredKind === 'assoc') flags += 'A'
489
+ if (v.integer) flags += 'i'
490
+ if (v.lower) flags += 'l'
491
+ if (v.readonly) flags += 'r'
492
+ if (v.upper) flags += 'u'
493
+ if (v.exported) flags += 'x'
494
+ const f = flags === '' ? '--' : '-' + flags
495
+ if (v.value === null) return `declare ${f} ${name}`
496
+ if (v.value.kind === 'string') return `declare ${f} ${name}=${dq(v.value.value)}`
497
+ if (v.value.kind === 'indexed') {
498
+ const items = [...v.value.values.entries()].sort((a, b) => a[0] - b[0]).map(([k, x]) => `[${k}]=${dq(x)}`)
499
+ return `declare ${f} ${name}=(${items.join(' ')})`
500
+ }
501
+ const items = [...v.value.values.entries()].map(([k, x]) => `[${shellQuote(k)}]=${dq(x)}`)
502
+ return `declare ${f} ${name}=(${items.join(' ')} )`
503
+ }
504
+
505
+ function dq(s: string): string {
506
+ return '"' + s.replace(/(["\\$`])/g, '\\$1') + '"'
507
+ }
508
+
509
+ function isName(s: string): boolean {
510
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(s)
511
+ }
512
+
513
+ async function declare(ctx: BuiltinContext, variant: 'declare' | 'local' | 'export' | 'readonly'): Promise<number> {
514
+ const st = ctx.proc.state
515
+ const args = ctx.argv.slice(1)
516
+ const on = new Set<string>()
517
+ const off = new Set<string>()
518
+ let i = 0
519
+ for (; i < args.length; i++) {
520
+ const a = args[i]
521
+ if (ctx.assignArg(a)) break
522
+ if (a === '--') {
523
+ i++
524
+ break
525
+ }
526
+ if (/^[-+][a-zA-Z]+$/.test(a)) {
527
+ for (const f of a.slice(1)) (a[0] === '-' ? on : off).add(f)
528
+ } else break
529
+ }
530
+ const names = args.slice(i)
531
+ // expand every assignment value before assigning any (bash semantics)
532
+ const frozen = new Map<string, A.Assign>()
533
+ for (const arg of names) {
534
+ const a = ctx.assignArg(arg)
535
+ if (a) frozen.set(arg, await ctx.interp.freezeAssign(a, ctx.proc))
536
+ }
537
+ if (variant === 'local' && st.locals.length === 0) {
538
+ ctx.error('can only be used in a function')
539
+ return 1
540
+ }
541
+ const inFunction = st.locals.length > 0
542
+ const local = variant === 'local' || ((variant === 'declare') && inFunction && !on.has('g'))
543
+ if (variant === 'export') on.add('x')
544
+ if (variant === 'readonly') on.add('r')
545
+
546
+ if ((on.has('f') || off.has('f')) && (variant === 'export' || on.has('x') || off.has('x'))) {
547
+ let status = 0
548
+ for (const n of names) {
549
+ const f = st.functions.get(n)
550
+ if (!f) {
551
+ ctx.error(`${n}: not a function`)
552
+ status = 1
553
+ continue
554
+ }
555
+ st.functions.set(n, { ...f, exported: !off.has('f') && !off.has('x') })
556
+ }
557
+ return status
558
+ }
559
+ if (on.has('f') || on.has('F')) {
560
+ for (const n of names.length > 0 ? names : [...st.functions.keys()].sort()) {
561
+ const f = st.functions.get(n)
562
+ if (!f) return 1
563
+ await ctx.out(on.has('F') ? (names.length > 0 ? `${n}\n` : `declare -f ${n}\n`) : `${n} () ${ctx.interp.rt.text(f.def.body)}\n`)
564
+ }
565
+ return 0
566
+ }
567
+ if (names.length === 0 || (on.has('p') && names.length === 0)) {
568
+ for (const n of st.visibleNames()) {
569
+ const v = st.lookup(n)!
570
+ if (variant === 'export' && !v.exported) continue
571
+ if (variant === 'readonly' && !v.readonly) continue
572
+ await ctx.out(variant === 'export' ? `declare -x ${n}${v.value?.kind === 'string' ? '=' + dq(v.value.value) : ''}\n` : declLine(n, v) + '\n')
573
+ }
574
+ return 0
575
+ }
576
+ if (on.has('p')) {
577
+ let status = 0
578
+ for (const n of names) {
579
+ const v = st.lookup(n)
580
+ if (!v) {
581
+ ctx.error(`${n}: not found`)
582
+ status = 1
583
+ continue
584
+ }
585
+ await ctx.out(declLine(n, v) + '\n')
586
+ }
587
+ return status
588
+ }
589
+ const kind = on.has('A') ? 'assoc' : on.has('a') ? 'indexed' : undefined
590
+ let status = 0
591
+ const applyAttrs = (v: Variable): void => {
592
+ if (on.has('x')) v.exported = true
593
+ if (off.has('x')) v.exported = false
594
+ if (on.has('i')) v.integer = true
595
+ if (off.has('i')) v.integer = false
596
+ if (on.has('l')) {
597
+ v.lower = true
598
+ v.upper = false
599
+ }
600
+ if (on.has('u')) {
601
+ v.upper = true
602
+ v.lower = false
603
+ }
604
+ }
605
+ for (const arg of names) {
606
+ const assign = frozen.get(arg) ?? null
607
+ if (assign) {
608
+ const v = local ? st.declareLocal(assign.name) : st.ensure(assign.name)
609
+ if (v.readonly) {
610
+ ctx.error(`${assign.name}: readonly variable`)
611
+ status = 1
612
+ continue
613
+ }
614
+ applyAttrs(v)
615
+ if (kind && v.value === null) v.declaredKind = kind
616
+ await ctx.interp.performAssign(assign, ctx.proc, { local, declareKind: kind })
617
+ if (on.has('r')) v.readonly = true
618
+ continue
619
+ }
620
+ const eq = arg.indexOf('=')
621
+ const name = eq === -1 ? arg : arg.slice(0, eq).replace(/\+$/, '')
622
+ if (!isName(name.replace(/\[.*\]$/, ''))) {
623
+ ctx.error(`\`${arg}': not a valid identifier`)
624
+ status = 1
625
+ continue
626
+ }
627
+ const v = local ? st.declareLocal(name) : (variant === 'declare' && on.has('g') ? globalVar(st, name) : st.ensure(name))
628
+ if (eq !== -1 && v.readonly) {
629
+ ctx.error(`${name}: readonly variable`)
630
+ status = 1
631
+ continue
632
+ }
633
+ applyAttrs(v)
634
+ if (kind) {
635
+ if (v.value === null) v.declaredKind = kind
636
+ else if (kind === 'indexed' && v.value.kind === 'string') v.value = { kind: 'indexed', values: new Map([[0, v.value.value]]) }
637
+ else if (kind === 'assoc' && v.value.kind !== 'assoc') v.value = { kind: 'assoc', values: new Map() }
638
+ }
639
+ if (eq !== -1) {
640
+ const append = arg[eq - 1] === '+'
641
+ const value = arg.slice(eq + 1)
642
+ const old = append ? st.getString(name) ?? '' : ''
643
+ ctx.interp.assignScalar(ctx.proc, name, old + value, { local })
644
+ }
645
+ if (on.has('r')) v.readonly = true
646
+ if (off.has('r') && v.readonly) {
647
+ ctx.error(`${name}: readonly variable`)
648
+ status = 1
649
+ }
650
+ }
651
+ return status
652
+ }
653
+
654
+ function globalVar(st: ShellState, name: string): Variable {
655
+ let v = st.globals.get(name)
656
+ if (!v) {
657
+ v = newVar(null)
658
+ st.globals.set(name, v)
659
+ }
660
+ return v
661
+ }
662
+
663
+ def(['declare', 'typeset'], (ctx) => declare(ctx, 'declare'))
664
+ def(['local'], (ctx) => declare(ctx, 'local'))
665
+ def(['export'], async (ctx) => {
666
+ if (ctx.argv[1] === '-n') {
667
+ for (const n of ctx.argv.slice(2)) {
668
+ const v = ctx.proc.state.lookup(n)
669
+ if (v) v.exported = false
670
+ }
671
+ return 0
672
+ }
673
+ return declare(ctx, 'export')
674
+ })
675
+ def(['readonly'], (ctx) => declare(ctx, 'readonly'))
676
+
677
+ def(['unset'], async (ctx) => {
678
+ const st = ctx.proc.state
679
+ let mode: 'v' | 'f' | 'auto' = 'auto'
680
+ let status = 0
681
+ for (const a of ctx.argv.slice(1)) {
682
+ if (a === '-v') {
683
+ mode = 'v'
684
+ continue
685
+ }
686
+ if (a === '-f') {
687
+ mode = 'f'
688
+ continue
689
+ }
690
+ if (a === '-n') continue
691
+ if (mode === 'f') {
692
+ st.functions.delete(a)
693
+ continue
694
+ }
695
+ const m = /^([A-Za-z_][A-Za-z0-9_]*)\[(.*)\]$/.exec(a)
696
+ if (m && (m[2] === '@' || m[2] === '*')) {
697
+ st.unset(m[1])
698
+ continue
699
+ }
700
+ if (m) {
701
+ const v = st.lookup(m[1])
702
+ if (v?.value?.kind === 'indexed') {
703
+ const idx = Number(await ctx.interp.arithText(m[2], ctx.proc))
704
+ v.value.values.delete(idx)
705
+ } else if (v?.value?.kind === 'assoc') v.value.values.delete(m[2])
706
+ continue
707
+ }
708
+ if (!isName(a)) {
709
+ ctx.error(`\`${a}': not a valid identifier`)
710
+ status = 1
711
+ continue
712
+ }
713
+ if (st.lookup(a)) {
714
+ if (!st.unset(a)) {
715
+ ctx.error(`${a}: cannot unset: readonly variable`)
716
+ status = 1
717
+ }
718
+ } else if (mode === 'auto') st.functions.delete(a)
719
+ }
720
+ return status
721
+ })
722
+
723
+ def(['set'], async (ctx) => {
724
+ const st = ctx.proc.state
725
+ const args = ctx.argv.slice(1)
726
+ if (args.length === 0) {
727
+ for (const n of st.visibleNames()) {
728
+ const v = st.lookup(n)!
729
+ if (v.value?.kind === 'string') await ctx.out(`${n}=${shellQuote(v.value.value)}\n`)
730
+ else if (v.value) await ctx.out(`${n}=(${varElements(v).map(shellQuote).join(' ')})\n`)
731
+ }
732
+ return 0
733
+ }
734
+ let i = 0
735
+ for (; i < args.length; i++) {
736
+ const a = args[i]
737
+ if (a === '--') {
738
+ st.positional = args.slice(i + 1)
739
+ return 0
740
+ }
741
+ if (a === '-' ) {
742
+ st.options.xtrace = false
743
+ st.options.verbose = false
744
+ st.positional = args.slice(i + 1)
745
+ return 0
746
+ }
747
+ if (!/^[-+]/.test(a)) break
748
+ const enable = a[0] === '-'
749
+ for (let j = 1; j < a.length; j++) {
750
+ const f = a[j]
751
+ if (f === 'o') {
752
+ const name = args[i + 1]
753
+ if (name === undefined || /^[-+]/.test(name)) {
754
+ for (const [n, opt] of Object.entries(SET_O_NAMES)) await ctx.out(`${n.padEnd(15)}\t${st.options[opt] ? 'on' : 'off'}\n`)
755
+ continue
756
+ }
757
+ i++
758
+ const opt = SET_O_NAMES[name]
759
+ if (!opt) {
760
+ if (['posix', 'emacs', 'vi', 'monitor', 'notify', 'hashall', 'braceexpand', 'histexpand', 'history', 'ignoreeof', 'interactive-comments', 'keyword', 'onecmd', 'physical', 'privileged', 'errtrace', 'functrace'].includes(name)) continue
761
+ ctx.error(`${name}: invalid option name`)
762
+ return 2
763
+ }
764
+ st.options[opt] = enable
765
+ continue
766
+ }
767
+ const opt = SET_FLAGS[f]
768
+ if (opt) st.options[opt] = enable
769
+ else if (!'hBmbHPTkt'.includes(f)) {
770
+ ctx.error(`${a[0]}${f}: invalid option`)
771
+ return 2
772
+ }
773
+ }
774
+ }
775
+ if (i < args.length) st.positional = args.slice(i)
776
+ return 0
777
+ })
778
+
779
+ def(['shopt'], async (ctx) => {
780
+ const st = ctx.proc.state
781
+ const args = ctx.argv.slice(1)
782
+ let mode: 's' | 'u' | 'print' = 'print'
783
+ let quiet = false
784
+ let printMode = false
785
+ const names: string[] = []
786
+ for (const a of args) {
787
+ if (a === '-s') mode = 's'
788
+ else if (a === '-u') mode = 'u'
789
+ else if (a === '-q') quiet = true
790
+ else if (a === '-p') printMode = true
791
+ else if (a === '-o') continue
792
+ else names.push(a)
793
+ }
794
+ let status = 0
795
+ for (const n of names) {
796
+ if (!(KNOWN_SHOPTS as readonly string[]).includes(n)) {
797
+ ctx.error(`${n}: invalid shell option name`)
798
+ status = 1
799
+ }
800
+ }
801
+ if (status !== 0) return status
802
+ if (mode === 's') for (const n of names) st.shopts.add(n)
803
+ else if (mode === 'u') for (const n of names) st.shopts.delete(n)
804
+ else {
805
+ const list = names.length > 0 ? names : [...KNOWN_SHOPTS]
806
+ for (const n of list) {
807
+ const on = st.shopts.has(n)
808
+ if (!on) status = 1
809
+ if (!quiet) await ctx.out(printMode ? `shopt ${on ? '-s' : '-u'} ${n}\n` : `${n.padEnd(20)}\t${on ? 'on' : 'off'}\n`) // bash 5 width (3.2: 15)
810
+ }
811
+ }
812
+ return status
813
+ })
814
+
815
+ def(['shift'], async (ctx) => {
816
+ const n = ctx.argv[1] === undefined ? 1 : Number(ctx.argv[1])
817
+ const st = ctx.proc.state
818
+ if (!Number.isInteger(n) || n < 0) {
819
+ ctx.error(`${ctx.argv[1]}: numeric argument required`)
820
+ return 1
821
+ }
822
+ if (n > st.positional.length) return 1
823
+ st.positional = st.positional.slice(n)
824
+ return 0
825
+ })
826
+
827
+ def(['let'], async (ctx) => {
828
+ if (ctx.argv.length < 2) {
829
+ ctx.error('expression expected')
830
+ return 1
831
+ }
832
+ let last = 0n
833
+ for (const e of ctx.argv.slice(1)) last = await ctx.interp.arithText(e, ctx.proc)
834
+ return last !== 0n ? 0 : 1
835
+ })
836
+
837
+ // ---------------------------------------------------------------------------
838
+ // read / mapfile
839
+ // ---------------------------------------------------------------------------
840
+
841
+ def(['read'], async (ctx) => {
842
+ const st = ctx.proc.state
843
+ const args = ctx.argv.slice(1)
844
+ let raw = false
845
+ let arrayName: string | null = null
846
+ let delim: number | null = 10
847
+ let nchars: number | null = null
848
+ let exactChars = false
849
+ let fd = 0
850
+ let timeout: number | null = null
851
+ let prompt: string | null = null
852
+ let i = 0
853
+ for (; i < args.length; i++) {
854
+ const a = args[i]
855
+ if (!a.startsWith('-') || a === '-') break
856
+ if (a === '--') {
857
+ i++
858
+ break
859
+ }
860
+ for (let j = 1; j < a.length; j++) {
861
+ const f = a[j]
862
+ const takeArg = (): string => {
863
+ const rest = a.slice(j + 1)
864
+ j = a.length
865
+ if (rest !== '') return rest
866
+ return args[++i] ?? ''
867
+ }
868
+ switch (f) {
869
+ case 'r': raw = true; break
870
+ case 's': case 'e': break
871
+ case 'a': arrayName = takeArg(); break
872
+ case 'd': {
873
+ const d = takeArg()
874
+ delim = d === '' ? 0 : d.charCodeAt(0)
875
+ break
876
+ }
877
+ case 'n': nchars = Number(takeArg()); break
878
+ case 'N': nchars = Number(takeArg()); exactChars = true; break
879
+ case 'u': fd = Number(takeArg()); break
880
+ case 't': timeout = Number(takeArg()); break
881
+ case 'p': prompt = takeArg(); break
882
+ case 'i': takeArg(); break
883
+ default:
884
+ ctx.error(`-${f}: invalid option`)
885
+ return 2
886
+ }
887
+ }
888
+ }
889
+ const names = args.slice(i)
890
+ const input = ctx.proc.fds.get(fd)
891
+ if (!input || !input.readable) {
892
+ ctx.error(`${fd}: invalid file descriptor: Bad file descriptor`)
893
+ return 1
894
+ }
895
+ if (prompt !== null) await ctx.err(prompt)
896
+ // read bytes
897
+ let bytes: Uint8Array | null
898
+ const doRead = async (signal?: AbortSignal): Promise<Uint8Array | null> => {
899
+ if (nchars !== null) {
900
+ // read up to n chars (bytes; good enough for ASCII), stopping at delim unless -N
901
+ let acc = new Uint8Array(0)
902
+ while (acc.length < nchars) {
903
+ const chunk = await input.read(signal)
904
+ if (chunk === null) break
905
+ let take = Math.min(chunk.length, nchars - acc.length)
906
+ if (!exactChars && delim !== null) {
907
+ const idx = chunk.subarray(0, take).indexOf(delim)
908
+ if (idx >= 0) take = idx + 1
909
+ }
910
+ const merged = new Uint8Array(acc.length + take)
911
+ merged.set(acc)
912
+ merged.set(chunk.subarray(0, take), acc.length)
913
+ acc = merged
914
+ input.unread(chunk.subarray(take))
915
+ if (!exactChars && delim !== null && acc[acc.length - 1] === delim) break
916
+ }
917
+ return acc.length === 0 ? null : acc
918
+ }
919
+ let acc = new Uint8Array(0)
920
+ for (;;) {
921
+ const part = await input.readUntil(delim)
922
+ if (part === null) return acc.length === 0 ? null : acc
923
+ const merged = new Uint8Array(acc.length + part.length)
924
+ merged.set(acc)
925
+ merged.set(part, acc.length)
926
+ acc = merged
927
+ // backslash-newline continuation without -r
928
+ if (!raw && delim === 10 && acc.length >= 2 && acc[acc.length - 1] === 10 && acc[acc.length - 2] === 92) continue
929
+ return acc
930
+ }
931
+ }
932
+ if (timeout !== null) {
933
+ const ac = new AbortController()
934
+ const timer = setTimeout(() => ac.abort(), timeout * 1000)
935
+ bytes = await doRead(ac.signal)
936
+ clearTimeout(timer)
937
+ if (ac.signal.aborted && bytes === null) return 142
938
+ } else {
939
+ bytes = await doRead()
940
+ }
941
+ if (bytes === null) {
942
+ for (const n of names.length > 0 ? names : arrayName ? [] : ['REPLY']) ctx.interp.assignScalar(ctx.proc, n, '')
943
+ if (arrayName) st.ensure(arrayName).value = { kind: 'indexed', values: new Map() }
944
+ return 1
945
+ }
946
+ let text = new TextDecoder().decode(bytes)
947
+ let hitDelim = false
948
+ if (delim !== null && nchars === null && text.length > 0 && text.charCodeAt(text.length - 1) === delim) {
949
+ text = text.slice(0, -1)
950
+ hitDelim = true
951
+ } else if (delim !== null && nchars !== null && !exactChars && text.length > 0 && text.charCodeAt(text.length - 1) === delim) {
952
+ text = text.slice(0, -1)
953
+ hitDelim = true
954
+ }
955
+ // process backslashes (without -r): \x -> x, \<newline> removed; mark escaped chars so IFS does not split them
956
+ const chars: { c: string; esc: boolean }[] = []
957
+ for (let k = 0; k < text.length; k++) {
958
+ const c = text[k]
959
+ if (!raw && c === '\\' && k + 1 < text.length) {
960
+ k++
961
+ if (text[k] === '\n') continue
962
+ chars.push({ c: text[k], esc: true })
963
+ continue
964
+ }
965
+ if (!raw && c === '\\') continue
966
+ chars.push({ c, esc: false })
967
+ }
968
+ const ifs = st.getString('IFS') ?? ' \t\n'
969
+ const isWs = (c: string): boolean => ifs.includes(c) && (c === ' ' || c === '\t' || c === '\n')
970
+ const isIfs = (c: { c: string; esc: boolean }): boolean => !c.esc && ifs.includes(c.c)
971
+ if (names.length === 0 && !arrayName) {
972
+ ctx.interp.assignScalar(ctx.proc, 'REPLY', chars.map((c) => c.c).join(''))
973
+ return hitDelim || nchars !== null ? 0 : 1
974
+ }
975
+ // split into fields
976
+ const fields: string[] = []
977
+ let pos = 0
978
+ const skipWs = (): void => {
979
+ while (pos < chars.length && !chars[pos].esc && isWs(chars[pos].c)) pos++
980
+ }
981
+ skipWs()
982
+ const targetCount = arrayName ? Infinity : names.length
983
+ while (pos < chars.length && fields.length < targetCount - 1) {
984
+ let f = ''
985
+ while (pos < chars.length && !isIfs(chars[pos])) f += chars[pos++].c
986
+ fields.push(f)
987
+ // consume one delimiter (with surrounding whitespace)
988
+ skipWs()
989
+ if (pos < chars.length && isIfs(chars[pos]) && !isWs(chars[pos].c)) {
990
+ pos++
991
+ skipWs()
992
+ }
993
+ }
994
+ if (pos < chars.length) {
995
+ // remainder goes to the last name; trim trailing IFS whitespace
996
+ let end = chars.length
997
+ while (end > pos && !chars[end - 1].esc && isWs(chars[end - 1].c)) end--
998
+ // a single trailing non-whitespace IFS delimiter is dropped from the last field
999
+ if (end - 1 > pos && isIfs(chars[end - 1]) && !isWs(chars[end - 1].c) && !chars.slice(pos, end - 1).some(isIfs)) {
1000
+ end--
1001
+ while (end > pos && !chars[end - 1].esc && isWs(chars[end - 1].c)) end--
1002
+ }
1003
+ let rest = chars.slice(pos, end).map((c) => c.c).join('')
1004
+ if (arrayName) {
1005
+ // arrays: split remainder fully
1006
+ rest = rest
1007
+ }
1008
+ fields.push(rest)
1009
+ }
1010
+ if (arrayName) {
1011
+ const values = new Map<number, string>()
1012
+ fields.forEach((f, k) => values.set(k, f))
1013
+ const v = st.ensure(arrayName)
1014
+ v.value = { kind: 'indexed', values }
1015
+ } else {
1016
+ names.forEach((n, k) => ctx.interp.assignScalar(ctx.proc, n, fields[k] ?? ''))
1017
+ }
1018
+ return hitDelim || nchars !== null ? 0 : 1
1019
+ })
1020
+
1021
+ def(['mapfile', 'readarray'], async (ctx) => {
1022
+ const args = ctx.argv.slice(1)
1023
+ let strip = false
1024
+ let count = Infinity
1025
+ let skip = 0
1026
+ let delim = 10
1027
+ let fd = 0
1028
+ let name = 'MAPFILE'
1029
+ for (let i = 0; i < args.length; i++) {
1030
+ const a = args[i]
1031
+ if (a === '-t') strip = true
1032
+ else if (a === '-n') count = Number(args[++i]) || Infinity
1033
+ else if (a === '-s') skip = Number(args[++i])
1034
+ else if (a === '-d') {
1035
+ const d = args[++i] ?? ''
1036
+ delim = d === '' ? 0 : d.charCodeAt(0)
1037
+ } else if (a === '-u') fd = Number(args[++i])
1038
+ else if (a === '-C' || a === '-c' || a === '-O') i++
1039
+ else name = a
1040
+ }
1041
+ const input = ctx.proc.fds.get(fd)
1042
+ const values = new Map<number, string>()
1043
+ if (input?.readable) {
1044
+ let n = 0
1045
+ for (;;) {
1046
+ if (values.size >= count) break
1047
+ const line = await input.readUntil(delim)
1048
+ if (line === null) break
1049
+ let s = new TextDecoder().decode(line)
1050
+ if (strip && s.length > 0 && s.charCodeAt(s.length - 1) === delim) s = s.slice(0, -1)
1051
+ if (n++ < skip) continue
1052
+ values.set(values.size, s)
1053
+ }
1054
+ }
1055
+ const v = ctx.proc.state.ensure(name)
1056
+ v.value = { kind: 'indexed', values }
1057
+ return 0
1058
+ })
1059
+
1060
+ // ---------------------------------------------------------------------------
1061
+ // test
1062
+ // ---------------------------------------------------------------------------
1063
+
1064
+ def(['test'], async (ctx) => evalTest(ctx.argv.slice(1), ctx.proc))
1065
+ def(['['], async (ctx) => {
1066
+ if (ctx.argv[ctx.argv.length - 1] !== ']') {
1067
+ ctx.error("missing `]'")
1068
+ return 2
1069
+ }
1070
+ return evalTest(ctx.argv.slice(1, -1), ctx.proc)
1071
+ })
1072
+
1073
+ // ---------------------------------------------------------------------------
1074
+ // Control flow
1075
+ // ---------------------------------------------------------------------------
1076
+
1077
+ function numArg(ctx: BuiltinContext, s: string | undefined, dflt: number): number {
1078
+ if (s === undefined) return dflt
1079
+ if (!/^-?\d+$/.test(s.trim())) {
1080
+ ctx.error(`${s}: numeric argument required`)
1081
+ return 2
1082
+ }
1083
+ return Number(s) & 0xff
1084
+ }
1085
+
1086
+ def(['exit'], async (ctx) => {
1087
+ throw new ExitSignal(numArg(ctx, ctx.argv[1], ctx.proc.state.lastStatus))
1088
+ })
1089
+
1090
+ def(['return'], async (ctx) => {
1091
+ const st = ctx.proc.state
1092
+ if (st.funcStack.length === 0 && !ctx.proc.sourcing) {
1093
+ ctx.error("can only `return' from a function or sourced script")
1094
+ return 2 // bash 5 (3.2: 1)
1095
+ }
1096
+ throw new ReturnSignal(numArg(ctx, ctx.argv[1], st.lastStatus))
1097
+ })
1098
+
1099
+ def(['break', 'continue'], async (ctx) => {
1100
+ const n = ctx.argv[1] === undefined ? 1 : Number(ctx.argv[1])
1101
+ if (!Number.isInteger(n) || n < 1) {
1102
+ ctx.error(`${ctx.argv[1]}: loop count out of range`)
1103
+ return 1
1104
+ }
1105
+ if (ctx.proc.loopDepth === 0) return 0
1106
+ throw new LoopSignal(ctx.argv[0] as 'break' | 'continue', Math.min(n, ctx.proc.loopDepth))
1107
+ })
1108
+
1109
+ def(['eval'], async (ctx) => {
1110
+ const src = ctx.argv.slice(1).join(' ')
1111
+ if (src.trim() === '') return 0
1112
+ let script: A.Script
1113
+ try {
1114
+ script = ctx.interp.rt.parse(src, 'eval')
1115
+ } catch (e) {
1116
+ if (e instanceof ParseError) {
1117
+ ctx.error(e.message)
1118
+ return 2
1119
+ }
1120
+ throw e
1121
+ }
1122
+ try {
1123
+ return await ctx.interp.execList(script.body, ctx.proc)
1124
+ } catch (e) {
1125
+ if (e instanceof DiscardError) {
1126
+ ctx.error(e.message)
1127
+ return e.status
1128
+ }
1129
+ throw e
1130
+ }
1131
+ })
1132
+
1133
+ def(['source', '.'], async (ctx) => {
1134
+ const file = ctx.argv[1]
1135
+ if (file === undefined) {
1136
+ ctx.error('filename argument required')
1137
+ return 2
1138
+ }
1139
+ const st = ctx.proc.state
1140
+ let resolved = path.resolve(st.cwd, file)
1141
+ if (!file.includes('/') && !fs.existsSync(resolved)) {
1142
+ for (const dir of (st.getString('PATH') ?? '').split(':')) {
1143
+ const p = path.resolve(st.cwd, dir || '.', file)
1144
+ if (fs.existsSync(p)) {
1145
+ resolved = p
1146
+ break
1147
+ }
1148
+ }
1149
+ }
1150
+ const a = await ctx.interp.rt.assessRedirects([{ kind: 'read', path: known(resolved), source: 'source' }], ctx.node, ctx.proc)
1151
+ if (a.action === 'abort') throw new AbortRun(a.reason, a.status)
1152
+ if (a.action === 'deny') {
1153
+ ctx.error(a.reason)
1154
+ return a.status
1155
+ }
1156
+ let text: string
1157
+ try {
1158
+ text = fs.readFileSync(resolved, 'utf8')
1159
+ } catch {
1160
+ ctx.error(`${file}: No such file or directory`)
1161
+ return 1
1162
+ }
1163
+ let script: A.Script
1164
+ try {
1165
+ script = ctx.interp.rt.parse(text, resolved)
1166
+ } catch (e) {
1167
+ if (e instanceof ParseError) {
1168
+ ctx.error(`${file}: ${e.message}`)
1169
+ return 2
1170
+ }
1171
+ throw e
1172
+ }
1173
+ const saved = st.positional
1174
+ if (ctx.argv.length > 2) st.positional = ctx.argv.slice(2)
1175
+ const wasSourcing = ctx.proc.sourcing
1176
+ ctx.proc.sourcing = true
1177
+ const savedSource = st.globals.get('BASH_SOURCE')
1178
+ st.globals.set('BASH_SOURCE', newVar({ kind: 'indexed', values: new Map([[0, file], ...[...(savedSource?.value?.kind === 'indexed' ? savedSource.value.values.values() : [])].map((v, i) => [i + 1, v] as [number, string])]) }))
1179
+ try {
1180
+ return await ctx.interp.execList(script.body, ctx.proc)
1181
+ } catch (e) {
1182
+ if (e instanceof ReturnSignal) return e.status
1183
+ throw e
1184
+ } finally {
1185
+ if (savedSource) st.globals.set('BASH_SOURCE', savedSource)
1186
+ else st.globals.delete('BASH_SOURCE')
1187
+ ctx.proc.sourcing = wasSourcing
1188
+ if (ctx.argv.length > 2) st.positional = saved
1189
+ }
1190
+ })
1191
+
1192
+ def(['exec'], async (ctx) => {
1193
+ const args = ctx.argv.slice(1)
1194
+ while (args[0] === '-c' || args[0] === '-l' || args[0] === '--') args.shift()
1195
+ if (args[0] === '-a') args.splice(0, 2)
1196
+ if (args.length === 0) {
1197
+ // make this command's redirections permanent
1198
+ const base = ctx.base.fds
1199
+ const mine = new Map(ctx.proc.fds.entries())
1200
+ for (const [fd] of base.entries()) if (!mine.has(fd)) base.close(fd)
1201
+ for (const [fd, file] of mine) if (base.get(fd) !== file) base.set(fd, file.retain())
1202
+ return 0
1203
+ }
1204
+ const status = await ctx.interp.runArgv(args, ctx.proc, ctx.node, { externalOnly: true })
1205
+ // the shell process was replaced: its EXIT trap never runs
1206
+ ctx.proc.state.traps.delete('EXIT')
1207
+ throw new ExitSignal(status)
1208
+ })
1209
+
1210
+ def(['command'], async (ctx) => {
1211
+ const args = ctx.argv.slice(1)
1212
+ let describe: 'v' | 'V' | null = null
1213
+ while (args[0]?.startsWith('-') && args[0] !== '-') {
1214
+ const a = args.shift()!
1215
+ if (a === '--') break
1216
+ if (a.includes('v')) describe = 'v'
1217
+ if (a.includes('V')) describe = 'V'
1218
+ }
1219
+ if (args.length === 0) return 0
1220
+ if (describe) {
1221
+ let status = 0
1222
+ for (const n of args) {
1223
+ const d = describeCommand(n, ctx.proc.state)
1224
+ if (!d) {
1225
+ if (describe === 'V') ctx.error(`${n}: not found`)
1226
+ status = 1
1227
+ continue
1228
+ }
1229
+ await ctx.out(describe === 'v' ? (d.kind === 'file' ? d.path! : n) + '\n' : d.text + '\n')
1230
+ }
1231
+ return status
1232
+ }
1233
+ return ctx.interp.runArgv(args, ctx.proc, ctx.node, { skipFunctions: true })
1234
+ })
1235
+
1236
+ def(['builtin'], async (ctx) => {
1237
+ const args = ctx.argv.slice(1)
1238
+ if (args.length === 0) return 0
1239
+ if (!BUILTINS.has(args[0])) {
1240
+ ctx.error(`${args[0]}: not a shell builtin`)
1241
+ return 1
1242
+ }
1243
+ return ctx.interp.runArgv(args, ctx.proc, ctx.node, { builtinOnly: true })
1244
+ })
1245
+
1246
+ interface CommandDescription {
1247
+ kind: 'keyword' | 'function' | 'builtin' | 'file' | 'alias'
1248
+ path?: string
1249
+ text: string
1250
+ }
1251
+
1252
+ const KEYWORDS = new Set(['if', 'then', 'else', 'elif', 'fi', 'case', 'esac', 'for', 'select', 'while', 'until', 'do', 'done', 'in', 'function', 'time', '{', '}', '!', '[[', ']]'])
1253
+
1254
+ function describeCommand(name: string, st: ShellState): CommandDescription | null {
1255
+ if (st.aliases.has(name)) return { kind: 'alias', text: `${name} is aliased to \`${st.aliases.get(name)}'` }
1256
+ if (KEYWORDS.has(name)) return { kind: 'keyword', text: `${name} is a shell keyword` }
1257
+ if (st.functions.has(name)) return { kind: 'function', text: `${name} is a function` }
1258
+ if (BUILTINS.has(name)) return { kind: 'builtin', text: `${name} is a shell builtin` }
1259
+ const p = resolveCommand(name, st)
1260
+ if (p) return { kind: 'file', path: p, text: `${name} is ${p}` }
1261
+ return null
1262
+ }
1263
+
1264
+ def(['type'], async (ctx) => {
1265
+ const args = ctx.argv.slice(1)
1266
+ let mode: 'normal' | 't' | 'p' | 'P' = 'normal'
1267
+ let all = false
1268
+ const names: string[] = []
1269
+ for (const a of args) {
1270
+ if (a === '-t') mode = 't'
1271
+ else if (a === '-p') mode = 'p'
1272
+ else if (a === '-P') mode = 'P'
1273
+ else if (a === '-a') all = true
1274
+ else if (a === '-f') continue
1275
+ else names.push(a)
1276
+ }
1277
+ let status = 0
1278
+ for (const n of names) {
1279
+ if (all && mode === 'normal') {
1280
+ const lines: string[] = []
1281
+ const d0 = describeCommand(n, ctx.proc.state)
1282
+ if (d0 && d0.kind !== 'file') lines.push(d0.text)
1283
+ for (const dir of (ctx.proc.state.getString('PATH') ?? '').split(':')) {
1284
+ const p = path.join(dir || '.', n)
1285
+ try {
1286
+ if (fs.statSync(p).isFile()) {
1287
+ fs.accessSync(p, fs.constants.X_OK)
1288
+ lines.push(`${n} is ${p}`)
1289
+ }
1290
+ } catch {
1291
+ // not here
1292
+ }
1293
+ }
1294
+ if (lines.length === 0) {
1295
+ ctx.error(`${n}: not found`)
1296
+ status = 1
1297
+ }
1298
+ for (const l of [...new Set(lines)]) await ctx.out(l + '\n')
1299
+ continue
1300
+ }
1301
+ const d = mode === 'P' ? (() => {
1302
+ const p = resolveCommand(n, ctx.proc.state)
1303
+ return p ? { kind: 'file' as const, path: p, text: '' } : null
1304
+ })() : describeCommand(n, ctx.proc.state)
1305
+ if (!d) {
1306
+ if (mode === 'normal') ctx.error(`${n}: not found`)
1307
+ status = 1
1308
+ continue
1309
+ }
1310
+ if (mode === 't') await ctx.out(d.kind + '\n')
1311
+ else if (mode === 'p' || mode === 'P') {
1312
+ if (d.kind === 'file') await ctx.out(d.path + '\n')
1313
+ } else await ctx.out(d.text + '\n')
1314
+ }
1315
+ return status
1316
+ })
1317
+
1318
+ def(['hash'], async (ctx) => {
1319
+ if (ctx.argv.includes('-r')) ctx.proc.state.hash.clear()
1320
+ if (ctx.argv[1] === '-t') {
1321
+ let status = 0
1322
+ for (const n of ctx.argv.slice(2)) {
1323
+ const p = ctx.proc.state.hash.get(n)
1324
+ if (p) await ctx.out(p + '\n')
1325
+ else {
1326
+ ctx.error(`${n}: not found`)
1327
+ status = 1
1328
+ }
1329
+ }
1330
+ return status
1331
+ }
1332
+ for (const n of ctx.argv.slice(1).filter((a) => !a.startsWith('-'))) {
1333
+ if (!resolveCommand(n, ctx.proc.state)) {
1334
+ ctx.error(`${n}: not found`)
1335
+ return 1
1336
+ }
1337
+ }
1338
+ return 0
1339
+ })
1340
+
1341
+ def(['alias'], async (ctx) => {
1342
+ const st = ctx.proc.state
1343
+ for (const a of ctx.argv.slice(1)) {
1344
+ const eq = a.indexOf('=')
1345
+ if (eq === -1) {
1346
+ const v = st.aliases.get(a)
1347
+ if (v === undefined) return 1
1348
+ await ctx.out(`alias ${a}=${shellQuote(v)}\n`)
1349
+ } else st.aliases.set(a.slice(0, eq), a.slice(eq + 1))
1350
+ }
1351
+ return 0
1352
+ })
1353
+
1354
+ def(['unalias'], async (ctx) => {
1355
+ for (const a of ctx.argv.slice(1)) {
1356
+ if (a === '-a') ctx.proc.state.aliases.clear()
1357
+ else ctx.proc.state.aliases.delete(a)
1358
+ }
1359
+ return 0
1360
+ })
1361
+
1362
+ // ---------------------------------------------------------------------------
1363
+ // Jobs, traps
1364
+ // ---------------------------------------------------------------------------
1365
+
1366
+ def(['wait'], async (ctx) => {
1367
+ const proc = ctx.proc
1368
+ const args = ctx.argv.slice(1).filter((a) => a !== '-f')
1369
+ if (args[0] === '-n') {
1370
+ const pending = [...proc.jobs.values()].filter((j) => j.status === null)
1371
+ if (pending.length === 0) return 127
1372
+ const s = await Promise.race(pending.map((j) => j.done))
1373
+ return s
1374
+ }
1375
+ if (args.length === 0) {
1376
+ for (const j of proc.jobs.values()) await j.done
1377
+ proc.jobs.clear()
1378
+ return 0
1379
+ }
1380
+ let status = 0
1381
+ for (const a of args) {
1382
+ const pid = Number(a.replace(/^%/, ''))
1383
+ const job = proc.jobs.get(pid) ?? [...proc.jobs.values()][pid - 1]
1384
+ if (!job) {
1385
+ ctx.error(`pid ${a} is not a child of this shell`)
1386
+ status = 127
1387
+ continue
1388
+ }
1389
+ status = await job.done
1390
+ proc.jobs.delete(job.pid)
1391
+ }
1392
+ return status
1393
+ })
1394
+
1395
+ def(['jobs'], async (ctx) => {
1396
+ let i = 1
1397
+ for (const j of ctx.proc.jobs.values()) {
1398
+ await ctx.out(ctx.argv.includes('-p') ? `${j.pid}\n` : `[${i++}] ${j.status === null ? 'Running' : 'Done'}\t${j.pid}\n`)
1399
+ }
1400
+ return 0
1401
+ })
1402
+
1403
+ def(['kill'], async (ctx) => {
1404
+ const args = ctx.argv.slice(1)
1405
+ let sig: NodeJS.Signals = 'SIGTERM'
1406
+ if (args[0] === '-l') {
1407
+ await ctx.out(Object.keys(os.constants.signals).map((s) => s.replace(/^SIG/, '')).join(' ') + '\n')
1408
+ return 0
1409
+ }
1410
+ if (args[0] === '-s' || args[0] === '-n') {
1411
+ args.shift()
1412
+ const s = args.shift() ?? 'TERM'
1413
+ sig = (/^\d+$/.test(s) ? Object.entries(os.constants.signals).find(([, n]) => n === Number(s))?.[0] : 'SIG' + s.replace(/^SIG/, '')) as NodeJS.Signals
1414
+ } else if (args[0]?.startsWith('-') && args[0] !== '--') {
1415
+ const s = args.shift()!.slice(1)
1416
+ if (s === '0') sig = 'SIG0' as NodeJS.Signals
1417
+ else sig = (/^\d+$/.test(s) ? Object.entries(os.constants.signals).find(([, n]) => n === Number(s))?.[0] ?? 'SIGTERM' : 'SIG' + s.replace(/^SIG/, '')) as NodeJS.Signals
1418
+ }
1419
+ if (args[0] === '--') args.shift()
1420
+ let status = 0
1421
+ for (const a of args) {
1422
+ const n = Number(a.replace(/^%/, ''))
1423
+ const job = ctx.proc.jobs.get(n) ?? (a.startsWith('%') ? [...ctx.proc.jobs.values()][n - 1] : undefined) ?? ctx.interp.rt.findJob(n)
1424
+ const signalZero = sig === undefined || (sig as string) === 'SIG0' || (sig as string) === 'SIGEXIT'
1425
+ if (job) {
1426
+ if (signalZero) {
1427
+ if (job.status !== null) status = 1
1428
+ continue
1429
+ }
1430
+ job.kill(sig)
1431
+ continue
1432
+ }
1433
+ if (n === ctx.interp.rt.shellPid || n === ctx.proc.pid) {
1434
+ // the VM's own (virtual) shell: never signal the host process
1435
+ if (signalZero) continue
1436
+ const name = String(sig).replace(/^SIG/, '')
1437
+ const trap = ctx.proc.state.traps.get(name)
1438
+ if (trap !== undefined) {
1439
+ if (trap !== '') await ctx.interp.execList(ctx.interp.rt.parse(trap, `trap ${name}`).body, ctx.proc)
1440
+ continue
1441
+ }
1442
+ throw new ExitSignal(128 + ((os.constants.signals as Record<string, number>)[sig] ?? 15))
1443
+ }
1444
+ try {
1445
+ process.kill(n, sig)
1446
+ } catch (e) {
1447
+ ctx.error(`(${a}) - ${(e as NodeJS.ErrnoException).code === 'ESRCH' ? 'No such process' : String(e)}`)
1448
+ status = 1
1449
+ }
1450
+ }
1451
+ return status
1452
+ })
1453
+
1454
+ def(['trap'], async (ctx) => {
1455
+ const st = ctx.proc.state
1456
+ const args = ctx.argv.slice(1)
1457
+ if (args.length === 0 || args[0] === '-p') {
1458
+ for (const [sig, action] of st.parentTraps ?? st.traps) {
1459
+ if (args[0] === '-p' && args.length > 1 && !args.slice(1).some((a) => a.toUpperCase().replace(/^SIG/, '') === sig)) continue
1460
+ const name = ['EXIT', 'ERR', 'DEBUG', 'RETURN'].includes(sig) ? sig : `SIG${sig}`
1461
+ await ctx.out(`trap -- ${shellQuote(action)} ${name}\n`)
1462
+ }
1463
+ return 0
1464
+ }
1465
+ if (args[0] === '-l') {
1466
+ await ctx.out(Object.keys(os.constants.signals).join(' ') + '\n')
1467
+ return 0
1468
+ }
1469
+ let action: string | null = args[0]
1470
+ let sigs = args.slice(1)
1471
+ if (/^\d+$/.test(action) && sigs.length === 0) {
1472
+ sigs = [action]
1473
+ action = null
1474
+ }
1475
+ if (action === '-') action = null
1476
+ st.parentTraps = undefined
1477
+ for (let s of sigs) {
1478
+ s = s.toUpperCase().replace(/^SIG/, '')
1479
+ if (s === '0') s = 'EXIT'
1480
+ if (action === null) st.traps.delete(s)
1481
+ else st.traps.set(s, action)
1482
+ }
1483
+ return 0
1484
+ })
1485
+
1486
+ def(['getopts'], async (ctx) => {
1487
+ const st = ctx.proc.state
1488
+ const [optstring, name, ...rest] = ctx.argv.slice(1)
1489
+ if (optstring === undefined || name === undefined) {
1490
+ ctx.error('usage: getopts optstring name [arg ...]')
1491
+ return 2
1492
+ }
1493
+ const args = rest.length > 0 ? rest : st.positional
1494
+ let optind = Number(st.getString('OPTIND') ?? '1') || 1
1495
+ const silent = optstring.startsWith(':')
1496
+ const spec = silent ? optstring.slice(1) : optstring
1497
+ const sub = Number(st.getString('__MASH_OPTPOS') ?? '1') || 1
1498
+ const arg = args[optind - 1]
1499
+ const set = (n: string, v: string): void => ctx.interp.assignScalar(ctx.proc, n, v)
1500
+ if (arg === undefined || !arg.startsWith('-') || arg === '-') {
1501
+ set(name, '?')
1502
+ return 1
1503
+ }
1504
+ if (arg === '--') {
1505
+ set('OPTIND', String(optind + 1))
1506
+ set(name, '?')
1507
+ return 1
1508
+ }
1509
+ const ch = arg[sub]
1510
+ let nextSub = sub + 1
1511
+ const advance = (): void => {
1512
+ if (nextSub >= arg.length) {
1513
+ optind++
1514
+ nextSub = 1
1515
+ }
1516
+ }
1517
+ const idx = spec.indexOf(ch)
1518
+ if (idx === -1 || ch === ':') {
1519
+ if (silent) {
1520
+ set('OPTARG', ch)
1521
+ } else {
1522
+ ctx.error(`illegal option -- ${ch}`)
1523
+ st.unset('OPTARG')
1524
+ }
1525
+ set(name, '?')
1526
+ advance()
1527
+ set('OPTIND', String(optind))
1528
+ set('__MASH_OPTPOS', String(nextSub))
1529
+ return 0
1530
+ }
1531
+ if (spec[idx + 1] === ':') {
1532
+ let optarg: string | undefined
1533
+ if (nextSub < arg.length) {
1534
+ optarg = arg.slice(nextSub)
1535
+ optind++
1536
+ } else {
1537
+ optarg = args[optind]
1538
+ optind += 2
1539
+ }
1540
+ nextSub = 1
1541
+ if (optarg === undefined) {
1542
+ if (silent) {
1543
+ set(name, ':')
1544
+ set('OPTARG', ch)
1545
+ } else {
1546
+ ctx.error(`option requires an argument -- ${ch}`)
1547
+ set(name, '?')
1548
+ }
1549
+ set('OPTIND', String(optind))
1550
+ set('__MASH_OPTPOS', '1')
1551
+ return 0
1552
+ }
1553
+ set('OPTARG', optarg)
1554
+ } else {
1555
+ st.unset('OPTARG')
1556
+ advance()
1557
+ }
1558
+ set(name, ch)
1559
+ set('OPTIND', String(optind))
1560
+ set('__MASH_OPTPOS', String(nextSub))
1561
+ return 0
1562
+ })
1563
+
1564
+ def(['umask'], async (ctx) => {
1565
+ const st = ctx.proc.state
1566
+ const args = ctx.argv.slice(1)
1567
+ const symbolic = args[0] === '-S'
1568
+ if (symbolic) args.shift()
1569
+ if (args[0] === '-p') args.shift()
1570
+ if (args.length === 0) {
1571
+ if (symbolic) {
1572
+ const perm = (shift: number): string => ['r', 'w', 'x'].filter((_, i) => !(st.umask & (4 >> i) << shift)).join('')
1573
+ await ctx.out(`u=${perm(6)},g=${perm(3)},o=${perm(0)}\n`)
1574
+ } else await ctx.out(st.umask.toString(8).padStart(4, '0') + '\n')
1575
+ return 0
1576
+ }
1577
+ if (!/^[0-7]{1,4}$/.test(args[0])) {
1578
+ ctx.error(`${args[0]}: octal number out of range (symbolic modes are not supported)`)
1579
+ return 1
1580
+ }
1581
+ st.umask = parseInt(args[0], 8)
1582
+ return 0
1583
+ })
1584
+
1585
+ def(['times'], async (ctx) => {
1586
+ await ctx.out('0m0.000s 0m0.000s\n0m0.000s 0m0.000s\n')
1587
+ return 0
1588
+ })
1589
+
1590
+ def(['caller'], async () => 1)