@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,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative behavior definitions: a table form for the common case
|
|
3
|
+
* "parse the arguments; operands and some option values are files that are
|
|
4
|
+
* read, written or deleted", compiled to the same {@link Behavior} interface
|
|
5
|
+
* as hand-written definitions.
|
|
6
|
+
*
|
|
7
|
+
* define({ names: ['sort'], summary: 'sort lines',
|
|
8
|
+
* spec: { short: 'nru', shortWithArg: 'okt', long: { output: '=o' } },
|
|
9
|
+
* operands: 'read*', flags: { o: 'write' } })
|
|
10
|
+
*
|
|
11
|
+
* Operand patterns are space-separated tokens matched against the operands in
|
|
12
|
+
* order. A token is `kind[:dir][~][*|?]`:
|
|
13
|
+
*
|
|
14
|
+
* read write append delete a path effect (resolved against the cwd);
|
|
15
|
+
* `-` as a read operand means stdin
|
|
16
|
+
* url a network effect with the operand as target
|
|
17
|
+
* pattern code subcommand a display hint (h.role), no effect
|
|
18
|
+
* arg any other operand (a mode, a size, a key), no effect
|
|
19
|
+
* cmd the rest of the operands is a command to analyze
|
|
20
|
+
* (wrappers; needs `spec.posix`)
|
|
21
|
+
* :dir display hint: the operand is a directory
|
|
22
|
+
* ~ the effect is only possible (`maybe`)
|
|
23
|
+
* * any number (at most one starred token per pattern)
|
|
24
|
+
* ? optional (documentation only: missing operands just have no effect)
|
|
25
|
+
*
|
|
26
|
+
* Operands beyond a pattern without `*` make the result incomplete. Unknown
|
|
27
|
+
* flags make it incomplete too (as with hand-written definitions).
|
|
28
|
+
*
|
|
29
|
+
* Anything the table cannot say goes into `when` (overrides applied when a
|
|
30
|
+
* flag is present), `subcommands` (a table per subcommand), or `extra` (a
|
|
31
|
+
* function post-processing the result). See docs/behaviors.md.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { AValue } from '../analysis/value.ts'
|
|
35
|
+
import { concrete, known } from '../analysis/value.ts'
|
|
36
|
+
import type { Effect } from '../policy/effects.ts'
|
|
37
|
+
import type { ArgSpec, Behavior, BehaviorHelpers, BehaviorResult, Invocation, OutputInfo, ParsedArgs } from './types.ts'
|
|
38
|
+
|
|
39
|
+
/** A fixed effect a command always has: paths may start with `~/` (the home directory); relative ones resolve against the cwd. */
|
|
40
|
+
export type StaticEffect =
|
|
41
|
+
| { kind: 'read' | 'write' | 'delete'; path: string; recursive?: boolean; maybe?: boolean }
|
|
42
|
+
| { kind: 'network'; detail: string; maybe?: boolean }
|
|
43
|
+
| { kind: 'process' | 'unknown'; detail: string }
|
|
44
|
+
|
|
45
|
+
export interface Def {
|
|
46
|
+
summary?: string
|
|
47
|
+
/** Option syntax (see {@link ArgSpec}). */
|
|
48
|
+
spec?: ArgSpec
|
|
49
|
+
/** Operand pattern, e.g. `'read*'`, `'arg write*'`, `'read* write'`. Default: no operands expected. */
|
|
50
|
+
operands?: string
|
|
51
|
+
/** Option values by canonical flag name: `{ o: 'write', f: 'read', e: 'pattern' }` (same kinds as operand tokens). */
|
|
52
|
+
flags?: Record<string, string>
|
|
53
|
+
/** Flags not in `spec` are accepted (they do not make the result incomplete): for query-only subcommands. */
|
|
54
|
+
anyFlags?: boolean
|
|
55
|
+
/** Path effects are recursive: always, or when one of these flags is present. */
|
|
56
|
+
recursive?: boolean | string[]
|
|
57
|
+
/** Reads are of names and metadata only (`ls`, `tree`, `du`): marked `names: true`, never content reads. */
|
|
58
|
+
namesOnly?: boolean
|
|
59
|
+
/** Without path operands, the first path token applies to the cwd (`ls`, `rg`). */
|
|
60
|
+
cwdIfNone?: boolean
|
|
61
|
+
/** Does it read stdin? `'operands'`: when no read operand is given or one is `-`. Default: `'operands'` if the pattern has a read token, else `'maybe'`. */
|
|
62
|
+
stdin?: boolean | 'maybe' | 'operands'
|
|
63
|
+
stdout?: OutputInfo
|
|
64
|
+
/** Effects the command always has, in addition to the operand ones. */
|
|
65
|
+
effects?: StaticEffect[]
|
|
66
|
+
/** The command runs code or does things the table cannot describe: an `unknown` effect with this detail, and `complete: false`. */
|
|
67
|
+
opaque?: string
|
|
68
|
+
/** Overrides applied when a flag is present; keys are canonical flag names, `|`-separated alternatives (`'a|append'`). Later entries win. */
|
|
69
|
+
when?: Record<string, Def>
|
|
70
|
+
/**
|
|
71
|
+
* The first operand is a subcommand looked up here (`'list|ls'` keys for
|
|
72
|
+
* aliases, `'*'` for any other, `''` for none). `spec` then describes the
|
|
73
|
+
* global options before it (parsed POSIX-style); the subcommand's own
|
|
74
|
+
* `spec` its options. A subcommand not in the table is incomplete.
|
|
75
|
+
*/
|
|
76
|
+
subcommands?: Record<string, Def>
|
|
77
|
+
/** Post-process the computed result (the escape hatch for what the table cannot express). */
|
|
78
|
+
extra?: (r: BehaviorResult, c: { inv: Invocation; h: BehaviorHelpers; p: ParsedArgs; operands: AValue[] }) => BehaviorResult
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface Token { kind: string; dir: boolean; maybe: boolean; star: boolean }
|
|
82
|
+
|
|
83
|
+
const KINDS = new Set(['read', 'write', 'append', 'delete', 'url', 'pattern', 'code', 'subcommand', 'arg', 'cmd'])
|
|
84
|
+
const PATH_KINDS = new Set(['read', 'write', 'append', 'delete'])
|
|
85
|
+
|
|
86
|
+
const tokenCache = new Map<string, Token[]>()
|
|
87
|
+
function tokens(pattern: string): Token[] {
|
|
88
|
+
let t = tokenCache.get(pattern)
|
|
89
|
+
if (t) return t
|
|
90
|
+
t = pattern.split(/\s+/).filter(Boolean).map((s): Token => {
|
|
91
|
+
const m = /^([a-z]+)(:dir)?(~)?([*?])?$/.exec(s)
|
|
92
|
+
if (!m || !KINDS.has(m[1])) throw new Error(`bad operand token '${s}'`)
|
|
93
|
+
return { kind: m[1], dir: !!m[2], maybe: !!m[3], star: m[4] === '*' }
|
|
94
|
+
})
|
|
95
|
+
if (t.filter((x) => x.star).length > 1) throw new Error(`more than one starred token in '${pattern}'`)
|
|
96
|
+
tokenCache.set(pattern, t)
|
|
97
|
+
return t
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Assign operands to tokens: fixed tokens before the star from the left, after it from the right. */
|
|
101
|
+
function match(toks: Token[], ops: AValue[]): { pairs: [Token, AValue][]; extra: AValue[] } {
|
|
102
|
+
const star = toks.findIndex((t) => t.star)
|
|
103
|
+
const pairs: [Token, AValue][] = []
|
|
104
|
+
const cmd = toks.findIndex((t) => t.kind === 'cmd')
|
|
105
|
+
if (star === -1) {
|
|
106
|
+
const n = cmd === -1 ? Math.min(toks.length, ops.length) : Math.min(cmd, ops.length)
|
|
107
|
+
for (let i = 0; i < n; i++) pairs.push([toks[i], ops[i]])
|
|
108
|
+
if (cmd !== -1 && ops.length > cmd) return { pairs: [...pairs, [toks[cmd], ops[cmd]]], extra: [] }
|
|
109
|
+
return { pairs, extra: ops.slice(n) }
|
|
110
|
+
}
|
|
111
|
+
const before = toks.slice(0, star)
|
|
112
|
+
const after = toks.slice(star + 1)
|
|
113
|
+
const nb = Math.min(before.length, ops.length)
|
|
114
|
+
for (let i = 0; i < nb; i++) pairs.push([before[i], ops[i]])
|
|
115
|
+
const rest = ops.slice(nb)
|
|
116
|
+
const na = Math.min(after.length, rest.length)
|
|
117
|
+
const mid = rest.slice(0, rest.length - na)
|
|
118
|
+
for (const o of mid) pairs.push([toks[star], o])
|
|
119
|
+
for (let i = 0; i < na; i++) pairs.push([after[after.length - na + i], rest[mid.length + i]])
|
|
120
|
+
return { pairs, extra: [] }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function has(p: ParsedArgs, key: string): boolean {
|
|
124
|
+
return key.split('|').some((k) => p.flags.has(k))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The definition after applying `when` overrides for the flags present. */
|
|
128
|
+
function effective(def: Def, p: ParsedArgs): Def {
|
|
129
|
+
if (!def.when) return def
|
|
130
|
+
let d: Def = { ...def }
|
|
131
|
+
for (const [k, o] of Object.entries(def.when)) if (has(p, k)) d = { ...d, ...o }
|
|
132
|
+
return d
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function staticEffect(e: StaticEffect, h: BehaviorHelpers): Effect {
|
|
136
|
+
if ('path' in e) {
|
|
137
|
+
const home = h.home()
|
|
138
|
+
const path = e.path === '~' || e.path.startsWith('~/')
|
|
139
|
+
? { alts: (home ?? known('~')).alts.map((a) => [...a, e.path.slice(1)].filter((s) => s !== '')) }
|
|
140
|
+
: h.resolve(known(e.path))
|
|
141
|
+
return { kind: e.kind, path, ...(e.recursive ? { recursive: true } : {}), ...(e.maybe ? { maybe: true } : {}) }
|
|
142
|
+
}
|
|
143
|
+
if (e.kind === 'network') return { kind: 'network', detail: e.detail, ...(e.maybe ? { maybe: true } : {}) }
|
|
144
|
+
return { kind: e.kind, detail: e.detail }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const isDash = (v: AValue): boolean => concrete(v) === '-'
|
|
148
|
+
|
|
149
|
+
/** Analyze `argv` (operands and options, argv[0] excluded) with a definition. */
|
|
150
|
+
export function analyzeDef(def: Def, inv: Invocation, argv: AValue[], h: BehaviorHelpers): BehaviorResult {
|
|
151
|
+
if (def.subcommands) return analyzeSubcommand(def, inv, argv, h)
|
|
152
|
+
const p = h.parseArgs(argv, def.spec ?? {})
|
|
153
|
+
const d = effective(def, p)
|
|
154
|
+
const toks = tokens(d.operands ?? '')
|
|
155
|
+
const recursive = d.recursive === true || (Array.isArray(d.recursive) && d.recursive.some((k) => has(p, k)))
|
|
156
|
+
const effects: Effect[] = []
|
|
157
|
+
let complete = p.unknownFlags.length === 0 || d.anyFlags === true
|
|
158
|
+
let readsStdin: boolean | 'maybe' | undefined
|
|
159
|
+
let stdout = d.stdout
|
|
160
|
+
let sawRead = false
|
|
161
|
+
let sawDash = false
|
|
162
|
+
let sawPath = false
|
|
163
|
+
|
|
164
|
+
const apply = (t: Token, v: AValue, option: boolean): void => {
|
|
165
|
+
if (t.dir) h.role(v, 'dir')
|
|
166
|
+
switch (t.kind) {
|
|
167
|
+
case 'read':
|
|
168
|
+
case 'write':
|
|
169
|
+
case 'append':
|
|
170
|
+
case 'delete': {
|
|
171
|
+
if (isDash(v) && (t.kind === 'read' || option)) {
|
|
172
|
+
if (t.kind === 'read') sawDash = true
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (t.kind === 'read' && !option) sawRead = true
|
|
176
|
+
if (!option) sawPath = true
|
|
177
|
+
const kind = t.kind === 'append' ? 'write' : t.kind
|
|
178
|
+
effects.push({ kind, path: h.resolve(v), ...(recursive ? { recursive: true } : {}), ...(t.kind === 'append' ? { append: true } : {}), ...(t.maybe ? { maybe: true } : {}) } as Effect)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
case 'url':
|
|
182
|
+
effects.push({ kind: 'network', target: v, detail: inv.name })
|
|
183
|
+
return
|
|
184
|
+
case 'pattern':
|
|
185
|
+
case 'code':
|
|
186
|
+
case 'subcommand':
|
|
187
|
+
h.role(v, t.kind)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const { pairs, extra } = match(toks, p.operands)
|
|
193
|
+
if (extra.length > 0) complete = false
|
|
194
|
+
for (const [t, v] of pairs) {
|
|
195
|
+
if (t.kind === 'cmd') {
|
|
196
|
+
const at = p.operands.indexOf(v)
|
|
197
|
+
const sub = h.subcommand(p.operands.slice(at))
|
|
198
|
+
effects.push(...sub.effects)
|
|
199
|
+
if (sub.complete === false) complete = false
|
|
200
|
+
readsStdin = sub.readsStdin
|
|
201
|
+
stdout ??= sub.stdout
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
apply(t, v, false)
|
|
205
|
+
}
|
|
206
|
+
if (d.cwdIfNone && !sawPath) {
|
|
207
|
+
const first = toks.find((t) => PATH_KINDS.has(t.kind))
|
|
208
|
+
if (first) {
|
|
209
|
+
const kind = first.kind === 'append' ? 'write' : first.kind
|
|
210
|
+
effects.push({ kind, path: inv.cwd, ...(recursive ? { recursive: true } : {}), ...(first.maybe ? { maybe: true } : {}) } as Effect)
|
|
211
|
+
if (kind === 'read') sawRead = true
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (const [k, pattern] of Object.entries(d.flags ?? {})) {
|
|
215
|
+
const t = tokens(pattern)[0]
|
|
216
|
+
for (const v of p.multi.get(k) ?? []) apply(t, v, true)
|
|
217
|
+
}
|
|
218
|
+
for (const e of d.effects ?? []) effects.push(staticEffect(e, h))
|
|
219
|
+
if (d.opaque) {
|
|
220
|
+
effects.push({ kind: 'unknown', detail: d.opaque })
|
|
221
|
+
complete = false
|
|
222
|
+
}
|
|
223
|
+
if (readsStdin === undefined) {
|
|
224
|
+
const stdin = d.stdin ?? (toks.some((t) => t.kind === 'read') ? 'operands' : 'maybe')
|
|
225
|
+
readsStdin = stdin === 'operands' ? !sawRead || sawDash : stdin
|
|
226
|
+
}
|
|
227
|
+
const r: BehaviorResult = { effects, readsStdin, complete, ...(stdout ? { stdout } : {}) }
|
|
228
|
+
return d.extra ? d.extra(r, { inv, h, p, operands: p.operands }) : r
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function analyzeSubcommand(def: Def, inv: Invocation, argv: AValue[], h: BehaviorHelpers): BehaviorResult {
|
|
232
|
+
const p = h.parseArgs(argv, { ...def.spec, posix: true })
|
|
233
|
+
const d = effective(def, p)
|
|
234
|
+
// the global options' own effects (option values, fixed effects)
|
|
235
|
+
const base: Effect[] = []
|
|
236
|
+
for (const [k, pattern] of Object.entries(d.flags ?? {})) {
|
|
237
|
+
const t = tokens(pattern)[0]
|
|
238
|
+
for (const v of p.multi.get(k) ?? []) {
|
|
239
|
+
if (t.dir) h.role(v, 'dir')
|
|
240
|
+
if (PATH_KINDS.has(t.kind) && !isDash(v)) base.push({ kind: t.kind === 'append' ? 'write' : t.kind, path: h.resolve(v) } as Effect)
|
|
241
|
+
else if (t.kind === 'pattern' || t.kind === 'code') h.role(v, t.kind)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const e of d.effects ?? []) base.push(staticEffect(e, h))
|
|
245
|
+
if (d.opaque) base.push({ kind: 'unknown', detail: d.opaque })
|
|
246
|
+
const globalComplete = (p.unknownFlags.length === 0 || d.anyFlags === true) && !d.opaque
|
|
247
|
+
const [subv, ...rest] = p.operands
|
|
248
|
+
const table = d.subcommands!
|
|
249
|
+
const lookup = (name: string): Def | undefined => {
|
|
250
|
+
for (const [k, v] of Object.entries(table)) if (k !== '*' && k.split('|').includes(name)) return v
|
|
251
|
+
return table['*']
|
|
252
|
+
}
|
|
253
|
+
if (subv === undefined) {
|
|
254
|
+
const none = table['']
|
|
255
|
+
if (!none) return { effects: base, readsStdin: false, complete: globalComplete }
|
|
256
|
+
const r = analyzeDef(none, inv, [], h)
|
|
257
|
+
return { ...r, effects: [...base, ...r.effects], complete: globalComplete && r.complete !== false }
|
|
258
|
+
}
|
|
259
|
+
const name = concrete(subv)
|
|
260
|
+
if (name === undefined) {
|
|
261
|
+
return { effects: [...base, { kind: 'unknown', detail: `${inv.name} subcommand is not known statically` }], readsStdin: 'maybe', complete: false }
|
|
262
|
+
}
|
|
263
|
+
h.role(subv, 'subcommand')
|
|
264
|
+
const sub = lookup(name)
|
|
265
|
+
if (!sub) return { effects: [...base, { kind: 'unknown', detail: `${inv.name} ${name}` }], readsStdin: 'maybe', complete: false }
|
|
266
|
+
// global options are usually accepted after the subcommand too
|
|
267
|
+
const r = analyzeDef({ ...sub, spec: mergeSpec(def.spec, sub.spec) }, inv, rest, h)
|
|
268
|
+
return { ...r, effects: [...base, ...r.effects], complete: globalComplete && r.complete !== false }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function mergeSpec(outer: ArgSpec | undefined, inner: ArgSpec | undefined): ArgSpec {
|
|
272
|
+
if (!outer) return inner ?? {}
|
|
273
|
+
if (!inner) return { ...outer, posix: false }
|
|
274
|
+
return {
|
|
275
|
+
...inner,
|
|
276
|
+
short: (inner.short ?? '') + (outer.short ?? ''),
|
|
277
|
+
shortWithArg: (inner.shortWithArg ?? '') + [...(outer.shortWithArg ?? '')].filter((c) => !(inner.short ?? '').includes(c)).join(''),
|
|
278
|
+
long: { ...outer.long, ...inner.long },
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Compile a table definition to a {@link Behavior}. */
|
|
283
|
+
export function define(def: Def & { names: string[] }): Behavior {
|
|
284
|
+
return {
|
|
285
|
+
names: def.names,
|
|
286
|
+
summary: def.summary,
|
|
287
|
+
analyze: (inv, h) => {
|
|
288
|
+
const r = analyzeDef(def, inv, inv.argv.slice(1), h)
|
|
289
|
+
if (!def.namesOnly) return r
|
|
290
|
+
return { ...r, effects: r.effects.map((e) => (e.kind === 'read' ? { ...e, names: true } : e)) }
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BehaviorRegistry } from './registry.ts'
|
|
2
|
+
import { coreBehaviors } from './defs/core.ts'
|
|
3
|
+
import { devtoolBehaviors } from './defs/devtools.ts'
|
|
4
|
+
import { toolBehaviors } from './defs/tools.ts'
|
|
5
|
+
import { macosBehaviors } from './defs/macos.ts'
|
|
6
|
+
|
|
7
|
+
export * from './types.ts'
|
|
8
|
+
export { BehaviorRegistry, parseArgs, resolvePath, basename } from './registry.ts'
|
|
9
|
+
export { coreBehaviors } from './defs/core.ts'
|
|
10
|
+
export { devtoolBehaviors } from './defs/devtools.ts'
|
|
11
|
+
export { toolBehaviors } from './defs/tools.ts'
|
|
12
|
+
export { macosBehaviors } from './defs/macos.ts'
|
|
13
|
+
export { define, analyzeDef, type Def, type StaticEffect } from './dsl.ts'
|
|
14
|
+
|
|
15
|
+
/** A registry with every bundled definition. */
|
|
16
|
+
export function defaultBehaviors(): BehaviorRegistry {
|
|
17
|
+
return new BehaviorRegistry([...coreBehaviors, ...devtoolBehaviors, ...toolBehaviors, ...macosBehaviors])
|
|
18
|
+
}
|
|
19
|
+
export { argRoles, lexicalRole } from './roles.ts'
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The behavior registry: looks up a definition by command basename and
|
|
3
|
+
* provides the helpers definitions use (abstract path resolution, GNU-style
|
|
4
|
+
* argument parsing over abstract values, nested command analysis).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'node:fs'
|
|
8
|
+
import * as os from 'node:os'
|
|
9
|
+
import type { AValue } from '../analysis/value.ts'
|
|
10
|
+
import { concat, concrete, describe, isHole, known, knownPrefix, unknownString } from '../analysis/value.ts'
|
|
11
|
+
import type { ArgSpec, Behavior, BehaviorHelpers, BehaviorResult, Invocation, ParsedArgs, ScriptInfo, StdinKind } from './types.ts'
|
|
12
|
+
|
|
13
|
+
/** Analyze a shell script (provided by the static analyzer, to avoid a dependency cycle). */
|
|
14
|
+
export type ScriptAnalyzer = (source: AValue, positional: AValue[], inv: Invocation, info?: ScriptInfo) => BehaviorResult
|
|
15
|
+
|
|
16
|
+
const MAX_DEPTH = 8
|
|
17
|
+
|
|
18
|
+
/** Observes helper calls (used to map effects back to the arguments they came from). */
|
|
19
|
+
export interface HelperTracer {
|
|
20
|
+
resolved(input: AValue, output: AValue): void
|
|
21
|
+
role(value: AValue, role: import('./types.ts').ArgRole): void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class BehaviorRegistry {
|
|
25
|
+
private readonly byName = new Map<string, Behavior>()
|
|
26
|
+
scriptAnalyzer: ScriptAnalyzer | null = null
|
|
27
|
+
/** When set, helper calls are reported to it (see roles.ts). */
|
|
28
|
+
tracer: HelperTracer | null = null
|
|
29
|
+
|
|
30
|
+
constructor(behaviors: Behavior[] = []) {
|
|
31
|
+
for (const b of behaviors) this.add(b)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
add(b: Behavior): this {
|
|
35
|
+
for (const n of b.names) this.byName.set(n, b)
|
|
36
|
+
return this
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get(name: string): Behavior | undefined {
|
|
40
|
+
return this.byName.get(basename(name))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
names(): string[] {
|
|
44
|
+
return [...this.byName.keys()].sort()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Analyze one invocation. Commands without a definition yield a single
|
|
49
|
+
* `unknown` effect and `complete: false`.
|
|
50
|
+
*/
|
|
51
|
+
analyze(inv: Invocation, depth = 0): BehaviorResult {
|
|
52
|
+
const name = inv.name
|
|
53
|
+
const b = this.byName.get(name)
|
|
54
|
+
if (!b) {
|
|
55
|
+
const script = this.shellScriptOf(inv)
|
|
56
|
+
if (script !== undefined && this.scriptAnalyzer && depth <= MAX_DEPTH) {
|
|
57
|
+
const r = this.scriptAnalyzer(known(script.text), inv.argv.slice(1), inv, { file: script.path })
|
|
58
|
+
return { ...r, effects: [{ kind: 'read', path: known(script.path), source: name }, ...r.effects] }
|
|
59
|
+
}
|
|
60
|
+
return { effects: [{ kind: 'unknown', detail: `no behavior definition for '${name}'`, source: name }], complete: false, readsStdin: 'maybe' }
|
|
61
|
+
}
|
|
62
|
+
if (depth > MAX_DEPTH) return { effects: [{ kind: 'unknown', detail: 'nesting too deep', source: name }], complete: false }
|
|
63
|
+
const h = this.helpers(inv, depth)
|
|
64
|
+
try {
|
|
65
|
+
const r = b.analyze(inv, h)
|
|
66
|
+
for (const e of r.effects) e.source ??= name
|
|
67
|
+
return { complete: true, readsStdin: 'maybe', ...r }
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return { effects: [{ kind: 'unknown', detail: `behavior '${name}' failed: ${(e as Error).message}`, source: name }], complete: false }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** An executable file with a bash/sh shebang named by argv[0]. */
|
|
74
|
+
private shellScriptOf(inv: Invocation): { path: string; text: string } | undefined {
|
|
75
|
+
const a0 = concrete(inv.argv[0])
|
|
76
|
+
if (a0 === undefined || !a0.includes('/')) return undefined
|
|
77
|
+
const p = concrete(resolvePath(known(a0), inv.cwd))
|
|
78
|
+
if (p === undefined) return undefined
|
|
79
|
+
try {
|
|
80
|
+
const st = fs.statSync(p)
|
|
81
|
+
if (!st.isFile() || st.size > 1024 * 1024) return undefined
|
|
82
|
+
const text = fs.readFileSync(p, 'utf8')
|
|
83
|
+
const m = /^#![ \t]*(\S+)(?:[ \t]+(\S+))?/.exec(text.split('\n')[0])
|
|
84
|
+
if (!m) return undefined
|
|
85
|
+
const interp = basename(m[1]) === 'env' ? m[2] : basename(m[1])
|
|
86
|
+
return interp === 'bash' || interp === 'sh' ? { path: p, text } : undefined
|
|
87
|
+
} catch {
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
helpers(inv: Invocation, depth: number): BehaviorHelpers {
|
|
93
|
+
const tracer = this.tracer
|
|
94
|
+
const resolve = (p: AValue): AValue => {
|
|
95
|
+
const r = resolvePath(p, inv.cwd)
|
|
96
|
+
tracer?.resolved(p, r)
|
|
97
|
+
return r
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
resolve,
|
|
101
|
+
role: (value, role) => tracer?.role(value, role),
|
|
102
|
+
parseArgs,
|
|
103
|
+
subcommand: (argv, opts = {}) => {
|
|
104
|
+
if (argv.length === 0) return { effects: [] }
|
|
105
|
+
const n = concrete(argv[0])
|
|
106
|
+
if (n === undefined) {
|
|
107
|
+
return { effects: [{ kind: 'unknown', detail: `command name ${describe(argv[0])} is not known statically`, source: inv.name }], complete: false }
|
|
108
|
+
}
|
|
109
|
+
const sub: Invocation = { ...inv, name: basename(n), argv, stdin: opts.stdin ?? inv.stdin, cwd: opts.cwd ?? inv.cwd }
|
|
110
|
+
const r = this.analyze(sub, depth + 1)
|
|
111
|
+
return { ...r, effects: [{ kind: 'exec', argv }, ...r.effects] }
|
|
112
|
+
},
|
|
113
|
+
script: (source, positional = [], info) => {
|
|
114
|
+
tracer?.role(source, 'code')
|
|
115
|
+
if (!this.scriptAnalyzer) return { effects: [{ kind: 'unknown', detail: 'no script analyzer', source: inv.name }], complete: false }
|
|
116
|
+
return this.scriptAnalyzer(source, positional, inv, info)
|
|
117
|
+
},
|
|
118
|
+
home: () => inv.env('HOME') ?? known(os.homedir()),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function basename(p: string): string {
|
|
124
|
+
const i = p.lastIndexOf('/')
|
|
125
|
+
return i === -1 ? p : p.slice(i + 1)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Resolve a path value against a cwd: absolute values stay, relative ones are joined. Literal `.`/`..` are left for the policy shape check. */
|
|
129
|
+
export function resolvePath(p: AValue, cwd: AValue): AValue {
|
|
130
|
+
const alts = p.alts.map((alt): AValue[] => {
|
|
131
|
+
const first = alt[0]
|
|
132
|
+
if (first === undefined) return [cwd]
|
|
133
|
+
if (typeof first === 'string' && first.startsWith('/')) return [{ alts: [alt] }]
|
|
134
|
+
if (isHole(first) && first.type === 'path' && first.under) return [{ alts: [alt] }]
|
|
135
|
+
// a `~` that reaches a behavior is literal: word expansion has already expanded unquoted tildes
|
|
136
|
+
// ("~/x" names ./~/x, as in bash)
|
|
137
|
+
return [cwd, known('/'), { alts: [alt] }]
|
|
138
|
+
})
|
|
139
|
+
const parts = alts.map((a) => concat(...a))
|
|
140
|
+
if (parts.length === 1) return parts[0]
|
|
141
|
+
return { alts: parts.flatMap((v) => v.alts) }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* GNU-style argument parsing over abstract values. Concrete arguments parse
|
|
146
|
+
* normally; an unknown argument whose known prefix starts with `-` is
|
|
147
|
+
* recorded as an unknown flag; one whose first character is unknown could be
|
|
148
|
+
* an option or an operand, so it is returned both as an operand and in
|
|
149
|
+
* `ambiguous`.
|
|
150
|
+
*/
|
|
151
|
+
export function parseArgs(args: AValue[], spec: ArgSpec): ParsedArgs {
|
|
152
|
+
const flags = new Map<string, AValue | true>()
|
|
153
|
+
const multi = new Map<string, AValue[]>()
|
|
154
|
+
const operands: AValue[] = []
|
|
155
|
+
const ambiguous: AValue[] = []
|
|
156
|
+
const unknownFlags: string[] = []
|
|
157
|
+
const short = spec.short ?? ''
|
|
158
|
+
const shortArg = spec.shortWithArg ?? ''
|
|
159
|
+
const long = spec.long ?? {}
|
|
160
|
+
const setFlag = (name: string, v: AValue | true): void => {
|
|
161
|
+
flags.set(name, v)
|
|
162
|
+
if (v !== true) {
|
|
163
|
+
const list = multi.get(name) ?? []
|
|
164
|
+
list.push(v)
|
|
165
|
+
multi.set(name, list)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
let onlyOperands = false
|
|
169
|
+
for (let i = 0; i < args.length; i++) {
|
|
170
|
+
const a = args[i]
|
|
171
|
+
const s = concrete(a)
|
|
172
|
+
if (onlyOperands) {
|
|
173
|
+
operands.push(a)
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
if (s === undefined) {
|
|
177
|
+
const prefix = knownPrefix(a)
|
|
178
|
+
if (prefix.startsWith('-') && prefix.length > 1) {
|
|
179
|
+
unknownFlags.push(describe(a))
|
|
180
|
+
} else if (prefix === '') {
|
|
181
|
+
operands.push(a)
|
|
182
|
+
ambiguous.push(a)
|
|
183
|
+
} else {
|
|
184
|
+
operands.push(a)
|
|
185
|
+
if (spec.posix) onlyOperands = true
|
|
186
|
+
}
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
if (s === '--') {
|
|
190
|
+
onlyOperands = true
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
const single = spec.longSingle === true && s.length > 2 && s[0] === '-' && s[1] !== '-'
|
|
194
|
+
if ((s.startsWith('--') && s.length > 2) || single) {
|
|
195
|
+
const body = s.slice(single ? 1 : 2)
|
|
196
|
+
const eq = body.indexOf('=')
|
|
197
|
+
const name = eq === -1 ? body : body.slice(0, eq)
|
|
198
|
+
// allow unambiguous prefixes (not for single-dash options, which fall back to short flags)
|
|
199
|
+
let key = Object.hasOwn(long, name) ? name : undefined
|
|
200
|
+
if (key === undefined && !single) {
|
|
201
|
+
const candidates = Object.keys(long).filter((k) => k.startsWith(name))
|
|
202
|
+
if (candidates.length === 1) key = candidates[0]
|
|
203
|
+
}
|
|
204
|
+
if (key !== undefined) {
|
|
205
|
+
const kind = long[key]
|
|
206
|
+
const canonical = typeof kind === 'string' && kind.startsWith('=') ? kind.slice(1) : key
|
|
207
|
+
const n = typeof kind === 'number' ? kind : typeof kind === 'string' ? (shortArg.includes(canonical) ? spec.arity?.[canonical] ?? 1 : 0) : kind ? 1 : 0
|
|
208
|
+
if (n > 0) {
|
|
209
|
+
if (eq !== -1 && n === 1) setFlag(canonical, known(body.slice(eq + 1)))
|
|
210
|
+
else if (i + n < args.length) {
|
|
211
|
+
i += n
|
|
212
|
+
setFlag(canonical, args[i])
|
|
213
|
+
} else {
|
|
214
|
+
i = args.length
|
|
215
|
+
setFlag(canonical, unknownString(`${s} (missing)`))
|
|
216
|
+
}
|
|
217
|
+
} else setFlag(canonical, true)
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
if (!single) {
|
|
221
|
+
unknownFlags.push(s)
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (spec.numeric && /^-\d+$/.test(s)) {
|
|
226
|
+
setFlag(spec.numeric, known(s.slice(1)))
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
if (s.startsWith('-') && s.length > 1) {
|
|
230
|
+
for (let j = 1; j < s.length; j++) {
|
|
231
|
+
const f = s[j]
|
|
232
|
+
if (shortArg.includes(f)) {
|
|
233
|
+
const rest = s.slice(j + 1)
|
|
234
|
+
const n = spec.arity?.[f] ?? 1
|
|
235
|
+
if (rest !== '' && n === 1) setFlag(f, known(rest))
|
|
236
|
+
else {
|
|
237
|
+
const need = rest !== '' ? n - 1 : n
|
|
238
|
+
if (i + need < args.length) {
|
|
239
|
+
i += need
|
|
240
|
+
setFlag(f, args[i])
|
|
241
|
+
} else {
|
|
242
|
+
i = args.length
|
|
243
|
+
setFlag(f, unknownString(`-${f} (missing)`))
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
break
|
|
247
|
+
}
|
|
248
|
+
if (short.includes(f)) setFlag(f, true)
|
|
249
|
+
else unknownFlags.push('-' + f)
|
|
250
|
+
}
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
operands.push(a)
|
|
254
|
+
if (spec.posix) onlyOperands = true
|
|
255
|
+
}
|
|
256
|
+
return { flags, multi, operands, ambiguous, unknownFlags }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function stdinOf(kind: StdinKind): StdinKind {
|
|
260
|
+
return kind
|
|
261
|
+
}
|