@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,477 @@
1
+ /**
2
+ * Shell pattern matching (`case`, `[[ == ]]`, `${x#pat}`, pathname globs).
3
+ *
4
+ * Patterns are strings in which quoted characters have been backslash-escaped
5
+ * (see `quotePattern`), so `"*"` from a quoted word matches a literal star.
6
+ */
7
+
8
+ import * as fs from 'node:fs'
9
+ import * as path from 'node:path'
10
+
11
+ const CLASSES: Record<string, string> = {
12
+ alpha: 'a-zA-Z', digit: '0-9', alnum: 'a-zA-Z0-9', upper: 'A-Z', lower: 'a-z',
13
+ space: ' \\t\\n\\r\\f\\v', blank: ' \\t', punct: '!-\\/:-@\\[-`{-~', xdigit: '0-9a-fA-F',
14
+ word: 'a-zA-Z0-9_', cntrl: '\\x00-\\x1f\\x7f', print: '\\x20-\\x7e', graph: '\\x21-\\x7e',
15
+ }
16
+
17
+ /** Escape pattern metacharacters so `s` matches literally. */
18
+ export function quotePattern(s: string): string {
19
+ return s.replace(/[\\*?[\]]/g, (c) => '\\' + c)
20
+ }
21
+
22
+ /** True if the pattern contains an unescaped `*`, `?` or `[`. */
23
+ export function hasGlobChars(pat: string): boolean {
24
+ for (let i = 0; i < pat.length; i++) {
25
+ const c = pat[i]
26
+ if (c === '\\') {
27
+ i++
28
+ continue
29
+ }
30
+ if (c === '*' || c === '?') return true
31
+ if ((c === '@' || c === '+' || c === '!') && pat[i + 1] === '(') return true
32
+ if (c === '[' && pat.indexOf(']', i + 2) !== -1) return true
33
+ }
34
+ return false
35
+ }
36
+
37
+ /** Remove pattern escapes: the literal string a glob-free pattern denotes. */
38
+ export function unescapePattern(pat: string): string {
39
+ return pat.replace(/\\(.)/gs, '$1')
40
+ }
41
+
42
+ function escapeRegex(c: string): string {
43
+ return /[\\^$.*+?()[\]{}|/-]/.test(c) ? '\\' + c : c
44
+ }
45
+
46
+ export interface PatternOptions {
47
+ /** Pathname matching: `*` and `?` do not match `/`. */
48
+ pathname?: boolean
49
+ nocase?: boolean
50
+ }
51
+
52
+ /** Translate a shell pattern to regex source (unanchored). */
53
+ export function patternToRegexSource(pat: string, opts: PatternOptions = {}): string {
54
+ let re = ''
55
+ const any = opts.pathname ? '[^/]' : '[\\s\\S]'
56
+ for (let i = 0; i < pat.length; i++) {
57
+ const c = pat[i]
58
+ if (c === '\\') {
59
+ i++
60
+ if (i < pat.length) re += escapeRegex(pat[i])
61
+ else re += '\\\\'
62
+ continue
63
+ }
64
+ if ('@*+?!'.includes(c) && pat[i + 1] === '(') {
65
+ // extglob: @(a|b) *(a|b) +(a|b) ?(a|b) !(a|b)
66
+ const close = groupEnd(pat, i + 1)
67
+ if (close !== -1) {
68
+ const alts = splitAlternatives(pat.slice(i + 2, close)).map((a) => patternToRegexSource(a, opts))
69
+ const group = `(?:${alts.join('|')})`
70
+ switch (c) {
71
+ case '@': re += group; break
72
+ case '*': re += group + '*'; break
73
+ case '+': re += group + '+'; break
74
+ case '?': re += group + '?'; break
75
+ case '!': re += `(?:(?!${group}$)${any}*?)`; break
76
+ }
77
+ i = close
78
+ continue
79
+ }
80
+ }
81
+ if (c === '*') {
82
+ while (pat[i + 1] === '*') i++
83
+ re += any + '*'
84
+ continue
85
+ }
86
+ if (c === '?') {
87
+ re += any
88
+ continue
89
+ }
90
+ if (c === '[') {
91
+ // bracket expression
92
+ let j = i + 1
93
+ let negate = false
94
+ if (pat[j] === '!' || pat[j] === '^') {
95
+ negate = true
96
+ j++
97
+ }
98
+ let body = ''
99
+ let first = true
100
+ let closed = false
101
+ for (; j < pat.length; j++) {
102
+ const d = pat[j]
103
+ if (d === ']' && !first) {
104
+ closed = true
105
+ break
106
+ }
107
+ first = false
108
+ if (d === '[' && pat[j + 1] === ':') {
109
+ const end = pat.indexOf(':]', j + 2)
110
+ if (end !== -1) {
111
+ const name = pat.slice(j + 2, end)
112
+ body += CLASSES[name] ?? ''
113
+ j = end + 1
114
+ continue
115
+ }
116
+ }
117
+ if (d === '\\' && j + 1 < pat.length) {
118
+ j++
119
+ body += '\\' + pat[j]
120
+ continue
121
+ }
122
+ if (d === '-' && body !== '' && pat[j + 1] !== ']') {
123
+ body += '-'
124
+ continue
125
+ }
126
+ body += /[\\\]^[]/.test(d) ? '\\' + d : d
127
+ }
128
+ if (!closed) {
129
+ re += '\\['
130
+ continue
131
+ }
132
+ re += (negate ? '[^' : '[') + body + (opts.pathname && negate ? '/' : '') + ']'
133
+ i = j
134
+ continue
135
+ }
136
+ re += escapeRegex(c)
137
+ }
138
+ return re
139
+ }
140
+
141
+ function groupEnd(pat: string, open: number): number {
142
+ let depth = 0
143
+ for (let i = open; i < pat.length; i++) {
144
+ if (pat[i] === '\\') {
145
+ i++
146
+ continue
147
+ }
148
+ if (pat[i] === '(') depth++
149
+ else if (pat[i] === ')' && --depth === 0) return i
150
+ }
151
+ return -1
152
+ }
153
+
154
+ function splitAlternatives(s: string): string[] {
155
+ const out: string[] = []
156
+ let depth = 0
157
+ let cur = ''
158
+ for (let i = 0; i < s.length; i++) {
159
+ const c = s[i]
160
+ if (c === '\\') {
161
+ cur += c + (s[i + 1] ?? '')
162
+ i++
163
+ continue
164
+ }
165
+ if (c === '(') depth++
166
+ else if (c === ')') depth--
167
+ if (c === '|' && depth === 0) {
168
+ out.push(cur)
169
+ cur = ''
170
+ continue
171
+ }
172
+ cur += c
173
+ }
174
+ out.push(cur)
175
+ return out
176
+ }
177
+
178
+ const cache = new Map<string, RegExp>()
179
+
180
+ /** Patterns with `!(...)` not at the very end need a backtracking matcher (regex cannot express it). */
181
+ function needsMatcher(pat: string): boolean {
182
+ for (let i = 0; i < pat.length; i++) {
183
+ if (pat[i] === '\\') {
184
+ i++
185
+ continue
186
+ }
187
+ if (pat[i] === '!' && pat[i + 1] === '(') {
188
+ const close = groupEnd(pat, i + 1)
189
+ return close !== -1 && close !== pat.length - 1
190
+ }
191
+ }
192
+ return false
193
+ }
194
+
195
+ type PNode =
196
+ | { k: 'lit'; c: string }
197
+ | { k: 'any' }
198
+ | { k: 'star' }
199
+ | { k: 'class'; re: RegExp }
200
+ | { k: 'group'; op: string; alts: PNode[][] }
201
+
202
+ function compileNodes(pat: string, opts: PatternOptions): PNode[] {
203
+ const out: PNode[] = []
204
+ for (let i = 0; i < pat.length; i++) {
205
+ const c = pat[i]
206
+ if (c === '\\') {
207
+ i++
208
+ if (i < pat.length) out.push({ k: 'lit', c: pat[i] })
209
+ continue
210
+ }
211
+ if ('@*+?!'.includes(c) && pat[i + 1] === '(') {
212
+ const close = groupEnd(pat, i + 1)
213
+ if (close !== -1) {
214
+ out.push({ k: 'group', op: c, alts: splitAlternatives(pat.slice(i + 2, close)).map((a) => compileNodes(a, opts)) })
215
+ i = close
216
+ continue
217
+ }
218
+ }
219
+ if (c === '*') out.push({ k: 'star' })
220
+ else if (c === '?') out.push({ k: 'any' })
221
+ else if (c === '[') {
222
+ const end = pat.indexOf(']', i + 2)
223
+ if (end === -1) out.push({ k: 'lit', c })
224
+ else {
225
+ out.push({ k: 'class', re: new RegExp('^' + patternToRegexSource(pat.slice(i, end + 1), opts) + '$', opts.nocase ? 'i' : '') })
226
+ i = end
227
+ }
228
+ } else out.push({ k: 'lit', c })
229
+ }
230
+ return out
231
+ }
232
+
233
+ function matchNodes(nodes: PNode[], ni: number, s: string, si: number, end: number, opts: PatternOptions): boolean {
234
+ if (ni === nodes.length) return si === end
235
+ const n = nodes[ni]
236
+ const okChar = (ch: string): boolean => !(opts.pathname && ch === '/')
237
+ switch (n.k) {
238
+ case 'lit':
239
+ return si < end && (opts.nocase ? s[si].toLowerCase() === n.c.toLowerCase() : s[si] === n.c) && matchNodes(nodes, ni + 1, s, si + 1, end, opts)
240
+ case 'any':
241
+ return si < end && okChar(s[si]) && matchNodes(nodes, ni + 1, s, si + 1, end, opts)
242
+ case 'class':
243
+ return si < end && n.re.test(s[si]) && matchNodes(nodes, ni + 1, s, si + 1, end, opts)
244
+ case 'star':
245
+ for (let k = si; k <= end; k++) {
246
+ if (matchNodes(nodes, ni + 1, s, k, end, opts)) return true
247
+ if (k < end && !okChar(s[k])) return false
248
+ }
249
+ return false
250
+ case 'group': {
251
+ const altMatches = (from: number, to: number): boolean => n.alts.some((a) => matchNodes(a, 0, s, from, to, opts))
252
+ if (n.op === '!') {
253
+ for (let k = si; k <= end; k++) if (!altMatches(si, k) && matchNodes(nodes, ni + 1, s, k, end, opts)) return true
254
+ return false
255
+ }
256
+ if (n.op === '@' || n.op === '?') {
257
+ if (n.op === '?' && matchNodes(nodes, ni + 1, s, si, end, opts)) return true
258
+ for (let k = si; k <= end; k++) if (altMatches(si, k) && matchNodes(nodes, ni + 1, s, k, end, opts)) return true
259
+ return false
260
+ }
261
+ // * and +: one or more repetitions (or zero for *)
262
+ const rep = (from: number, count: number): boolean => {
263
+ if ((n.op === '*' || count > 0) && matchNodes(nodes, ni + 1, s, from, end, opts)) return true
264
+ for (let k = from + 1; k <= end; k++) if (altMatches(from, k) && rep(k, count + 1)) return true
265
+ return false
266
+ }
267
+ return rep(si, 0)
268
+ }
269
+ }
270
+ }
271
+
272
+ /** A compiled pattern: a RegExp when possible, else a backtracking matcher with the same `test` API. */
273
+ export function patternRegex(pat: string, opts: PatternOptions = {}): { test(s: string): boolean } {
274
+ if (needsMatcher(pat)) {
275
+ const nodes = compileNodes(pat, opts)
276
+ return { test: (s: string) => matchNodes(nodes, 0, s, 0, s.length, opts) }
277
+ }
278
+ return patternRegexOnly(pat, opts)
279
+ }
280
+
281
+ function patternRegexOnly(pat: string, opts: PatternOptions = {}): RegExp {
282
+ const key = `${opts.pathname ? 'p' : ''}${opts.nocase ? 'i' : ''}:${pat}`
283
+ let r = cache.get(key)
284
+ if (!r) {
285
+ r = new RegExp('^(?:' + patternToRegexSource(pat, opts) + ')$', opts.nocase ? 'is' : 's')
286
+ if (cache.size > 2000) cache.clear()
287
+ cache.set(key, r)
288
+ }
289
+ return r
290
+ }
291
+
292
+ export function matchPattern(s: string, pat: string, opts: PatternOptions = {}): boolean {
293
+ return patternRegex(pat, opts).test(s)
294
+ }
295
+
296
+ /** `${x#pat}` / `${x##pat}` */
297
+ export function removePrefix(s: string, pat: string, longest: boolean): string {
298
+ const re = patternRegex(pat)
299
+ if (longest) {
300
+ for (let i = s.length; i >= 0; i--) if (re.test(s.slice(0, i))) return s.slice(i)
301
+ } else {
302
+ for (let i = 0; i <= s.length; i++) if (re.test(s.slice(0, i))) return s.slice(i)
303
+ }
304
+ return s
305
+ }
306
+
307
+ /** `${x%pat}` / `${x%%pat}` */
308
+ export function removeSuffix(s: string, pat: string, longest: boolean): string {
309
+ const re = patternRegex(pat)
310
+ if (longest) {
311
+ for (let i = 0; i <= s.length; i++) if (re.test(s.slice(i))) return s.slice(0, i)
312
+ } else {
313
+ for (let i = s.length; i >= 0; i--) if (re.test(s.slice(i))) return s.slice(0, i)
314
+ }
315
+ return s
316
+ }
317
+
318
+ /** `${x/pat/rep}` and friends. */
319
+ /**
320
+ * `${s/pat/rep}`. `rep` is a string, or a function of the matched text (bash
321
+ * 5.2 `patsub_replacement`: an unquoted `&` in the replacement is the match).
322
+ */
323
+ export function replacePattern(s: string, pat: string, rep: string | ((match: string) => string), mode: 'first' | 'all' | 'prefix' | 'suffix'): string {
324
+ const re = patternRegex(pat)
325
+ const r = typeof rep === 'string' ? (): string => rep : rep
326
+ if (mode === 'prefix') {
327
+ for (let j = s.length; j >= 0; j--) if (re.test(s.slice(0, j))) return r(s.slice(0, j)) + s.slice(j)
328
+ return s
329
+ }
330
+ if (mode === 'suffix') {
331
+ for (let i = 0; i <= s.length; i++) if (re.test(s.slice(i))) return s.slice(0, i) + r(s.slice(i))
332
+ return s
333
+ }
334
+ if (pat === '') return s
335
+ let out = ''
336
+ let i = 0
337
+ while (i <= s.length) {
338
+ let matched = -1
339
+ for (let j = s.length; j > i; j--) {
340
+ if (re.test(s.slice(i, j))) {
341
+ matched = j
342
+ break
343
+ }
344
+ }
345
+ if (matched === -1) {
346
+ if (i < s.length) out += s[i]
347
+ i++
348
+ continue
349
+ }
350
+ out += r(s.slice(i, matched))
351
+ i = matched
352
+ if (mode === 'first') return out + s.slice(i)
353
+ }
354
+ return out
355
+ }
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // Pathname expansion
359
+ // ---------------------------------------------------------------------------
360
+
361
+ export interface GlobOptions {
362
+ cwd: string
363
+ dotglob?: boolean
364
+ globstar?: boolean
365
+ nocase?: boolean
366
+ }
367
+
368
+ function splitPattern(pat: string): string[] {
369
+ const segs: string[] = []
370
+ let cur = ''
371
+ for (let i = 0; i < pat.length; i++) {
372
+ const c = pat[i]
373
+ if (c === '\\' && i + 1 < pat.length) {
374
+ cur += c + pat[i + 1]
375
+ i++
376
+ continue
377
+ }
378
+ if (c === '/') {
379
+ segs.push(cur)
380
+ cur = ''
381
+ continue
382
+ }
383
+ cur += c
384
+ }
385
+ segs.push(cur)
386
+ return segs
387
+ }
388
+
389
+ function readdirSafe(dir: string): string[] {
390
+ try {
391
+ return fs.readdirSync(dir)
392
+ } catch {
393
+ return []
394
+ }
395
+ }
396
+
397
+ function isDir(p: string): boolean {
398
+ try {
399
+ return fs.statSync(p).isDirectory()
400
+ } catch {
401
+ return false
402
+ }
403
+ }
404
+
405
+ function exists(p: string): boolean {
406
+ try {
407
+ fs.lstatSync(p)
408
+ return true
409
+ } catch {
410
+ return false
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Expand a pathname pattern against the filesystem. Returns matches sorted
416
+ * (C locale), or an empty array when nothing matches.
417
+ */
418
+ export function globPaths(pattern: string, opts: GlobOptions): string[] {
419
+ const absolute = pattern.startsWith('/')
420
+ const segs = splitPattern(absolute ? pattern.slice(1) : pattern)
421
+ // candidates are [displayPath, fsPath]
422
+ let current: [string, string][] = [[absolute ? '/' : '', absolute ? '/' : opts.cwd]]
423
+ for (let si = 0; si < segs.length; si++) {
424
+ const seg = segs[si]
425
+ const last = si === segs.length - 1
426
+ const next: [string, string][] = []
427
+ const join = (display: string, name: string): string => (display === '' ? name : display.endsWith('/') ? display + name : display + '/' + name)
428
+ if (seg === '') {
429
+ // trailing slash or `//`
430
+ for (const [d, f] of current) if (isDir(f)) next.push([d.endsWith('/') ? d : d + '/', f])
431
+ current = next
432
+ continue
433
+ }
434
+ if (!hasGlobChars(seg)) {
435
+ const name = unescapePattern(seg)
436
+ for (const [d, f] of current) {
437
+ const fp = path.join(f, name)
438
+ if (last ? exists(fp) : isDir(fp)) next.push([join(d, name), fp])
439
+ }
440
+ current = next
441
+ continue
442
+ }
443
+ if (seg === '**' && opts.globstar) {
444
+ for (const [d, f] of current) {
445
+ // zero or more directories
446
+ const stack: [string, string][] = [[d, f]]
447
+ while (stack.length > 0) {
448
+ const [dd, ff] = stack.pop()!
449
+ next.push([dd, ff])
450
+ for (const name of readdirSafe(ff).sort()) {
451
+ if (name.startsWith('.') && !opts.dotglob) continue
452
+ const fp = path.join(ff, name)
453
+ if (isDir(fp)) stack.push([join(dd, name), fp])
454
+ else if (last) next.push([join(dd, name), fp])
455
+ }
456
+ }
457
+ }
458
+ current = next.filter(([d]) => d !== '' || !last)
459
+ continue
460
+ }
461
+ const re = patternRegex(seg, { pathname: true, nocase: opts.nocase })
462
+ const allowDot = seg.startsWith('.') || seg.startsWith('\\.') || opts.dotglob
463
+ for (const [d, f] of current) {
464
+ for (const name of readdirSafe(f)) {
465
+ if (name.startsWith('.') && !allowDot) continue
466
+ if (name === '.' || name === '..') continue
467
+ if (!re.test(name)) continue
468
+ const fp = path.join(f, name)
469
+ if (!last && !isDir(fp)) continue
470
+ next.push([join(d, name), fp])
471
+ }
472
+ }
473
+ current = next
474
+ }
475
+ const out = current.map(([d]) => d).filter((d) => d !== '')
476
+ return [...new Set(out)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
477
+ }
package/src/vm/proc.ts ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Execution contexts. A {@link Proc} is one shell "process" as the script
3
+ * sees it: a {@link ShellState} plus an fd table and a virtual pid. Contexts
4
+ * that differ only in fds or errexit suppression (redirected builtins, `if`
5
+ * conditions) are derived views sharing the same state.
6
+ */
7
+
8
+ import type { ChildProcess } from 'node:child_process'
9
+ import { FdTable } from './io.ts'
10
+ import { ShellState } from './state.ts'
11
+
12
+ export class ExitSignal {
13
+ readonly status: number
14
+ constructor(status: number) {
15
+ this.status = status
16
+ }
17
+ }
18
+
19
+ export class ReturnSignal {
20
+ readonly status: number
21
+ constructor(status: number) {
22
+ this.status = status
23
+ }
24
+ }
25
+
26
+ export class LoopSignal {
27
+ readonly kind: 'break' | 'continue'
28
+ levels: number
29
+ constructor(kind: 'break' | 'continue', levels: number) {
30
+ this.kind = kind
31
+ this.levels = levels
32
+ }
33
+ }
34
+
35
+ /** Abort the whole run (policy abort, external cancellation). */
36
+ export class AbortRun extends Error {
37
+ readonly status: number
38
+ constructor(reason: string, status = 126) {
39
+ super(reason)
40
+ this.name = 'AbortRun'
41
+ this.status = status
42
+ }
43
+ }
44
+
45
+ /** A fatal expansion error: prints a message and exits the (sub)shell. */
46
+ export class FatalError extends Error {
47
+ readonly status: number
48
+ constructor(message: string, status = 1) {
49
+ super(message)
50
+ this.name = 'FatalError'
51
+ this.status = status
52
+ }
53
+ }
54
+
55
+ /**
56
+ * An expansion error that discards the current top-level command (bash's
57
+ * `jump_to_top_level(DISCARD)`, e.g. arithmetic errors): the script goes on
58
+ * with the next top-level command, `eval` fails with status 1.
59
+ */
60
+ export class DiscardError extends Error {
61
+ readonly status: number
62
+ constructor(message: string, status = 1) {
63
+ super(message)
64
+ this.name = 'DiscardError'
65
+ this.status = status
66
+ }
67
+ }
68
+
69
+ /** A non-fatal command error: prints a message, the command fails with `status`. */
70
+ export class CommandError extends Error {
71
+ readonly status: number
72
+ constructor(message: string, status = 1) {
73
+ super(message)
74
+ this.name = 'CommandError'
75
+ this.status = status
76
+ }
77
+ }
78
+
79
+ export interface Job {
80
+ pid: number
81
+ nodeId: number
82
+ done: Promise<number>
83
+ status: number | null
84
+ kill(signal?: NodeJS.Signals): void
85
+ }
86
+
87
+ export class Proc {
88
+ state: ShellState
89
+ fds: FdTable
90
+ /** Virtual `$BASHPID`. */
91
+ pid: number
92
+ parentPid: number
93
+ /** `$BASH_SUBSHELL` */
94
+ depth: number
95
+ /** >0 inside if/while conditions, `!`, and non-final `&&`/`||` items. */
96
+ errexitSuppressed = 0
97
+ loopDepth = 0
98
+ /** Background jobs started from this shell process. */
99
+ jobs: Map<number, Job>
100
+ /** Status of the last command substitution (for `x=$(false)`). */
101
+ lastSubstStatus: number | null = null
102
+ /** Line currently executing ($LINENO). */
103
+ line = 0
104
+ /** Process-substitution fds to close when the current command finishes (shared by derived views). */
105
+ procSubstFds: { fds: FdTable; fd: number }[] = []
106
+ /** Running `source`: `return` is allowed. */
107
+ sourcing = false
108
+ /** Set inside background jobs: aborted by `kill` on the job. */
109
+ jobSignal: AbortSignal | undefined = undefined
110
+ /** OS child processes started inside a background job (for `kill $!`). */
111
+ jobChildren: Set<ChildProcess> | undefined = undefined
112
+ /** Exit-trap ownership: traps set in this process. */
113
+ readonly isSubshell: boolean
114
+
115
+ constructor(state: ShellState, fds: FdTable, pid: number, parentPid: number, depth: number, jobs: Map<number, Job>, isSubshell: boolean) {
116
+ this.state = state
117
+ this.fds = fds
118
+ this.pid = pid
119
+ this.parentPid = parentPid
120
+ this.depth = depth
121
+ this.jobs = jobs
122
+ this.isSubshell = isSubshell
123
+ }
124
+
125
+ /** A view sharing state, with different fds and/or errexit suppression. */
126
+ derive(opts: { fds?: FdTable; suppress?: boolean; loop?: boolean }): Proc {
127
+ const p = Object.create(Proc.prototype) as Proc
128
+ Object.assign(p, this)
129
+ if (opts.fds) p.fds = opts.fds
130
+ if (opts.suppress) p.errexitSuppressed = this.errexitSuppressed + 1
131
+ if (opts.loop) p.loopDepth = this.loopDepth + 1
132
+ return p
133
+ }
134
+
135
+ /** A subshell: deep-copied state (traps reset), cloned fds. */
136
+ subshell(pid: number): Proc {
137
+ const state = this.state.clone()
138
+ state.parentTraps = this.state.parentTraps ?? new Map(this.state.traps)
139
+ state.traps.clear()
140
+ const p = new Proc(state, this.fds.clone(), pid, this.pid, this.depth + 1, new Map(), true)
141
+ p.errexitSuppressed = this.errexitSuppressed
142
+ p.line = this.line
143
+ p.jobSignal = this.jobSignal
144
+ p.jobChildren = this.jobChildren
145
+ return p
146
+ }
147
+ }