@sema-agent/core
Version:
Stateless, task-oriented AI agent core
802 lines (801 loc) • 39.5 kB
JavaScript
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"));
}
}