UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

802 lines (801 loc) • 39.5 kB
import { lexShell, PARSE_FAILURE_SENTENCE, substitutionsInHeredocBody, literalTildeSpelling, } from "../../shell/lex.js"; import { compoundArrayElements, dropDefinitionHeads, expandsOf, readOfText, readsOf, rewritten, spelledWithHome, isArithmeticBody, unreadableSentenceOf } from "./shell-read-word.js"; import { LAUNCHER_TABLE, SHELL_BLOCK_WORDS, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, } from "./bash-program-position.js"; import { ASSIGNMENT_VALUE_NOT_PATHS, DECLARATION_BUILTINS, INERT_BUILTINS, INTERPRETER_INLINE_PROGRAMS, NO_PATH_OPERAND_COMMANDS, NO_PATH_OPERAND_EVALUATORS, POLL_LOOP_MAX_BEATS, RECURSIVE_READ_FORMS, SHELL_REENTRY_PROGRAM_RE, UNMODELLED_BASE_MOVERS, argvProgramOf, homeMayBeRebound, attachedOptionPayloads, resolveOperandLexically, inlineProgramClause, interpreterKey, isPathShapedToken, messageSlotWords, readVerbOptionSlot, sedScriptSlotFiles, segmentSelectsRecursiveRead, slotValueCandidates, suppliesFirstOperandSlot, verbFirstOperandSlot, verbOptionsArePositionIndependent, } from "./bash-readonly-classifier.js"; import { isAbsolutePathForm } from "./safety.js"; import { codeBodies, reentryText } from "./shell-reentry-text.js"; export const READ_CANDIDATE_PROVENANCES = Object.freeze([ "operand", "option_value", "stdin_redirect", "write_redirect_target", "env_assignment", "unlisted_word", "traversal_root", "cd_target", "program_path", "script_slot_file", ]); const NESTING_MAX = 4; const BASE_CANDIDATE_MAX = 256; export function planShellReads(command, decl, depth = 0, homeRebound = false, arithmetic = false) { const shape = { grouped: false, backgrounded: false, strayRedirection: false }; if (depth > NESTING_MAX) return { segments: [], unreadable: `command text nested more than ${NESTING_MAX} levels deep`, shape }; const lexed = lexShell(command); if ("parseFailure" in lexed) return { segments: [], unreadable: PARSE_FAILURE_SENTENCE[lexed.parseFailure.reason], shape }; const home = homeRebound || homeMayBeRebound(command) ? undefined : decl.homeDir; const unreadable = unreadableSentenceOf(lexed, command, !arithmetic); const nestedPlan = (text, arithmetic = false) => planShellReads(text, decl, depth + 1, home === undefined, arithmetic); const projected = lexed.segments.map((segment) => projectSegment(segment)); const planned = projected.map((p, si) => segmentFacts(lexed.segments[si], p, decl, home, nestedPlan)); const inert = depth === 0 && unreadable === undefined && planned.every(segmentOpensNothing); const facts = inert ? planned.map((f) => ({ ...f, reads: f.reads.filter((c) => c.provenance !== "env_assignment") })) : planned; shape.grouped = projected.some((p) => p.grouped); shape.backgrounded = lexed.segments.some((s) => s.connector === "&") || facts.some((f) => f.backgrounds === true); shape.strayRedirection = projected.some((p) => p.argv.length === 0 && p.redirects.length > 0); const basesOf = facts.map(() => new Set()); const unmodelled = facts.map(() => false); let candidates = [{ base: decl.cwd, skippedCd: -1 }]; let baseLost = false; let floorBases; const loseBase = (reachable, floorTarget) => { baseLost = true; floorBases ??= [...new Set([...reachable.map((c) => c.base), ...(floorTarget !== undefined ? [floorTarget] : [])])]; }; let andRunStart = 0; let crossedAllAnd = true; let crossedAnyOr = false; let previousEmitted = -1; const reaches = facts.map(() => undefined); const loops = []; const thread = (si, replay) => { const f = facts[si]; if (previousEmitted >= 0) { const connector = f.connector; if (replay || connector !== "&&") crossedAllAnd = false; if (!replay && connector === "||") crossedAnyOr = true; } if (f.empty) return; if (previousEmitted >= 0 && !crossedAllAnd) andRunStart = crossedAnyOr ? si + 1 : si; const pipeFed = f.connector === "|" || facts[si + 1]?.connector === "|"; reaches[si] ??= { andJoined: previousEmitted >= 0 && crossedAllAnd, orBroken: !crossedAllAnd && crossedAnyOr, moveProvable: f.moveProvable && !replay && !pipeFed }; crossedAllAnd = true; crossedAnyOr = false; previousEmitted = si; const reachable = candidates.filter((c) => c.skippedCd < andRunStart); if (baseLost) unmodelled[si] = true; else for (const c of reachable) basesOf[si].add(c.base); const move = f.baseMove; if (move === undefined || baseLost) return; if (typeof move.target === "string" || move.target.expands !== false) { loseBase(reachable, typeof move.target !== "string" ? move.target.floorSpelled : move.target === "home" ? decl.homeDir : undefined); return; } const target = move.target.spelled; const provable = reaches[si].moveProvable; const branched = []; for (const c of candidates) { if (c.skippedCd < andRunStart) { const moved = joinBase(c.base, target, decl.homeDir); if (moved === undefined) { loseBase(reachable); return; } branched.push({ base: moved, skippedCd: c.skippedCd }); } branched.push({ base: c.base, skippedCd: provable ? si : c.skippedCd }); } const byBase = new Map(); for (const c of branched) { const prior = byBase.get(c.base); if (prior === undefined || c.skippedCd < prior.skippedCd) byBase.set(c.base, c); } const next = [...byBase.values()]; if (next.length > BASE_CANDIDATE_MAX) loseBase(reachable); else candidates = next; }; for (let si = 0; si < facts.length; si++) { const f = facts[si]; if (f.loopHead !== undefined) { loops.push({ bodyStart: si + 1, beats: f.loopHead.beats }); thread(si, false); continue; } if (f.loopEnd) { thread(si, false); const frame = loops.pop(); if (frame === undefined) continue; const body = facts.slice(frame.bodyStart, si); const moves = body.some((b) => b.baseMove !== undefined); if (!moves) continue; if (frame.beats === undefined) { for (let k = frame.bodyStart; k < si; k++) unmodelled[k] = true; loseBase(candidates.filter((c) => c.skippedCd < andRunStart)); continue; } for (let beat = 1; beat < frame.beats && !baseLost; beat++) { for (let k = frame.bodyStart; k < si; k++) thread(k, true); } continue; } thread(si, false); } const segments = []; for (let si = 0; si < facts.length; si++) { const f = facts[si]; if (f.empty) continue; const bases = unmodelled[si] ? "unmodelled" : [...basesOf[si]]; const floor = unmodelled[si] && floorBases !== undefined && floorBases.length > 0 ? floorBases : undefined; const withBases = (c) => { const relative = relativeSpelling(c.spelled); return { ...c, bases: relative ? bases : [], ...(relative && floor !== undefined ? { floorBases: floor } : {}) }; }; segments.push({ text: f.text, connector: f.connector, argv: f.argv, launcherChain: f.launcherChain, ...(f.verb !== undefined ? { verb: f.verb } : {}), ...(f.reentry !== undefined ? { reentry: f.reentry } : {}), ...(f.unreadProgramText !== undefined ? { unreadProgramText: f.unreadProgramText } : {}), declines: f.declines, reads: dedupe(f.reads).map(withBases), redirects: f.redirects.map((r) => (r.target === undefined ? { op: r.op, direction: r.direction } : { op: r.op, direction: r.direction, target: withBases(r.target) })), ...(f.baseMove !== undefined ? { baseMove: { target: typeof f.baseMove.target === "string" ? f.baseMove.target : withBases(f.baseMove.target) } } : {}), bases, nested: f.nested.map((n) => rebasePlan(n, decl.cwd, bases)), reach: reaches[si] ?? { andJoined: false, orBroken: false, moveProvable: false }, }); } return { segments, shape, ...(unreadable !== undefined ? { unreadable } : {}) }; } function segmentOpensNothing(f) { if (f.empty) return true; const program = f.verb; const at = program === undefined ? -1 : f.argv.findIndex((w) => w.folded === program.name); const inertProgram = program === undefined || (program.name === program.basename && INERT_BUILTINS.has(program.basename) && !(DECLARATION_BUILTINS.has(program.basename) && f.argv.slice(at + 1).some((w) => /^[-+]/.test(w.folded)))); return inertProgram && f.argv.every((w) => !/[[\]$`]/.test(w.raw)) && f.nested.length === 0 && f.reentry === undefined && f.declines.length === 0 && f.launcherChain.length === 0 && f.redirects.every((r) => r.direction !== "in") && f.reads.every((c) => c.provenance === "env_assignment"); } function projectSegment(segment) { const tokens = [...segment.tokens]; let grouped = tokens.some((t) => t.kind === "group"); dropDefinitionHeads(tokens); const words = tokens.filter((t) => { if (t.kind !== "word") return false; if (t.raw === t.folded && (t.folded === "{" || t.folded === "}")) { grouped = true; return false; } return true; }); return { argv: words, redirects: tokens.filter((t) => t.kind === "redirect"), grouped, }; } function segmentFacts(segment, p, decl, home, nestedPlan) { const facts = segmentFactsRead(segment, p, decl, home, nestedPlan); const unresolvedRaw = new Set(); for (const t of segment.tokens) { const w = t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined; if (w !== undefined && w.pieces.some((p) => p.literal === undefined && p.expansion === undefined)) unresolvedRaw.add(w.raw); } const homeWithheld = home === undefined && decl.homeDir !== undefined; const floor = (c) => { const f = (c.expands === "unresolvable" || c.spelled.startsWith("./~")) && decl.homeDir !== undefined ? spelledWithHome(c.spelled, c.raw, decl.homeDir) : undefined; return f === undefined ? c : { ...c, floorSpelled: f }; }; const unsettle = (c) => floor((unresolvedRaw.has(c.raw) || (homeWithheld && c.spelled.startsWith("~"))) && c.expands !== "unresolvable" ? { ...c, expands: "unresolvable" } : c); const move = facts.baseMove; return { ...facts, reads: facts.reads.map(unsettle), ...(move !== undefined && typeof move.target !== "string" ? { baseMove: { target: unsettle(move.target) } } : {}), }; } function segmentFactsRead(segment, p, decl, home, nestedPlan) { const nested = []; const arithmetic = segment.tokens.flatMap((t) => (t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined)?.pieces ?? []).flatMap((q) => (q.expansion?.kind === "arithmetic" && q.expansion.body !== undefined && isArithmeticBody(q.expansion.body) ? [q.expansion.body] : [])); const count = (list, body) => list.filter((b) => b === body).length; for (const body of segment.substitutions) nested.push(nestedPlan(body, count(arithmetic, body) === count(segment.substitutions, body))); let unreadableHeredoc = false; for (const t of segment.tokens) { if (t.kind !== "heredoc" || t.quoted) continue; const inner = substitutionsInHeredocBody(t.body); if (inner === undefined) { unreadableHeredoc = true; continue; } for (const s of inner) nested.push(nestedPlan(s.body, s.kind === "arithmetic" && isArithmeticBody(s.body))); } if (unreadableHeredoc) nested.push({ segments: [], unreadable: "a here-document body carries a substitution this lexer cannot delimit", shape: { grouped: false, backgrounded: false, strayRedirection: false } }); const redirects = p.redirects.map((r) => redirectFact(r, home)); const reads = []; for (const r of redirects) if (r.direction === "in" && r.target !== undefined) reads.push(r.target); const base = { text: segment.text, connector: segment.connector, argv: p.argv, redirects, nested, }; const toks = readsOf(p.argv, home); const head = toks[0]?.folded; if (head === "coproc" && toks[0].raw === head && toks.length > 1) { const tail = (from) => ({ argv: p.argv.slice(from), redirects: p.redirects, grouped: p.grouped }); const first = segmentFacts(segment, tail(1), decl, home, nestedPlan); if (toks.length < 3) return { ...first, backgrounds: true }; const second = segmentFacts(segment, tail(2), decl, home, nestedPlan); const seen = new Set(first.nested.map((n) => JSON.stringify(n.segments.map((sg) => sg.text)))); const extraNested = second.nested.filter((n) => !seen.has(JSON.stringify(n.segments.map((sg) => sg.text)))); return { ...first, ...(second.reentry === "shell" ? { reentry: second.reentry } : {}), reads: [...first.reads, ...second.reads], nested: [...first.nested, ...extraNested], backgrounds: true }; } if (head !== undefined && toks[0].raw === head && SHELL_BLOCK_WORDS.has(head)) { if (head === "for" || head === "select") return { ...base, empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopHead: { beats: literalBeats(toks) }, loopEnd: false }; if (head === "done") return { ...base, empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopEnd: true }; return { ...base, empty: false, launcherChain: [], declines: [], reads, moveProvable: false, loopEnd: false }; } const pos = readProgramPosition(p.argv); const written = pos.written; if (written === undefined) return { ...base, empty: toks.length === 0 && redirects.length === 0 && nested.length === 0, launcherChain: [], declines: [], reads, moveProvable: false, loopEnd: false }; const loopHead = pos.controlWords.some((i) => toks[i].folded === "while" || toks[i].folded === "until") ? { beats: undefined } : undefined; const launcherChain = pos.launchers.map((l) => l.name); const launcherIndices = new Set(pos.launchers.map((l) => l.index)); const declines = []; const decline = shellScanDeclineOf(pos, NO_PATH_OPERAND_COMMANDS); if (decline !== undefined) declines.push(decline); const program = pos.program; const basename = program?.basename; const listed = basename !== undefined && (decl.readers.has(basename) || basename === "cd"); const verb = program === undefined ? undefined : { name: program.name, basename: program.basename, listed }; for (const k of pos.assignments) pushAssignment(reads, toks[k].word, "env_assignment"); for (const l of pos.launchers) { const r = toks[l.index]; if (isPathShapedToken(r.folded)) reads.push(bare(r, "program_path")); } { const controlIndices = new Set(pos.controlWords); const assignmentIndices = new Set(pos.assignments); const chainEnd = pos.program?.index ?? toks.length; const launcherValueIndices = new Set(); for (const l of pos.launchers) { const row = LAUNCHER_TABLE.get(programBasename(l.name)); if (row === undefined) continue; for (let j = l.index + 1; j < chainEnd; j++) { const w = toks[j].folded; if (!w.startsWith("-") || w === "-") break; const arity = row.optionArity.get(w); if (arity === undefined) break; if (arity === 1) launcherValueIndices.add(++j); } } for (let j = 0; j < chainEnd; j++) { if (launcherIndices.has(j) || controlIndices.has(j) || assignmentIndices.has(j) || launcherValueIndices.has(j) || j === written) continue; const r = toks[j]; const word = r.folded; if (word.startsWith("-") && word !== "-") { for (const payload of attachedOptionPayloads(word)) reads.push(bare(r, "unlisted_word", payload)); if (isPathShapedToken(word)) reads.push(bare(r, "unlisted_word")); continue; } if (r.expands === "unresolvable") { reads.push(bare(r, "unlisted_word")); continue; } if (r.word.assignment !== undefined) { pushAssignment(reads, r.word, "env_assignment"); continue; } reads.push(bare(r, "unlisted_word")); } } let reentry; if (pos.argvFromStdin) reentry = "argv"; if (pos.refusal !== undefined) { const owner = programBasename(pos.refusal.owner); const row = LAUNCHER_TABLE.get(owner); if (pos.refusal.kind === "option_at_program_position" && row?.commandString !== undefined) { const text = commandStringValue(row.commandString, toks, pos.refusal.option); if (text !== undefined) { nested.push(nestedPlan(text.value)); reentry = "shell"; unlistedWords(reads, toks, text.next, launcherIndices, "unlisted_word"); return { ...base, empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false }; } } if (programPositionRunsShell(pos, SHELL_REENTRY_PROGRAM_RE)) { reentry = "shell"; const first = pos.launchers[0]?.index ?? written; const text = toks.slice(first + 1).map((r) => r.folded).filter((w) => !w.startsWith("-") || w === "-").join(" "); if (text.length > 0) nested.push(nestedPlan(text)); return { ...base, empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false }; } const from = pos.launchers[0]?.index ?? written; unlistedWords(reads, toks, from, launcherIndices, "unlisted_word"); return { ...base, empty: false, launcherChain, ...(verb !== undefined ? { verb } : {}), ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false }; } if (program === undefined) { if (pos.launchers.length > 0) unlistedWords(reads, toks, pos.launchers[0].index, launcherIndices, "unlisted_word"); return { ...base, empty: false, launcherChain, ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false }; } const argReads = toks.slice(program.index + 1); const args = argReads.map((r) => r.folded); const texts = argReads.map((r) => (r.word.pieces.every((q) => q.literal !== undefined) ? r.word.pieces.map((q) => q.literal).join("") : r.folded)); const programRead = toks[program.index]; if (rewritten(programRead) || isPathShapedToken(programRead.folded)) reads.push(bare(programRead, "program_path")); if (SHELL_REENTRY_PROGRAM_RE.test(basename)) { reentry = "shell"; const text = reentryText(basename, texts); for (const t of new Set([text, reentryText(basename, args)])) if (t !== undefined) nested.push(nestedPlan(t)); if (basename === "source" || basename === ".") { const file = argReads.find((a) => !a.folded.startsWith("-") || a.folded === "-"); if (file !== undefined) reads.push(bare(file, "operand")); } else if (text === undefined) { const file = argReads.find((a) => !a.folded.startsWith("-") || a.folded === "-"); if (file !== undefined && file.folded !== "-") reads.push(bare(file, "operand")); } return { ...base, empty: false, launcherChain, verb: verb, reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false }; } const interpreter = INTERPRETER_INLINE_PROGRAMS.get(interpreterKey(basename)); if (interpreter !== undefined && inlineProgramClause(interpreter, args) !== undefined) reentry = "interpreter"; const moveProvable = pos.controlWords.length === 0 && pos.launchers.length === 0 && pos.assignments.length === 0 && program.name === basename; let baseMove; if (basename === "cd") { baseMove = cdMove(argReads, reads); } else if (UNMODELLED_BASE_MOVERS.has(basename)) { baseMove = { target: "unmodelled" }; unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", program); } else if (listed) { listedVerbCandidates(basename, argReads, reads); } else if (DECLARATION_BUILTINS.has(basename) && program.name === basename) { for (const r of argReads) { const a = r.word.assignment; if (a !== undefined && (r.expands !== "unresolvable" || a.value.expansions.length > 0)) pushAssignment(reads, r.word, "env_assignment"); else if (r.expands === "unresolvable") { reads.push(bare(r, "unlisted_word")); pushAssignment(reads, r.word, "unlisted_word"); } } } else { unlistedWords(reads, toks, program.index, launcherIndices, "unlisted_word", program); } let unreadProgramText; if (basename === "sed") { const slot = sedScriptSlotFiles(args); for (const f of slot.reads) reads.push(bare(readOfText(f), "script_slot_file")); for (const f of slot.writes) reads.push(bare(readOfText(f), "write_redirect_target")); for (const c of slot.commands) nested.push(nestedPlan(c)); if (slot.commands.length > 0 || slot.executesPatternSpace) reentry = "shell"; unreadProgramText = slot.unread; } const bodies = new Set([...codeBodies(basename, texts), ...codeBodies(basename, args)]); if (bodies.size > 0) reentry = "shell"; for (const body of bodies) nested.push(nestedPlan(body)); return { ...base, empty: false, launcherChain, verb: verb, ...(reentry !== undefined ? { reentry } : {}), ...(unreadProgramText !== undefined ? { unreadProgramText } : {}), declines, reads, ...(baseMove !== undefined ? { baseMove } : {}), moveProvable, ...(loopHead !== undefined ? { loopHead } : {}), loopEnd: false, }; } function rebasePlan(plan, from, to) { if (to !== "unmodelled" && to.length === 1 && to[0] === from) return plan; const fromSep = from.endsWith("/") || from.endsWith("\\") ? from : `${from}/`; const moveBase = (b) => { if (to === "unmodelled") return []; if (b === from) return [...to]; if (b.startsWith(fromSep)) return to.map((t) => `${t.endsWith("/") || t.endsWith("\\") ? t : `${t}/`}${b.slice(fromSep.length)}`); return [b]; }; const moveCandidate = (c) => { if (c.bases === "unmodelled" || c.bases.length === 0) return c; if (to === "unmodelled") return { ...c, bases: "unmodelled" }; return { ...c, bases: [...new Set(c.bases.flatMap(moveBase))] }; }; return { ...plan, segments: plan.segments.map((s) => ({ ...s, reads: s.reads.map(moveCandidate), redirects: s.redirects.map((r) => (r.target === undefined ? r : { ...r, target: moveCandidate(r.target) })), ...(s.baseMove !== undefined ? { baseMove: { target: typeof s.baseMove.target === "string" ? s.baseMove.target : moveCandidate(s.baseMove.target) } } : {}), bases: s.bases === "unmodelled" || to === "unmodelled" ? "unmodelled" : [...new Set(s.bases.flatMap(moveBase))], nested: s.nested.map((n) => rebasePlan(n, from, to)), })), }; } function dedupe(reads) { const seen = new Set(); const out = []; for (const c of reads) { const key = `${c.provenance}\u0000${c.spelled}\u0000${c.expands}\u0000${c.raw}\u0000${c.denyOnly === true}`; if (seen.has(key)) continue; seen.add(key); out.push(c); } return out; } function bare(r, provenance, spelled = r.folded) { return { spelled, raw: r.raw, provenance, expands: r.expands }; } function relativeSpelling(spelled) { return !(isAbsolutePathForm(spelled) || spelled.startsWith("~")); } function joinBase(base, target, homeDir) { return resolveOperandLexically(base, target, homeDir); } function redirectFact(r, home) { if (r.target === undefined) return { op: r.op, direction: "dup" }; if (r.op === "<") return { op: r.op, direction: "in", target: bare(readsOf([r.target], home)[0], "stdin_redirect") }; if (r.op === "<<<") return { op: r.op, direction: "in" }; return { op: r.op, direction: "out", target: bare(readsOf([r.target], home)[0], "write_redirect_target") }; } function pushAssignment(reads, w, provenance) { const a = w.assignment; if (a === undefined) return; const expandsTilde = a.value.expansions.some((p) => p.expansion?.kind === "tilde"); const value = expandsTilde ? a.value.folded : literalTildeSpelling(a.value.folded); const settled = a.op === "=" && !expandsTilde; const named = a.value.folded === "" || (a.op === "+=" ? expandsTilde : !settled) ? undefined : value; if (a.value.folded === "" && settled) return; const v = { spelled: value, raw: a.bare ? a.value.raw : w.raw, provenance }; const unsettled = provenance !== "env_assignment" ? { ...v, expands: "unresolvable" } : v.spelled.startsWith("~") && !v.spelled.includes(":") ? { ...v, expands: expandsOf(a.value) } : undefined; if (!settled && unsettled !== undefined) reads.push(unsettled); const substitutes = a.value.expansions.some((p) => p.expansion?.kind === "command" || p.expansion?.kind === "process" || p.expansion?.kind === "arithmetic"); if (named !== undefined && !substitutes) reads.push({ spelled: named, raw: w.raw, provenance, expands: expandsOf(w) }); if (a.value.folded.includes(":")) for (const m of a.value.folded.split(":")) if (m !== "") { const r = readOfText(m); reads.push({ spelled: r.folded, raw: r.raw, provenance, expands: r.expands, denyOnly: true }); } if (a.form === "compound") for (const e of compoundArrayElements(a.value)) reads.push({ spelled: e.folded, raw: e.raw, provenance, expands: e.expands, ...(e.denyOnly === true ? { denyOnly: true } : {}) }); } function unlistedWords(reads, toks, from, launcherIndices, provenance, program) { const argv = program !== undefined ? { program: program.basename, from: program.index } : argvProgramOf(toks.map((r) => r.word)); const argvStart = argv?.from ?? toks.length; const slotVerb = argv?.program; const message = slotVerb === undefined || argv === undefined ? new Set() : messageSlotWords(slotVerb, toks, argvStart); const declaredNotPaths = ASSIGNMENT_VALUE_NOT_PATHS.has(slotVerb ?? ""); const noPathOperands = slotVerb !== undefined && NO_PATH_OPERAND_COMMANDS.has(slotVerb); const permutes = slotVerb === undefined || !verbOptionsArePositionIndependent(slotVerb); const expanding = new Set(); for (let j = from; j < toks.length; j++) { if (launcherIndices.has(j)) continue; if (noPathOperands && j > argvStart && !NO_PATH_OPERAND_EVALUATORS.has(slotVerb)) break; if (toks[j].expands !== "unresolvable") continue; expanding.add(j); reads.push(bare(toks[j], j === argvStart ? "program_path" : provenance)); if (j < argvStart) pushAssignment(reads, toks[j].word, "env_assignment"); else if (j > argvStart && !declaredNotPaths) pushAssignment(reads, toks[j].word, provenance); } const positional = []; let stdinOperand = false; let endOfOptions = false; let sawArgvOperand = false; for (let j = from; j < toks.length; j++) { if (launcherIndices.has(j) || expanding.has(j) || message.has(j)) continue; const r = toks[j]; const word = r.folded; if (j > from && !endOfOptions && word === "--") { endOfOptions = true; continue; } if (noPathOperands && j >= argvStart) break; if (j === argvStart && !isPathShapedToken(word)) continue; const optionShaped = !endOfOptions && word.startsWith("-") && word !== "-"; if (optionShaped && j > argvStart) { const slot = slotVerb === undefined ? undefined : readVerbOptionSlot(slotVerb, word); if (sawArgvOperand && permutes) { reads.push(bare(r, provenance)); if (slot !== undefined && slot.form === "separated") { const ahead = toks[j + 1]; if (ahead !== undefined && ahead.expands !== "unresolvable") for (const c of slotValueCandidates(ahead.folded, slot.kind)) reads.push(bare(ahead, "option_value", c)); } } if (slot !== undefined && slot.form !== "unexplained" && !(slot.form === "separated" && sawArgvOperand && permutes)) { if (slot.form === "attached") { for (const c of slotValueCandidates(literalTildeSpelling(slot.value), slot.kind)) reads.push(bare(r, "option_value", c)); } else if (slot.form === "separated") { j++; const value = toks[j]; if (value !== undefined && !expanding.has(j)) { const named = slotValueCandidates(value.folded, slot.kind); for (const c of named) reads.push(bare(value, "option_value", c)); if (named.length > 0) positional.push(value); } } continue; } for (const payload of attachedOptionPayloads(word)) reads.push(bare(r, provenance, payload)); if (isPathShapedToken(word)) reads.push(bare(r, provenance)); continue; } if (optionShaped) { for (const payload of attachedOptionPayloads(word)) reads.push(bare(r, provenance, payload)); if (isPathShapedToken(word)) reads.push(bare(r, provenance)); continue; } if (j > argvStart) { sawArgvOperand = true; if (word === "-") { const m = slotVerb === undefined ? undefined : RECURSIVE_READ_FORMS[slotVerb]; if (m === undefined || m.dashIsStdin === true) stdinOperand = true; else positional.push(r); } else positional.push(r); } reads.push(bare(r, j === argvStart ? "program_path" : provenance)); if (j < argvStart) pushAssignment(reads, r.word, "env_assignment"); else if (!declaredNotPaths) pushAssignment(reads, r.word, provenance); } if (slotVerb !== undefined && argv !== undefined && segmentSelectsRecursiveRead(slotVerb, toks.slice(argvStart + 1).map((x) => x.folded))) { const roots = positional.length > 0 ? positional : stdinOperand ? [] : [readOfText(".")]; for (const root of roots) reads.push(bare(root, "traversal_root")); } for (let j = from; j < toks.length; j++) { if (launcherIndices.has(j) || j === argvStart) continue; if (noPathOperands && j > argvStart) break; if (toks[j].expands === "glob") reads.push(bare(toks[j], provenance)); } } function commandStringValue(options, toks, option) { const at = toks.findIndex((r) => r.folded === option); if (at < 0) return undefined; for (const spelling of options) { if (option === spelling) { const value = toks[at + 1]?.folded; return value === undefined ? undefined : { value, next: at + 2 }; } if (spelling.startsWith("--") && option.startsWith(`${spelling}=`)) return { value: option.slice(spelling.length + 1), next: at + 1 }; if (!spelling.startsWith("--") && option.startsWith(spelling) && option.length > spelling.length) return { value: option.slice(spelling.length), next: at + 1 }; } return undefined; } function literalBeats(toks) { const at = toks.findIndex((r) => r.folded === "in"); if (at < 0) return undefined; const words = toks.slice(at + 1); if (words.length === 0) return undefined; const only = words.length === 1 && words[0].word.pieces.length === 1 ? words[0].word.pieces[0].expansion : undefined; if (only !== undefined) { const m = only.kind === "command" ? /^\s*seq\s+(\d+)\s+(\d+)\s*$/.exec(only.body ?? "") : null; if (m === null) return undefined; const beats = Number(m[2]) - Number(m[1]) + 1; return beats >= 1 && beats <= POLL_LOOP_MAX_BEATS ? beats : undefined; } for (const w of words) if (w.expands !== false) return undefined; return words.length <= POLL_LOOP_MAX_BEATS ? words.length : undefined; } function cdMove(args, reads) { const target = args.find((t) => !t.folded.startsWith("-") || t.folded === "-"); if (target === undefined) return { target: "home" }; if (target.folded === "-") return { target: "previous" }; const c = bare(target, "cd_target"); reads.push(c); return { target: c }; } function listedVerbCandidates(name, argReads, reads) { if (NO_PATH_OPERAND_COMMANDS.has(name)) return; const args = argReads.map((r) => r.folded); const expanding = new Set(); argReads.forEach((r, k) => { if (!rewritten(r)) return; expanding.add(k); reads.push({ spelled: r.folded, raw: r.raw, provenance: "operand", expands: "unresolvable" }); }); const eoo = args.indexOf("--"); const firstOperandSlot = verbFirstOperandSlot(name); const firstOperandSuppliedByFlag = firstOperandSlot !== undefined && (eoo === -1 ? args : args.slice(0, eoo)).some((t) => suppliesFirstOperandSlot(firstOperandSlot, t)); const positional = []; let sawOperand = false; let stdinOperand = false; let endOfOptions = false; const permutes = !verbOptionsArePositionIndependent(name); const push = (k, text, provenance) => { reads.push(bare(argReads[k], provenance, text)); }; for (let k = 0; k < args.length; k++) { const t = args[k]; if (expanding.has(k)) continue; if (!endOfOptions) { if (t === "--") { endOfOptions = true; continue; } if (t.startsWith("-") && t !== "-") { const slot = readVerbOptionSlot(name, t); if (sawOperand && permutes) { push(k, t, "operand"); if (slot !== undefined && slot.form === "separated") { const ahead = args[k + 1]; if (ahead !== undefined && !expanding.has(k + 1)) for (const c of slotValueCandidates(ahead, slot.kind)) push(k + 1, c, "option_value"); } } if (slot !== undefined && slot.form !== "unexplained" && !(slot.form === "separated" && sawOperand && permutes)) { if (slot.form === "attached") { for (const c of slotValueCandidates(literalTildeSpelling(slot.value), slot.kind)) push(k, c, "option_value"); } else if (slot.form === "separated") { const value = args[++k]; if (value !== undefined && !expanding.has(k)) { const named = slotValueCandidates(value, slot.kind); for (const c of named) push(k, c, "option_value"); if (named.length > 0) positional.push(argReads[k]); } } continue; } for (const payload of attachedOptionPayloads(t)) push(k, payload, "option_value"); continue; } } const inFirstOperandSlot = firstOperandSlot !== undefined && !firstOperandSuppliedByFlag && !sawOperand; sawOperand = true; if (t === "-") { if (inFirstOperandSlot) continue; const m = RECURSIVE_READ_FORMS[name]; if (m === undefined || m.dashIsStdin === true) { stdinOperand = true; continue; } positional.push(argReads[k]); continue; } if (inFirstOperandSlot) { const named = slotValueCandidates(t, firstOperandSlot.kind); for (const c of named) push(k, c, "operand"); if (named.length > 0) positional.push(argReads[k]); continue; } positional.push(argReads[k]); push(k, t, "operand"); if (!ASSIGNMENT_VALUE_NOT_PATHS.has(name)) { pushAssignment(reads, argReads[k].word, "operand"); } } for (let k = 0; k < args.length; k++) { if (expanding.has(k)) continue; if (argReads[k].expands === "glob") reads.push(bare(argReads[k], "operand")); } if (segmentSelectsRecursiveRead(name, args)) { const roots = positional.length > 0 ? positional : stdinOperand ? [] : [readOfText(".")]; for (const root of roots) reads.push(bare(root, "traversal_root")); } }