@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
863 lines • 45.1 kB
JavaScript
// ========= 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