UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

831 lines • 37.5 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { PolicyDenied } from "../../../../policy/errors.js"; import { arraySet, buildAssocLiteral, buildIndexedLiteral, } from "../../../../shell/array.js"; import { encodeText } from "../../../../shell/bytes.js"; import { ArithError, DiscardSignal } from "../../../../shell/errors.js"; import { storedFunctionText } from "../../../../shell/printer.js"; import { attrLetters, VarAttr, VarKind, } from "../../../../shell/variable.js"; import { traceAssignment } from "../../../../shell/xtrace.js"; import { concat } from "../../../../utils/bytes.js"; import { varHidden } from "../../../../utils/hidden.js"; import { compareCodePoints } from "../../../../utils/sort.js"; import { sessionEntry, setSessionEntry } from "../../../session/session.js"; import { appended, conversionScalar, deref, inCallEnv, outliveCall, setAttr, shadowLocal, subscriptIndex, } from "../../../session/state.js"; import { bodyAliasValues } from "../alias/alias.js"; import { builtinUsage, fail, isValidName, ok, readonlyLine, refusal, requireView, result, } from "../shared.js"; import { ANSI_C_ESCAPES, BARE_KEY_RE, CONTROL_RE, EXPORT_FLAGS, LISTED_ATTRIBUTES, READONLY_FLAGS, SUBSCRIPT_RE, VISIBLE_SCOPE_BUILTINS, } from "./constants.js"; /** * Put a declaration's value-shaping marks on a name before its value * stores. The session view coerces on write by reading the record's attributes, so * for the declaration's *own* value to coerce (`declare -i n=3+4` stores * `7`), the attribute has to be there first; a `+` letter comes off first * too, so `declare -i N=5; declare +i N+=x` stores `5x`. Gated like every * other mark, and a no-op with nothing to shape. `followRef` is the write's: * a `-n` declaration shapes the reference itself. */ export async function premark(view, name, shaping, followRef = true) { for (const [attr, on] of shaping) await view.mark(name, attr, on, followRef); } /** * The array kind a declaration's `-a` / `-A` asks for, `-A` winning when both * are given (bash's `export -aA B=(1)` builds a map), or null for neither. */ export function declaredKind(flags) { if (flags.has('A')) return VarKind.Assoc; if (flags.has('a')) return VarKind.Indexed; return null; } /** * The value a declaration's `NAME=...` lands on: the variable a `declare -n` * reference names. */ export function heldValue(session, name) { return sessionEntry(session.vars, deref(session, name) || name)?.value ?? null; } /** * The attributes a new local takes from the variable it shadows: the export * mark alone (`local I=2+3` over `declare -i I` stores `2+3`), or with `-I` * every one but a reference, as bash's `local -I` keeps `-i` and drops `-n`. */ export function localAttrs(v, inherit) { if (v === undefined) return new Set(); if (inherit) return new Set([...v.attrs].filter((a) => a !== VarAttr.Nameref)); return new Set([...v.attrs].filter((a) => a === VarAttr.Export)); } /** * Reset a name the running function just shadowed to what a new local starts * as: unset, with the attributes `localAttrs` keeps, or under `-I` the * shadowed value too, so `local -I A=new` over `A=(old keep)` writes element * 0 of `(old keep)`. This is the scope's own bookkeeping, not a session * write: the caller's record is the frame's to put back on return, so no * policy is asked to delete it. The local's value lands later through the * gated session view, which judges that write. */ export function startLocal(session, name, inherit) { const v = sessionEntry(session.vars, name); const kept = localAttrs(v, inherit); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete session.vars[name]; if (v !== undefined && (kept.size > 0 || inherit)) { setSessionEntry(session.vars, name, { value: inherit ? v.value : null, attrs: kept }); } } /** * bash's refusal when a declared array kind meets a value of the other * kind, or null when they agree. */ export function kindConflict(held, kind) { if (kind === VarKind.Assoc && Array.isArray(held)) return 'cannot convert indexed to associative array'; if (kind === VarKind.Indexed && held !== null && typeof held === 'object' && !Array.isArray(held)) return 'cannot convert associative to indexed array'; return null; } /** * What a declaration's `NAME=value` stores, and the elements it assigns * (`coerceValue`). An array keeps its kind and takes the value at element 0 * (key `"0"` in a map), as a plain `NAME=value` does, leaving the other * elements as stored; otherwise `-A` makes the map `([0]=value)` and `-a` * the one-element array, a held scalar converting to that element first, * and with neither the value stays a scalar. A `NAME+=value` arrives here * as `appended` hands it. */ export function scalarValue(held, value, kind) { const scalar = typeof held === 'string' ? held : null; const map = held !== null && typeof held === 'object' && !Array.isArray(held) ? held : null; if (map !== null || kind === VarKind.Assoc) { return [{ ...map, '0': value }, new Set(['0'])]; } if (Array.isArray(held) || kind === VarKind.Indexed) { const arr = Array.isArray(held) ? [...held] : []; if (scalar !== null) arr.push(scalar); arraySet(arr, 0, value); return [arr, new Set([0])]; } return [value, null]; } /** * Whether a listing's `-a` / `-A` keep `name`: `-a` lists only indexed * arrays, `-A` only associative ones, both nothing. */ export function kindListed(session, name, flags) { if (flags.has('a') && !Object.hasOwn(session.arrays, name)) return false; return !flags.has('A') || Object.hasOwn(session.assocs, name); } /** * Put `attr` on the variable a write to `name` landed on. The write's gate * covered `checked`, the target before it, so the mark rides on that * decision; a write that re-aimed an unset `declare -n` reference * (`declare -n r; export r=X`) landed on a target no gate has seen, so that * mark goes through the gated session view, as does the reference mark itself * (`+n`), which belongs to the reference's own record. `followRef` is false * when the write was a `declare -n` declaration's, on the reference itself. */ export async function markWritten(session, view, name, checked, attr, on = true, followRef = true) { const follows = followRef && attr !== VarAttr.Nameref; const target = follows ? deref(session, name) || name : name; if (target === checked) setAttr(session, target, attr, on); else await view.mark(name, attr, on, followRef); } /** * Put a declaration's attribute marks on what one operand landed on, as * soon as it lands: `declare -r R=1 R=2` refuses the second write, and * under `-g` the global record takes them. A written operand's marks ride * on its write's gate (`markWritten`); a bare one (`checked` null) wrote * nothing, so each mark goes through the gated session view. */ export async function stampMarks(session, view, name, checked, marks, followRef = true) { for (const [attr, on] of marks) { if (checked === null) await view.mark(name, attr, on, followRef); else await markWritten(session, view, name, checked, attr, on, followRef); } } /** * Take the mark off an unaimed `declare -n` reference a declared array kind * is about to land on, silently, as bash's `export -a ref=v` does (an * undeclared array warns at the session view). */ export async function dropReference(session, view, name) { const target = deref(session, name) || name; if (sessionEntry(session.vars, target)?.attrs.has(VarAttr.Nameref) === true) { await view.mark(target, VarAttr.Nameref, false); } } /** * A name's own record, undefined when unset or hidden: a hidden name reads * as unset, so no refusal can quote or describe its value. */ export function visibleRecord(session, name) { if (varHidden(session.visibility, name)) return undefined; return sessionEntry(session.vars, name); } /** * The line a `+letter` earns on one operand, if any. Two letters cannot be * taken off: `+r` on a readonly name is `declare: R: readonly variable` and * the name stays frozen, as is `+n` on a frozen reference; `+a` / `+A` on an array is `cannot destroy array * variables in this way`, since the kind is what the value is, not a mark. * Either skips that operand's value and marks, and the others still declare * (pinned on 5.2.37). */ export function plusRefusal(cmd, session, view, name, plus) { const own = visibleRecord(session, name); const value = own?.value ?? null; const reference = own?.attrs.has(VarAttr.Nameref) === true; if ((plus.includes('r') && view.isReadonly(name)) || (plus.includes('n') && reference && view.isReadonly(name, false))) { return readonlyLine(cmd, name); } const isMap = value !== null && typeof value === 'object' && !Array.isArray(value); if ((plus.includes('a') && Array.isArray(value)) || (plus.includes('A') && isMap)) { return `bash: ${cmd}: ${name}: cannot destroy array variables in this way`; } return null; } /** * Store a declaration's array literals through the session view, the first * of bash's two passes over a declaration. * * bash stores every literal before it runs any other operand, then goes * through all of them in order, assigning the plain values and marking each * name, a literal's included, at its own place: so `declare -r R=1 R=(2)` * stores `(2)`, writes 1 over element 0 and freezes `R`, and a fatal literal * leaves every other operand undone (pinned on 5.2.37). Only the * value-shaping attributes go on before a literal stores (`shaping`); the * caller's second pass puts the rest on the literals `stored` reports, by * position, against the variable each write's gate cleared, so an operand * that re-aims a reference in between cannot carry a mark past the gate. * * The builtin owns the store; readonly is the shell's rule, checked per name * before the session view, and the session view's gate covers the policy half. Names are * processed in order, so an earlier operand stays stored when a later one * refuses, as bash does. A readonly refusal or kind conflict of an array * literal is a variable-assignment error in GNU, not a builtin failure: for * `export`/`readonly` (and `declare` at top level) `fatal` abandons the rest * of the line, while `local` and a function-scoped `declare` refuse in the * builtin's voice into `errors` and the body keeps running (pinned on bash * 5.2, debian:stable-slim); under `-g` a readonly name refuses fatally even * inside a function, a kind conflict does not. Returns the refusal result, * or null. * * Inside a function `declare` and `local` make each name local, starting as * `startLocal` leaves it (with `-I`, the value it shadows); `export` and * `readonly` (`VISIBLE_SCOPE_BUILTINS`) and `-g` write the visible one. * * `kind` is the kind `-a` / `-A` declared: `-A` builds every literal as an * associative map, and without it a name that already holds one still * builds a map, since a plain `m+=([k]=v)` keeps the variable's own kind. * `warnings` is filled with the `must use subscript` lines for the plain * words a keyed associative literal cannot take; GNU stores the valid * elements and the status stays 0. */ export async function storeStagedArrays(cmd, session, view, operands, errors, warnings, fatal = false, stored = null, kind = null, shaping = [], globalScope = false, inherit = false) { const scoped = !globalScope && !VISIBLE_SCOPE_BUILTINS.has(cmd); const locals = scoped ? session.localVars : null; for (const [position, operand] of operands.entries()) { if (typeof operand === 'string') continue; const { name, append, items } = operand; if (view.isReadonly(name)) { if (fatal || globalScope) throw new DiscardSignal(encodeText(`bash: ${name}: readonly variable\n`)); errors.push(readonlyLine(cmd, name)); continue; } const fresh = locals !== null && !locals.has(name); if (locals !== null) shadowLocal(session, locals, name); if (fresh && !inCallEnv(session, name)) startLocal(session, name, inherit); const held = fresh && !inherit ? null : heldValue(session, name); const conflict = kindConflict(held, kind); if (conflict !== null) { if (fatal) throw new DiscardSignal(encodeText(`bash: ${name}: ${conflict}\n`)); errors.push(`bash: ${cmd}: ${name}: ${conflict}`); continue; } try { await premark(view, name, shaping); if (kind !== null) await dropReference(session, view, name); } catch (err) { if (err instanceof PolicyDenied) return refusal(cmd, err); throw err; } let base; const checked = deref(session, name) || name; // One try around the literal and the write: a subscript in the // literal may assign (`([x=2]=v)`), and that lands through the same // session view. try { if (kind === VarKind.Assoc || Object.hasOwn(session.assocs, name)) { const { map, badWords } = buildAssocLiteral(session.assocs[name] ?? null, items, append); for (const word of badWords) { warnings.push(`bash: ${name}: '${word}': must use subscript when assigning associative array`); } base = map; } else { let indexed = session.arrays[name] ?? null; if (append && indexed === null) { const scalar = conversionScalar(session, name); indexed = scalar === undefined ? null : [scalar]; } base = await buildIndexedLiteral(indexed, items, append, (sub) => subscriptIndex(session, sub, view)); } await view.set(name, base); } catch (err) { if (err instanceof PolicyDenied) return refusal(cmd, err); // An element or subscript of a literal names no builtin, as an // assignment's does (`declare -ai A=(2+)` is `2+: ...`). if (err instanceof ArithError) throw err.signal('', true); throw err; } if (stored !== null) stored.set(position, checked); } return null; } function isControl(ch) { const code = ch.codePointAt(0) ?? 0; return code < 0x20 || code === 0x7f; } /** * Quote a value the way bash `declare -p` / `export -p` does. * * A value holding any control character takes the `$'...'` form, with the * named escapes bash uses (`\a \b \t \n \v \f \r`, and `\E` for escape) and * three-digit octal for the rest; `"`, `$` and backtick need no escaping * there because `$'...'` does not expand. Everything else is double-quoted * with escapes for `\`, `"`, `$` and backtick. Non-ASCII printable text * stays literal, which is what bash emits in a UTF-8 locale. */ export function bashDeclareQuote(value) { let out = ''; if (CONTROL_RE.test(value)) { for (const ch of value) { const escape = ANSI_C_ESCAPES[ch]; if (escape !== undefined) out += escape; else if (isControl(ch)) out += `\\${(ch.codePointAt(0) ?? 0).toString(8).padStart(3, '0')}`; else out += ch; } return `$'${out}'`; } for (const ch of value) { if (ch === '\\' || ch === '"' || ch === '$' || ch === '`') out += `\\${ch}`; else out += ch; } return `"${out}"`; } export function splitDeclFlags(args, allowed) { const flags = new Set(); let i = 0; while (i < args.length) { const tok = args[i] ?? ''; if (typeof tok !== 'string') break; if (tok === '--') { i += 1; break; } if (tok.startsWith('-') && tok.length > 1) { const body = tok.slice(1); for (const ch of body) { if (!allowed.has(ch)) return { flags, names: args.slice(i), bad: ch }; } for (const ch of body) flags.add(ch); i += 1; continue; } break; } return { flags, names: args.slice(i), bad: null }; } /** * One associative key as `declare -p` spells it. * * Bare when every character is one GNU leaves unquoted (pinned by a * character sweep on 5.2.37: alphanumerics and `_ % + , - . / : = @ ~`), * quoted like a value otherwise. A key that *is* `@` or `*` quotes even * though the character is bare mid-key, since the bare spelling would * read back as a splat. */ function assocKeyText(key) { if (key !== '@' && key !== '*' && BARE_KEY_RE.test(key)) return key; return bashDeclareQuote(key); } /** * The `=(...)` tail of an associative `declare` line. * * Sorted keys (mirage's pinned order, where GNU prints hash order) and * GNU's trailing space before the closing paren, which an empty map * does not carry: `m=([a]="1" )` but `m=()`. */ export function assocBody(amap) { const keys = Object.keys(amap).sort(compareCodePoints); if (keys.length === 0) return '=()'; const parts = keys.map((k) => `[${assocKeyText(k)}]=${bashDeclareQuote(amap[k] ?? '')}`); return `=(${parts.join(' ')} )`; } /** * A declaration operand as its name, whether it appends, and its value, null * for a bare name: `X+=y` appends `y` to `X`. */ export function operandParts(word) { const eq = word.indexOf('='); if (eq < 0) return [word, false, null]; const name = word.slice(0, eq); const append = name.endsWith('+'); return [append ? name.slice(0, -1) : name, append, word.slice(eq + 1)]; } /** * GNU's `not a valid identifier` line for one declaration operand. * * A declaration builtin refuses a name it cannot declare rather than * storing it: `export 1BAD=x` used to land a variable that `$1BAD` can * never name back (bash reads that as `$1` then `BAD`) and then shipped * it to every child environment. * * Which text GNU quotes depends on why the word failed, and both * spellings are pinned. A word that is not a valid assignment at all is * echoed whole (``export: `1BAD=x'``); a word whose target parses but is * not a plain name -- an array element -- is echoed as just that target * (``export: `arr[0]'``), since the value it would have taken is not * what is wrong with it. */ export function identifierRefusal(cmd, word) { const [name] = operandParts(word); if (isValidName(name)) return null; const quoted = SUBSCRIPT_RE.test(name) ? name : word; return `bash: ${cmd}: \`${quoted}': not a valid identifier`; } /** * A declaration's answer once every operand ran: each warning, then each * refusal, one line apiece, and exit 1 when an operand refused; an empty * refusal fails without a word, as bash's do for a reference given `-i`. The good * operands on the same line are already stored: GNU reports each and keeps * going, so `export GOOD=1 1BAD=x GOOD2=2` exports both good names. A * warning alone (`must use subscript`) leaves the status 0. */ export function declarationResult(cmd, errors, warnings = [], failed = errors.length > 0) { const lines = [...warnings, ...errors.filter((line) => line !== '')]; return result(cmd, { exitCode: failed ? 1 : 0, stderr: lines.map((line) => `${line}\n`).join(''), }); } /** * The `declare -p` line for one name, or null when it has none. * * The attribute cluster is `attrLetters`, which is why this renders * `declare -rx` and `declare -ar` without a table of its own: the record * already knows its own letters and their print order. bash spells an * empty cluster `--`, and that spelling is the caller's because only a * `declare` line needs it. * * A hidden name answers null, the same way `isReadonly` answers false for * one: reporting it as declared would leak it. */ export function declareLine(session, name) { if (varHidden(session.visibility, name)) return null; const v = sessionEntry(session.vars, name); if (v === undefined) return null; const letters = attrLetters(v); const head = letters ? `declare -${letters}` : 'declare --'; if (v.value === null) return `${head} ${name}`; if (Array.isArray(v.value)) { const parts = []; for (let i = 0; i < v.value.length; i++) { const el = v.value[i]; if (el !== null && el !== undefined) { parts.push(`[${String(i)}]=${bashDeclareQuote(el)}`); } } return `${head} ${name}=(${parts.join(' ')})`; } if (typeof v.value !== 'string') return `${head} ${name}${assocBody(v.value)}`; return `${head} ${name}=${bashDeclareQuote(v.value)}`; } /** * Whether a no-name `declare` listing's letters keep `name`: `-a` / `-A` * narrow it to that array kind (`kindListed`), and any of `-i -l -n -r -t * -u -x` keeps a name carrying one of them. */ export function declarationListed(session, name, flags) { const v = sessionEntry(session.vars, name); if (v === undefined || !kindListed(session, name, flags)) return false; const letters = new Set(attrLetters(v)); const wanted = [...flags].filter((c) => LISTED_ATTRIBUTES.has(c)); return wanted.length === 0 || wanted.some((c) => letters.has(c)); } /** * Run `declare -p`: render declarations for names, or for all; with no * names the declaration's letters narrow the list (`declarationListed`). */ export function handleDeclarePrint(names, session, flags = new Set()) { const targets = names.length > 0 ? names : Object.keys(session.vars) .filter((name) => declarationListed(session, name, flags)) .sort(compareCodePoints); const lines = []; const errors = []; for (const name of targets) { const line = declareLine(session, name); if (line === null) errors.push(`bash: declare: ${name}: not found`); else lines.push(line); } return result('declare', { out: encodeText(lines.map((line) => `${line}\n`).join('')), exitCode: errors.length > 0 ? 1 : 0, stderr: errors.map((error) => `${error}\n`).join(''), }); } /** * The letters `declare` prints a function with: `f`, then `r` when * `readonly -f` froze it and `x` when `export -f` marked it. */ export function functionFlags(session, name) { const readonly = session.readonlyFunctions.has(name) ? 'r' : ''; const exported = session.exportedFunctions.has(name) ? 'x' : ''; return `f${readonly}${exported}`; } /** * Print functions as `declare` lists them. A body prints as bash renders it * (`storedFunctionText`), followed with `marks` by a `declare -fx NAME` line * when the function has an attribute; without bodies each function is its * `declare` line, or its bare name. */ export function functionLines(session, names, bodies, marks, parser) { const lines = []; for (const name of names) { const flags = functionFlags(session, name); if (bodies) { lines.push(storedFunctionText(name, session.functions[name] ?? '', parser, bodyAliasValues(session, name))); if (marks && flags !== 'f') lines.push(`declare -${flags} ${name}`); } else { lines.push(marks ? `declare -${flags} ${name}` : name); } } return lines; } /** * Run the function half of `declare`: `-f` / `-F`. * * `-p` only prints, whatever attributes come with it: `-F NAME` the * attribute line, `-f NAME` the body and, for a function with an attribute, * that line (`functionLines`); a missing name is `not found`, exit 1. * Without `-p`, `-r` freezes the named functions as `readonly -f` does, `-x` * marks them for export and `+x` takes the mark off, printing nothing; a `+` * letter wins over its `-` twin, and `+r` refuses a frozen function, which * then keeps every attribute (`readonly function`, exit 1); with no * attribute `-F NAME` prints the name and `-f NAME` the body, and a missing * name is exit 1 with no message. With no names every function * lists as `-p` prints it; `-r` or `-x` narrows the list to the functions * holding either attribute, and a `+` attribute does not. */ export function handleDeclareFunctions(cmd, session, flags, names, plus, parser) { const printing = flags.has('p'); const wanted = ['r', 'x'].filter((c) => flags.has(c)); let present = names.filter((name) => name in session.functions); const missing = names.filter((name) => !(name in session.functions)); if (names.length > 0 && !printing && (wanted.length > 0 || plus.has('r') || plus.has('x'))) { const frozen = present.filter((name) => plus.has('r') && session.readonlyFunctions.has(name)); for (const name of present) { if (frozen.includes(name)) continue; if (flags.has('r') && !plus.has('r')) session.readonlyFunctions.add(name); if (plus.has('x')) session.exportedFunctions.delete(name); else if (flags.has('x')) session.exportedFunctions.add(name); } return result(cmd, { exitCode: missing.length > 0 || frozen.length > 0 ? 1 : 0, stderr: frozen.map((name) => `bash: ${cmd}: ${name}: readonly function\n`).join(''), }); } if (names.length === 0) { present = Object.keys(session.functions) .sort(compareCodePoints) .filter((name) => wanted.length === 0 || wanted.some((c) => functionFlags(session, name).includes(c))); } const lines = functionLines(session, present, !flags.has('F'), printing || names.length === 0, parser); return result(cmd, { out: encodeText(lines.map((line) => `${line}\n`).join('')), exitCode: missing.length > 0 ? 1 : 0, stderr: printing ? missing.map((name) => `bash: ${cmd}: ${name}: not found\n`).join('') : '', }); } /** * Run `export` or `readonly`: mark names, or print them. An invalid option * letter fails with status 2 and the GNU usage line. `-f` marks functions * instead (`markFunctions`). With no names, every name carrying the * keyword's mark prints as `declare -p` prints it, the whole cluster * (`declare -rx R="1"`), a reference as itself rather than its target; `-a` * / `-A` narrow the listing to that array kind (`kindListed`). Otherwise * each operand is assigned and marked through the gated view * (`markVariables`); `-a` / `-A` shape only an assigned value, and * `export -n` takes the mark off. */ export async function markNames(assignments, session, state, attr, parser) { const exporting = attr === VarAttr.Export; const cmd = exporting ? 'export' : 'readonly'; const { flags, names, bad } = splitDeclFlags(assignments, exporting ? EXPORT_FLAGS : READONLY_FLAGS); if (bad !== null) { const usage = builtinUsage(exporting ? 'export' : 'readonly'); return fail(cmd, `bash: ${cmd}: -${bad}: invalid option\n${usage}`, 2); } const on = !flags.has('n'); const kind = declaredKind(flags); if (flags.has('f')) { const marked = exporting ? session.exportedFunctions : session.readonlyFunctions; return markFunctions(cmd, session, marked, names, on, state, parser, kind); } if (names.length === 0) { const lines = Object.entries(session.vars) .filter(([name, v]) => v.attrs.has(attr) && kindListed(session, name, flags)) .map(([name]) => name) .sort(compareCodePoints) .map((name) => declareLine(session, name)) .filter((line) => line !== null); return ok(cmd, encodeText(lines.map((line) => `${line}\n`).join(''))); } return markVariables(cmd, session, requireView(state), names, attr, on, kind); } /** * Run `readonly -f` or `export -f`: mark functions, or list them. A name * that is not a function is `not a function`, exit 1, and the other operands * are still marked (or, with `on` false, unmarked), in the order typed. An * array literal (`export -f ARR=(a b)`) still stores first, with no * attribute and its `must use subscript` warnings, and its name is then * checked at its place, as bash assigns every literal before it looks for * the functions. With no names the marked functions print as bodies, each * followed by its `declare` line. */ export async function markFunctions(cmd, session, marked, operands, on, state = null, parser, kind = null) { const errors = []; const warnings = []; if (operands.some((operand) => typeof operand !== 'string')) { const refused = await storeStagedArrays(cmd, session, requireView(state), operands, errors, warnings, true, null, kind); if (refused !== null) return refused; } const names = operands.map((operand) => (typeof operand === 'string' ? operand : operand.name)); if (names.length === 0) { const listed = [...marked].filter((name) => name in session.functions).sort(compareCodePoints); const lines = functionLines(session, listed, true, true, parser); return ok(cmd, encodeText(lines.map((line) => `${line}\n`).join(''))); } for (const name of names) { if (!(name in session.functions)) errors.push(`bash: ${cmd}: ${name}: not a function`); else if (on) marked.add(name); else marked.delete(name); } return declarationResult(cmd, errors, warnings); } /** * Run `export` or `readonly` over its operands: assign each value and put * the keyword's mark on, or with `on` false take it off (`export -n`). * bash's two passes (`storeStagedArrays`): every array literal stores first, * then each operand in order is assigned and marked, a literal's name at its * own place. So `readonly R=1 R=2 X=3` keeps 1, refuses the second write and * still sets `X`, and `readonly A=(1) A=(2)` keeps `(2)` while a later `A=3` * refuses. * * Under `set -x` each assignment is traced as it is made, among the refusals * and under the command's redirects (`+ r=2` then `r: readonly variable`); a * bad name or option traces nothing, and with `-a` / `-A` bash assigns as * `declare` does, untraced. */ export async function markVariables(cmd, session, view, operands, attr, on, kind) { const errors = []; const warnings = []; const stored = new Map(); const refused = await storeStagedArrays(cmd, session, view, operands, errors, warnings, true, stored, kind); const traced = kind === null && session.shellOptions.xtrace === true; let failed = false; try { for (const [position, operand] of operands.entries()) { if (typeof operand === 'string') { const line = refused !== null ? null : await markOperand(cmd, session, view, operand, attr, on, kind, traced ? errors : null); if (line !== null) { errors.push(line); failed = true; } } else { // A literal is marked at its place, even when a policy refused a // later literal. const checked = stored.get(position); if (checked !== undefined) await markWritten(session, view, operand.name, checked, attr, on); } } } catch (err) { if (err instanceof PolicyDenied) return refusal(cmd, err); if (err instanceof ArithError) { const signal = err.signal(cmd, true); const shown = [...warnings, ...errors].filter((line) => line !== ''); signal.stderr = concat([encodeText(shown.map((line) => `${line}\n`).join('')), signal.stderr]); throw signal; } throw err; } return refused ?? declarationResult(cmd, errors, warnings, failed); } /** * Assign and mark one `export` / `readonly` word. A value of the other array * kind is refused and the name is still marked, as bash does. The bare form * writes no value, so it marks through the session view's no-value call rather than * inventing an empty string: on a new name that leaves it *unset* and * marked, bash's own third state (`export Z` prints `declare -x Z` and stays * out of `env`). Still gated, since marking is a session write: through * `setAttr` a deployment refusing `AWS_*` saw `readonly AWS_KEY` exit 0 and * freeze the name against every later write. Under `set -x` the * assignment's trace line goes to `traced` before it is made. Returns the * operand's refusal line, or null; a policy denial or an `-i` value that * does not evaluate throws. */ async function markOperand(cmd, session, view, word, attr, on, kind, traced) { const badName = identifierRefusal(cmd, word); if (badName !== null) return badName; const [key, append, val] = operandParts(word); if (traced !== null && val !== null) traced.push(traceAssignment(key, val, append)); if (val !== null && view.isReadonly(key)) return readonlyLine(cmd, key); const held = val !== null ? heldValue(session, key) : null; const conflict = val !== null ? kindConflict(held, kind) : null; if (val !== null && conflict === null) { const checked = deref(session, key) || key; const integer = sessionEntry(session.vars, checked)?.attrs.has(VarAttr.Integer) === true; const [slot, added] = append ? appended(held, val, integer) : [val, null]; const [value, assigned] = scalarValue(held, slot, kind); if (kind !== null) await dropReference(session, view, key); await view.set(key, value, true, assigned, added); // Rides on the gate the `view.set` above passed, unless the write // re-aimed a reference (`markWritten`). await markWritten(session, view, key, checked, attr, on); } else { await view.mark(key, attr, on); } if (on) outliveCall(session, key); return conflict === null ? null : `bash: ${cmd}: ${key}: ${conflict}`; } /** * Record the caller's array before a function shadows `name`. * * `local -a` / `declare -a` inside a function shadow the caller's array, * so the old value (or its absence) has to be remembered for the teardown * in `executeCommand`. Returns true when a function scope is active, so * the caller should shadow rather than reuse whatever is already there. */ export function noteLocalArray(session, name) { const locals = session.localVars; if (locals === null) return false; shadowLocal(session, locals, name); return true; } /** * The line `declare -n NAME=TARGET` earns when TARGET is unusable: bash * refuses a target that is not a variable name, a self reference, and * (mirage-only) a target spelled as an array element, since the resolver * maps names to names. */ export function namerefRefusal(cmd, name, target) { if (SUBSCRIPT_RE.test(target)) { return `mirage: ${cmd}: ${target}: name reference to an array element is not supported`; } if (!isValidName(target)) { return `bash: ${cmd}: \`${target}': invalid variable name for name reference`; } if (target === name) { return `bash: ${cmd}: ${name}: nameref variable self references not allowed`; } return null; } /** * The line the name a `declare -n` operand lands on earns, ahead of its * readonly mark (pinned on 5.2.37). An array cannot become a reference * (`reference variable cannot be an array`). A given value was judged on * its own (`namerefRefusal`); a bare `declare -n NAME` aims the name at the * value it already holds, so that value has to name a variable, though here * it may name NAME itself. */ export function referenceRefusal(cmd, name, own, bare) { if (own !== undefined && own.value !== null && typeof own.value === 'object') { return `bash: ${cmd}: ${name}: reference variable cannot be an array`; } if (!bare || own === undefined || typeof own.value !== 'string' || own.attrs.has(VarAttr.Nameref) || own.value === name) { return null; } return namerefRefusal(cmd, name, own.value); } //# sourceMappingURL=declare.js.map