@sema-agent/core 7.24.2 → 7.24.3

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.
@@ -0,0 +1,700 @@
1
+ import { lexShellText, substitutionsInHeredocBody, wordsOf, literalTildeSpelling, SUBSTITUTION_PLACEHOLDER, } from "./bash-lexer.js";
2
+ import { LAUNCHER_TABLE, SHELL_BLOCK_WORDS, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, } from "./bash-program-position.js";
3
+ import { ASSIGNMENT_VALUE_NOT_PATHS, INTERPRETER_INLINE_PROGRAMS, NO_PATH_OPERAND_COMMANDS, POLL_LOOP_MAX_BEATS, RECURSIVE_READ_FORMS, SHELL_REENTRY_PROGRAM_RE, UNMODELLED_BASE_MOVERS, argvProgramOf, assignmentWordPayload, attachedOptionPayloads, foldHomeVariableSpelling, resolveOperandLexically, hasUnquotedExpansionMetachar, hasUnquotedGlobMetachar, inlineProgramClause, interpreterKey, isPathShapedToken, readVerbOptionSlot, sedScriptSlotFiles, segmentSelectsRecursiveRead, slotValueCandidates, suppliesFirstOperandSlot, verbFirstOperandSlot, verbOptionsArePositionIndependent, } from "./bash-readonly-classifier.js";
4
+ import { isAbsolutePathForm } from "./safety.js";
5
+ export const READ_CANDIDATE_PROVENANCES = [
6
+ "operand",
7
+ "option_value",
8
+ "stdin_redirect",
9
+ "write_redirect_target",
10
+ "env_assignment",
11
+ "unlisted_word",
12
+ "traversal_root",
13
+ "cd_target",
14
+ "program_path",
15
+ "script_slot_file",
16
+ ];
17
+ const NESTING_MAX = 4;
18
+ const BASE_CANDIDATE_MAX = 256;
19
+ export function planShellReads(command, decl, depth = 0) {
20
+ const shape = { grouped: false, backgrounded: false, strayRedirection: false };
21
+ if (depth > NESTING_MAX)
22
+ return { segments: [], unreadable: `command text nested more than ${NESTING_MAX} levels deep`, shape };
23
+ const lexed = lexShellText(foldHomeVariableSpelling(command, decl.homeDir), { mode: "lenient" });
24
+ if ("reject" in lexed)
25
+ return { segments: [], unreadable: lexed.reject, shape };
26
+ const nestedPlan = (text) => planShellReads(text, decl, depth + 1);
27
+ const projected = lexed.segments.map((segment) => projectSegment(segment));
28
+ const facts = projected.map((p, si) => segmentFacts(lexed.segments[si], p, decl, nestedPlan));
29
+ shape.grouped = projected.some((p) => p.grouped);
30
+ shape.backgrounded = lexed.segments.some((s) => s.connector === "&") || facts.some((f) => f.backgrounds === true);
31
+ shape.strayRedirection = projected.some((p) => p.words.folded.length === 0 && p.redirects.length > 0);
32
+ const basesOf = facts.map(() => new Set());
33
+ const unmodelled = facts.map(() => false);
34
+ let candidates = [{ base: decl.cwd, skippedCd: -1 }];
35
+ let baseLost = false;
36
+ let andRunStart = 0;
37
+ let crossedAllAnd = true;
38
+ let crossedAnyOr = false;
39
+ let previousEmitted = -1;
40
+ const reaches = facts.map(() => undefined);
41
+ const loops = [];
42
+ const thread = (si, replay) => {
43
+ const f = facts[si];
44
+ if (previousEmitted >= 0) {
45
+ const connector = f.connector;
46
+ if (replay || connector !== "&&")
47
+ crossedAllAnd = false;
48
+ if (!replay && connector === "||")
49
+ crossedAnyOr = true;
50
+ }
51
+ if (f.empty)
52
+ return;
53
+ if (previousEmitted >= 0 && !crossedAllAnd)
54
+ andRunStart = crossedAnyOr ? si + 1 : si;
55
+ const pipeFed = f.connector === "|" || facts[si + 1]?.connector === "|";
56
+ reaches[si] ??= { andJoined: previousEmitted >= 0 && crossedAllAnd, orBroken: !crossedAllAnd && crossedAnyOr, moveProvable: f.moveProvable && !replay && !pipeFed };
57
+ crossedAllAnd = true;
58
+ crossedAnyOr = false;
59
+ previousEmitted = si;
60
+ const reachable = candidates.filter((c) => c.skippedCd < andRunStart);
61
+ if (baseLost)
62
+ unmodelled[si] = true;
63
+ else
64
+ for (const c of reachable)
65
+ basesOf[si].add(c.base);
66
+ const move = f.baseMove;
67
+ if (move === undefined || baseLost)
68
+ return;
69
+ if (typeof move.target === "string" || move.target.expands !== false) {
70
+ baseLost = true;
71
+ return;
72
+ }
73
+ const target = move.target.spelled;
74
+ const provable = reaches[si].moveProvable;
75
+ const branched = [];
76
+ for (const c of candidates) {
77
+ if (c.skippedCd < andRunStart) {
78
+ const moved = joinBase(c.base, target, decl.homeDir);
79
+ if (moved === undefined) {
80
+ baseLost = true;
81
+ return;
82
+ }
83
+ branched.push({ base: moved, skippedCd: c.skippedCd });
84
+ }
85
+ branched.push({ base: c.base, skippedCd: provable ? si : c.skippedCd });
86
+ }
87
+ const byBase = new Map();
88
+ for (const c of branched) {
89
+ const prior = byBase.get(c.base);
90
+ if (prior === undefined || c.skippedCd < prior.skippedCd)
91
+ byBase.set(c.base, c);
92
+ }
93
+ const next = [...byBase.values()];
94
+ if (next.length > BASE_CANDIDATE_MAX)
95
+ baseLost = true;
96
+ else
97
+ candidates = next;
98
+ };
99
+ for (let si = 0; si < facts.length; si++) {
100
+ const f = facts[si];
101
+ if (f.loopHead !== undefined) {
102
+ loops.push({ bodyStart: si + 1, beats: f.loopHead.beats });
103
+ thread(si, false);
104
+ continue;
105
+ }
106
+ if (f.loopEnd) {
107
+ thread(si, false);
108
+ const frame = loops.pop();
109
+ if (frame === undefined)
110
+ continue;
111
+ const body = facts.slice(frame.bodyStart, si);
112
+ const moves = body.some((b) => b.baseMove !== undefined);
113
+ if (!moves)
114
+ continue;
115
+ if (frame.beats === undefined) {
116
+ for (let k = frame.bodyStart; k < si; k++)
117
+ unmodelled[k] = true;
118
+ baseLost = true;
119
+ continue;
120
+ }
121
+ for (let beat = 1; beat < frame.beats && !baseLost; beat++) {
122
+ for (let k = frame.bodyStart; k < si; k++)
123
+ thread(k, true);
124
+ }
125
+ continue;
126
+ }
127
+ thread(si, false);
128
+ }
129
+ const segments = [];
130
+ for (let si = 0; si < facts.length; si++) {
131
+ const f = facts[si];
132
+ if (f.empty)
133
+ continue;
134
+ const bases = unmodelled[si] ? "unmodelled" : [...basesOf[si]];
135
+ const withBases = (c) => ({ ...c, bases: relativeSpelling(c.spelled) ? bases : [] });
136
+ segments.push({
137
+ text: f.text,
138
+ connector: f.connector,
139
+ argv: f.argv,
140
+ launcherChain: f.launcherChain,
141
+ ...(f.verb !== undefined ? { verb: f.verb } : {}),
142
+ ...(f.reentry !== undefined ? { reentry: f.reentry } : {}),
143
+ declines: f.declines,
144
+ reads: dedupe(f.reads).map(withBases),
145
+ redirects: f.redirects.map((r) => (r.target === undefined ? { op: r.op, direction: r.direction } : { op: r.op, direction: r.direction, target: withBases(r.target) })),
146
+ ...(f.baseMove !== undefined ? { baseMove: { target: typeof f.baseMove.target === "string" ? f.baseMove.target : withBases(f.baseMove.target) } } : {}),
147
+ bases,
148
+ nested: f.nested,
149
+ reach: reaches[si] ?? { andJoined: false, orBroken: false, moveProvable: false },
150
+ });
151
+ }
152
+ return { segments, shape };
153
+ }
154
+ function projectSegment(segment) {
155
+ const tokens = [...segment.tokens];
156
+ let grouped = tokens.some((t) => t.kind === "group");
157
+ for (let k = 0; k + 2 < tokens.length; k++) {
158
+ const [a, b, c] = [tokens[k], tokens[k + 1], tokens[k + 2]];
159
+ if (a.kind === "word" && b.kind === "group" && b.op === "(" && c.kind === "group" && c.op === ")" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(a.folded))
160
+ tokens.splice(k, 3);
161
+ }
162
+ const words = tokens.filter((t) => {
163
+ if (t.kind !== "word")
164
+ return false;
165
+ if (t.raw === t.folded && (t.folded === "{" || t.folded === "}")) {
166
+ grouped = true;
167
+ return false;
168
+ }
169
+ return true;
170
+ });
171
+ return {
172
+ words: wordsOf({ ...segment, tokens: words }),
173
+ argv: words,
174
+ redirects: tokens.filter((t) => t.kind === "redirect"),
175
+ grouped,
176
+ };
177
+ }
178
+ function segmentFacts(segment, p, decl, nestedPlan) {
179
+ const nested = [];
180
+ for (const body of segment.substitutions)
181
+ nested.push(nestedPlan(body));
182
+ let unreadableHeredoc = false;
183
+ for (const t of segment.tokens) {
184
+ if (t.kind !== "heredoc" || t.quoted)
185
+ continue;
186
+ const inner = substitutionsInHeredocBody(t.body);
187
+ if (inner === undefined) {
188
+ unreadableHeredoc = true;
189
+ continue;
190
+ }
191
+ for (const body of inner)
192
+ nested.push(nestedPlan(body));
193
+ }
194
+ if (unreadableHeredoc)
195
+ nested.push({ segments: [], unreadable: "a here-document body carries a substitution this lexer cannot delimit", shape: { grouped: false, backgrounded: false, strayRedirection: false } });
196
+ const redirects = p.redirects.map((r) => redirectFact(r));
197
+ const reads = [];
198
+ for (const r of redirects)
199
+ if (r.direction === "in" && r.target !== undefined)
200
+ reads.push(r.target);
201
+ const base = {
202
+ text: segment.text,
203
+ connector: segment.connector,
204
+ argv: p.argv,
205
+ redirects,
206
+ nested,
207
+ };
208
+ const toks = p.words;
209
+ const head = toks.folded[0];
210
+ if (head === "coproc" && toks.raw[0] === head && toks.folded.length > 1) {
211
+ const tail = (from) => ({ words: { folded: toks.folded.slice(from), raw: toks.raw.slice(from) }, argv: p.argv.slice(from), redirects: p.redirects, grouped: p.grouped });
212
+ const first = segmentFacts(segment, tail(1), decl, nestedPlan);
213
+ if (toks.folded.length < 3)
214
+ return { ...first, backgrounds: true };
215
+ const second = segmentFacts(segment, tail(2), decl, nestedPlan);
216
+ const seen = new Set(first.nested.map((n) => JSON.stringify(n.segments.map((sg) => sg.text))));
217
+ const extraNested = second.nested.filter((n) => !seen.has(JSON.stringify(n.segments.map((sg) => sg.text))));
218
+ return { ...first, reads: [...first.reads, ...second.reads], nested: [...first.nested, ...extraNested], backgrounds: true };
219
+ }
220
+ if (head !== undefined && toks.raw[0] === head && SHELL_BLOCK_WORDS.has(head)) {
221
+ if (head === "for" || head === "select")
222
+ return { ...base, empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopHead: { beats: literalBeats(toks) }, loopEnd: false };
223
+ if (head === "done")
224
+ return { ...base, empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopEnd: true };
225
+ return { ...base, empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopEnd: false };
226
+ }
227
+ const pos = readProgramPosition(toks);
228
+ const written = pos.written;
229
+ if (written === undefined)
230
+ return { ...base, empty: toks.folded.length === 0 && redirects.length === 0 && nested.length === 0, launcherChain: [], declines: [], reads, moveProvable: false, loopEnd: false };
231
+ const loopHead = pos.controlWords.some((i) => toks.folded[i] === "while" || toks.folded[i] === "until") ? { beats: undefined } : undefined;
232
+ const launcherChain = pos.launchers.map((l) => l.name);
233
+ const launcherIndices = new Set(pos.launchers.map((l) => l.index));
234
+ const declines = [];
235
+ const decline = shellScanDeclineOf(pos, NO_PATH_OPERAND_COMMANDS);
236
+ if (decline !== undefined)
237
+ declines.push(decline);
238
+ const program = pos.program;
239
+ const basename = program?.basename;
240
+ const listed = basename !== undefined && (decl.readers.has(basename) || basename === "cd");
241
+ const verb = program === undefined ? undefined : { name: program.name, basename: program.basename, listed };
242
+ for (const k of pos.assignments)
243
+ pushAssignment(reads, toks, k, "env_assignment");
244
+ for (const l of pos.launchers) {
245
+ const raw = toks.raw[l.index];
246
+ const folded = toks.folded[l.index];
247
+ if (isPathShapedToken(folded))
248
+ reads.push(bare(raw, folded, "program_path"));
249
+ }
250
+ {
251
+ const controlIndices = new Set(pos.controlWords);
252
+ const assignmentIndices = new Set(pos.assignments);
253
+ const chainEnd = pos.program?.index ?? toks.folded.length;
254
+ for (let j = 0; j < chainEnd; j++) {
255
+ if (launcherIndices.has(j) || controlIndices.has(j) || assignmentIndices.has(j) || j === written)
256
+ continue;
257
+ const word = toks.folded[j];
258
+ const raw = toks.raw[j];
259
+ if (word.startsWith("-") && word !== "-") {
260
+ for (const payload of attachedOptionPayloads(word))
261
+ reads.push(bare(raw, payload, "unlisted_word"));
262
+ if (isPathShapedToken(word))
263
+ reads.push(bare(raw, word, "unlisted_word"));
264
+ continue;
265
+ }
266
+ if (expandsOf(raw, word) === "unresolvable") {
267
+ reads.push({ spelled: word, raw, provenance: "unlisted_word", expands: "unresolvable" });
268
+ continue;
269
+ }
270
+ if (assignmentWordPayload(word) !== undefined) {
271
+ pushAssignment(reads, toks, j, "env_assignment");
272
+ continue;
273
+ }
274
+ reads.push(bare(raw, word, "unlisted_word"));
275
+ }
276
+ }
277
+ let reentry;
278
+ if (pos.argvFromStdin)
279
+ reentry = "argv";
280
+ if (pos.refusal !== undefined) {
281
+ const owner = programBasename(pos.refusal.owner);
282
+ const row = LAUNCHER_TABLE.get(owner);
283
+ if (pos.refusal.kind === "option_at_program_position" && row?.commandString !== undefined) {
284
+ const text = commandStringValue(row.commandString, toks, pos.refusal.option);
285
+ if (text !== undefined) {
286
+ nested.push(nestedPlan(text.value));
287
+ reentry = "shell";
288
+ unlistedWords(reads, toks, text.next, launcherIndices, "unlisted_word");
289
+ return { ...base, empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false };
290
+ }
291
+ }
292
+ if (programPositionRunsShell(pos, SHELL_REENTRY_PROGRAM_RE)) {
293
+ reentry = "shell";
294
+ const first = pos.launchers[0]?.index ?? written;
295
+ const text = toks.folded.slice(first + 1).filter((w) => !w.startsWith("-") || w === "-").join(" ");
296
+ if (text.length > 0)
297
+ nested.push(nestedPlan(text));
298
+ return { ...base, empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false };
299
+ }
300
+ const from = pos.launchers[0]?.index ?? written;
301
+ unlistedWords(reads, toks, from, launcherIndices, "unlisted_word");
302
+ return { ...base, empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false };
303
+ }
304
+ if (program === undefined) {
305
+ if (pos.launchers.length > 0)
306
+ unlistedWords(reads, toks, pos.launchers[0].index, launcherIndices, "unlisted_word");
307
+ return { ...base, empty: false, launcherChain, ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false };
308
+ }
309
+ const args = toks.folded.slice(program.index + 1);
310
+ const rawArgs = toks.raw.slice(program.index + 1);
311
+ const programWordUnread = program.name.includes(SUBSTITUTION_PLACEHOLDER) || hasUnquotedExpansionMetachar(toks.raw[program.index]);
312
+ if (programWordUnread)
313
+ reads.push(bare(toks.raw[program.index], program.name, "program_path"));
314
+ else if (isPathShapedToken(program.name))
315
+ reads.push(bare(toks.raw[program.index], program.name, "program_path"));
316
+ if (SHELL_REENTRY_PROGRAM_RE.test(basename)) {
317
+ reentry = "shell";
318
+ const text = reentryText(basename, args);
319
+ if (text !== undefined)
320
+ nested.push(nestedPlan(text));
321
+ if (basename === "source" || basename === ".") {
322
+ const file = args.find((a) => !a.startsWith("-") || a === "-");
323
+ if (file !== undefined)
324
+ reads.push(bare(rawArgs[args.indexOf(file)], file, "operand"));
325
+ }
326
+ else if (text === undefined) {
327
+ const file = args.find((a) => !a.startsWith("-") || a === "-");
328
+ if (file !== undefined && file !== "-")
329
+ reads.push(bare(rawArgs[args.indexOf(file)], file, "operand"));
330
+ }
331
+ return { ...base, empty: false, launcherChain, verb: verb, reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false };
332
+ }
333
+ const interpreter = INTERPRETER_INLINE_PROGRAMS.get(interpreterKey(basename));
334
+ if (interpreter !== undefined && inlineProgramClause(interpreter, args) !== undefined)
335
+ reentry = "interpreter";
336
+ const moveProvable = pos.controlWords.length === 0 && pos.launchers.length === 0 && pos.assignments.length === 0 && program.name === basename;
337
+ let baseMove;
338
+ if (basename === "cd") {
339
+ baseMove = cdMove(args, rawArgs, reads);
340
+ }
341
+ else if (UNMODELLED_BASE_MOVERS.has(basename)) {
342
+ baseMove = { target: "unmodelled" };
343
+ unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", program);
344
+ }
345
+ else if (listed) {
346
+ listedVerbCandidates(basename, args, rawArgs, reads);
347
+ }
348
+ else {
349
+ unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", program);
350
+ }
351
+ if (basename === "sed" && listed) {
352
+ const slot = sedScriptSlotFiles(args);
353
+ for (const f of slot.reads)
354
+ reads.push(bare(f, f, "script_slot_file"));
355
+ for (const f of slot.writes)
356
+ reads.push(bare(f, f, "write_redirect_target"));
357
+ for (const c of slot.commands)
358
+ nested.push(nestedPlan(c));
359
+ }
360
+ return {
361
+ ...base,
362
+ empty: false,
363
+ launcherChain,
364
+ verb: verb,
365
+ ...(reentry !== undefined ? { reentry } : {}),
366
+ declines,
367
+ reads,
368
+ ...(baseMove !== undefined ? { baseMove } : {}),
369
+ moveProvable,
370
+ ...(loopHead !== undefined ? { loopHead } : {}),
371
+ loopEnd: false,
372
+ };
373
+ }
374
+ function dedupe(reads) {
375
+ const seen = new Set();
376
+ const out = [];
377
+ for (const c of reads) {
378
+ const key = `${c.provenance}\u0000${c.spelled}\u0000${c.expands}`;
379
+ if (seen.has(key))
380
+ continue;
381
+ seen.add(key);
382
+ out.push(c);
383
+ }
384
+ return out;
385
+ }
386
+ function bare(raw, folded, provenance) {
387
+ return { spelled: folded, raw, provenance, expands: expandsOf(raw, folded) };
388
+ }
389
+ function expandsOf(raw, folded) {
390
+ if (folded.includes(SUBSTITUTION_PLACEHOLDER) || hasUnquotedExpansionMetachar(raw))
391
+ return "unresolvable";
392
+ if (hasUnquotedGlobMetachar(raw))
393
+ return "glob";
394
+ return false;
395
+ }
396
+ function relativeSpelling(spelled) {
397
+ return !(isAbsolutePathForm(spelled) || spelled.startsWith("~"));
398
+ }
399
+ function joinBase(base, target, homeDir) {
400
+ return resolveOperandLexically(base, target, homeDir);
401
+ }
402
+ function redirectFact(r) {
403
+ if (r.target === undefined)
404
+ return { op: r.op, direction: "dup" };
405
+ if (r.op === "<")
406
+ return { op: r.op, direction: "in", target: bare(r.target.raw, r.target.folded, "stdin_redirect") };
407
+ if (r.op === "<<<")
408
+ return { op: r.op, direction: "in" };
409
+ return { op: r.op, direction: "out", target: bare(r.target.raw, r.target.folded, "write_redirect_target") };
410
+ }
411
+ function pushAssignment(reads, toks, k, provenance) {
412
+ const word = toks.folded[k];
413
+ const payload = assignmentWordPayload(word);
414
+ if (payload === undefined)
415
+ return;
416
+ if (payload.kind === "value")
417
+ reads.push(bare(toks.raw[k], payload.value, provenance));
418
+ else
419
+ reads.push({ spelled: word, raw: toks.raw[k], provenance, expands: "unresolvable" });
420
+ }
421
+ function unlistedWords(reads, toks, from, launcherIndices, provenance, program) {
422
+ const argv = program !== undefined ? { program: program.basename, from: program.index } : argvProgramOf(toks);
423
+ const argvStart = argv?.from ?? toks.folded.length;
424
+ const slotVerb = argv?.program;
425
+ const declaredNotPaths = ASSIGNMENT_VALUE_NOT_PATHS.has(slotVerb ?? "");
426
+ const noPathOperands = slotVerb !== undefined && NO_PATH_OPERAND_COMMANDS.has(slotVerb);
427
+ const permutes = slotVerb === undefined || !verbOptionsArePositionIndependent(slotVerb);
428
+ const expanding = new Set();
429
+ for (let j = from; j < toks.folded.length; j++) {
430
+ if (launcherIndices.has(j))
431
+ continue;
432
+ if (expandsOf(toks.raw[j], toks.folded[j]) !== "unresolvable")
433
+ continue;
434
+ expanding.add(j);
435
+ reads.push({ spelled: toks.folded[j], raw: toks.raw[j], provenance: j === argvStart ? "program_path" : provenance, expands: "unresolvable" });
436
+ }
437
+ const positional = [];
438
+ let stdinOperand = false;
439
+ let endOfOptions = false;
440
+ let sawArgvOperand = false;
441
+ for (let j = from; j < toks.folded.length; j++) {
442
+ if (launcherIndices.has(j) || expanding.has(j))
443
+ continue;
444
+ const word = toks.folded[j];
445
+ const raw = toks.raw[j];
446
+ if (j > from && !endOfOptions && word === "--") {
447
+ endOfOptions = true;
448
+ continue;
449
+ }
450
+ if (noPathOperands && j >= argvStart)
451
+ break;
452
+ if (j === argvStart && !isPathShapedToken(word))
453
+ continue;
454
+ const optionShaped = !endOfOptions && word.startsWith("-") && word !== "-";
455
+ if (optionShaped && j > argvStart) {
456
+ const slot = slotVerb === undefined ? undefined : readVerbOptionSlot(slotVerb, word);
457
+ if (sawArgvOperand && permutes) {
458
+ reads.push(bare(raw, word, provenance));
459
+ if (slot !== undefined && slot.form === "separated") {
460
+ const ahead = toks.folded[j + 1];
461
+ if (ahead !== undefined && expandsOf(toks.raw[j + 1], ahead) !== "unresolvable")
462
+ for (const c of slotValueCandidates(ahead, slot.kind))
463
+ reads.push(bare(toks.raw[j + 1], c, "option_value"));
464
+ }
465
+ }
466
+ if (slot !== undefined && slot.form !== "unexplained" && !(slot.form === "separated" && sawArgvOperand && permutes)) {
467
+ if (slot.form === "attached") {
468
+ for (const c of slotValueCandidates(literalTildeSpelling(slot.value), slot.kind))
469
+ reads.push(bare(raw, c, "option_value"));
470
+ }
471
+ else if (slot.form === "separated") {
472
+ j++;
473
+ const value = toks.folded[j];
474
+ if (value !== undefined && !expanding.has(j)) {
475
+ const named = slotValueCandidates(value, slot.kind);
476
+ for (const c of named)
477
+ reads.push(bare(toks.raw[j], c, "option_value"));
478
+ if (named.length > 0)
479
+ positional.push(value);
480
+ }
481
+ }
482
+ continue;
483
+ }
484
+ for (const payload of attachedOptionPayloads(word))
485
+ reads.push(bare(raw, payload, provenance));
486
+ if (isPathShapedToken(word))
487
+ reads.push(bare(raw, word, provenance));
488
+ continue;
489
+ }
490
+ if (optionShaped) {
491
+ for (const payload of attachedOptionPayloads(word))
492
+ reads.push(bare(raw, payload, provenance));
493
+ if (isPathShapedToken(word))
494
+ reads.push(bare(raw, word, provenance));
495
+ continue;
496
+ }
497
+ if (j > argvStart) {
498
+ sawArgvOperand = true;
499
+ if (word === "-") {
500
+ const m = slotVerb === undefined ? undefined : RECURSIVE_READ_FORMS[slotVerb];
501
+ if (m === undefined || m.dashIsStdin === true)
502
+ stdinOperand = true;
503
+ else
504
+ positional.push(word);
505
+ }
506
+ else
507
+ positional.push(word);
508
+ }
509
+ reads.push(bare(raw, word, j === argvStart ? "program_path" : provenance));
510
+ if (j < argvStart || !declaredNotPaths)
511
+ pushAssignment(reads, toks, j, "env_assignment");
512
+ }
513
+ if (slotVerb !== undefined && argv !== undefined && segmentSelectsRecursiveRead(slotVerb, toks.folded.slice(argvStart + 1))) {
514
+ const roots = positional.length > 0 ? positional : stdinOperand ? [] : ["."];
515
+ for (const root of roots)
516
+ reads.push({ spelled: root, raw: root, provenance: "traversal_root", expands: false });
517
+ }
518
+ for (let j = from; j < toks.folded.length; j++) {
519
+ if (launcherIndices.has(j) || j === argvStart)
520
+ continue;
521
+ const raw = toks.raw[j];
522
+ if (expandsOf(raw, toks.folded[j]) === "glob")
523
+ reads.push(bare(raw, toks.folded[j], provenance));
524
+ }
525
+ }
526
+ function commandStringValue(options, toks, option) {
527
+ const at = toks.folded.indexOf(option);
528
+ if (at < 0)
529
+ return undefined;
530
+ for (const spelling of options) {
531
+ if (option === spelling) {
532
+ const value = toks.folded[at + 1];
533
+ return value === undefined ? undefined : { value, next: at + 2 };
534
+ }
535
+ if (spelling.startsWith("--") && option.startsWith(`${spelling}=`))
536
+ return { value: option.slice(spelling.length + 1), next: at + 1 };
537
+ if (!spelling.startsWith("--") && option.startsWith(spelling) && option.length > spelling.length)
538
+ return { value: option.slice(spelling.length), next: at + 1 };
539
+ }
540
+ return undefined;
541
+ }
542
+ function reentryText(basename, args) {
543
+ if (basename === "eval")
544
+ return args.length === 0 ? undefined : args.join(" ");
545
+ if (basename === "parallel")
546
+ return args.filter((a) => !a.startsWith("-")).join(" ") || undefined;
547
+ if (basename === "source" || basename === ".")
548
+ return undefined;
549
+ for (let k = 0; k < args.length; k++) {
550
+ const a = args[k];
551
+ if (a === "--")
552
+ return undefined;
553
+ if (!a.startsWith("-") || a === "-")
554
+ return undefined;
555
+ const c = a.indexOf("c");
556
+ if (a.startsWith("--"))
557
+ continue;
558
+ if (c > 0)
559
+ return a.length > c + 1 ? a.slice(c + 1) : args[k + 1];
560
+ }
561
+ return undefined;
562
+ }
563
+ function literalBeats(toks) {
564
+ const at = toks.folded.indexOf("in");
565
+ if (at < 0)
566
+ return undefined;
567
+ const words = toks.folded.slice(at + 1);
568
+ const raws = toks.raw.slice(at + 1);
569
+ if (words.length === 0)
570
+ return undefined;
571
+ if (words.length === 1 && words[0].includes(SUBSTITUTION_PLACEHOLDER)) {
572
+ const m = /^\$\(\s*seq\s+(\d+)\s+(\d+)\s*\)$/.exec(raws[0]);
573
+ if (m === null)
574
+ return undefined;
575
+ const beats = Number(m[2]) - Number(m[1]) + 1;
576
+ return beats >= 1 && beats <= POLL_LOOP_MAX_BEATS ? beats : undefined;
577
+ }
578
+ for (let k = 0; k < words.length; k++) {
579
+ if (expandsOf(raws[k], words[k]) !== false)
580
+ return undefined;
581
+ }
582
+ return words.length <= POLL_LOOP_MAX_BEATS ? words.length : undefined;
583
+ }
584
+ function cdMove(args, rawArgs, reads) {
585
+ const idx = args.findIndex((t) => !t.startsWith("-") || t === "-");
586
+ if (idx < 0)
587
+ return { target: "home" };
588
+ const target = args[idx];
589
+ if (target === "-")
590
+ return { target: "previous" };
591
+ const c = bare(rawArgs[idx], target, "cd_target");
592
+ reads.push(c);
593
+ return { target: c };
594
+ }
595
+ function listedVerbCandidates(name, args, rawArgs, reads) {
596
+ if (NO_PATH_OPERAND_COMMANDS.has(name))
597
+ return;
598
+ const expanding = new Set();
599
+ rawArgs.forEach((raw, k) => {
600
+ if (!hasUnquotedExpansionMetachar(raw))
601
+ return;
602
+ expanding.add(k);
603
+ reads.push({ spelled: args[k], raw, provenance: "operand", expands: "unresolvable" });
604
+ });
605
+ const eoo = args.indexOf("--");
606
+ const firstOperandSlot = verbFirstOperandSlot(name);
607
+ const firstOperandSuppliedByFlag = firstOperandSlot !== undefined && (eoo === -1 ? args : args.slice(0, eoo)).some((t) => suppliesFirstOperandSlot(firstOperandSlot, t));
608
+ const positional = [];
609
+ let sawOperand = false;
610
+ let stdinOperand = false;
611
+ let endOfOptions = false;
612
+ const permutes = !verbOptionsArePositionIndependent(name);
613
+ const push = (k, text, provenance) => {
614
+ reads.push(bare(rawArgs[k], text, provenance));
615
+ };
616
+ for (let k = 0; k < args.length; k++) {
617
+ const t = args[k];
618
+ if (expanding.has(k))
619
+ continue;
620
+ if (!endOfOptions) {
621
+ if (t === "--") {
622
+ endOfOptions = true;
623
+ continue;
624
+ }
625
+ if (t.startsWith("-") && t !== "-") {
626
+ const slot = readVerbOptionSlot(name, t);
627
+ if (sawOperand && permutes) {
628
+ push(k, t, "operand");
629
+ if (slot !== undefined && slot.form === "separated") {
630
+ const ahead = args[k + 1];
631
+ if (ahead !== undefined && !expanding.has(k + 1))
632
+ for (const c of slotValueCandidates(ahead, slot.kind))
633
+ push(k + 1, c, "option_value");
634
+ }
635
+ }
636
+ if (slot !== undefined && slot.form !== "unexplained" && !(slot.form === "separated" && sawOperand && permutes)) {
637
+ if (slot.form === "attached") {
638
+ for (const c of slotValueCandidates(literalTildeSpelling(slot.value), slot.kind))
639
+ push(k, c, "option_value");
640
+ }
641
+ else if (slot.form === "separated") {
642
+ const value = args[++k];
643
+ if (value !== undefined && !expanding.has(k)) {
644
+ const named = slotValueCandidates(value, slot.kind);
645
+ for (const c of named)
646
+ push(k, c, "option_value");
647
+ if (named.length > 0)
648
+ positional.push(value);
649
+ }
650
+ }
651
+ continue;
652
+ }
653
+ for (const payload of attachedOptionPayloads(t))
654
+ push(k, payload, "option_value");
655
+ continue;
656
+ }
657
+ }
658
+ const inFirstOperandSlot = firstOperandSlot !== undefined && !firstOperandSuppliedByFlag && !sawOperand;
659
+ sawOperand = true;
660
+ if (t === "-") {
661
+ if (inFirstOperandSlot)
662
+ continue;
663
+ const m = RECURSIVE_READ_FORMS[name];
664
+ if (m === undefined || m.dashIsStdin === true) {
665
+ stdinOperand = true;
666
+ continue;
667
+ }
668
+ positional.push(t);
669
+ continue;
670
+ }
671
+ if (inFirstOperandSlot) {
672
+ const named = slotValueCandidates(t, firstOperandSlot.kind);
673
+ for (const c of named)
674
+ push(k, c, "operand");
675
+ if (named.length > 0)
676
+ positional.push(t);
677
+ continue;
678
+ }
679
+ positional.push(t);
680
+ push(k, t, "operand");
681
+ if (!ASSIGNMENT_VALUE_NOT_PATHS.has(name)) {
682
+ const payload = assignmentWordPayload(t);
683
+ if (payload?.kind === "value")
684
+ push(k, payload.value, "env_assignment");
685
+ else if (payload !== undefined)
686
+ reads.push({ spelled: t, raw: rawArgs[k], provenance: "env_assignment", expands: "unresolvable" });
687
+ }
688
+ }
689
+ for (let k = 0; k < args.length; k++) {
690
+ if (expanding.has(k))
691
+ continue;
692
+ if (expandsOf(rawArgs[k], args[k]) === "glob")
693
+ reads.push(bare(rawArgs[k], args[k], "operand"));
694
+ }
695
+ if (segmentSelectsRecursiveRead(name, args)) {
696
+ const roots = positional.length > 0 ? positional : stdinOperand ? [] : ["."];
697
+ for (const root of roots)
698
+ reads.push({ spelled: root, raw: root, provenance: "traversal_root", expands: false });
699
+ }
700
+ }