UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

863 lines • 45.1 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 { runInCommandScope } from "../../cache/index/scope.js"; import { isProgramInvocation, redirectPathsFor, redirectRunnerFor, redirectSyntaxFor, runAsProgram, runWithEvaluation, runWithRedirectPaths, } from "../../context/session_context.js"; import { newExecutionId } from "../../execution/context.js"; import { share } from "../../io/async_line_iterator.js"; import { IOResult } from "../../io/types.js"; import { PolicyDenied } from "../../policy/errors.js"; import { applyBarrier, BarrierPolicy } from "../../shell/barrier.js"; import { encodeText } from "../../shell/bytes.js"; import { CallStack } from "../../shell/call_stack.js"; import { FORK_FAILED, FORK_FAILED_STATUS, SIGPIPE_STATUS } from "../../shell/constants.js"; import { ArithError, ExitSignal, PipeClosed, ReadonlyVariableError } from "../../shell/errors.js"; import { commandRedirects, getCaseItems, getCaseWord, getCforParts, getForParts, getFunctionName, getFunctionSource, getIfBranches, getListParts, getNegatedCommand, getParts, getPipelineStages, getRedirects, getText, getUnsetArgs, getWhileParts, readRow, withOwnRedirects, } from "../../shell/helpers.js"; import { NodeKind, nodeKind, pipelineTransparent } from "../../shell/node_kind.js"; import { NodeType as NT, Redirect, RedirectKind } from "../../shell/types.js"; import { PathSpec } from "../../types.js"; import { makeAbortError, mergeSignals } from "../../utils/abort.js"; import { concat } from "../../utils/bytes.js"; import { childContext } from "../evaluation.js"; import { ExecutionScope } from "../execution.js"; import { aliasMark, aliasView } from "../executor/builtins/alias/index.js"; import { installExecRedirects } from "../executor/builtins/exec/index.js"; import { handleTest, handleUnset } from "../executor/builtins/index.js"; import { fail, isValidName, result } from "../executor/builtins/shared.js"; import { executeBody, handleCase, handleCfor, handleFor, handleIf, handleSelect, handleWhile, isUnwinding, LoopSignal, trailed, } from "../executor/control.js"; import { drained } from "../executor/jobs.js"; import { handleConnection, handlePipe, handleSubshell } from "../executor/pipes.js"; import { handleRedirect } from "../executor/redirect.js"; import { assignmentStatus, forks, ignoringErrexit, recordStatus } from "../executor/statement.js"; import { endShell } from "../executor/traps.js"; import { globOptions, resolveGlobs } from "../expand/globs.js"; import { expandArith, expandNode } from "../expand/node.js"; import { expandAndClassify } from "../expand/parts.js"; import { expandPattern } from "../expand/pattern.js"; import { expandRedirect } from "../expand/redirects.js"; import { landedArith } from "../session/elements.js"; import { positionalParams, sessionView } from "../session/state.js"; import { ExecutionNode } from "../types.js"; import { admitTest } from "./admission.js"; import { executeAssignment } from "./assignment.js"; import { executeCommand } from "./command_dispatch.js"; import { executeDeclaration } from "./declaration.js"; import { diagnosticStderr } from "./diagnostics.js"; import { claimantFor, definedAt } from "./occurrence.js"; import { ProcessSubScope } from "./process_sub.js"; import { executeProgram } from "./program.js"; import { expandDoubleBracket } from "./test_expr.js"; import { timedBy, timingReport } from "./timing.js"; const STREAMING_KINDS = new Set([ NodeKind.PROGRAM, NodeKind.COMPOUND, NodeKind.LIST, NodeKind.SUBSHELL, NodeKind.IF, NodeKind.FOR, NodeKind.CFOR, NodeKind.SELECT, NodeKind.WHILE, NodeKind.UNTIL, NodeKind.CASE, NodeKind.NEGATED, ]); /** * The deps for a subtree that runs on `handed`. * * The hand-off a subtree's gates read and the one its nested * evaluations run under are one fact, set together here so the walker * can never carry one hand-off and evaluate under another. Everything a * command hands a line to (eval, source, xargs, command, a substitution, * a herestring, a redirect target) re-enters through `executeFn`, so the * hand-off is bound into it rather than into the line's closure: a * background job's subtree runs on a hand-off of the job's own, and a * line it evaluates after the typed line has ended has to stand under * that one. Under the line's, the inner gate could not see the grant the * job holds and asked again, and what it claimed went back to a hand-off * nothing revokes any more. */ export function withHandOff(deps, handed) { const inner = deps.executeFn; const executeFn = (cmd, opts) => inner(cmd, { handed, ...opts }); return { ...deps, handed, executeFn }; } /** * Layer per-call overrides onto the walker's deps. * * Written field by field rather than spread so an explicitly undefined * override cannot erase a dep under exactOptionalPropertyTypes. */ function withOpts(base, opts) { if (opts === undefined) return base; let next = { ...base }; if (opts.sink !== undefined) next.sink = opts.sink; if (opts.signal !== undefined) next.signal = opts.signal; if (opts.executionScope !== undefined) next.executionScope = opts.executionScope; if (opts.jobTable !== undefined) { // The lines the subtree runs (`eval`, `source`, an EXIT action) see // its job list too. const { jobTable } = opts; const inner = next.executeFn; next.jobTable = jobTable; next.executeFn = (cmd, o) => inner(cmd, { jobTable, ...o }); } if (opts.handed !== undefined) next = withHandOff(next, opts.handed); return next; } /** * A C-style for slot's text as bash evaluates it: its source up to the `;` * or `))` that ends it, each node's expansions substituted. Mirrors * Python's _slot_text. */ async function slotText(exprs, context, executeFn, callStack, view) { const first = exprs[0]; const last = exprs[exprs.length - 1]; if (first === undefined || last === undefined) return ''; const parent = first.parent ?? null; const source = parent?.text ?? ''; const base = parent?.startIndex ?? 0; const parts = []; let at = first.startIndex ?? 0; for (const expr of exprs) { const start = expr.startIndex ?? at; parts.push(source.slice(at - base, start - base)); parts.push(expr.isNamed === true ? await expandArith(expr, context, executeFn, callStack, view) : expr.text); at = expr.endIndex ?? start + expr.text.length; } const end = last.nextSibling?.startIndex ?? at; parts.push(source.slice(at - base, end - base)); return parts.join(''); } /** * Evaluate one C-style for expression slot: the slot's integer value, * or the default for an empty slot (1 for the condition so `for * ((;;))` loops, 0 for init/update). Throws the slot's ArithError, which * the loop prints as bash's `((: expr: reason` diagnostic, and * ReadonlyVariableError when the expression assigns to a readonly variable, once * the writes before it land (ExitSignal for one inside a subscript). */ async function evalCforExpr(exprs, dflt, context, executeFn, callStack, view) { if (exprs.length === 0) return dflt; const text = await slotText(exprs, context, executeFn, callStack, view); try { return Number(await landedArith(context.session, view ?? null, text)); } catch (err) { if (err instanceof ReadonlyVariableError && err.inSubscript) throw err.signal(); throw err; } } /** * Recurse wrapper for a re-associated trailing redirect: the list's right * operand runs under the hoisted redirects, bound by the same rule in turn * (`runRedirected`), so a pipeline there hands them to its last command * and a nested list to its own right operand; targets expand only at that * point (after the left side ran, so cwd changes apply). Every other node * recurses normally. */ /** The empty command a redirect with no command name runs under. */ function noCommand() { return Promise.resolve([null, new IOResult(), new ExecutionNode({ command: '', exitCode: 0 })]); } async function recurseReassociated(recurse, dispatch, executeFn, registry, namespace, redirects, right, signal, processes, node, context, stdin, callStack, opts) { if (node.id !== right.id) return recurse(node, context, stdin, callStack, opts); return runRedirected(recurse, dispatch, executeFn, registry, namespace, right, withOwnRedirects(right, redirects), signal, processes, opts?.sink, context, stdin, callStack); } /** * Recurse wrapper for a list the parse pulled into a pipeline's first * stage: the list's right operand, where the pipeline starts, runs the * pipeline; every other node recurses normally. */ async function recurseLifted(recurse, dispatch, executeFn, registry, namespace, stages, right, signal, processes, node, context, stdin, callStack, opts) { if (node !== right) return recurse(node, context, stdin, callStack, opts); return runPipeline(recurse, dispatch, executeFn, registry, namespace, stages, context, stdin, callStack, signal, processes, opts?.sink); } /** * Recurse wrapper for one pipeline stage. A stage the parse hoisted * redirects off runs under them, with the `2>&1` of a `|&` after it * applied last, as bash applies it after the command's own redirections; * a stage holding its own redirects gets that `2>&1` from * `recursePipeStderr`. */ async function recurseStage(recurse, dispatch, executeFn, registry, namespace, stages, targets, signal, processes, node, context, stdin, callStack, opts) { const index = stages.commands.indexOf(node); const hoisted = index < 0 ? [] : (stages.redirects[index] ?? []); if (hoisted.length === 0) { return recursePipeStderr(recurse, dispatch, executeFn, registry, namespace, targets, node, context, stdin, callStack, opts); } const bound = [...hoisted]; if (targets.includes(node)) { bound.push(new Redirect({ fd: 2, target: 1, kind: RedirectKind.STDERR_TO_STDOUT })); } return runRedirected(recurse, dispatch, executeFn, registry, namespace, node, bound, signal, processes, undefined, context, stdin, callStack); } /** * Run a pipeline as bash reads it (`getPipelineStages`). A list the parse * pulled into the first stage runs as the list it is, its right operand * standing for the pipeline, so the pipeline runs only when the list's * operator says it does and its status is the list's. A leading `!` * negates the whole pipeline's status. */ async function runPipeline(recurse, dispatch, executeFn, registry, namespace, stages, context, stdin, callStack, signal, processes, sink) { const session = context.session; if (stages.lead !== null) { const [left, op, right] = stages.lead; const wrapped = recurseLifted.bind(null, recurse, dispatch, executeFn, registry, namespace, { ...stages, lead: null }, right, signal, processes); return handleConnection(wrapped, left, op, right, context, stdin, callStack, executeFn); } const targets = stages.commands.filter((_, i) => stages.stderrFlags[i] === true); const pipeRecurse = recurseStage.bind(null, recurse, dispatch, executeFn, registry, namespace, stages, targets, signal, processes); const piped = () => handlePipe(pipeRecurse, stages.commands, stages.stderrFlags, context, stdin, callStack, signal, processes, executeFn, registry.io.bufferBytes, sink); const [stdout, io, execNode] = stages.negated ? await ignoringErrexit(context.session, piped) : await piped(); if (!stages.negated) return [stdout, io, execNode]; const flipped = new IOResult({ exitCode: io.exitCode !== 0 ? 0 : 1, stderr: io.stderr, refusal: io.refusal, }); execNode.exitCode = flipped.exitCode; session.errexitImmune = true; return [stdout, flipped, execNode]; } /** What `!` makes of the statement it wraps once that has run. */ async function negated(rawStdout, io, execNode, context, inner) { const session = context.session; // Lazy exit codes (grep's) must be final before // inverting, or `! grep miss f` negates the provisional 0. const stdout = await applyBarrier(rawStdout, io, BarrierPolicy.VALUE); // bash reports the negated pipeline's own statuses in PIPESTATUS // (`! false` leaves `1`), so what `!` wraps is closed as a statement // of its own before `$?` inverts. recordStatus(session, io.exitCode, pipelineTransparent(inner)); const flipped = new IOResult({ exitCode: io.exitCode !== 0 ? 0 : 1, stderr: io.stderr, refusal: io.refusal, }); execNode.exitCode = flipped.exitCode; session.errexitImmune = true; return [stdout, flipped, execNode]; } /** * Run a redirected statement: the command under its redirects. * * The parse hoists a trailing redirect over whatever precedes it, so the * redirects are bound where bash binds them first: past a list to its * right operand, past a pipeline to its last stage, and inside a `!` to * the command it negates, recursively, until they reach the command they * follow. */ async function runRedirected(recurse, dispatch, executeFn, registry, namespace, command, redirects, signal, processes, sink, context, stdin, callStack) { const session = context.session; if (command !== null && command.type === NT.REDIRECTED_STATEMENT) { const [inner, own] = getRedirects(command); return runRedirected(recurse, dispatch, executeFn, registry, namespace, inner, [...own, ...redirects], signal, processes, sink, context, stdin, callStack); } if (command !== null && command.type === NT.FUNCTION_DEFINITION) { // The redirects belong to the function, applied at each call // (getFunctionBody), not to the definition. return recurse(command, context, stdin, callStack); } if (command !== null && command.type === NT.LIST) { // the parser hoists a trailing redirect over the whole &&/|| // list; bash binds it to the last command: // redirected(list(L, op, R), r) == list(L, op, redirected(R, r)) // Re-associate and defer target expansion until R runs, so // `cd /x && echo hi > f` writes under /x. R is bound by this same // rule, so `a && b | c < f` reaches `c`, not the pipeline. Compound // and subshell bodies keep the whole-body redirect (bash group // semantics). const [left, op, right] = getListParts(command); const wrapped = recurseReassociated.bind(null, recurse, dispatch, executeFn, registry, namespace, redirects, right, signal, processes); return handleConnection(wrapped, left, op, right, context, stdin, callStack, executeFn); } if (command !== null && command.type === NT.PIPELINE) { return runPipeline(recurse, dispatch, executeFn, registry, namespace, getPipelineStages(command, redirects), context, stdin, callStack, signal, processes); } if (command !== null && command.type === NT.NEGATED_COMMAND) { // `! cmd < f` parses as redirected(negated(cmd), < f), but the // redirect is the command's: bash negates what `cmd < f` returns, a // redirect that failed to open included. const inner = getNegatedCommand(command); const [stdout, io, execNode] = await ignoringErrexit(context.session, () => runRedirected(recurse, dispatch, executeFn, registry, namespace, inner, inner === null ? redirects : withOwnRedirects(inner, redirects), signal, processes, sink, context, stdin, callStack)); return negated(stdout, io, execNode, context, inner ?? command); } const forked = forks(command, context.session); const expandAs = (forkedNow) => (redirect) => expandRedirect(redirect, context, executeFn, registry, callStack, sessionView(session, registry.policies, context.frame.diagnostics), forkedNow, namespace.sessionLinks(context.session)); const expand = expandAs(forked); if (isBareExec(command)) { if (redirects.some((r) => { const target = r.targetNode; return target !== null && namesProcessSub(target); })) { // A substitution lives only as long as its command, so it cannot back // a descriptor `exec` keeps (`>(cat)""` too). const err = encodeText('mirage: unsupported: exec onto a process substitution\n'); return [ null, new IOResult({ exitCode: 2, stderr: err }), new ExecutionNode({ command: 'exec', exitCode: 2, stderr: err }), ]; } return await installExecRedirects(dispatch, session, redirects, stdin, expand); } // A heredoc's operator line reads the routed stdout, so then it is // returned rather than written. A simple command expands its words // before its redirects apply, so what that printed (a substitution's // stderr) goes around them; a compound body expands inside them. const simple = command !== null && (command.type === NT.COMMAND || command.type === NT.DECLARATION_COMMAND || command.type === NT.VARIABLE_ASSIGNMENT || command.type === NT.VARIABLE_ASSIGNMENTS); const outer = context.frame.diagnostics; if (simple) context.frame.diagnostics = []; let stdout; let io; let execNode; try { if (command !== null && command.type === NT.COMMAND) { const underRedirects = (run, guard, name, args) => { // What the head ran as decides whether bash forks: `$cmd` naming a // builtin is the shell's own. const ranForked = forks(command, context.session, [name, ...args]); return handleRedirect((_node, _current, given, _stack, options) => run(given, options?.sink, redirectPathsFor(command)), dispatch, command, redirects, context, stdin, callStack, false, sink, expandAs(ranForked), guard, name, args, ranForked); }; [stdout, io, execNode] = await runWithRedirectPaths(command, [], () => recurse(command, context, stdin, callStack, { ...(sink ? { sink } : {}), ownDiagnostics: false, }), underRedirects, redirects); } else { ; [stdout, io, execNode] = await handleRedirect(simple ? (n, s, i, cs, opts) => recurse(n, s, i, cs, { ...opts, ownDiagnostics: false }) : recurse, dispatch, command, redirects, context, stdin, callStack, false, sink, expand, undefined, '', [], forked); } if (simple && context.frame.diagnostics.length > 0) { const err = diagnosticStderr(command, context); io.stderr = concat([err, await io.materializeStderr()]); execNode.stderr = concat([err, execNode.stderr]); } } catch (err) { if (simple && err instanceof ExitSignal) { err.stderr = concat([diagnosticStderr(command, context), err.stderr]); } throw err; } finally { context.frame.diagnostics = outer; } return [stdout, io, execNode]; } async function recursePipeStderr(recurse, dispatch, executeFn, registry, namespace, targets, node, context, stdin, callStack, opts) { if (!targets.includes(node) || nodeKind(node) !== NodeKind.REDIRECT) { return recurse(node, context, stdin, callStack, opts); } const [command, redirects] = getRedirects(node); redirects.push(new Redirect({ fd: 2, target: 1, kind: RedirectKind.STDERR_TO_STDOUT })); return runRedirected(recurse, dispatch, executeFn, registry, namespace, command, redirects, undefined, undefined, opts?.sink, context, stdin, callStack); } /** * Whether a redirect target names a process substitution of its own, alone or * in a longer word; one inside a command substitution is done with before the * target is spelled (`exec >"$(cat <(echo f))"`). Mirrors Python's * _names_process_sub. */ function namesProcessSub(word) { if (word.type === NT.PROCESS_SUBSTITUTION) return true; if (word.type === NT.COMMAND_SUBSTITUTION) return false; return word.children.some((child) => namesProcessSub(child)); } /** * Whether a redirected statement's command is a bare `exec`, which installs * its redirects on the shell for every later statement; `exec cmd` is a * command. */ function isBareExec(command) { if (command?.type !== NT.COMMAND) return false; const named = getParts(command); return named.length === 1 && named[0]?.type === NT.COMMAND_NAME && getText(named[0]) === 'exec'; } export async function executeNode(deps, node, context, stdin = null, callStack = null, // What expanding the node printed (a substitution's stderr) goes out with // the node's own stderr, unless its caller collects it: a simple // command's words expand before its redirects apply. ownDiagnostics = true, // The node is the whole of a child shell (a background job), which runs // its EXIT action when the node ends, its evaluator bound the way // `executeNodeBody` binds it for the node's own lines. endsShell = false) { const session = context.session; if (endsShell) { const { signal, executionScope } = deps; const executeFn = (cmd, opts) => deps.executeFn(cmd, { context, ...(signal !== undefined ? { signal } : {}), ...(executionScope !== undefined ? { executionScope } : {}), ...opts, }); return endShell(executeFn, session, stdin, callStack, executeNode(deps, node, context, stdin, callStack, ownDiagnostics)); } const executionScope = deps.executionScope ?? new ExecutionScope(undefined, newExecutionId(deps.registry.ids)); await executionScope.checkpoint(deps.signal ?? context.frame.abortSignal ?? undefined); // The node's own `<(...)` files go when it ends, so a command still // reading one is read out first. const processSubs = new ProcessSubScope(context, deps.registry, deps.sink, deps.signal); const previous = context.frame.processSub; context.frame.processSub = processSubs.open; try { if (!ownDiagnostics) { const [stdout, io, execNode] = await executeNodeBody(deps, node, context, stdin, callStack, executionScope, false); if (deps.signal?.aborted === true || context.frame.abortSignal?.aborted === true) { throw makeAbortError(deps.signal?.aborted === true ? deps.signal : (context.frame.abortSignal ?? undefined)); } return [processSubs.own(stdout, io, execNode), io, execNode]; } const outer = context.frame.diagnostics; context.frame.diagnostics = []; try { const [stdout, io, execNode] = await executeNodeBody(deps, node, context, stdin, callStack, executionScope); // A statement that settles after the caller aborted is an orphan: its // status must not reach the shell the caller was already released from. if (deps.signal?.aborted === true || context.frame.abortSignal?.aborted === true) { throw makeAbortError(deps.signal?.aborted === true ? deps.signal : (context.frame.abortSignal ?? undefined)); } if (context.frame.diagnostics.length > 0) { const err = diagnosticStderr(node, context); const existing = await io.materializeStderr(); const merged = new Uint8Array(err.length + existing.length); merged.set(err); merged.set(existing, err.length); io.stderr = merged; execNode.stderr = merged; } return [processSubs.own(stdout, io, execNode), io, execNode]; } catch (err) { if (err instanceof ExitSignal) { const extra = diagnosticStderr(node, context); const merged = new Uint8Array(extra.length + err.stderr.length); merged.set(extra); merged.set(err.stderr, extra.length); err.stderr = merged; } throw err; } finally { context.frame.diagnostics = outer; } } catch (err) { // `exit`, `return` or `break` ends the node as its last command did: // output consumers finish, rather than being cut off. if (isUnwinding(err)) await trailed(err, ...(await processSubs.settle())); else await processSubs.close(true); throw err; } finally { context.frame.processSub = previous; } } /** * Walk one node. The scope and signal it runs under are bound into * `executeFn`, so a `$(...)` in a background job never dies of the caller's * abort. `set -n` stops every node at any depth, as GNU answers `if true; * then set -n; echo BAD; fi` with nothing; the program loop's own stop is * what silences `set -v` for the lines it never reads. `ownDiagnostics`: * whether a node drained into the sink flushes its own diagnostics; a * redirect's simple command leaves them for the redirect to put outside it. */ async function executeNodeBody(deps, node, context, stdin, callStack, executionScope, ownDiagnostics = true) { const session = context.session; const inner = deps.executeFn; const signal = deps.signal; deps = { ...deps, executionScope, executeFn: (cmd, opts) => { if (opts.executionScope !== undefined) return inner(cmd, opts); const merged = mergeSignals(signal, opts.signal); return inner(cmd, { ...opts, executionScope, ...(merged !== undefined ? { signal: merged } : {}), }); }, }; const { sink, ...captureDeps } = deps; const recurse = (n, s, i, cs, opts) => executeNode(withOpts(captureDeps, opts), n, s, i, cs, opts?.ownDiagnostics !== false, opts?.endsShell === true); const stream = sink === undefined ? recurse : (n, s, i, cs, opts) => executeNode(withOpts(deps, opts), n, s, i, cs, opts?.ownDiagnostics !== false, opts?.endsShell === true); const { dispatch, registry, jobTable } = deps; // Capture the walker's session before any await; a concurrent line's // ambient frame cannot identify this node's nested evaluations. const executeFn = (cmd, opts) => deps.executeFn(cmd, { context, ...opts }); const kind = nodeKind(node); const view = sessionView(session, registry.policies, context.frame.diagnostics); // A root run on a caller's frames is the caller's own line (eval, // source, an alias, `$( )`); one given none is a shell of its own. const inline = callStack !== null; callStack ??= new CallStack(session.shellNesting); // The statements a construct runs all read one descriptor, as bash's // do: `read` takes its line and the command after it gets the rest, in // a group, a loop, a list, a subshell or a nested shell alike. if (STREAMING_KINDS.has(kind)) stdin = share(stdin); if (session.shellOptions.noexec === true) { return [null, new IOResult(), new ExecutionNode({ command: '', exitCode: 0 })]; } if (deps.signal?.aborted === true || context.frame.abortSignal?.aborted === true) { throw makeAbortError(deps.signal?.aborted === true ? deps.signal : (context.frame.abortSignal ?? undefined)); } session.errexitImmune = false; // A sink turns this walk from "return your output" into "write your // output". Sequencing constructs pass it to their children so each // statement lands as it finishes, and a pipeline streams its last // stage; everything else runs unchanged and has its result drained // here. Only these kinds inherit a sink, so capture sites keep // receiving their output as a value. if (sink !== undefined && !STREAMING_KINDS.has(kind) && kind !== NodeKind.COMMAND && kind !== NodeKind.PIPELINE && kind !== NodeKind.REDIRECT && kind !== NodeKind.VAR_ASSIGN && kind !== NodeKind.VAR_ASSIGNS) { return drained(sink, ...(await recurse(node, context, stdin, callStack, { ownDiagnostics }))); } if (kind === NodeKind.TIMED) { const started = performance.now(); const portable = timedBy(node); const reported = (stderr, timed) => { const elapsed = (performance.now() - started) / 1000; const report = timingReport(elapsed, timed, session.env.TIMEFORMAT); const merged = new Uint8Array(stderr.length + report.length); merged.set(stderr); merged.set(report, stderr.length); return merged; }; const inner = node.namedChildren[0]; let ran; try { ran = inner === undefined ? [null, new IOResult(), new ExecutionNode({ command: '', exitCode: 0 })] : await stream(inner, context, stdin, callStack).then(async ([body, out, ranNode]) => [await applyBarrier(body, out, BarrierPolicy.VALUE), out, ranNode]); } catch (err) { // bash reports the time of a pipeline that `exit`, `break`, `continue` // or an error discarding the line leaves, though not of one `return` // leaves. if (portable !== null) { if (err instanceof ExitSignal) err.stderr = reported(err.stderr, portable); else if (err instanceof LoopSignal) err.io.stderr = reported(await err.io.materializeStderr(), portable); } throw err; } const [stdout, io, execNode] = ran; if (portable !== null) io.stderr = reported(await io.materializeStderr(), portable); return [stdout, io, execNode]; } if (kind === NodeKind.COMMENT) { return [null, new IOResult(), new ExecutionNode({ command: '', exitCode: 0 })]; } if (kind === NodeKind.PROGRAM) { const pending = redirectSyntaxFor(node); const statements = node.namedChildren.filter((child) => child.type !== NT.COMMENT); const last = statements.at(-1); if (pending.length > 0 && last === undefined) { return runRedirected(recurse, dispatch, executeFn, registry, deps.namespace, null, [...pending], deps.signal, jobTable.processes, sink, context, stdin, callStack); } const programRecurse = pending.length > 0 && last !== undefined ? (child, current, given, stack, options) => recurseReassociated(recurse, dispatch, executeFn, registry, deps.namespace, pending, last, deps.signal, jobTable.processes, child, current, given, stack, options) : recurse; return executeProgram(programRecurse, node, context, stdin, callStack, jobTable, dispatch, deps.handed ?? null, registry.decisions, sink ?? null, inline, executeFn); } if (kind === NodeKind.COMMAND && !node.children.some((c) => c.type === NT.COMMAND_NAME)) { // Assignments with redirects among them and no command name // (`a=1 > f b=$a`): bash assigns in order, as an assignment statement // does, then opens the redirects with no command; one that fails fails // the line, the assignments standing, and else the status is the last // substitution's, a redirect's included. const subSeq = context.frame.cmdsubSeq; let mergedIo = new IOResult(); for (const child of node.namedChildren) { if (child.type !== NT.VARIABLE_ASSIGNMENT) continue; const [, io] = await recurse(child, context, stdin, callStack, { ownDiagnostics: false }); mergedIo = await mergedIo.merge(io); } const runner = redirectRunnerFor(node); const [, opened] = runner === null ? await runRedirected(recurse, dispatch, executeFn, registry, deps.namespace, null, commandRedirects(node), deps.signal, jobTable.processes, undefined, context, stdin, callStack) : await runner(noCommand, undefined, '', []); mergedIo = await mergedIo.merge(opened); mergedIo.exitCode = opened.exitCode !== 0 ? opened.exitCode : assignmentStatus(context.frame, subSeq); const ran = [ null, mergedIo, new ExecutionNode({ command: getText(node), exitCode: mergedIo.exitCode }), ]; return sink === undefined ? ran : drained(sink, ...ran); } const own = kind === NodeKind.COMMAND && redirectRunnerFor(node) === null ? commandRedirects(node) : []; if (own.length > 0) { const ran = await runRedirected(recurse, dispatch, executeFn, registry, deps.namespace, node, own, deps.signal, jobTable.processes, sink, context, stdin, callStack); return sink === undefined ? ran : drained(sink, ...ran); } if (kind === NodeKind.COMMAND) { const expanded = { words: null }; try { const ran = await runInCommandScope(() => executeCommand(recurse, dispatch, registry, deps.namespace, executeFn, node, context, stdin, callStack, jobTable, deps.signal, deps.parser, deps.handed, sink, expanded)); return sink === undefined ? ran : await drained(sink, ...ran); } catch (err) { // SIGPIPE ends a program alone; a builtin or a function writes as the // shell, which dies. What the head expanded to decides (`$cmd` naming // printf is the shell's); a command that never expanded (an alias) // leaves it to its caller. if (!(err instanceof PipeClosed) || expanded.words === null || !forks(node, session, expanded.words)) throw err; return [ null, new IOResult({ exitCode: SIGPIPE_STATUS }), new ExecutionNode({ command: getText(node), exitCode: SIGPIPE_STATUS }), ]; } } if (kind === NodeKind.PIPELINE) { // `! a | b` parses as pipeline(negated_command(a), b), and a redirect // followed by `|` closes over everything to its left, so the stages // are read the way bash reads them rather than as the parse nested // them (see getPipelineStages). const ran = await runPipeline(recurse, dispatch, executeFn, registry, deps.namespace, getPipelineStages(node), context, stdin, callStack, deps.signal, jobTable.processes, sink); return sink === undefined ? ran : drained(sink, ...ran); } if (kind === NodeKind.LIST) { const [left, op, right] = getListParts(node); return handleConnection(stream, left, op, right, context, stdin, callStack, executeFn); } if (kind === NodeKind.REDIRECT) { const [command, redirects] = getRedirects(node); const ran = await runRedirected(recurse, dispatch, executeFn, registry, deps.namespace, command, redirects, deps.signal, jobTable.processes, sink, context, stdin, callStack); return sink === undefined ? ran : drained(sink, ...ran); } if (kind === NodeKind.SUBSHELL) { // A subshell is its own shell: background jobs started inside live // in a private job table (`$!`/`wait`/`kill` in the body see them; // the parent's table never does), mirroring bash's forked process. const subTable = jobTable.child(); const abort = new AbortController(); const subDeps = { ...captureDeps, jobTable: subTable, signal: deps.signal === undefined ? abort.signal : AbortSignal.any([deps.signal, abort.signal]), }; // The opts parameter is load-bearing, not decoration: a job started // inside the subshell body hands `handleBackground` its own console // and abort signal through it. Dropping it (a 4-parameter closure // still satisfies ExecuteNodeFn, since function parameters are // bivariant) would run the nested job against the enclosing job's // sink and signal instead. const subRecurse = (n, s, inp, cs, opts) => executeNode(withOpts(subDeps, opts), n, s, inp, cs, opts?.ownDiagnostics !== false, opts?.endsShell === true); const child = childContext(context); const asProgram = isProgramInvocation(session); let result; let process; try { process = subTable.processes.start({ sessionId: session.sessionId, command: node.text, cwd: PathSpec.fromStrPath(session.cwd), parentPid: session.processId, cancel: () => { abort.abort(); }, limit: session.processes.max, started: (info) => { child.session.processId = info.pid; }, run: async () => { const body = () => handleSubshell(subRecurse, node.children, child, stdin, callStack, subTable, dispatch, deps.handed ?? null, registry.decisions, sink ?? null, executeFn); try { result = await runWithEvaluation(child, () => asProgram ? runAsProgram(child.session, body) : body()); } finally { await child.session.closeInputs(); } return result[1].exitCode; }, }); } catch (error) { if (error.code === 'EAGAIN') throw new ExitSignal(FORK_FAILED_STATUS, encodeText(FORK_FAILED)); throw error; } await process.task; if (result === undefined) throw new Error('subshell completed without a result'); return result; } if (kind === NodeKind.ARITH) { const text = getText(node); const expr = await expandArith(node, context, executeFn, callStack, view); let value; try { value = await landedArith(session, view, expr); } catch (err) { if (err instanceof PolicyDenied) return fail(text, `bash: ${err.message}\n`); if (!(err instanceof ArithError || err instanceof ReadonlyVariableError)) throw err; if (err.inSubscript) throw err.signal(); return fail(text, `bash: ${err instanceof ArithError ? '((: ' : ''}${err.message}\n`); } return result(text, { exitCode: value !== 0n ? 0 : 1 }); } const run = (nodes, bound) => executeBody(stream, nodes, context, stdin, callStack, jobTable, deps.handed ?? null, registry.decisions, executeFn, sink ?? null, bound); if (kind === NodeKind.COMPOUND) return run(node.namedChildren); if (kind === NodeKind.IF) { const [branches, elseBody] = getIfBranches(node); return handleIf(run, branches, elseBody, session); } if (kind === NodeKind.CFOR) { const [exprs, body] = getCforParts(node); const evalExpr = (e, d) => evalCforExpr(e, d, context, executeFn, callStack, view); return callStack.loop(() => handleCfor(run, exprs, body, evalExpr, session)); } if (kind === NodeKind.FOR || kind === NodeKind.SELECT) { const [variable, values, body] = getForParts(node); if (!isValidName(variable)) return fail(kind, `bash: \`${variable}': not a valid identifier\n`); const resolved = values === null ? positionalParams(session, callStack) : await runInCommandScope(async () => { const classified = await expandAndClassify(values, context, executeFn, registry, session.cwd, callStack, view); // The loop word list is consumed by the shell (WordPolicy.SHELL): // globs resolve to matches before iteration starts. return resolveGlobs(classified, registry, session.shellOptions.noglob === true, deps.namespace.sessionLinks(session), globOptions(session)); }); if (kind === NodeKind.SELECT) { const signal = mergeSignals(deps.signal, context.frame.abortSignal); return callStack.loop(() => handleSelect(run, variable, resolved, body, context, stdin, registry.policies, signal, sink)); } return callStack.loop(() => handleFor(run, variable, resolved, body, context, registry.policies)); } if (kind === NodeKind.WHILE || kind === NodeKind.UNTIL) { const [test, body] = getWhileParts(node); return callStack.loop(() => handleWhile(run, test, body, session, kind === NodeKind.UNTIL)); } if (kind === NodeKind.CASE) { const word = await expandNode(getCaseWord(node), context, executeFn, callStack, view); return handleCase(run, word, getCaseItems(node), session, (pattern) => expandPattern(pattern, context, executeFn, callStack, view)); } if (kind === NodeKind.FUNCTION_DEF) { const name = getFunctionName(node); if (session.readonlyFunctions.has(name)) { // `readonly -f f` froze the body: either definition syntax refuses // with `f: readonly function`, exit 1, and the old body stays, // pinned on 5.2.37. return fail(`function ${name}`, `bash: ${name}: readonly function\n`); } const source = getFunctionSource(node); session.functions[name] = source; session.functionSites.set(name, { source, mark: aliasMark(session, node.startPosition?.row ?? 0), origin: definedAt(node, deps.handed ?? null), aliases: aliasView(session, node, aliasMark(session, readRow(node))), }); return result(`function ${name}`); } if (kind === NodeKind.DECLARATION) { return await runInCommandScope(() => executeDeclaration(node, context, executeFn, registry, deps.namespace, callStack, deps.parser)); } if (kind === NodeKind.UNSET) { return handleUnset(getUnsetArgs(node), session, view); } if (kind === NodeKind.TEST) { // The shell's own `[[`, which no command gate admits: it is admitted by // its own name on what it tests. const signal = mergeSignals(deps.signal, context.frame.abortSignal); const claimant = claimantFor(node, deps.handed); const admitted = (words) => admitTest('[[', words, session, registry, deps.namespace, signal, claimant); const tree = await expandDoubleBracket(node, context, executeFn, callStack, view); return handleTest(dispatch, deps.namespace, tree, session, '[[', view, admitted, signal); } if (kind === NodeKind.NEGATED) { const inner = getNegatedCommand(node); // A bare `!` negates the empty command, which succeeds. if (inner === null) return negated(null, new IOResult(), new ExecutionNode({ command: '', exitCode: 0 }), context, node); const [stdout, io, execNode] = await ignoringErrexit(context.session, () => stream(inner, context, stdin, callStack)); return negated(stdout, io, execNode, context, inner); } if (kind === NodeKind.VAR_ASSIGN) { return await executeAssignment(node, context, executeFn, registry, deps.namespace, callStack); } if (kind === NodeKind.VAR_ASSIGNS) { const subSeq = context.frame.cmdsubSeq; let mergedIo = new IOResult(); for (const child of node.namedChildren) { if (child.type !== NT.VARIABLE_ASSIGNMENT) continue; const [, io] = await recurse(child, context, stdin, callStack, { ownDiagnostics: false }); mergedIo = await mergedIo.merge(io); } // The statement's status follows the last command substitution // performed across ALL its assignments, not the last child's. const code = assignmentStatus(context.frame, subSeq); mergedIo.exitCode = code; return [null, mergedIo, new ExecutionNode({ command: getText(node), exitCode: code })]; } // Constructs the parser accepts but the executor cannot honor // (future grammar additions). return fail(node.text, `mirage: unsupported shell construct: ${node.type}\n`, 2); } //# sourceMappingURL=execute_node.js.map