@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.
- package/LICENSE +21 -0
- package/README.md +275 -2
- package/bin/mash.ts +253 -0
- package/dist/bin/mash.d.ts +39 -0
- package/dist/bin/mash.d.ts.map +1 -0
- package/dist/bin/mash.js +281 -0
- package/dist/bin/mash.js.map +1 -0
- package/dist/src/analysis/analyze.d.ts +97 -0
- package/dist/src/analysis/analyze.d.ts.map +1 -0
- package/dist/src/analysis/analyze.js +1406 -0
- package/dist/src/analysis/analyze.js.map +1 -0
- package/dist/src/analysis/fs.d.ts +39 -0
- package/dist/src/analysis/fs.d.ts.map +1 -0
- package/dist/src/analysis/fs.js +117 -0
- package/dist/src/analysis/fs.js.map +1 -0
- package/dist/src/analysis/outputs.d.ts +16 -0
- package/dist/src/analysis/outputs.d.ts.map +1 -0
- package/dist/src/analysis/outputs.js +277 -0
- package/dist/src/analysis/outputs.js.map +1 -0
- package/dist/src/analysis/value.d.ts +76 -0
- package/dist/src/analysis/value.d.ts.map +1 -0
- package/dist/src/analysis/value.js +248 -0
- package/dist/src/analysis/value.js.map +1 -0
- package/dist/src/behaviors/defs/core.d.ts +15 -0
- package/dist/src/behaviors/defs/core.d.ts.map +1 -0
- package/dist/src/behaviors/defs/core.js +1084 -0
- package/dist/src/behaviors/defs/core.js.map +1 -0
- package/dist/src/behaviors/defs/devtools.d.ts +9 -0
- package/dist/src/behaviors/defs/devtools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/devtools.js +432 -0
- package/dist/src/behaviors/defs/devtools.js.map +1 -0
- package/dist/src/behaviors/defs/macos.d.ts +12 -0
- package/dist/src/behaviors/defs/macos.d.ts.map +1 -0
- package/dist/src/behaviors/defs/macos.js +454 -0
- package/dist/src/behaviors/defs/macos.js.map +1 -0
- package/dist/src/behaviors/defs/tools.d.ts +9 -0
- package/dist/src/behaviors/defs/tools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/tools.js +639 -0
- package/dist/src/behaviors/defs/tools.js.map +1 -0
- package/dist/src/behaviors/dsl.d.ts +95 -0
- package/dist/src/behaviors/dsl.d.ts.map +1 -0
- package/dist/src/behaviors/dsl.js +267 -0
- package/dist/src/behaviors/dsl.js.map +1 -0
- package/dist/src/behaviors/index.d.ts +12 -0
- package/dist/src/behaviors/index.d.ts.map +1 -0
- package/dist/src/behaviors/index.js +18 -0
- package/dist/src/behaviors/index.js.map +1 -0
- package/dist/src/behaviors/registry.d.ts +45 -0
- package/dist/src/behaviors/registry.d.ts.map +1 -0
- package/dist/src/behaviors/registry.js +266 -0
- package/dist/src/behaviors/registry.js.map +1 -0
- package/dist/src/behaviors/roles.d.ts +29 -0
- package/dist/src/behaviors/roles.d.ts.map +1 -0
- package/dist/src/behaviors/roles.js +198 -0
- package/dist/src/behaviors/roles.js.map +1 -0
- package/dist/src/behaviors/types.d.ts +120 -0
- package/dist/src/behaviors/types.d.ts.map +1 -0
- package/dist/src/behaviors/types.js +12 -0
- package/dist/src/behaviors/types.js.map +1 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/integrations/deepsea.d.ts +121 -0
- package/dist/src/integrations/deepsea.d.ts.map +1 -0
- package/dist/src/integrations/deepsea.js +199 -0
- package/dist/src/integrations/deepsea.js.map +1 -0
- package/dist/src/machine.d.ts +139 -0
- package/dist/src/machine.d.ts.map +1 -0
- package/dist/src/machine.js +400 -0
- package/dist/src/machine.js.map +1 -0
- package/dist/src/parser/ast.d.ts +369 -0
- package/dist/src/parser/ast.d.ts.map +1 -0
- package/dist/src/parser/ast.js +17 -0
- package/dist/src/parser/ast.js.map +1 -0
- package/dist/src/parser/parser.d.ts +48 -0
- package/dist/src/parser/parser.d.ts.map +1 -0
- package/dist/src/parser/parser.js +1950 -0
- package/dist/src/parser/parser.js.map +1 -0
- package/dist/src/policy/effects.d.ts +86 -0
- package/dist/src/policy/effects.d.ts.map +1 -0
- package/dist/src/policy/effects.js +53 -0
- package/dist/src/policy/effects.js.map +1 -0
- package/dist/src/policy/policy.d.ts +130 -0
- package/dist/src/policy/policy.d.ts.map +1 -0
- package/dist/src/policy/policy.js +532 -0
- package/dist/src/policy/policy.js.map +1 -0
- package/dist/src/printer/doc.d.ts +151 -0
- package/dist/src/printer/doc.d.ts.map +1 -0
- package/dist/src/printer/doc.js +409 -0
- package/dist/src/printer/doc.js.map +1 -0
- package/dist/src/printer/highlight.d.ts +53 -0
- package/dist/src/printer/highlight.d.ts.map +1 -0
- package/dist/src/printer/highlight.js +339 -0
- package/dist/src/printer/highlight.js.map +1 -0
- package/dist/src/printer/html.d.ts +72 -0
- package/dist/src/printer/html.d.ts.map +1 -0
- package/dist/src/printer/html.js +423 -0
- package/dist/src/printer/html.js.map +1 -0
- package/dist/src/printer/print.d.ts +63 -0
- package/dist/src/printer/print.d.ts.map +1 -0
- package/dist/src/printer/print.js +484 -0
- package/dist/src/printer/print.js.map +1 -0
- package/dist/src/printer/summary.d.ts +50 -0
- package/dist/src/printer/summary.d.ts.map +1 -0
- package/dist/src/printer/summary.js +621 -0
- package/dist/src/printer/summary.js.map +1 -0
- package/dist/src/trace/events.d.ts +183 -0
- package/dist/src/trace/events.d.ts.map +1 -0
- package/dist/src/trace/events.js +9 -0
- package/dist/src/trace/events.js.map +1 -0
- package/dist/src/trace/progress.d.ts +58 -0
- package/dist/src/trace/progress.d.ts.map +1 -0
- package/dist/src/trace/progress.js +83 -0
- package/dist/src/trace/progress.js.map +1 -0
- package/dist/src/vm/arith.d.ts +29 -0
- package/dist/src/vm/arith.d.ts.map +1 -0
- package/dist/src/vm/arith.js +258 -0
- package/dist/src/vm/arith.js.map +1 -0
- package/dist/src/vm/builtins.d.ts +31 -0
- package/dist/src/vm/builtins.d.ts.map +1 -0
- package/dist/src/vm/builtins.js +1773 -0
- package/dist/src/vm/builtins.js.map +1 -0
- package/dist/src/vm/emulate.d.ts +75 -0
- package/dist/src/vm/emulate.d.ts.map +1 -0
- package/dist/src/vm/emulate.js +380 -0
- package/dist/src/vm/emulate.js.map +1 -0
- package/dist/src/vm/expand.d.ts +68 -0
- package/dist/src/vm/expand.d.ts.map +1 -0
- package/dist/src/vm/expand.js +744 -0
- package/dist/src/vm/expand.js.map +1 -0
- package/dist/src/vm/external.d.ts +34 -0
- package/dist/src/vm/external.d.ts.map +1 -0
- package/dist/src/vm/external.js +228 -0
- package/dist/src/vm/external.js.map +1 -0
- package/dist/src/vm/fifo.d.ts +66 -0
- package/dist/src/vm/fifo.d.ts.map +1 -0
- package/dist/src/vm/fifo.js +232 -0
- package/dist/src/vm/fifo.js.map +1 -0
- package/dist/src/vm/interp.d.ts +209 -0
- package/dist/src/vm/interp.d.ts.map +1 -0
- package/dist/src/vm/interp.js +1641 -0
- package/dist/src/vm/interp.js.map +1 -0
- package/dist/src/vm/io.d.ts +180 -0
- package/dist/src/vm/io.d.ts.map +1 -0
- package/dist/src/vm/io.js +502 -0
- package/dist/src/vm/io.js.map +1 -0
- package/dist/src/vm/pattern.d.ts +46 -0
- package/dist/src/vm/pattern.d.ts.map +1 -0
- package/dist/src/vm/pattern.js +498 -0
- package/dist/src/vm/pattern.js.map +1 -0
- package/dist/src/vm/proc.d.ts +94 -0
- package/dist/src/vm/proc.d.ts.map +1 -0
- package/dist/src/vm/proc.js +130 -0
- package/dist/src/vm/proc.js.map +1 -0
- package/dist/src/vm/state.d.ts +104 -0
- package/dist/src/vm/state.d.ts.map +1 -0
- package/dist/src/vm/state.js +233 -0
- package/dist/src/vm/state.js.map +1 -0
- package/dist/src/vm/test.d.ts +11 -0
- package/dist/src/vm/test.d.ts.map +1 -0
- package/dist/src/vm/test.js +277 -0
- package/dist/src/vm/test.js.map +1 -0
- package/package.json +63 -4
- package/src/analysis/analyze.ts +1395 -0
- package/src/analysis/fs.ts +121 -0
- package/src/analysis/outputs.ts +242 -0
- package/src/analysis/value.ts +264 -0
- package/src/behaviors/defs/core.ts +970 -0
- package/src/behaviors/defs/devtools.ts +400 -0
- package/src/behaviors/defs/macos.ts +426 -0
- package/src/behaviors/defs/tools.ts +596 -0
- package/src/behaviors/dsl.ts +293 -0
- package/src/behaviors/index.ts +19 -0
- package/src/behaviors/registry.ts +261 -0
- package/src/behaviors/roles.ts +187 -0
- package/src/behaviors/types.ts +127 -0
- package/src/index.ts +27 -0
- package/src/integrations/deepsea.ts +259 -0
- package/src/machine.ts +483 -0
- package/src/parser/ast.ts +472 -0
- package/src/parser/parser.ts +1826 -0
- package/src/policy/effects.ts +102 -0
- package/src/policy/policy.ts +578 -0
- package/src/printer/doc.ts +454 -0
- package/src/printer/highlight.ts +283 -0
- package/src/printer/html.ts +441 -0
- package/src/printer/print.ts +528 -0
- package/src/printer/summary.ts +601 -0
- package/src/trace/events.ts +102 -0
- package/src/trace/progress.ts +119 -0
- package/src/vm/arith.ts +236 -0
- package/src/vm/builtins.ts +1590 -0
- package/src/vm/emulate.ts +337 -0
- package/src/vm/expand.ts +721 -0
- package/src/vm/external.ts +265 -0
- package/src/vm/fifo.ts +226 -0
- package/src/vm/interp.ts +1623 -0
- package/src/vm/io.ts +570 -0
- package/src/vm/pattern.ts +477 -0
- package/src/vm/proc.ts +147 -0
- package/src/vm/state.ts +267 -0
- 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
|
+
}
|