@sema-agent/core
Version:
Stateless, task-oriented AI agent core
1,457 lines • 63.5 kB
JavaScript
import { literalTildeSpelling } from "../../shell/lex.js";
import { PROGRAM_ROWS } from "./program-read-model.rows.js";
export const DATUM_FORMS = Object.freeze(["count", "pattern", "name-pattern", "label", "message", "delimiter"]);
export const PATH_VALUE_FORMS = Object.freeze(["indirect", "form", "header", "url"]);
export const OPTION_MODES = Object.freeze(["banner", "interactive", "names-program", "follows", "repeats", "quiet", "forced", "queries"]);
export const ARG_ROLE_KINDS = Object.freeze(["path", "mount-source", "object-path", "program-text", "program-file", "shell-text", "command", "assignment", "binds-name", "entry-probe", "datum"]);
export const PATH_EFFECTS = Object.freeze(["read", "names", "write", "create", "edit-in-place"]);
export const WRITING_EFFECTS = new Set(["write", "create", "edit-in-place"]);
const ROWS_BY_KEY = new Map(PROGRAM_ROWS.map((r) => [r.key, r]));
export function programRow(name) {
const exact = ROWS_BY_KEY.get(name);
if (exact !== undefined)
return exact;
const stripped = name.replace(/\d+(?:\.\d+)*$/, "");
const row = stripped === name ? undefined : ROWS_BY_KEY.get(stripped);
return row?.versioned === true ? row : undefined;
}
export function programRows() {
return PROGRAM_ROWS;
}
export const LAUNCHER_NAMES = new Set(PROGRAM_ROWS.filter((r) => r.launcher !== undefined).map((r) => r.key));
export const BLOCK_WORD_NAMES = new Set(PROGRAM_ROWS.filter((r) => r.syntax === "block").map((r) => r.key));
export function runsShellText(name) {
return programRow(name)?.reentry?.kind === "shell";
}
export function feedsArgvFromStdin(name) {
return programRow(name)?.reentry?.kind === "argv";
}
export function streamDefaultProgram(name) {
const r = programRow(name)?.reentry;
return r?.kind === "argv" ? r.defaultProgram : undefined;
}
export function opensNoOperand(name) {
const rest = programRow(name)?.operands?.rest;
return rest?.kind === "datum" || rest?.kind === "entry-probe";
}
export function evaluatesOperands(name) {
return programRow(name)?.operands?.evaluates === true;
}
export function assignmentArgumentsAreNotPaths(name) {
return programRow(name)?.operands?.assignmentArguments === "not-paths";
}
export function isDeclarationBuiltin(name) {
return programRow(name)?.builtin === "declaration";
}
export function declaredAttributes(row, args) {
const out = new Set(row?.assigns !== undefined ? [row.assigns] : []);
for (const s of selectedOptions(row, args))
if (s.assigns !== undefined)
out.add(s.assigns);
if (row === undefined)
return out;
if (readArgRoles(row, args).some((e) => e.kind === "option" && e.reading === "speculative"))
out.add("unsettled");
const plus = row.grammars.find((g) => g.plusShort !== undefined)?.plusShort;
if (plus === undefined)
return out;
const end = args.indexOf("--");
for (const a of end < 0 ? args : args.slice(0, end)) {
if (/^\+./.test(a) && [...a.slice(1)].some((ch) => plus[ch] === undefined || plus[ch]?.assigns === "unsettled"))
out.add("unsettled");
}
return out;
}
export function isInertBuiltin(name) {
const b = programRow(name)?.builtin;
return b === "inert" || b === "declaration";
}
export function movesBaseByStack(name) {
return programRow(name)?.movesBase === "stack";
}
export function longOptionNameOf(tok) {
if (!tok.startsWith("--") || tok.length === 2)
return undefined;
const body = tok.slice(2);
const eq = body.indexOf("=");
const name = eq >= 0 ? body.slice(0, eq) : body;
return name.length > 0 ? name : undefined;
}
const abbreviates = (g) => g.acceptsAbbreviations !== false;
export function isLongOptionSpellingOf(name, full, accepts) {
return full === name || (accepts && full.startsWith(name));
}
const MERGED = new WeakMap();
function mergedTable(row, table) {
let byTable = MERGED.get(row);
if (byTable === undefined) {
byTable = new Map();
MERGED.set(row, byTable);
}
const cached = byTable.get(table);
if (cached !== undefined)
return cached;
const out = new Map();
const names = new Set(row.grammars.flatMap((g) => Object.keys(g[table] ?? {})));
for (const name of names) {
const declaring = row.grammars.filter((g) => g[table]?.[name] !== undefined);
const specs = declaring.map((g) => g[table][name]);
const arities = new Set(specs.map((s) => s.arity));
const widest = specs.reduce((a, b) => (roleRank(b.value) > roleRank(a.value) ? b : a));
out.set(name, { arity: arities.size === 1 ? specs[0].arity : "disputed", spec: widest, takesValueEverywhere: specs.every((s) => s.arity === 1), ownsRestEverywhere: specs.every((s) => s.arity !== 0), takesValueSomewhere: specs.some((s) => s.arity === 1), abbreviable: declaring.some(abbreviates) });
}
byTable.set(table, out);
return out;
}
function preciseRole(role) {
if (role === undefined)
return false;
return role.kind === "path" || role.kind === "datum" || role.kind === "mount-source" || role.kind === "object-path" || role.kind === "program-text" || role.kind === "program-file";
}
export const READ_LANGUAGES = new Set(["sed"]);
const PROGRAM_TEXT_OPENS = {
sed: "parsed",
arithmetic: "arithmetic",
jq: ["import", "include"],
awk: ["getline", "system", "ARGV", "ARGC", "@", "(", "|"],
bc: "never",
};
export function programTextOpensOf(language) {
return PROGRAM_TEXT_OPENS[language] ?? "any";
}
export function programTextMayOpen(language, text) {
const opens = programTextOpensOf(language);
if (opens === "any")
return true;
if (typeof opens === "string")
return false;
return opens.some((t) => (/^\w+$/.test(t) ? new RegExp(`(?<![\\w$])${t}(?!\\w)`).test(text) : text.includes(t)));
}
function roleRank(role) {
if (role?.kind !== "path")
return 0;
switch (role.valueForm) {
case "indirect": return 5;
case undefined: return 4;
case "form": return 3;
case "header": return 2;
case "url": return 1;
}
}
function viewTable(row, table, view) {
const out = new Map();
for (const [name, m] of mergedTable(row, table)) {
const role = m.spec.value;
const spec = m.spec;
const abbreviable = m.abbreviable;
if (view === "roles") {
if (m.arity === 0)
out.set(name, { arity: 0, spec, abbreviable });
else if (m.arity === 1)
out.set(name, { arity: 1, spec, abbreviable, ...(role !== undefined ? { role } : {}) });
else if (m.arity === "suffix")
out.set(name, { arity: "suffix", spec, abbreviable, ...(role !== undefined ? { role } : {}) });
else if (m.arity === "attached" || (m.arity === "disputed" && table === "long"))
out.set(name, { arity: "attached", spec, abbreviable, ...(role !== undefined ? { role } : {}) });
continue;
}
if (m.arity === 0)
out.set(name, { arity: 0, spec, abbreviable });
else if (m.arity === "disputed") {
if (table === "long" && preciseRole(role))
out.set(name, { arity: "attached", role: role, spec, abbreviable });
}
else if (m.arity === 1 && preciseRole(role))
out.set(name, { arity: 1, role: role, spec, abbreviable });
else if (m.arity === "attached" && table === "long" && preciseRole(role))
out.set(name, { arity: "attached", role: role, spec, abbreviable });
}
return out;
}
function readOptionIn(row, tok, view, next) {
if (!tok.startsWith("-") || tok === "-" || tok === "--")
return { form: "unexplained" };
if (tok.startsWith("--")) {
const long = longOptionNameOf(tok);
if (long === undefined)
return { form: "unexplained" };
const longs = viewTable(row, "long", view);
const eq = tok.indexOf("=");
const attached = eq >= 0 ? tok.slice(eq + 1) : undefined;
const withValue = (role, spec) => (attached !== undefined ? { form: "attached", value: attached, role, ...(spec !== undefined ? { spec } : {}) } : { form: "separated", role, ...(spec !== undefined ? { spec } : {}) });
const exact = longs.get(long);
if (exact !== undefined) {
if (exact.arity === 1)
return withValue(exact.role, exact.spec);
if (exact.arity === "attached")
return attached !== undefined ? { form: "attached", value: attached, role: exact.role, spec: exact.spec } : { form: "no-value" };
return attached === undefined ? { form: "no-value" } : { form: "unexplained" };
}
const reach = [...longs.entries()].filter(([full, s]) => isLongOptionSpellingOf(long, full, s.abbreviable));
const valueNames = reach.filter(([, s]) => s.arity === 1 || (attached !== undefined && s.arity === "attached"));
const plainNames = reach.filter(([, s]) => s.arity === 0 || (attached === undefined && s.arity === "attached"));
if (valueNames.length === 0)
return plainNames.length > 0 && attached === undefined ? { form: "no-value" } : { form: "unexplained" };
const roles = valueNames.map(([, s]) => s.role);
const only = valueNames.length === 1 ? valueNames[0][1].spec : undefined;
if (attached !== undefined)
return { form: "attached", value: attached, role: roles.reduce((a, b) => (roleRank(b) > roleRank(a) ? b : a)), ...(only !== undefined ? { spec: only } : {}) };
if (plainNames.length > 0 || new Set(roles.map(roleKey)).size !== 1)
return { form: "unexplained" };
return withValue(roles[0], only);
}
const shorts = viewTable(row, "short", view);
const dashWords = viewTable(row, "dashWords", view);
const word = dashWords.size > 0 ? /^-([A-Za-z_][\w-]+)$/.exec(tok)?.[1] : undefined;
if (word !== undefined) {
if (shorts.get(word[0])?.arity === 1)
return { form: "unexplained" };
const spec = dashWords.get(word);
if (spec?.arity === 1)
return { form: "separated", role: spec.role, spec: spec.spec };
if (spec?.arity === 0)
return { form: "no-value" };
return { form: "unexplained" };
}
for (let i = 1; i < tok.length; i++) {
const spec = shorts.get(tok[i]);
if (spec?.arity === 1) {
const rest = tok.slice(i + 1);
return rest !== "" ? { form: "attached", value: rest, role: spec.role, spec: spec.spec } : { form: "separated", role: spec.role, spec: spec.spec };
}
if (spec?.arity === "attached" || spec?.arity === "suffix") {
const rest = tok.slice(i + 1);
if (rest !== "")
return { form: "attached", value: rest, role: spec.role, spec: spec.spec };
return spec.arity === "suffix" && (view === "roles" || suffixSpelled(next)) ? { form: "separated", role: spec.role, spec: spec.spec } : { form: "no-value" };
}
if (spec?.arity !== 0)
return { form: "unexplained" };
}
return { form: "no-value" };
}
export function readOptionSlot(row, tok) {
const r = readOptionIn(row, tok, "candidates");
if (r.form === "attached" || r.form === "separated")
return r.role === undefined ? { form: "unexplained" } : { ...r, role: r.role };
return r;
}
export function readOptionRole(row, tok, next) {
return readOptionIn(row, tok, "roles", next);
}
function suffixSpelled(tok) {
return tok !== undefined && (tok === "" || tok.startsWith("."));
}
const roleKey = (r) => (r === undefined ? "?" : r.kind === "path" ? `path:${r.valueForm ?? ""}` : JSON.stringify(r));
export function firstOperandSlot(row, view = "candidates") {
const role = row?.operands?.positional?.[0];
if (row === undefined || role === undefined || (view === "candidates" && !preciseRole(role)))
return undefined;
const shorts = [...mergedTable(row, "short")];
const letters = shorts.filter(([, m]) => m.spec.fillsSlot === true).map(([l]) => l).join("");
const valueLetters = shorts.filter(([, m]) => m.spec.fillsSlot !== true && m.ownsRestEverywhere).map(([l]) => l).join("");
const supplying = [...mergedTable(row, "long")].filter(([, m]) => m.spec.fillsSlot === true);
return { role, suppliedByLetters: letters, suppliedByLongNames: supplying.map(([n]) => n), abbreviableLongNames: supplying.filter(([, m]) => m.abbreviable).map(([n]) => n), valueLetters };
}
export function suppliesFirstOperandSlot(slot, tok) {
if (tok.startsWith("--")) {
const long = longOptionNameOf(tok);
return long !== undefined && slot.suppliedByLongNames.some((full) => isLongOptionSpellingOf(long, full, slot.abbreviableLongNames.includes(full)));
}
if (!tok.startsWith("-"))
return false;
for (let i = 1; i < tok.length; i++) {
const ch = tok[i];
if (!/[A-Za-z0-9]/.test(ch))
return false;
if (slot.suppliedByLetters.includes(ch))
return true;
if (slot.valueLetters.includes(ch))
return false;
}
return false;
}
export function optionsStandAnywhere(row) {
return row !== undefined && row.grammars.length > 0 && row.grammars.every((g) => g.afterFirstOperand === "options");
}
function localFileUrlPath(value) {
const m = /^file:(?:\/\/[^/?#]*)?(\/[^?#]*)/i.exec(value.trim());
if (m === null)
return undefined;
const raw = m[1];
try {
return decodeURIComponent(raw);
}
catch {
return raw;
}
}
function formSpecFileNames(value) {
const out = [];
const after = (s) => {
const at = s.search(/[@<]/);
if (at !== -1 && at < s.length - 1)
out.push(s.slice(at + 1));
};
after(value);
for (const part of value.split(/[;,]/))
after(part);
return [...new Set(out)];
}
export const PROGRAM_GLOB_MAX = 256;
function globRange(text) {
const close = text.indexOf("]");
if (close < 0)
return undefined;
const body = text.slice(0, close);
const letters = /^([A-Za-z])-(.)(?::\s*\+?(\d+))?$/.exec(body);
if (letters !== null) {
const lo = letters[1].charCodeAt(0), hi = letters[2].charCodeAt(0), step = letters[3] !== undefined ? Number(letters[3]) : 1;
if (step === 0 || (lo === hi && step !== 1) || (lo !== hi && (lo > hi || step > hi - lo || hi - lo > 25)))
return undefined;
const values = [];
for (let c = lo; c <= hi && values.length <= PROGRAM_GLOB_MAX; c += step)
values.push(String.fromCharCode(c));
return { values, length: close + 1 };
}
const digits = /^(\d+)-[ \t]*(\d+)(?::\s*\+?(\d+))?$/.exec(body);
if (digits === null)
return undefined;
const pad = digits[1].startsWith("0") ? digits[1].length : 0;
const lo = Number(digits[1]), hi = Number(digits[2]), step = digits[3] !== undefined ? Number(digits[3]) : 1;
if (!Number.isSafeInteger(lo) || !Number.isSafeInteger(hi) || step === 0 || (lo === hi && step !== 1) || (lo !== hi && (lo > hi || step > hi - lo)))
return undefined;
const values = [];
for (let n = lo; n <= hi && values.length <= PROGRAM_GLOB_MAX; n += step)
values.push(String(n).padStart(pad, "0"));
return { values, length: close + 1 };
}
const IPV6_LITERAL = /^\[[0-9A-Fa-f.]*:[0-9A-Fa-f.]*:[0-9A-Fa-f:.]*(?:%[^\]]*)?\]/;
export function programGlobExpansions(value) {
let out = [""];
const append = (alternatives) => {
out = out.flatMap((head) => alternatives.map((tail) => head + tail));
return out.length <= PROGRAM_GLOB_MAX;
};
let literal = "";
let i = 0;
while (i < value.length) {
const ch = value[i];
if (ch === "\\" && "{[}]".includes(value[i + 1] ?? "\u0000")) {
literal += value[i + 1];
i += 2;
continue;
}
if (ch === "}" || ch === "]")
return undefined;
if (ch === "[" && IPV6_LITERAL.test(value.slice(i))) {
const end = value.indexOf("]", i) + 1;
literal += value.slice(i, end);
i = end;
continue;
}
if (ch !== "{" && ch !== "[") {
literal += ch;
i++;
continue;
}
if (!append([literal]))
return undefined;
literal = "";
if (ch === "[") {
const range = globRange(value.slice(i + 1));
if (range === undefined)
return undefined;
if (!append(range.values))
return undefined;
i += 1 + range.length;
continue;
}
const set = [];
let element = "";
let j = i + 1;
for (;; j++) {
const c = value[j];
if (c === undefined || c === "{" || c === "[" || c === "]")
return undefined;
if (c === "}") {
if (j === i + 1)
return undefined;
set.push(element);
break;
}
if (c === ",") {
set.push(element);
element = "";
continue;
}
if (c === "\\" && value[j + 1] !== undefined) {
element += value[++j];
continue;
}
element += c;
}
if (!append(set))
return undefined;
i = j + 1;
}
return append([literal]) ? out : undefined;
}
export function slotValueCandidates(value, role, globs = "expanded") {
if (role.kind === "mount-source")
return mountSourceOf(value, role.form);
if (role.kind === "program-file")
return [value];
if (role.kind !== "path")
return [];
if (role.kind === "path" && role.programGlob === true && globs === "expanded") {
const values = programGlobExpansions(value);
if (values === undefined)
return undefined;
const out = [];
for (const v of values)
for (const c of slotValueCandidates(v, role, "literal") ?? [])
if (!out.includes(c))
out.push(c);
return out;
}
const form = role.kind === "path" ? role.valueForm : undefined;
if (form === "url") {
const local = localFileUrlPath(value);
return local !== undefined ? [local] : [];
}
if (form === "form")
return formSpecFileNames(value).map(literalTildeSpelling);
const named = value.startsWith("@") && value.length > 1 ? literalTildeSpelling(value.slice(1)) : undefined;
if (form === "header")
return named !== undefined ? [value, named] : [];
const out = [value];
if (form === "indirect" && named !== undefined)
out.push(named);
return out;
}
function mountSourceOf(value, form) {
const source = form === "volume" ? value.split(":")[0] : value.split(",").map((kv) => /^(?:source|src)=(.*)$/i.exec(kv)?.[1]).find((v) => v !== undefined) ?? "";
return source !== "" && (source.includes("/") || source.startsWith(".") || source.startsWith("~")) ? [source] : [];
}
export function objectPathsOf(value) {
if (/^[A-Za-z][\w+.-]*:\/\//.test(value))
return [];
const out = [];
for (let i = value.indexOf(":"); i >= 0; i = value.indexOf(":", i + 1)) {
const path = value.slice(i + 1);
if (path !== "" && !out.includes(path))
out.push(path);
}
return out;
}
function walkArgs(row, words, readOption, slot, readsRole, ownedValue) {
const out = [];
const valueAt = new Set();
const folded = words.map((w) => w.folded);
const unsettling = (at, anyOption) => (folded[at] === "--" || (anyOption && folded[at].startsWith("-") && folded[at] !== "-") ? { unsettled: true } : {});
const plain = slot === undefined ? [] : walkArgs(row, words, readOption, undefined, readsRole);
const eoo = plain.find((e) => e.kind === "end-of-options")?.at ?? -1;
const operandsPast = new Set(plain.flatMap((e) => (e.kind === "dash-word-operand" ? [e.at] : [])));
const notOptions = new Set(plain.flatMap((e) => (e.kind === "value" && (e.consumed || operandsPast.has(e.of)) ? [e.at] : [])));
const slotSupplied = slot !== undefined && folded.some((t, k) => (eoo === -1 || k < eoo) && !notOptions.has(k) && suppliesFirstOperandSlot(slot, t));
const permutes = !optionsStandAnywhere(row);
const dashRead = row?.operands?.dash === "file" ? "file" : "stdin";
const plus = row?.grammars.some((g) => g.plusOptions === true) === true;
let sawOperand = false;
let ordinal = 0;
let endOfOptions = false;
for (let k = 0; k < words.length; k++) {
if (words[k].skip === true)
continue;
const t = plus && /^\+[^+]/.test(folded[k]) ? `-${folded[k].slice(1)}` : folded[k];
if (!endOfOptions && t === "--") {
endOfOptions = true;
out.push({ kind: "end-of-options", at: k });
continue;
}
if (!endOfOptions && t.startsWith("-") && t !== "-") {
const r = row === undefined ? { form: "unexplained" } : readOption(t, folded[k + 1]);
const both = sawOperand && permutes;
if (both) {
out.push({ kind: "dash-word-operand", at: k });
if (r.form === "separated" && k + 1 < words.length) {
out.push({ kind: "value", at: k + 1, of: k, role: r.role, consumed: false, ...(r.spec !== undefined ? { spec: r.spec } : {}), ...unsettling(k + 1, false) });
valueAt.add(k + 1);
}
}
const owned = r.form === "no-value" || r.form === "unexplained" ? (valueAt.has(k) ? undefined : ownedValue?.(t, folded[k + 1])) : undefined;
const maybeValue = () => {
if (owned !== undefined && k + 1 < words.length) {
out.push({ kind: "value", at: k + 1, of: k, role: owned.role, consumed: false, ...(owned.spec !== undefined ? { spec: owned.spec } : {}), ...unsettling(k + 1, true) });
valueAt.add(k + 1);
}
};
if (r.form === "no-value") {
out.push({ kind: "option", at: k, reading: "no-value" });
maybeValue();
continue;
}
if (r.form === "attached") {
out.push({ kind: "option", at: k, reading: { attached: r.value, role: r.role, ...(r.spec !== undefined ? { spec: r.spec } : {}) } });
continue;
}
if (r.form === "separated" && !both) {
if (k + 1 < words.length)
out.push({ kind: "value", at: k + 1, of: k, role: r.role, consumed: true, ...(r.spec !== undefined ? { spec: r.spec } : {}) });
k++;
continue;
}
out.push({ kind: "option", at: k, reading: "speculative" });
maybeValue();
continue;
}
const inSlot = slot !== undefined && !slotSupplied && !sawOperand;
sawOperand = true;
const declared = inSlot ? undefined : slot !== undefined ? row?.operands?.positional?.[slotSupplied ? ordinal + 1 : ordinal] ?? row?.operands?.rest : row?.operands?.rest;
const role = declared !== undefined && readsRole(declared) ? declared : undefined;
out.push({ kind: "operand", at: k, ordinal: ordinal++, ...(inSlot ? { slot: slot.role } : {}), ...(role !== undefined ? { role } : {}), ...(t === "-" ? { dash: dashRead } : {}) });
}
return out;
}
export function readArgs(row, words, opts) {
const slot = opts.firstOperandSlot === false ? undefined : firstOperandSlot(row);
const owned = opts.ownedValues === true && row?.traversal?.discloses === "contents"
? (tok, next) => {
const v = bareOptionValueOf(row, tok, next);
return v !== undefined && preciseRole(v.role) ? v : undefined;
}
: undefined;
return walkArgs(row, words, (tok) => readOptionSlot(row, tok), opts.firstOperandSlot === "program-text" && slot?.role.kind !== "program-text" ? undefined : slot, preciseRole, owned);
}
export function readArgRoles(row, args) {
return walkArgs(row, args.map((folded) => ({ folded })), (tok, next) => readOptionRole(row, tok, next), firstOperandSlot(row, "roles"), () => true);
}
function keyWordIndex(row, args) {
if (row.traversal?.keyWordLetters === undefined)
return undefined;
const at = args.findIndex((t) => t.length > 0);
return at >= 0 && /^[A-Za-z]+$/.test(args[at]) ? at : undefined;
}
export function deliversOperands(row, args) {
if (row === undefined)
return false;
const key = keyWordIndex(row, args);
if (key !== undefined) {
const shorts = mergedTable(row, "short");
if ([...args[key]].some((ch) => shorts.get(ch)?.spec.delivers === "operands"))
return true;
}
return selectedOptions(row, key === undefined ? args : args.filter((_, k) => k !== key)).some((s) => s.delivers === "operands");
}
export function selectsContentsTraversal(row, args) {
const model = row?.traversal;
if (row === undefined || model === undefined || model.discloses !== "contents")
return false;
if (model.always === true)
return true;
if (model.keyWordLetters !== undefined) {
const at = keyWordIndex(row, args);
if (at !== undefined && [...args[at]].some((ch) => model.keyWordLetters.includes(ch)))
return true;
}
const shorts = mergedTable(row, "short");
const longs = mergedTable(row, "long");
let endOfOptions = false;
for (let k = 0; k < args.length; k++) {
const t = args[k];
if (endOfOptions)
continue;
if (t === "--") {
const prev = k > 0 ? args[k - 1] : undefined;
const prevMayOwnValue = prev !== undefined && prev.startsWith("--") && prev.length > 2 && !prev.includes("=");
if (!prevMayOwnValue)
endOfOptions = true;
continue;
}
if (t.startsWith("--")) {
const long = longOptionNameOf(t);
if (long === undefined)
continue;
if ([...longs].some(([full, m]) => m.spec.selects === "traversal" && isLongOptionSpellingOf(long, full, m.abbreviable)))
return true;
for (const [full, m] of longs) {
const recursive = m.spec.selectsWhenValue;
if (recursive === undefined || !isLongOptionSpellingOf(long, full, m.abbreviable))
continue;
const eq = t.indexOf("=");
const v = eq >= 0 ? t.slice(eq + 1) : args[k + 1];
if (v !== undefined && v.length > 0 && recursive.startsWith(v.toLowerCase()))
return true;
}
continue;
}
if (!t.startsWith("-") || t === "-")
continue;
for (let i = 1; i < t.length; i++) {
const ch = t[i];
const m = shorts.get(ch);
if (m?.spec.selects === "traversal")
return true;
if (m?.spec.selectsWhenValue !== undefined) {
const v = i === t.length - 1 ? args[k + 1] : t.slice(i + 1);
if (v !== undefined && v.length > 0 && m.spec.selectsWhenValue.startsWith(v.toLowerCase()))
return true;
if (i === t.length - 1)
k++;
break;
}
if (m?.takesValueEverywhere === true) {
if (i === t.length - 1)
k++;
break;
}
if (!/[A-Za-z0-9]/.test(ch))
break;
}
}
return false;
}
export function walksCwdWithoutOperand(row) {
return row !== undefined && row.grammars.some((g) => g.walksCwdWithoutOperand === true);
}
export function valuesAreLiteral(row, args) {
if (row === undefined || !row.grammars.some((g) => [...Object.values(g.short), ...Object.values(g.long)].some((s) => s.literalValues === true)))
return false;
const shorts = mergedTable(row, "short");
const longs = mergedTable(row, "long");
const negates = (t) => {
const name = t.startsWith("--no-") ? longOptionNameOf(t)?.slice(3) : undefined;
return name !== undefined && name.length > 0 && [...longs].some(([full, m]) => m.spec.literalValues === true && isLongOptionSpellingOf(name, full, m.abbreviable));
};
if (args.some(negates))
return false;
let doubt = false;
for (const e of readArgRoles(row, args)) {
if (e.kind === "option" && e.reading === "speculative") {
doubt = true;
continue;
}
const at = e.kind === "option" ? e.at : e.kind === "value" ? e.of : undefined;
if (at === undefined || doubt)
continue;
const t = args[at];
if (t.startsWith("--")) {
const long = longOptionNameOf(t);
if (long === undefined || t.includes("="))
continue;
const exact = longs.get(long);
const hits = exact !== undefined ? [exact] : [...longs].filter(([full, m]) => isLongOptionSpellingOf(long, full, m.abbreviable)).map(([, m]) => m);
if (hits.length === 1 && hits[0].spec.literalValues === true)
return true;
continue;
}
for (const ch of t.slice(1)) {
const m = shorts.get(ch);
if (m === undefined || m.arity !== 0)
break;
if (m.spec.literalValues === true)
return true;
}
}
return false;
}
export function dashOperandIsStdin(row) {
return row?.operands?.dash !== "file";
}
export function declaresOption(row, pred) {
return row.grammars.some((g) => [...Object.values(g.short), ...Object.values(g.long), ...Object.values(g.dashWords ?? {})].some(pred));
}
const isDatumValue = (m) => m.spec.value?.kind === "datum" && m.spec.fillsSlot !== true;
const isFileValue = (spec) => spec.value === undefined || spec.value.kind === "path" || spec.value.kind === "program-file";
export function takesSeparatedDatum(row, tok) {
if (row === undefined)
return false;
if (tok.startsWith("--")) {
if (tok.includes("="))
return false;
const name = longOptionNameOf(tok);
return name !== undefined && [...mergedTable(row, "long")].some(([full, m]) => isLongOptionSpellingOf(name, full, m.abbreviable) && m.takesValueSomewhere && isDatumValue(m));
}
if (!/^-[A-Za-z0-9]/.test(tok))
return false;
const shorts = mergedTable(row, "short");
for (let i = 1; i < tok.length; i++) {
const ch = tok[i];
const m = shorts.get(ch);
if (m !== undefined && m.arity !== 0)
return m.takesValueSomewhere && isDatumValue(m) && i === tok.length - 1;
if (!/[A-Za-z0-9]/.test(ch))
return false;
}
return false;
}
const COUNT_SPELLING = /^[+-]?[0-9_]+$/;
const UNDECLARED_VALUE = { kind: "datum", form: "label" };
export function bareOptionValueOf(row, tok, next) {
if (next === undefined || !tok.startsWith("-") || tok === "-" || tok === "--")
return undefined;
if (tok.startsWith("--") && !tok.includes("=")) {
const name = longOptionNameOf(tok);
if (name !== undefined && ![...mergedTable(row, "long")].some(([full, m]) => isLongOptionSpellingOf(name, full, m.abbreviable)))
return { role: UNDECLARED_VALUE };
}
const owns = (s) => s.value !== undefined && (s.arity === 1 || (s.arity === "attached" && s.value.kind === "datum" && s.value.form === "count" && COUNT_SPELLING.test(next)));
for (const g of row.grammars) {
let found;
if (tok.startsWith("--")) {
const name = longOptionNameOf(tok);
if (name === undefined || tok.includes("="))
continue;
const exact = g.long[name];
const reach = exact !== undefined ? [exact] : Object.entries(g.long).filter(([full]) => isLongOptionSpellingOf(name, full, abbreviates(g))).map(([, s]) => s);
if (reach.length > 0 && reach.every(owns))
found = reach[0];
}
else {
for (let i = 1; i < tok.length; i++) {
const spec = g.short[tok[i]];
if (spec === undefined || spec.arity === 0)
continue;
if (i === tok.length - 1 && owns(spec))
found = spec;
break;
}
}
if (found?.value !== undefined)
return { role: found.value, spec: found };
}
return undefined;
}
export function attachedStdinFile(row, tok) {
if (row === undefined)
return false;
if (tok.startsWith("--")) {
const eq = tok.indexOf("=");
const name = longOptionNameOf(tok);
if (eq < 0 || tok.slice(eq + 1) !== "-" || name === undefined)
return false;
const longs = mergedTable(row, "long");
const exact = longs.get(name);
const reach = exact !== undefined ? [exact] : [...longs].filter(([full, m]) => isLongOptionSpellingOf(name, full, m.abbreviable)).map(([, m]) => m);
return reach.length === 0 || reach.some((m) => m.arity !== 0 && isFileValue(m.spec));
}
if (!/^-[A-Za-z0-9]/.test(tok))
return false;
const shorts = mergedTable(row, "short");
for (let i = 1; i < tok.length; i++) {
const ch = tok[i];
const m = shorts.get(ch);
if (m !== undefined && m.arity !== 0)
return tok.slice(i + 1) === "-" && isFileValue(m.spec);
if (!/[A-Za-z0-9]/.test(ch))
return false;
}
return false;
}
const IMPLEMENTATIONS = new WeakMap();
export function implementationsOf(row) {
let views = IMPLEMENTATIONS.get(row);
if (views === undefined) {
views = row.grammars.length <= 1 ? [row] : row.grammars.map((g) => ({ ...row, grammars: [g] }));
IMPLEMENTATIONS.set(row, views);
}
return views;
}
export function selectedOptions(row, args, under) {
return row === undefined ? [] : [...scanOptions(row, args, under === "one-implementation").specs];
}
export function selectedSpellings(row, args) {
const scan = scanOptions(row, args, true);
return { names: scan.names, values: scan.values };
}
export function implementationRejects(row, view, words) {
if (view === row)
return false;
const declares = (n) => (n.startsWith("--") ? mergedTable(view, "long").has(n.slice(2)) : mergedTable(view, n.length === 2 ? "short" : "dashWords").has(n.slice(1)));
return selectedSpellings(row, words).names.some((n) => !declares(n));
}
function scanOptions(row, args, one) {
const stopsAtOperand = one && row.grammars[0]?.afterFirstOperand === "operands";
const shorts = mergedTable(row, "short");
const longs = mergedTable(row, "long");
const dashWords = mergedTable(row, "dashWords");
const obsolete = row.grammars.flatMap((g) => (g.obsoleteFirst === undefined ? [] : [g.obsoleteFirst]));
const specs = [];
const names = [];
const values = [];
const operands = [];
const select = (spec, name) => { specs.push(spec); names.push(name); };
let endOfOptions = false;
for (let k = 0; k < args.length; k++) {
const t = args[k];
if (endOfOptions) {
operands.push(k);
continue;
}
const traditional = k === 0 || !one ? obsolete.filter((o) => o.signs.includes(t[0] ?? "") && /^.\d+[A-Za-z]*$/.test(t)) : [];
if (traditional.length > 0) {
for (const o of traditional)
for (const ch of t.replace(/^.\d+/, "")) {
const spec = o.letters[ch];
if (spec !== undefined)
select(spec, `-${ch}`);
}
continue;
}
if (t === "--") {
if (one)
endOfOptions = true;
continue;
}
if (!t.startsWith("-") || t === "-") {
operands.push(k);
if (stopsAtOperand)
endOfOptions = true;
continue;
}
const take = (spec, name, attached) => {
if (attached !== undefined) {
values.push({ at: k, value: attached, spec, name });
return;
}
if (one && k + 1 < args.length) {
values.push({ at: k + 1, spec, name });
k++;
}
};
if (t.startsWith("--")) {
const name = longOptionNameOf(t);
if (name === undefined)
continue;
const eq = t.indexOf("=");
const exact = longs.get(name);
const hits = exact !== undefined ? [[name, exact]] : [...longs].filter(([full, m]) => isLongOptionSpellingOf(name, full, m.abbreviable));
for (const [full, m] of hits)
select(m.spec, `--${full}`);
const [full, only] = hits[0] ?? [];
if (hits.length === 1 && (only.arity === 1 || (eq >= 0 && only.arity === "attached")))
take(only.spec, `--${full}`, eq >= 0 ? t.slice(eq + 1) : undefined);
else if (one && eq < 0 && hits.length > 1 && hits.every(([, m]) => m.arity === 1))
k++;
continue;
}
const word = /^-([A-Za-z_][\w-]+)$/.exec(t)?.[1];
const dash = word === undefined ? undefined : dashWords.get(word);
if (dash !== undefined) {
select(dash.spec, t);
if (one) {
if (dash.arity === 1)
take(dash.spec, t, undefined);
continue;
}
}
for (let i = 1; i < t.length; i++) {
const m = shorts.get(t[i]);
if (m === undefined) {
if (/[A-Za-z0-9]/.test(t[i]))
continue;
break;
}
select(m.spec, `-${t[i]}`);
if (m.arity === 0)
continue;
const rest = t.slice(i + 1);
if (rest !== "")
values.push({ at: k, value: rest, spec: m.spec, name: `-${t[i]}` });
else if (m.arity === 1 || m.arity === "suffix")
take(m.spec, `-${t[i]}`, undefined);
break;
}
}
return { specs, names, values, operands };
}
export function writesPath(role) {
return role?.kind === "path" && WRITING_EFFECTS.has(role.effect);
}
export function writeReadings(row, args) {
return implementationsOf(row).map((view) => {
const ops = view.operands;
const scan = scanOptions(view, args, true);
const inPlace = scan.specs.some((s) => s.selects === "edit-in-place");
const unstated = scan.specs.some((s) => s.effect !== "none" && s.selects !== "edit-in-place" && s.writesAt === undefined && s.fillsLast !== true && !writesPath(s.value));
const ordinary = inPlace && ops?.editedInPlace !== undefined ? ops.editedInPlace : ops?.rest;
const operandRole = writesPath(ordinary) ? ordinary : writesPath(ops?.last) ? ops.last : undefined;
const suffixes = scan.values.flatMap((v) => (v.spec.selects === "edit-in-place" ? [v.value ?? args[v.at]] : [])).filter((x) => x !== "");
const written = [];
let unexplained = unstated;
for (const e of readArgRoles(view, args)) {
if (e.kind === "option") {
if (e.reading === "speculative")
unexplained = true;
else if (e.reading !== "no-value" && writesPath(e.reading.role))
written.push({ at: e.at, value: e.reading.attached, role: e.reading.role });
}
else if (e.kind === "value") {
if (writesPath(e.role))
written.push({ at: e.at, role: e.role });
}
else if (e.kind === "operand") {
if (e.dash === "stdin" || e.slot !== undefined)
continue;
const role = e.role ?? ordinary;
if (writesPath(role))
written.push({ at: e.at, role });
}
else if (e.kind === "dash-word-operand") {
if (writesPath(ordinary))
written.push({ at: e.at, role: ordinary });
}
}
let copy;
const last = ops?.last;
if (writesPath(last)) {
const filled = scan.values.filter((v) => v.spec.fillsLast === true).at(-1);
const destination = filled !== undefined ? { at: filled.at, ...(filled.value !== undefined ? { value: filled.value } : {}) } : scan.operands.length > 0 ? { at: scan.operands.at(-1) } : undefined;
if (destination !== undefined) {
copy = { destination, sources: scan.operands.filter((at) => at !== destination.at) };
for (let i = written.length - 1; i >= 0; i--)
if (written[i].at === destination.at && written[i].value === destination.value)
written.splice(i, 1);
written.push({ at: destination.at, ...(destination.value !== undefined ? { value: destination.value } : {}), role: last });
}
}
return { written, ...(operandRole !== undefined ? { operandRole } : {}), unexplained, suffixes, ...(copy !== undefined ? { copy } : {}) };
});
}
export function programDelivery(row, args) {
const delivery = row?.delivery;
if (row === undefined || delivery === undefined)
return undefined;
const longs = mergedTable(row, "long");
const dashWords = mergedTable(row, "dashWords");
const shorts = mergedTable(row, "short");
const namesByValue = (spec) => (spec.value?.kind === "program-file" && spec.fillsSlot === true) || (spec.mode === "names-program" && spec.arity === 1);
const positional = row.operands?.positional ?? [];
const programAt = positional.findIndex((r) => r?.kind === "program-text" || r?.kind === "program-file");
const programIsText = programAt >= 0 && positional[programAt].kind === "program-text";
let endOfOptions = false;
let banner = false;
let interactive = false;
let named = false;
let stdin = false;
let explained = true;
let operands = 0;
scan: for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (!endOfOptions && arg === "--") {
endOfOptions = true;
continue;
}
if (arg === "-") {
if (!programIsText)
stdin = true;
continue;
}
if (!endOfOptions && arg.startsWith("-")) {
const eq = arg.indexOf("=");
const whole = eq < 0 ? arg : arg.slice(0, eq);
const valueOf = () => (eq < 0 ? args[++i] : arg.slice(eq + 1));
const spec = (whole.startsWith("--") ? longs.get(whole.slice(2)) : whole.length > 2 ? dashWords.get(whole.slice(1)) : undefined)?.spec;
if (spec !== undefined) {
if (spec.value?.kind === "program-text")
return "text";
if (namesByValue(spec)) {
const value = valueOf();
if (value === "-") {
stdin = true;
continue;
}
if (value !== undefined && row.modules?.[value]?.kind === "program-text")
return "text";
named = true;
if (explained && spec.endsOptions === true)
break scan;
continue;
}
if (spec.mode === "names-program") {
named = true;
continue;
}
if (spec.mode === "banner") {
banner = true;
continue;
}
if (spec.arity === 1) {
valueOf();
continue;
}
continue;
}
if (arg.startsWith("--")) {
if (eq < 0)
explained = false;
continue;
}
for (let k = 1; k < arg.length; k++) {
const ch = arg[k];
if (ch >= "0" && ch <= "9")
continue;
const s = shorts.get(ch)?.spec;
if (s?.value?.kind === "program-text")
return "text";
if (s !== undefined && namesByValue(s)) {
const value = k + 1 < arg.length ? arg.slice(k + 1) : args[++i];
if (value === "-") {
stdin = true;
break;
}
if (explained && value !== undefined && row.modules?.[value]?.kind === "program-text")
return "text";
if (!explained)
break;
named = true;
if (explained && s.endsOptions === true)
break scan;
break;
}
if (s?.arity === "attached") {
if (s.mode === "banner")
banner = true;
break;
}
if (s?.mode === "banner") {
banner = true;
continue;
}
if (s?.mode === "interactive") {
interactive = true;
continue;
}
if (s?.arity === 1) {
if (k + 1 >= arg.length)
i++;
break;
}
if (s?.arity !== 0)
explained = false;
}
continue;
}
operands++;
if (!named && explained && programAt >= 0 && operands === programAt + 1) {
if (programIsText)
return "text";
named = true;
break;
}
}
if (!named && !explained && programIsText && operands > programAt)
return "text";
if (stdin || interactive)
return "stdin";
if (named || banner)
return undefined;
return delivery.stdinWhenUnnamed ? "stdin" : undefined;
}
export function programLanguageOf(row) {
if (row?.delivery === undefined)
return undefined;
const roles = [...(row.operands?.positional ?? []), ...row.grammars.flatMap((g) => [...Object.values(g.short), ...Object.values(g.long), ...Object.values(g.dashWords ?? {})].map((s) => s.value))];
for (const r of roles)
if (r?.kind === "program-text" || r?.kind === "program-file")
return r.language;
return undefined;
}
export function wordsReadBySomeImplementations(row, args) {
if (row === undefined || row.grammars.length < 2)
return new Set();
const opened = (role) => role === undefined || (role.kind === "path" && (role.effect === "read" || role.effect === "edit-in-place")) || role.kind === "program-file";
const readings = [];
for (const view of implementationsOf(row)) {
const reads = new Set();
for (const e of readArgRoles(view, args)) {
if (e.kind === "option" && e.reading === "speculative")
return new Set();
if (e.kind === "operand") {
if (e.slot === undefined && e.dash === undefined && opened(e.role))
reads.add(e.at);
}
else if (e.kind === "value") {
if (e.role !== undefined && opened(e.role) && e.consumed)
reads.add(e.at);
}
}
readings.push(reads);
}
const some = new Set(readings.flatMap((r) => [...r]));
return new Set([...some].filter((k) => !readings.every((r) => r.has(k))));
}
export function moduleTailOf(row, args) {
if (row?.modules === undefined)
return undefined;
for (const e of readArgRoles(row, args)) {
const spec = e.kind === "value" ? e.spec : e.kind === "option" && typeof e.reading === "object" ? e.reading.spec : undefined;
if (spec?.mode !== "names-program")
continue;
const name = e.kind === "value" ? args[e.at] : e.kind === "option" && typeof e.reading === "object" ? e.reading.attached : undefined;
const role = name === undefined || !Object.hasOwn(row.modules, name) ? undefined : row.modules[name];
if (role !== undefined)
return { from: e.at + 1, role };
}
return undefined;
}
function steppableLauncherOption(row, tok) {
const m = tok.startsWith("--") ? (tok.includes("=") ? undefined : mergedTable(row, "long").get(tok.slice(2))) : tok.length === 2 && tok.startsWith("-") ? mergedTable(row, "short").get(tok[1]) : undefined;
if (m === undefined || m.spec.value?.kind === "shell-text")
return undefined;
return m.arity !== "disputed" || takesNoWordBare(row, tok) ? m : undefined;
}
function takesNoWordBare(row, spelling) {
const key = spelling.startsWith("--") ? spelling.slice(2) : spelling.slice(1);
return row.grammars.every((g) => {
const s = (spelling.startsWith("--") ? g.long : g.short)[key];
return s === undefined || s.arity === 0 || s.arity === "attached";
});
}
export function launcherOptionArity(row, tok) {
if (row.grammars.length === 0)
return undefined;
if (tok === "--")
return row.grammars.every((g) => g.stepsDashDash === true) ? 0 : undefined;
const m = steppableLauncherOption(row, tok);
return m === undefined ? undefined : m.arity === 1 ? 1 : 0;
}
function readLauncherOption(row, tok) {
if (tok === "-")
return row.launcher?.bareDash === true ? { step: 1 } : undefined;
if (tok === "--")
return launcherOptionArity(row, tok) === 0 ? { step: 1 } : undefined;
if (tok.startsWith("--")) {
const eq = tok.indexOf("=");
const m = steppableLauncherOption(row, eq > 2 ? tok.slice(0, eq) : tok);
if (m === undefined)
return undefined;
if (eq > 2)
return m.arity === 0 || m.arity === "disputed" ? undefined : { step: 1, valued: m };
return m.arity === 1 ? { step: 2, valued: m } : { step: 1 };
}
if (!/^-[^-]/.test(tok))
return undefined;
for (let i = 1; i < tok.length; i++) {
const m = steppableLauncherOption(row, `-${tok[i]}`);
if (m === undefined)
return undefined;
if (m.arity === 0)
continue;
if (i + 1 < tok.length)
return m.arity === "disputed" ? undefined : { step: 1, valued: m };
return m.arity === 1 ? { step: 2, valued: m } : { step: 1 };
}
return { step: 1 };
}
export function launcherOptionPathRole(row, tok) {
const role = readLauncherOption(row, tok)?.valued?.spec.value;
return role?.kind === "path" ? role : undefined;
}
export function launcherOptionMovesBase(row, tok) {
const moves = (m) => m?.spec.value?.kind === "path" && m.spec.value.base !== undefined;
if (tok === "-" || tok === "--")
return false;
if (tok.startsWith("--")) {
const eq = tok.indexOf("=");
return moves(steppableLauncherOption(row, eq > 2 ? tok.slice(0, eq) : tok));
}
if (!/^-[^-]/.test(tok))
return false;
for (let i = 1; i < tok.length; i++) {
const m = steppableLauncherOption(row, `-${tok[i]}`);
if (m === undefined)
return false;
if (moves(m))
return true;
if (m.arity !== 0)
return false;
}
return false;
}
export function launcherOptionQueries(row, tok) {
if (tok === "-" || tok === "--")
return false;
if (tok.startsWith("--")) {
const eq = tok.indexOf("=");
return steppableLauncherOption(row, eq > 2 ? tok.slice(0, eq) : tok)?.spec.mode === "queries";
}
if (!/^-[^-]/.test(tok))
return false;
for (let i = 1; i < tok.length; i++) {
const m = steppableLauncherOption(row, `-${tok[i]}`);
if (m === undefined)
return false;
if (m.spec.mode === "queries")
return true;
if (m.arity !== 0)
return false;
}
return false;
}
export function launcherOptionStep(row, tok) {
return readLauncherOption(row, tok)?.step;
}
export function launcherRunsShell(row, args) {
const l = row.launcher;
if (l?.shellText === undefined)
return false;
const programWith = l.programWith ?? [];
return !implementationsOf(row).some((view) => selectedSpellings(view, args).names.some((n) => programWith.includes(n)));
}
export function targetUserOf(row, words, operand) {
const as = row.launcher?.asUser;
if (as === undefined)
return undefined;
const own = selectedSpellings(row, words);
const user = own.values.filter((v) => as.userWith?.includes(v.name) === true).at(-1);
const named = user !== undefined ? user.value ?? words[user.at] : operand === undefined ? "root" : operand.name;
const login = operand?.dash === true || own.names.some((n) => as.loginWith.includes(n));
const keepsCaller = selectedOptions(row, words).some((o) => o.mode === "names-program");
const name = named === "#0" ? "root" : named !== undefined && /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(named) ? named : undefined;
return { ...(name !== undefined ? { name } : {}), home: login ? "target" : as.home === "policy" ? "either" : keepsCaller ? "caller" : "target", login };
}
export function commandLineOptionSpellings(row) {
if (row === undefined)
return [];
return [
...[...mergedTable(row, "short")].filter(([, m]) => m.spec.value?.kind === "shell-text").map(([l]) => `-${l}`),
...[...mergedTable(row, "long")].filter(([, m]) => m.spec.value?.kind === "shell-text").map(([n]) => `--${n}`),
];
}
export function commandLineOf(row, tok) {
const lines = new Set(commandLineOptionSpellings(row));
if (tok.startsWith("--")) {
const eq = tok.indexOf("=");
if (!lines.has(eq > 2 ? tok.slice(0, eq) : tok))
return undefined;
return eq > 2 ? { attached: tok.slice(eq + 1) } : "next";
}
if (!/^-[^-]/.test(tok))
return undefined;
for (let i = 1; i < tok.length; i++) {
const letter = `-${tok[i]}`;
if (lines.has(letter))
return i + 1 < tok.length ? { attached: tok.slice(i + 1) } : "next";
if (steppableLauncherOption(row, letter)?.arity !== 0)
return undefined;
}
return undefined;
}
export function readProgramTextLanguage(row) {
const first = row?.operands?.positional?.[0];
return first?.kind === "program-text" && READ_LANGUAGES.has(first.language) ? first.language : undefined;
}
export function programTextReadings(row, args) {
return row.grammars.map((g) => programTextUnder(g, args));
}
function programTextUnder(g, args) {
const scripts = [];
let supplied = false;
let endOfOptions = false;
let firstOperand;
const longs = Object.entries(g.long);
const reach = (long, pick) => longs.some(([full, s]) => pick(s) && isLongOptionSpellingOf(long, full, abbreviates(g)));
for (let k = 0; k < args.length; k++) {
const tok = args[k];
if (!endOfOptions && tok === "--") {
endOfOptions = true;
continue;
}
if (endOfOptions || !tok.startsWith("-") || tok === "-") {
if (firstOperand === undefined) {
firstOperand = tok;
if (g.afterFirstOperand === "operands")
endOfOptions = true;
}
continue;
}
if (tok.startsWith("--")) {
const long = longOptionNameOf(tok);
if (long === undefined)
return "unparsed";
const eq = tok.indexOf("=");
const attached = eq >= 0 ? tok.slice(eq + 1) : undefined;
if (reach(long, (s) => s.fillsSlot === true && s.value?.kind !== "program-text"))
return "from-file";
if (reach(long, (s) => s.fillsSlot === true && s.value?.kind === "program-text")) {
const value = attached ?? args[++k];
if (value === undefined)
return "unparsed";
scripts.push(value);
supplied = true;
continue;
}
if (reach(long, (s) => s.arity === 1)) {
if (attached === undefined)
k++;
continue;
}
if (reach(long, (s) => s.arity === 0 || s.arity === "attached"))
continue;
return "unparsed";
}
for (let c = 1; c < tok.length; c++) {
const s = g.short[tok[c]];
if (s === undefined)
return "unparsed";
if (s.arity === "suffix") {
if (c + 1 === tok.length && suffixSpelled(args[k + 1]))
k++;
break;
}
if (s.arity === "attached")
break;
if (s.arity === 1) {
const rest = tok.slice(c + 1);
const value = rest !== "" ? rest : args[++k];
if (s.fillsSlot === true && s.value?.kind !== "program-text")
return "from-file";
if (s.fillsSlot === true) {
if (value === undefined)
return "unparsed";
scripts.push(value);
supplied = true;
}
break;
}
}
}
const script = supplied ? scripts.join("\n") : firstOperand;
return script === undefined ? "none" : { text: script };
}
function exactSpec(g, tok) {
if (tok.startsWith("--"))
return tok.includes("=") ? undefined : g.long[tok.slice(2)];
return tok.length === 2 && tok.startsWith("-") ? g.short[tok[1]] : undefined;
}
const isMessage = (s) => s?.value?.kind === "datum" && s.value.form === "message";
const SUBCOMMAND_VIEWS = new WeakMap();
export function subcommandOf(row, words) {
const global = row?.grammars[0];
if (row?.subcommands === undefined || global === undefined)
return undefined;
let relocated = false;
let j = 0;
for (; j < words.length; j++) {
const w = words[j];
if (!w.startsWith("-"))
break;
const spec = exactSpec(global, w);
const longWithValue = w.startsWith("--") && w.includes("=") ? global.long[w.slice(2, w.indexOf("="))] : undefined;
const taken = spec ?? longWithValue;
if (taken?.value?.kind === "path" && taken.value.base !== undefined)
relocated = true;
if (spec?.arity === 1)
j++;
else if (spec?.arity !== 0 && longWithValue?.arity !== 1)
return undefined;
}
const name = words[j];
const sub = name === undefined || !Object.hasOwn(row.subcommands, name) ? undefined : row.subcommands[name];
if (name === undefined || sub === undefined)
return undefined;
let views = SUBCOMMAND_VIEWS.get(row);
if (views === undefined) {
views = new Map();
SUBCOMMAND_VIEWS.set(row, views);
}
let view = views.get(name);
if (view === undefined) {
view = { key: `${row.key} ${name}`, grammars: [sub.grammar], ...(sub.operands !== undefined ? { operands: sub.operands } : {}), ...(sub.traversal !== undefined ? { traversal: sub.traversal } : {}), witness: row.witness };
views.set(name, view);
}
return { name, at: j, row: view, relocated };
}
export function messageWords(program, words, programIndex) {
const out = new Set();
const found = subcommandOf(programRow(program), words.slice(programIndex + 1).map((w) => w.folded));
if (found === undefined)
return out;
const j = programIndex + 1 + found.at;
const sub = found.row.grammars[0];
const settled = (k) => words[k] !== undefined && words[k].expands === false;
const oneArgument = (k) => {
const pieces = words[k]?.word?.expansions ?? [];
return pieces.length > 0 && pieces.every((p) => p.expansion?.kind === "command" && p.expansion.yields === "one");
};
const longValues = Object.entries(sub.long).filter(([, s]) => s.arity === 1);
let pending;
for (let k = j + 1; k < words.length; k++) {
const w = words[k].folded;
if (pending !== undefined) {
if (pending === "message" && (settled(k) || oneArgument(k)))
out.add(k);
pending = undefined;
continue;
}
if (w === "--")
break;
if (!settled(k)) {
pending = "other";
continue;
}
if (!w.startsWith("-") || w === "-")
continue;
if (w.startsWith("--")) {
const eq = w.indexOf("=");
const name = w.slice(2, eq < 0 ? undefined : eq);
const takers = longValues.filter(([n]) => isLongOptionSpellingOf(name, n, abbreviates(sub)));
const message = (name.length > 0 && takers.length > 0 && takers.every(([, s]) => isMessage(s))) || isMessage(sub.long[name]);
if (eq >= 0) {
if (message)
out.add(k);
continue;
}
if (takers.length > 0)
pending = message ? "message" : "other";
continue;
}
const letters = w.slice(1);
for (let i = 0; i < letters.length; i++) {
const s = sub.short[letters[i]];
if (s?.arity === 0)
continue;
if (s?.arity === "attached")
break;
if (s?.arity === 1) {
if (i < letters.length - 1) {
if (isMessage(s))
out.add(k);
}
else
pending = isMessage(s) ? "message" : "other";
break;
}
pending = "other";
break;
}
}
return out;
}
export const SMALL_BYTE_BOUND_MAX = 1_000_000;
export function byteBoundIsSmall(row, args) {
if (row === undefined)
return false;
let byteBound = false;
for (const e of readArgRoles(row, args)) {
if (e.kind === "operand" || e.kind === "end-of-options")
continue;
if (e.kind === "value" && e.spec?.bounds === "bytes") {
if (!/^\d+$/.test(args[e.at]) || Number(args[e.at]) > SMALL_BYTE_BOUND_MAX)
return false;
byteBound = true;
continue;
}
if (e.kind === "option" && typeof e.reading === "object" && e.reading.spec?.bounds === "bytes") {
if (!/^\d+$/.test(e.reading.attached) || Number(e.reading.attached) > SMALL_BYTE_BOUND_MAX)
return false;
byteBound = true;
continue;
}
return false;
}
return byteBound;
}