@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,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-VM emulation of commands that run other commands on data only known at
|
|
3
|
+
* runtime: `xargs` (arguments from stdin) and the actions of `find`
|
|
4
|
+
* (`-exec ... ;`, `-exec ... +`, `-execdir`, `-delete`).
|
|
5
|
+
*
|
|
6
|
+
* Instead of handing the whole job to the real binary (where every nested
|
|
7
|
+
* command is invisible), mash runs `find ... -print0` for the matches, or
|
|
8
|
+
* reads the xargs input itself, and then runs each resulting command through
|
|
9
|
+
* the VM. Every one is traced and checked against the policy with its
|
|
10
|
+
* concrete arguments. `-execdir` runs each command in a subshell whose cwd is
|
|
11
|
+
* the match's directory. Forms that cannot be emulated command by command
|
|
12
|
+
* (`-ok`, an action inside a compound find expression, xargs `-p` or an
|
|
13
|
+
* unknown option) are refused with status 126 rather than handed to the real
|
|
14
|
+
* binary, whose nested commands would run unchecked.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from 'node:fs'
|
|
18
|
+
import * as path from 'node:path'
|
|
19
|
+
import type { Interpreter } from './interp.ts'
|
|
20
|
+
import { Capture, FdTable, NullFile } from './io.ts'
|
|
21
|
+
import type { Proc } from './proc.ts'
|
|
22
|
+
import { runExternal } from './external.ts'
|
|
23
|
+
|
|
24
|
+
const BSD = process.platform === 'darwin' || process.platform.endsWith('bsd')
|
|
25
|
+
|
|
26
|
+
export interface EmulationRequest {
|
|
27
|
+
execId: number
|
|
28
|
+
/** The resolved path of the real binary (find, for listing matches). */
|
|
29
|
+
path: string
|
|
30
|
+
argv: string[]
|
|
31
|
+
env: Record<string, string>
|
|
32
|
+
proc: Proc
|
|
33
|
+
node: import('../parser/ast.ts').SimpleCommand
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Split xargs input into arguments (default mode: blanks/newlines, with '', "" and \\ quoting). */
|
|
37
|
+
function xargsTokens(text: string): string[] {
|
|
38
|
+
const out: string[] = []
|
|
39
|
+
let cur = ''
|
|
40
|
+
let has = false
|
|
41
|
+
for (let i = 0; i < text.length; i++) {
|
|
42
|
+
const c = text[i]
|
|
43
|
+
if (c === '\\' && i + 1 < text.length) {
|
|
44
|
+
cur += text[++i]
|
|
45
|
+
has = true
|
|
46
|
+
} else if (c === "'" || c === '"') {
|
|
47
|
+
const end = text.indexOf(c, i + 1)
|
|
48
|
+
if (end === -1) throw new Error(`unterminated quote in xargs input`)
|
|
49
|
+
cur += text.slice(i + 1, end)
|
|
50
|
+
has = true
|
|
51
|
+
i = end
|
|
52
|
+
} else if (c === ' ' || c === '\t' || c === '\n') {
|
|
53
|
+
if (has) out.push(cur)
|
|
54
|
+
cur = ''
|
|
55
|
+
has = false
|
|
56
|
+
} else {
|
|
57
|
+
cur += c
|
|
58
|
+
has = true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (has) out.push(cur)
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Refuse a form the emulation cannot run command by command: the real binary
|
|
67
|
+
* would run its nested commands unchecked.
|
|
68
|
+
*/
|
|
69
|
+
function refuse(interp: Interpreter, req: EmulationRequest, what: string): number {
|
|
70
|
+
interp.errorMessage(req.proc, `${path.basename(req.argv[0])}: ${what} is not supported by mash (its commands could not be checked against the policy)`)
|
|
71
|
+
return 126
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface XargsOptions {
|
|
75
|
+
nul: boolean; delim: string | null; replace: string | null; replaceOne: string | null; maxArgs: number; maxLines: number
|
|
76
|
+
noRunIfEmpty: boolean; trace: boolean; eof: string | null; argFile: string | null; command: string[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Parse xargs options, or name the one that cannot be emulated. */
|
|
80
|
+
export function planXargs(argv: string[]): XargsOptions | { refuse: string } {
|
|
81
|
+
// split bundled short options: `-0r` → `-0 -r`, `-0n1` → `-0 -n1`
|
|
82
|
+
const args: string[] = []
|
|
83
|
+
for (let k = 0; k < argv.length; k++) {
|
|
84
|
+
const a = argv[k]
|
|
85
|
+
const m = /^-([0rtx]+)([0rtxIndLPEsSJ].*)?$/.exec(a)
|
|
86
|
+
if (a === '--' || !a.startsWith('-')) {
|
|
87
|
+
args.push(...argv.slice(k))
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
if (m && (m[1].length > 1 || m[2])) {
|
|
91
|
+
for (const c of m[1]) args.push('-' + c)
|
|
92
|
+
if (m[2]) args.push('-' + m[2])
|
|
93
|
+
} else args.push(a)
|
|
94
|
+
}
|
|
95
|
+
let nul = false
|
|
96
|
+
let argFile: string | null = null
|
|
97
|
+
let replaceOne: string | null = null
|
|
98
|
+
let delim: string | null = null
|
|
99
|
+
let replace: string | null = null
|
|
100
|
+
let maxArgs = Infinity
|
|
101
|
+
let maxLines = 0
|
|
102
|
+
let noRunIfEmpty = BSD
|
|
103
|
+
let trace = false
|
|
104
|
+
let eof: string | null = null
|
|
105
|
+
let i = 0
|
|
106
|
+
for (; i < args.length; i++) {
|
|
107
|
+
const a = args[i]
|
|
108
|
+
if (!a.startsWith('-') || a === '-') break
|
|
109
|
+
if (a === '--') {
|
|
110
|
+
i++
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
const take = (): string | undefined => args[++i]
|
|
114
|
+
if (a === '-0' || a === '--null') nul = true
|
|
115
|
+
else if (a === '-r' || a === '--no-run-if-empty') noRunIfEmpty = true
|
|
116
|
+
else if (a === '-t' || a === '--verbose') trace = true
|
|
117
|
+
else if (a === '-d') delim = take() ?? null
|
|
118
|
+
else if (a.startsWith('--delimiter=')) delim = a.slice(12)
|
|
119
|
+
else if (a === '-I') replace = take() ?? null
|
|
120
|
+
else if (a === '-i' || a === '--replace') replace = '{}'
|
|
121
|
+
else if (a.startsWith('-I')) replace = a.slice(2)
|
|
122
|
+
else if (a.startsWith('--replace=')) replace = a.slice(10)
|
|
123
|
+
else if (a === '-n') maxArgs = Number(take())
|
|
124
|
+
else if (/^-n\d+$/.test(a)) maxArgs = Number(a.slice(2))
|
|
125
|
+
else if (a.startsWith('--max-args=')) maxArgs = Number(a.slice(11))
|
|
126
|
+
else if (a === '-L') maxLines = Number(take())
|
|
127
|
+
else if (/^-L\d+$/.test(a)) maxLines = Number(a.slice(2))
|
|
128
|
+
else if (a === '-P') take() // run sequentially
|
|
129
|
+
else if (a.startsWith('-P') || a.startsWith('--max-procs=')) continue
|
|
130
|
+
else if (a === '-E') eof = take() ?? null
|
|
131
|
+
else if (/^-e/.test(a)) eof = a.length > 2 ? a.slice(2) : null
|
|
132
|
+
else if (a.startsWith('--eof')) eof = a.startsWith('--eof=') ? a.slice(6) : null
|
|
133
|
+
else if (a === '-l' || a === '--max-lines') maxLines = 1
|
|
134
|
+
else if (/^-l\d+$/.test(a)) maxLines = Number(a.slice(2))
|
|
135
|
+
else if (a.startsWith('--max-lines=')) maxLines = Number(a.slice(12))
|
|
136
|
+
else if (a === '-a') argFile = take() ?? null
|
|
137
|
+
else if (a.startsWith('--arg-file=')) argFile = a.slice(11)
|
|
138
|
+
else if (a === '-J') replaceOne = take() ?? null
|
|
139
|
+
else if (a === '-s' || a === '-S' || a === '-R') take() // size and replacement limits: no effect on what runs
|
|
140
|
+
else if (/^-[sSR]\d+$/.test(a) || a.startsWith('--max-chars=') || a.startsWith('--process-slot-var=')) continue
|
|
141
|
+
else if (a === '-x' || a === '--exit' || a === '-o' || a === '--open-tty') continue
|
|
142
|
+
else return { refuse: `option ${a}` } // -p (prompts), unknown options
|
|
143
|
+
}
|
|
144
|
+
if (Number.isNaN(maxArgs) || Number.isNaN(maxLines)) return { refuse: 'a non-numeric count' }
|
|
145
|
+
const command = args.slice(i).length > 0 ? args.slice(i) : ['echo']
|
|
146
|
+
return { nul, delim, replace, replaceOne, maxArgs, maxLines, noRunIfEmpty, trace, eof, argFile, command }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Emulate xargs. Options it cannot emulate are refused rather than handed to the real xargs. */
|
|
150
|
+
export async function emulateXargs(interp: Interpreter, req: EmulationRequest): Promise<number | null> {
|
|
151
|
+
const plan = planXargs(req.argv.slice(1))
|
|
152
|
+
if ('refuse' in plan) return refuse(interp, req, plan.refuse)
|
|
153
|
+
const { nul, delim, replace, replaceOne, maxArgs, maxLines, noRunIfEmpty, trace, eof, argFile, command } = plan
|
|
154
|
+
let text: string
|
|
155
|
+
if (argFile !== null) {
|
|
156
|
+
try {
|
|
157
|
+
text = fs.readFileSync(path.resolve(req.proc.state.cwd, argFile), 'utf8')
|
|
158
|
+
} catch (e) {
|
|
159
|
+
interp.errorMessage(req.proc, `xargs: ${argFile}: ${(e as NodeJS.ErrnoException).code === 'ENOENT' ? 'No such file or directory' : (e as Error).message}`)
|
|
160
|
+
return 1
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
const stdin = req.proc.fds.get(0)
|
|
164
|
+
text = stdin?.readable ? new TextDecoder().decode(await stdin.readAll()) : ''
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// build the list of invocations
|
|
168
|
+
const invocations: string[][] = []
|
|
169
|
+
if (replace !== null) {
|
|
170
|
+
const lines = text.split(nul ? '\0' : delim ?? '\n').map((l) => (nul || delim ? l : l.replace(/^[ \t]+/, ''))).filter((l) => l !== '')
|
|
171
|
+
for (const line of lines) {
|
|
172
|
+
if (eof !== null && line === eof) break
|
|
173
|
+
invocations.push(command.map((c) => c.split(replace!).join(line)))
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
let items: string[]
|
|
177
|
+
try {
|
|
178
|
+
items = nul ? text.split('\0').filter((x) => x !== '') : delim !== null ? text.split(delim).filter((x, k, all) => !(k === all.length - 1 && x === '')) : xargsTokens(text)
|
|
179
|
+
} catch (e) {
|
|
180
|
+
interp.errorMessage(req.proc, `xargs: ${(e as Error).message}`)
|
|
181
|
+
return 1
|
|
182
|
+
}
|
|
183
|
+
if (eof !== null) {
|
|
184
|
+
const k = items.indexOf(eof)
|
|
185
|
+
if (k >= 0) items = items.slice(0, k)
|
|
186
|
+
}
|
|
187
|
+
if (maxLines > 0 && !nul && delim === null) {
|
|
188
|
+
const lines = text.split('\n').filter((l) => l.trim() !== '')
|
|
189
|
+
for (let k = 0; k < lines.length; k += maxLines) invocations.push([...command, ...xargsTokens(lines.slice(k, k + maxLines).join('\n'))])
|
|
190
|
+
} else {
|
|
191
|
+
const batch = maxArgs === Infinity ? 5000 : maxArgs
|
|
192
|
+
// BSD -J: the items replace the first argument equal to the marker
|
|
193
|
+
const at = replaceOne === null ? -1 : command.indexOf(replaceOne)
|
|
194
|
+
const place = (chunk: string[]): string[] => (at === -1 ? [...command, ...chunk] : [...command.slice(0, at), ...chunk, ...command.slice(at + 1)])
|
|
195
|
+
for (let k = 0; k < items.length; k += batch) invocations.push(place(items.slice(k, k + batch)))
|
|
196
|
+
}
|
|
197
|
+
if (items.length === 0 && !noRunIfEmpty) invocations.push(replaceOne === null ? [...command] : command.filter((c) => c !== replaceOne))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// run each invocation through the VM with stdin from /dev/null
|
|
201
|
+
let status = 0
|
|
202
|
+
const fds = req.proc.fds.clone()
|
|
203
|
+
fds.set(0, new NullFile(interp.rt.channel('null', '/dev/null'), interp.rt.listeners))
|
|
204
|
+
const child = req.proc.derive({ fds })
|
|
205
|
+
try {
|
|
206
|
+
for (const argv of invocations) {
|
|
207
|
+
if (trace) await interp.write(child, 2, argv.join(' ') + '\n').catch(() => {})
|
|
208
|
+
const s = await interp.runArgv(argv, child, req.node, { skipFunctions: true })
|
|
209
|
+
if (s === 255) return 124
|
|
210
|
+
if (s === 127 || s === 126) return s
|
|
211
|
+
if (s > 128) return 125
|
|
212
|
+
if (s !== 0) status = BSD ? 1 : 123
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
fds.closeAll()
|
|
216
|
+
}
|
|
217
|
+
return status
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const FIND_ACTIONS = new Set(['-exec', '-execdir', '-ok', '-okdir', '-delete'])
|
|
221
|
+
/** Primaries with output or side effects: with one of these besides the action, listing matches would not be equivalent. */
|
|
222
|
+
const FIND_OUTPUTS = new Set(['-print', '-print0', '-printf', '-fprint', '-fprint0', '-fprintf', '-ls', '-fls'])
|
|
223
|
+
|
|
224
|
+
export type FindPlan =
|
|
225
|
+
| { kind: 'real' }
|
|
226
|
+
| { kind: 'refuse'; what: string }
|
|
227
|
+
| { kind: 'emulate'; action: string; cmd: string[]; batch: boolean; before: string[]; printEach: boolean }
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* How `find ARGS` is run. The action is emulated when it is the last element
|
|
231
|
+
* of the expression and the only action: then `find TESTS -print0`, with the
|
|
232
|
+
* action replaced in place, lists exactly the files it would act on, however
|
|
233
|
+
* complex the tests before it are (parentheses, `-o`, `-prune`, `-not`).
|
|
234
|
+
* An action inside parentheses or followed by more expression, several
|
|
235
|
+
* actions, and the prompting `-ok`/`-okdir` are refused.
|
|
236
|
+
*/
|
|
237
|
+
export function planFind(args: string[]): FindPlan {
|
|
238
|
+
const actionAt = args.findIndex((a) => FIND_ACTIONS.has(a))
|
|
239
|
+
if (actionAt === -1) return { kind: 'real' }
|
|
240
|
+
const action = args[actionAt]
|
|
241
|
+
// -ok/-okdir prompt for each match: not emulated, and the real find would run them unchecked
|
|
242
|
+
if (action === '-ok' || action === '-okdir') return { kind: 'refuse', what: action }
|
|
243
|
+
let cmd: string[] = []
|
|
244
|
+
let batch = false
|
|
245
|
+
let end = actionAt + 1
|
|
246
|
+
if (action === '-exec' || action === '-execdir') {
|
|
247
|
+
const term = args.findIndex((a, k) => k > actionAt && (a === ';' || a === '+'))
|
|
248
|
+
// no terminator, or `{}` not last before `+`: the real find reports the usage error without running anything
|
|
249
|
+
if (term === -1) return { kind: 'real' }
|
|
250
|
+
cmd = args.slice(actionAt + 1, term)
|
|
251
|
+
batch = args[term] === '+'
|
|
252
|
+
if (batch && cmd[cmd.length - 1] !== '{}') return { kind: 'real' }
|
|
253
|
+
end = term + 1
|
|
254
|
+
}
|
|
255
|
+
let before = args.slice(0, actionAt)
|
|
256
|
+
// `find … -print -delete`: print each match before acting on it
|
|
257
|
+
const printEach = before[before.length - 1] === '-print'
|
|
258
|
+
if (printEach) before = before.slice(0, -1)
|
|
259
|
+
if (end !== args.length) return { kind: 'refuse', what: `${action} followed by more expression (or inside parentheses)` }
|
|
260
|
+
if (before.some((a) => FIND_OUTPUTS.has(a))) return { kind: 'refuse', what: `${action} together with other output actions` }
|
|
261
|
+
return { kind: 'emulate', action, cmd, batch, before, printEach }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Emulate `find`'s `-exec`/`-execdir`/`-delete` actions (see {@link planFind}).
|
|
266
|
+
* Returns null to run the real find unchanged when there is no action.
|
|
267
|
+
*/
|
|
268
|
+
export async function emulateFind(interp: Interpreter, req: EmulationRequest): Promise<number | null> {
|
|
269
|
+
const plan = planFind(req.argv.slice(1))
|
|
270
|
+
if (plan.kind === 'real') return null
|
|
271
|
+
if (plan.kind === 'refuse') return refuse(interp, req, plan.what)
|
|
272
|
+
const { action, cmd, batch, before, printEach } = plan
|
|
273
|
+
|
|
274
|
+
// 1. list matches with the real find (a read-only invocation)
|
|
275
|
+
const listArgv = [req.argv[0], ...before, ...(action === '-delete' ? ['-depth'] : []), '-print0']
|
|
276
|
+
const capture = new Capture(interp.rt.channel('capture', `find matches (${listArgv.join(' ')})`, { nodeId: req.node.id }), interp.rt.listeners)
|
|
277
|
+
const fds = req.proc.fds.clone()
|
|
278
|
+
fds.set(1, capture.retain())
|
|
279
|
+
const lister = req.proc.derive({ fds })
|
|
280
|
+
let findStatus: number
|
|
281
|
+
try {
|
|
282
|
+
findStatus = await runExternal(interp, { execId: req.execId, path: req.path, argv: listArgv, env: req.env, cwd: req.proc.state.cwd, proc: lister, readsStdin: false })
|
|
283
|
+
} finally {
|
|
284
|
+
fds.closeAll()
|
|
285
|
+
}
|
|
286
|
+
const matches = capture.text().split('\0').filter((m) => m !== '')
|
|
287
|
+
capture.release()
|
|
288
|
+
|
|
289
|
+
// 2. run the action per match (or batched for `+`) through the VM
|
|
290
|
+
let status = findStatus === 0 ? 0 : 1
|
|
291
|
+
const run = async (argv: string[]): Promise<number> => interp.runArgv(argv, req.proc, req.node, { skipFunctions: true })
|
|
292
|
+
if (printEach) await interp.write(req.proc, 1, matches.map((m) => m + '\n').join('')).catch(() => {})
|
|
293
|
+
if (action === '-delete') {
|
|
294
|
+
for (const m of matches) {
|
|
295
|
+
if (m === '.' || m === path.resolve(req.proc.state.cwd)) continue
|
|
296
|
+
const s = await run(['rm', '-df', '--', m])
|
|
297
|
+
if (s !== 0) status = 1
|
|
298
|
+
}
|
|
299
|
+
return status
|
|
300
|
+
}
|
|
301
|
+
if (action === '-execdir') {
|
|
302
|
+
// each command runs in the directory of its match, with {} as ./NAME
|
|
303
|
+
const groups = new Map<string, string[]>()
|
|
304
|
+
for (const m of matches) {
|
|
305
|
+
const dir = path.resolve(req.proc.state.cwd, path.dirname(m))
|
|
306
|
+
const name = './' + path.basename(m)
|
|
307
|
+
if (batch) groups.set(dir, [...(groups.get(dir) ?? []), name])
|
|
308
|
+
else groups.set(`${groups.size}\0${dir}`, [name])
|
|
309
|
+
}
|
|
310
|
+
for (const [key, names] of groups) {
|
|
311
|
+
const dir = key.includes('\0') ? key.slice(key.indexOf('\0') + 1) : key
|
|
312
|
+
const sub = req.proc.subshell(interp.rt.nextPid())
|
|
313
|
+
sub.state.cwd = dir
|
|
314
|
+
const pwd = sub.state.ensure('PWD')
|
|
315
|
+
if (!pwd.readonly) pwd.value = { kind: 'string', value: dir }
|
|
316
|
+
try {
|
|
317
|
+
const argv = batch ? [...cmd.slice(0, -1), ...names] : cmd.map((c) => c.split('{}').join(names[0]))
|
|
318
|
+
const s = await interp.runArgv(argv, sub, req.node, { skipFunctions: true })
|
|
319
|
+
if (s !== 0 && batch) status = 1
|
|
320
|
+
} finally {
|
|
321
|
+
sub.fds.closeAll()
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return status
|
|
325
|
+
}
|
|
326
|
+
if (batch) {
|
|
327
|
+
for (let k = 0; k < matches.length; k += 5000) {
|
|
328
|
+
const argv = [...cmd.slice(0, -1), ...matches.slice(k, k + 5000)]
|
|
329
|
+
if ((await run(argv)) !== 0) status = 1
|
|
330
|
+
}
|
|
331
|
+
return status
|
|
332
|
+
}
|
|
333
|
+
for (const m of matches) await run(cmd.map((c) => c.split('{}').join(m)))
|
|
334
|
+
return status
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export { FdTable }
|