@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,721 @@
1
+ /**
2
+ * Word expansion, in bash order: brace expansion, tilde expansion, parameter
3
+ * / arithmetic / command substitution (left to right), field splitting,
4
+ * pathname expansion, quote removal.
5
+ */
6
+
7
+ import * as os from 'node:os'
8
+ import type * as A from '../parser/ast.ts'
9
+ import { ArithError } from './arith.ts'
10
+ import type { Proc } from './proc.ts'
11
+ import { CommandError, DiscardError, FatalError } from './proc.ts'
12
+ import { globPaths, hasGlobChars, matchPattern, quotePattern, removePrefix, removeSuffix, replacePattern, unescapePattern } from './pattern.ts'
13
+ import { dashFlags, type Variable } from './state.ts'
14
+
15
+ export interface ExpandHost {
16
+ commandSubst(node: A.CmdSubst, proc: Proc): Promise<string>
17
+ processSubst(node: A.ProcSubst, proc: Proc): Promise<string>
18
+ arith(expr: A.ArithExpr | null, proc: Proc): Promise<bigint>
19
+ /** Assign a scalar (for `${x:=v}`). */
20
+ assign(proc: Proc, name: string, value: string): void
21
+ /** Value of dynamic variables (RANDOM, SECONDS, BASHPID, ...), or undefined. */
22
+ dynamic(proc: Proc, name: string): string | undefined
23
+ shellPid: number
24
+ }
25
+
26
+ /** A piece of expanded text. `split`: subject to field splitting; `quoted`: protected from globbing. */
27
+ interface Seg {
28
+ text: string
29
+ quoted: boolean
30
+ split: boolean
31
+ }
32
+ type Piece = Seg | { brk: true }
33
+
34
+ const BRK: Piece = { brk: true }
35
+
36
+ function isBrk(p: Piece): p is { brk: true } {
37
+ return 'brk' in p
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Brace expansion (on the part list, before anything else)
42
+ // ---------------------------------------------------------------------------
43
+
44
+ type Atom = { ch: string } | { part: A.WordPart }
45
+
46
+ function toAtoms(parts: A.WordPart[]): Atom[] {
47
+ const atoms: Atom[] = []
48
+ for (const p of parts) {
49
+ if (p.type === 'Lit') for (const ch of p.value) atoms.push({ ch })
50
+ else atoms.push({ part: p })
51
+ }
52
+ return atoms
53
+ }
54
+
55
+ function fromAtoms(atoms: Atom[], like: A.Word): A.WordPart[] {
56
+ const parts: A.WordPart[] = []
57
+ let lit = ''
58
+ const flush = (): void => {
59
+ if (lit !== '') parts.push({ type: 'Lit', value: lit, id: like.id, span: like.span })
60
+ lit = ''
61
+ }
62
+ for (const a of atoms) {
63
+ if ('ch' in a) lit += a.ch
64
+ else {
65
+ flush()
66
+ parts.push(a.part)
67
+ }
68
+ }
69
+ flush()
70
+ return parts
71
+ }
72
+
73
+ function seqExpansion(body: string): string[] | null {
74
+ let m = /^(-?\d+)\.\.(-?\d+)(?:\.\.(-?\d+))?$/.exec(body)
75
+ if (m) {
76
+ const a = Number(m[1])
77
+ const b = Number(m[2])
78
+ let step = m[3] ? Math.abs(Number(m[3])) : 1
79
+ if (step === 0) step = 1
80
+ const width = (m[1].replace('-', '').startsWith('0') && m[1].length > 1) || (m[2].replace('-', '').startsWith('0') && m[2].length > 1)
81
+ ? Math.max(m[1].length, m[2].length) : 0
82
+ const out: string[] = []
83
+ if (Math.abs(b - a) / step > 100000) return null
84
+ for (let i = a; a <= b ? i <= b : i >= b; i += a <= b ? step : -step) {
85
+ let s = String(Math.abs(i))
86
+ if (width) s = s.padStart(width - (i < 0 ? 1 : 0), '0')
87
+ out.push((i < 0 ? '-' : '') + s)
88
+ }
89
+ return out
90
+ }
91
+ m = /^([a-zA-Z])\.\.([a-zA-Z])(?:\.\.(-?\d+))?$/.exec(body)
92
+ if (m) {
93
+ const a = m[1].charCodeAt(0)
94
+ const b = m[2].charCodeAt(0)
95
+ const step = m[3] ? Math.abs(Number(m[3])) || 1 : 1
96
+ const out: string[] = []
97
+ for (let i = a; a <= b ? i <= b : i >= b; i += a <= b ? step : -step) out.push(String.fromCharCode(i))
98
+ return out
99
+ }
100
+ return null
101
+ }
102
+
103
+ function braceExpandAtoms(atoms: Atom[]): Atom[][] {
104
+ for (let i = 0; i < atoms.length; i++) {
105
+ const a = atoms[i]
106
+ if (!('ch' in a) || a.ch !== '{') continue
107
+ // find matching close and top-level commas
108
+ let depth = 0
109
+ const commas: number[] = []
110
+ let close = -1
111
+ for (let j = i + 1; j < atoms.length; j++) {
112
+ const b = atoms[j]
113
+ if (!('ch' in b)) continue
114
+ if (b.ch === '{') depth++
115
+ else if (b.ch === '}') {
116
+ if (depth === 0) {
117
+ close = j
118
+ break
119
+ }
120
+ depth--
121
+ } else if (b.ch === ',' && depth === 0) commas.push(j)
122
+ }
123
+ if (close === -1) continue
124
+ const prefix = atoms.slice(0, i)
125
+ const suffixes = braceExpandAtoms(atoms.slice(close + 1))
126
+ let alternatives: Atom[][]
127
+ if (commas.length > 0) {
128
+ alternatives = []
129
+ let start = i + 1
130
+ for (const c of [...commas, close]) {
131
+ alternatives.push(...braceExpandAtoms(atoms.slice(start, c)))
132
+ start = c + 1
133
+ }
134
+ } else {
135
+ const inner = atoms.slice(i + 1, close)
136
+ if (!inner.every((x) => 'ch' in x)) continue
137
+ const seq = seqExpansion(inner.map((x) => (x as { ch: string }).ch).join(''))
138
+ if (!seq) continue
139
+ alternatives = seq.map((s) => [...s].map((ch) => ({ ch })))
140
+ }
141
+ const out: Atom[][] = []
142
+ for (const alt of alternatives) for (const suf of suffixes) out.push([...prefix, ...alt, ...suf])
143
+ return out
144
+ }
145
+ return [atoms]
146
+ }
147
+
148
+ export function braceExpand(word: A.Word): A.Word[] {
149
+ if (!word.parts.some((p) => p.type === 'Lit' && p.value.includes('{'))) return [word]
150
+ const results = braceExpandAtoms(toAtoms(word.parts))
151
+ if (results.length === 1) return [word]
152
+ return results.map((atoms) => ({ ...word, parts: fromAtoms(atoms, word) }))
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Expander
157
+ // ---------------------------------------------------------------------------
158
+
159
+ export interface FieldOptions {
160
+ /** Skip pathname expansion. */
161
+ noGlob?: boolean
162
+ }
163
+
164
+ export class Expander {
165
+ private readonly host: ExpandHost
166
+
167
+ constructor(host: ExpandHost) {
168
+ this.host = host
169
+ }
170
+
171
+ /** Expand words into fields (full expansion, for argv and `for` lists). */
172
+ async expandFields(words: A.Word[], proc: Proc, opts: FieldOptions = {}): Promise<string[]> {
173
+ const out: string[] = []
174
+ for (const w of words) {
175
+ for (const bw of braceExpand(w)) {
176
+ const pieces = await this.expandParts(bw.parts, proc, false, true)
177
+ for (const field of this.split(pieces, proc)) {
178
+ out.push(...this.glob(field, proc, opts))
179
+ }
180
+ }
181
+ }
182
+ return out
183
+ }
184
+
185
+ /** Expand without splitting or globbing (assignments, redirect targets, case word, [[ ]] operands). */
186
+ /**
187
+ * The replacement of `${x/pat/rep}`. With `patsub_replacement` (on by
188
+ * default, as in bash 5.2) every `&` in an unquoted part, including the
189
+ * value of an unquoted `$var`, stands for the matched text; quoted parts and
190
+ * `\&` are literal.
191
+ */
192
+ private async replacement(word: A.Word, proc: Proc): Promise<string | ((match: string) => string)> {
193
+ if (!proc.state.shopts.has('patsub_replacement')) return this.expandString(word, proc)
194
+ const pieces: (string | null)[] = []
195
+ for (const [i, part] of word.parts.entries()) {
196
+ const text = await this.expandString({ ...word, parts: i === 0 ? word.parts.slice(0, 1) : [part] }, proc)
197
+ if (part.type === 'SglQuoted' || part.type === 'DblQuoted' || part.type === 'Escaped') pieces.push(text)
198
+ else text.split('&').forEach((t, j) => (j > 0 ? pieces.push(null, t) : pieces.push(t)))
199
+ }
200
+ if (!pieces.includes(null)) return pieces.join('')
201
+ return (match) => pieces.map((p) => (p === null ? match : p)).join('')
202
+ }
203
+
204
+ async expandString(word: A.Word, proc: Proc, opts: { tildeAssign?: boolean } = {}): Promise<string> {
205
+ const pieces = await this.expandParts(word.parts, proc, false, true, opts.tildeAssign)
206
+ return this.joinPieces(pieces)
207
+ }
208
+
209
+ /** Expand to a pattern: quoted text is escaped, unquoted expansions stay pattern-active. */
210
+ async expandPattern(word: A.Word, proc: Proc): Promise<string> {
211
+ const pieces = await this.expandParts(word.parts, proc, false, true)
212
+ let s = ''
213
+ for (const p of pieces) {
214
+ if (isBrk(p)) {
215
+ s += ' '
216
+ continue
217
+ }
218
+ s += p.quoted ? quotePattern(p.text) : p.text
219
+ }
220
+ return s
221
+ }
222
+
223
+ /** Expand a here-doc body (parameter/command/arith expansion only). */
224
+ async expandHeredoc(word: A.Word, proc: Proc): Promise<string> {
225
+ const pieces = await this.expandParts(word.parts, proc, true, false)
226
+ return this.joinPieces(pieces)
227
+ }
228
+
229
+ private joinPieces(pieces: Piece[]): string {
230
+ let s = ''
231
+ for (const p of pieces) s += isBrk(p) ? ' ' : p.text
232
+ return s
233
+ }
234
+
235
+ private ifs(proc: Proc): string {
236
+ const v = proc.state.getString('IFS')
237
+ return v === undefined ? ' \t\n' : v
238
+ }
239
+
240
+ private ifsFirst(proc: Proc): string {
241
+ const v = proc.state.getString('IFS')
242
+ return v === undefined ? ' ' : v.slice(0, 1)
243
+ }
244
+
245
+ private async expandParts(parts: A.WordPart[], proc: Proc, quoted: boolean, tilde: boolean, tildeAssign = false): Promise<Piece[]> {
246
+ const out: Piece[] = []
247
+ for (let i = 0; i < parts.length; i++) {
248
+ const p = parts[i]
249
+ switch (p.type) {
250
+ case 'Lit': {
251
+ if (!quoted && tilde && (i === 0 || tildeAssign)) {
252
+ const t = this.tilde(p.value, proc, i === 0, tildeAssign)
253
+ if (t !== null) {
254
+ out.push(...t)
255
+ break
256
+ }
257
+ }
258
+ out.push({ text: p.value, quoted, split: false })
259
+ break
260
+ }
261
+ case 'SglQuoted':
262
+ case 'Escaped':
263
+ out.push({ text: p.value, quoted: true, split: false })
264
+ break
265
+ case 'DblQuoted': {
266
+ const inner = await this.expandParts(p.parts, proc, true, false)
267
+ const onlyAtExpansions = p.parts.length > 0 && p.parts.every((q) => q.type === 'ParamExp' && isAtExpansion(q))
268
+ if (inner.length === 0 && !onlyAtExpansions) out.push({ text: '', quoted: true, split: false })
269
+ else out.push(...inner)
270
+ break
271
+ }
272
+ case 'ParamExp':
273
+ out.push(...(await this.expandParam(p, proc, quoted)))
274
+ break
275
+ case 'CmdSubst': {
276
+ const text = await this.host.commandSubst(p, proc)
277
+ out.push({ text, quoted, split: !quoted })
278
+ break
279
+ }
280
+ case 'ArithExp': {
281
+ let v: bigint
282
+ try {
283
+ v = await this.host.arith(p.expr, proc)
284
+ } catch (e) {
285
+ if (e instanceof ArithError) throw new DiscardError(e.message)
286
+ throw e
287
+ }
288
+ out.push({ text: String(v), quoted, split: !quoted })
289
+ break
290
+ }
291
+ case 'ProcSubst': {
292
+ const path = await this.host.processSubst(p, proc)
293
+ out.push({ text: path, quoted: true, split: false })
294
+ break
295
+ }
296
+ }
297
+ }
298
+ return out
299
+ }
300
+
301
+ private tilde(text: string, proc: Proc, atStart: boolean, assign: boolean): Piece[] | null {
302
+ if (!text.includes('~')) return null
303
+ const home = proc.state.getString('HOME') ?? os.homedir()
304
+ const expandOne = (prefix: string): string | null => {
305
+ if (prefix === '') return home
306
+ if (prefix === '+') return proc.state.getString('PWD') ?? proc.state.cwd
307
+ if (prefix === '-') return proc.state.getString('OLDPWD') ?? null
308
+ if (prefix === os.userInfo().username) return os.homedir()
309
+ return null
310
+ }
311
+ if (!assign) {
312
+ if (!atStart || !text.startsWith('~')) return null
313
+ const slash = text.indexOf('/')
314
+ const prefix = slash === -1 ? text.slice(1) : text.slice(1, slash)
315
+ const h = expandOne(prefix)
316
+ if (h === null) return null
317
+ return [{ text: h, quoted: true, split: false }, { text: slash === -1 ? '' : text.slice(slash), quoted: false, split: false }]
318
+ }
319
+ // assignment: after = (start of value) and after each unquoted ':'
320
+ const pieces: Piece[] = []
321
+ const chunks = text.split(':')
322
+ chunks.forEach((chunk, idx) => {
323
+ if (idx > 0) pieces.push({ text: ':', quoted: false, split: false })
324
+ if (chunk.startsWith('~') && (idx > 0 || atStart)) {
325
+ const slash = chunk.indexOf('/')
326
+ const prefix = slash === -1 ? chunk.slice(1) : chunk.slice(1, slash)
327
+ const h = expandOne(prefix)
328
+ if (h !== null) {
329
+ pieces.push({ text: h, quoted: true, split: false })
330
+ pieces.push({ text: slash === -1 ? '' : chunk.slice(slash), quoted: false, split: false })
331
+ return
332
+ }
333
+ }
334
+ pieces.push({ text: chunk, quoted: false, split: false })
335
+ })
336
+ return pieces
337
+ }
338
+
339
+ // -------------------------------------------------------------------------
340
+ // Parameters
341
+ // -------------------------------------------------------------------------
342
+
343
+ /** Raw value: a string, an array of fields (`$@`, `${a[@]}`), or undefined (unset). */
344
+ async paramValue(p: A.ParamExp, proc: Proc): Promise<{ value: string | string[] | undefined; star: boolean }> {
345
+ const st = proc.state
346
+ let name = p.name
347
+ if (p.indirect && !(p.index && isAllIndex(p.index))) {
348
+ const target = await this.simpleParamValue(name, null, proc)
349
+ if (target === undefined || target === '') {
350
+ throw new FatalError(`${name}: invalid indirect expansion`)
351
+ }
352
+ const m = /^([A-Za-z_][A-Za-z0-9_]*|[0-9]+|[@*#?$!-])(?:\[(.*)\])?$/s.exec(target)
353
+ if (!m) throw new FatalError(`${target}: invalid variable name`)
354
+ name = m[1]
355
+ if (m[2] !== undefined) {
356
+ if (m[2] === '@' || m[2] === '*') return { value: this.arrayValues(name, proc), star: m[2] === '*' }
357
+ return { value: this.elementValue(name, m[2], proc), star: false }
358
+ }
359
+ return this.paramValue({ ...p, name, indirect: false, index: p.index }, proc)
360
+ }
361
+ if (p.prefixNames) {
362
+ return { value: st.visibleNames().filter((n) => n.startsWith(name) && st.isSet(n)), star: p.prefixNames === '*' }
363
+ }
364
+ if (name === '@' || name === '*') return { value: [...st.positional], star: name === '*' }
365
+ if (p.index) {
366
+ if (isAllIndex(p.index)) {
367
+ const star = literalIndex(p.index) === '*'
368
+ if (p.indirect) return { value: this.arrayKeys(name, proc), star }
369
+ return { value: this.arrayValues(name, proc), star }
370
+ }
371
+ const v = st.lookup(name)
372
+ let key: string
373
+ if (v?.value?.kind === 'assoc' || v?.declaredKind === 'assoc') key = await this.expandString(p.index, proc)
374
+ else {
375
+ const idxText = await this.expandString(p.index, proc)
376
+ key = String(await this.host.arith(parseIndexExpr(idxText), proc))
377
+ }
378
+ return { value: this.elementValue(name, key, proc), star: false }
379
+ }
380
+ return { value: await this.simpleParamValue(name, null, proc), star: false }
381
+ }
382
+
383
+ private async simpleParamValue(name: string, _index: null, proc: Proc): Promise<string | undefined> {
384
+ const st = proc.state
385
+ switch (name) {
386
+ case '#': return String(st.positional.length)
387
+ case '?': return String(st.lastStatus)
388
+ case '-': return dashFlags(st)
389
+ case '$': return String(this.host.shellPid)
390
+ case '!': return st.lastBgPid === null ? undefined : String(st.lastBgPid)
391
+ case '0': return st.arg0
392
+ case '@': case '*': return st.positional.join(this.ifsFirst(proc))
393
+ }
394
+ if (/^[0-9]+$/.test(name)) return st.positional[Number(name) - 1]
395
+ const dyn = this.host.dynamic(proc, name)
396
+ if (dyn !== undefined) return dyn
397
+ return st.getString(name)
398
+ }
399
+
400
+ private arrayValues(name: string, proc: Proc): string[] {
401
+ if (name === 'FUNCNAME') return proc.state.funcStack.length > 0 ? [...proc.state.funcStack, 'main'] : []
402
+ const v = proc.state.lookup(name)
403
+ return varElements(v)
404
+ }
405
+
406
+ private arrayKeys(name: string, proc: Proc): string[] {
407
+ const v = proc.state.lookup(name)
408
+ if (!v || v.value === null) return []
409
+ if (v.value.kind === 'string') return ['0']
410
+ if (v.value.kind === 'indexed') return [...v.value.values.keys()].sort((a, b) => a - b).map(String)
411
+ return [...v.value.values.keys()]
412
+ }
413
+
414
+ private elementValue(name: string, key: string, proc: Proc): string | undefined {
415
+ if (name === 'FUNCNAME') return proc.state.funcStack.length > 0 ? [...proc.state.funcStack, 'main'][Number(key)] : undefined
416
+ const v = proc.state.lookup(name)
417
+ if (!v || v.value === null) return undefined
418
+ if (v.value.kind === 'string') return key === '0' ? v.value.value : undefined
419
+ if (v.value.kind === 'indexed') {
420
+ let k = Number(key)
421
+ if (k < 0) {
422
+ const keys = [...v.value.values.keys()]
423
+ const max = keys.length === 0 ? -1 : Math.max(...keys)
424
+ k = max + 1 + k
425
+ }
426
+ return v.value.values.get(k)
427
+ }
428
+ return v.value.values.get(key)
429
+ }
430
+
431
+ private async expandParam(p: A.ParamExp, proc: Proc, quoted: boolean): Promise<Piece[]> {
432
+ const st = proc.state
433
+ const { value: raw, star } = await this.paramValue(p, proc)
434
+ const isList = Array.isArray(raw)
435
+ const op = p.op
436
+
437
+ // ${#x}
438
+ if (p.length) {
439
+ if (isList) return [{ text: String(raw.length), quoted, split: !quoted }]
440
+ if (raw === undefined && st.options.nounset && !(p.name === '@' || p.name === '*')) throw new FatalError(`${p.name}: unbound variable`, 127)
441
+ return [{ text: String([...(raw ?? '')].length), quoted, split: !quoted }]
442
+ }
443
+
444
+ const isUnset = raw === undefined || (isList && raw.length === 0 && (p.name === '@' || p.name === '*' || (p.index !== null)))
445
+ const isNull = isUnset || raw === '' || (isList && (raw.length === 0 || (raw.length === 1 && raw[0] === '')))
446
+
447
+ if (op && (op.kind === 'default' || op.kind === 'assign' || op.kind === 'error' || op.kind === 'alternative')) {
448
+ const missing = op.colon ? isNull : isUnset
449
+ if (op.kind === 'alternative') {
450
+ if (missing) return []
451
+ return op.word ? this.expandOpWord(op.word, proc, quoted) : []
452
+ }
453
+ if (missing) {
454
+ if (op.kind === 'default') return op.word ? this.expandOpWord(op.word, proc, quoted) : [{ text: '', quoted, split: false }]
455
+ if (op.kind === 'assign') {
456
+ const v = op.word ? await this.expandString(op.word, proc) : ''
457
+ this.host.assign(proc, p.name, v)
458
+ return [{ text: v, quoted, split: !quoted }]
459
+ }
460
+ const msg = op.word ? await this.expandString(op.word, proc) : (op.colon ? 'parameter null or not set' : 'parameter not set')
461
+ throw new FatalError(`${p.name}: ${msg}`, 127)
462
+ }
463
+ // present: fall through to the plain value
464
+ } else if (raw === undefined && st.options.nounset && !(p.name === '@' || p.name === '*') && !p.prefixNames && !(p.index && isAllIndex(p.index))) {
465
+ throw new FatalError(`${p.index ? `${p.name}[${literalIndex(p.index) ?? '…'}]` : p.name}: unbound variable`, 127)
466
+ }
467
+
468
+ let values: string[] = isList ? raw : raw === undefined ? [] : [raw]
469
+ const scalarUnset = !isList && raw === undefined
470
+
471
+ if (op) {
472
+ switch (op.kind) {
473
+ case 'removePrefix':
474
+ case 'removeSuffix': {
475
+ const pat = await this.expandPattern(op.pattern, proc)
476
+ values = values.map((v) => (op.kind === 'removePrefix' ? removePrefix(v, pat, op.longest) : removeSuffix(v, pat, op.longest)))
477
+ break
478
+ }
479
+ case 'replace': {
480
+ const pat = await this.expandPattern(op.pattern, proc)
481
+ const rep = op.replacement ? await this.replacement(op.replacement, proc) : ''
482
+ values = values.map((v) => replacePattern(v, pat, rep, op.mode))
483
+ break
484
+ }
485
+ case 'case': {
486
+ const pat = op.pattern ? await this.expandPattern(op.pattern, proc) : null
487
+ const conv = (ch: string): string => (pat === null || matchPattern(ch, pat) ? (op.upper ? ch.toUpperCase() : ch.toLowerCase()) : ch)
488
+ values = values.map((v) => {
489
+ if (op.all) return [...v].map(conv).join('')
490
+ return v === '' ? v : conv(v[0]) + v.slice(1)
491
+ })
492
+ break
493
+ }
494
+ case 'substring': {
495
+ const off = Number(await this.arithSafe(op.offset, proc))
496
+ const len = op.length === null ? null : Number(await this.arithSafe(op.length, proc))
497
+ if (isList) {
498
+ // positional slicing includes $0 at offset 0
499
+ let list = values
500
+ if (p.name === '@' || p.name === '*') list = [st.arg0, ...values]
501
+ let start = off < 0 ? list.length + off : off
502
+ if (p.name !== '@' && p.name !== '*' && off >= 0) start = off
503
+ if (start < 0) {
504
+ values = []
505
+ break
506
+ }
507
+ const end = len === null ? list.length : len < 0 ? list.length + len : start + len
508
+ values = list.slice(start, Math.max(start, end))
509
+ if ((p.name === '@' || p.name === '*') && off === 0 && false) values = list
510
+ } else {
511
+ values = values.map((v) => {
512
+ const chars = [...v]
513
+ let start = off < 0 ? chars.length + off : off
514
+ if (start < 0) return ''
515
+ if (start > chars.length) start = chars.length
516
+ let end = len === null ? chars.length : len < 0 ? chars.length + len : start + len
517
+ if (len !== null && len < 0 && end < start) throw new FatalError(`${len}: substring expression < 0`)
518
+ end = Math.min(end, chars.length)
519
+ return chars.slice(start, end).join('')
520
+ })
521
+ }
522
+ break
523
+ }
524
+ case 'transform': {
525
+ values = values.map((v) => transform(v, op.operator, p.name, st.lookup(p.name)))
526
+ break
527
+ }
528
+ default:
529
+ break
530
+ }
531
+ }
532
+
533
+ if (scalarUnset && values.length === 0) return []
534
+ if (isList) {
535
+ if (quoted && !star) {
536
+ const out: Piece[] = []
537
+ values.forEach((v, i) => {
538
+ if (i > 0) out.push(BRK)
539
+ out.push({ text: v, quoted: true, split: false })
540
+ })
541
+ return out
542
+ }
543
+ if (quoted && star) return [{ text: values.join(this.ifsFirst(proc)), quoted: true, split: false }]
544
+ // unquoted: each element split separately
545
+ const out: Piece[] = []
546
+ values.forEach((v, i) => {
547
+ if (i > 0) out.push(BRK)
548
+ out.push({ text: v, quoted: false, split: true })
549
+ })
550
+ return out
551
+ }
552
+ return [{ text: values[0] ?? '', quoted, split: !quoted }]
553
+ }
554
+
555
+ private async arithSafe(e: A.ArithExpr, proc: Proc): Promise<bigint> {
556
+ try {
557
+ return await this.host.arith(e, proc)
558
+ } catch (err) {
559
+ if (err instanceof ArithError) throw new DiscardError(err.message)
560
+ throw err
561
+ }
562
+ }
563
+
564
+ private async expandOpWord(word: A.Word, proc: Proc, quoted: boolean): Promise<Piece[]> {
565
+ const pieces = await this.expandParts(word.parts, proc, quoted, !quoted)
566
+ if (quoted) return pieces
567
+ // unquoted ${v:-a b}: the operand's unquoted text is subject to word splitting
568
+ return pieces.map((p) => (isBrk(p) || p.quoted ? p : { ...p, split: true }))
569
+ }
570
+
571
+ // -------------------------------------------------------------------------
572
+ // Field splitting and globbing
573
+ // -------------------------------------------------------------------------
574
+
575
+ /** Split pieces into fields; each field keeps its value and its glob pattern. */
576
+ private split(pieces: Piece[], proc: Proc): { value: string; pattern: string; globbable: boolean }[] {
577
+ const ifs = this.ifs(proc)
578
+ const ws = (c: string): boolean => (c === ' ' || c === '\t' || c === '\n') && ifs.includes(c)
579
+ const fields: { value: string; pattern: string; globbable: boolean }[] = []
580
+ let value = ''
581
+ let pattern = ''
582
+ let globbable = false
583
+ let started = false
584
+ let afterWs = false
585
+ const push = (): void => {
586
+ fields.push({ value, pattern, globbable })
587
+ value = ''
588
+ pattern = ''
589
+ globbable = false
590
+ started = false
591
+ }
592
+ for (const p of pieces) {
593
+ if (isBrk(p)) {
594
+ push()
595
+ afterWs = false
596
+ continue
597
+ }
598
+ if (!p.split || ifs === '') {
599
+ if (p.quoted) {
600
+ value += p.text
601
+ pattern += quotePattern(p.text)
602
+ started = true
603
+ } else {
604
+ value += p.text
605
+ pattern += p.text
606
+ if (hasGlobChars(p.text)) globbable = true
607
+ if (p.text !== '') started = true
608
+ }
609
+ if (p.text !== '' || p.quoted) afterWs = false
610
+ continue
611
+ }
612
+ for (const ch of p.text) {
613
+ if (!ifs.includes(ch)) {
614
+ value += ch
615
+ pattern += ch === '\\' ? '\\\\' : ch
616
+ if (ch === '*' || ch === '?' || ch === '[') globbable = true
617
+ started = true
618
+ afterWs = false
619
+ continue
620
+ }
621
+ if (ws(ch)) {
622
+ if (started) {
623
+ push()
624
+ afterWs = true
625
+ }
626
+ } else if (started) {
627
+ push()
628
+ afterWs = false
629
+ } else if (afterWs) {
630
+ afterWs = false
631
+ } else {
632
+ push()
633
+ }
634
+ }
635
+ }
636
+ if (started) push()
637
+ return fields
638
+ }
639
+
640
+ private glob(field: { value: string; pattern: string; globbable: boolean }, proc: Proc, opts: FieldOptions): string[] {
641
+ const st = proc.state
642
+ if (!field.globbable || opts.noGlob || st.options.noglob || !hasGlobChars(field.pattern)) return [field.value]
643
+ const matches = globPaths(field.pattern, {
644
+ cwd: st.cwd,
645
+ dotglob: st.shopts.has('dotglob'),
646
+ globstar: st.shopts.has('globstar'),
647
+ nocase: st.shopts.has('nocaseglob'),
648
+ })
649
+ if (matches.length > 0) return matches
650
+ if (st.shopts.has('failglob')) throw new CommandError(`no match: ${field.value}`)
651
+ if (st.shopts.has('nullglob')) return []
652
+ return [unescapePattern(field.pattern) === field.value ? field.value : field.value]
653
+ }
654
+ }
655
+
656
+ function isAtExpansion(p: A.ParamExp): boolean {
657
+ if (p.length) return false
658
+ if ((p.name === '@') && !p.index) return p.op === null || p.op.kind !== 'default'
659
+ return p.index !== null && literalIndex(p.index) === '@'
660
+ }
661
+
662
+ function literalIndex(w: A.Word): string | null {
663
+ if (w.parts.length === 1 && w.parts[0].type === 'Lit') return w.parts[0].value
664
+ return null
665
+ }
666
+
667
+ function isAllIndex(w: A.Word): boolean {
668
+ const l = literalIndex(w)
669
+ return l === '@' || l === '*'
670
+ }
671
+
672
+ /** Elements of a variable in index order. */
673
+ export function varElements(v: Variable | undefined): string[] {
674
+ if (!v || v.value === null) return []
675
+ if (v.value.kind === 'string') return [v.value.value]
676
+ if (v.value.kind === 'indexed') return [...v.value.values.entries()].sort((a, b) => a[0] - b[0]).map((e) => e[1])
677
+ return [...v.value.values.values()]
678
+ }
679
+
680
+ /** Subscript text → an arithmetic expression node (parsed lazily by the host). */
681
+ function parseIndexExpr(text: string): A.ArithExpr {
682
+ return { type: 'ArithWord', id: 0, span: { start: 0, end: 0 }, word: { type: 'Word', id: 0, span: { start: 0, end: 0 }, parts: [{ type: 'SglQuoted', value: text, ansi: false, id: 0, span: { start: 0, end: 0 } }] } }
683
+ }
684
+
685
+ export function shellQuote(s: string): string {
686
+ if (s === '') return "''"
687
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(s)) return s
688
+ if (/[\x00-\x1f\x7f]/.test(s)) {
689
+ return "$'" + s.replace(/[\\']/g, (c) => '\\' + c).replace(/[\x00-\x1f\x7f]/g, (c) => {
690
+ const map: Record<string, string> = { '\n': '\\n', '\t': '\\t', '\r': '\\r', '\x1b': '\\E' }
691
+ return map[c] ?? '\\' + c.charCodeAt(0).toString(8).padStart(3, '0')
692
+ }) + "'"
693
+ }
694
+ return "'" + s.replace(/'/g, "'\\''") + "'"
695
+ }
696
+
697
+ function transform(v: string, op: string, name: string, variable: Variable | undefined): string {
698
+ switch (op) {
699
+ case 'Q': return shellQuote(v)
700
+ case 'U': return v.toUpperCase()
701
+ case 'u': return v === '' ? v : v[0].toUpperCase() + v.slice(1)
702
+ case 'L': return v.toLowerCase()
703
+ case 'E': return v.replace(/\\(n|t|r|e|E|\\|'|"|a|b|f|v)/g, (_, c: string) => ({ n: '\n', t: '\t', r: '\r', e: '\x1b', E: '\x1b', '\\': '\\', "'": "'", '"': '"', a: '\x07', b: '\b', f: '\f', v: '\v' } as Record<string, string>)[c])
704
+ case 'A': return `${name}=${shellQuote(v)}`
705
+ case 'a': {
706
+ if (!variable) return ''
707
+ let s = ''
708
+ if (variable.value?.kind === 'indexed' || variable.declaredKind === 'indexed') s += 'a'
709
+ if (variable.value?.kind === 'assoc' || variable.declaredKind === 'assoc') s += 'A'
710
+ if (variable.integer) s += 'i'
711
+ if (variable.lower) s += 'l'
712
+ if (variable.readonly) s += 'r'
713
+ if (variable.upper) s += 'u'
714
+ if (variable.exported) s += 'x'
715
+ return s
716
+ }
717
+ case 'P': return v
718
+ case 'K': return shellQuote(v)
719
+ default: throw new FatalError(`${op}: bad substitution`)
720
+ }
721
+ }