UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

156 lines • 7.89 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 { usageHint } from "../../../../commands/spec/usage.js"; import { isProgramInvocation } from "../../../../context/session_context.js"; import { IOResult } from "../../../../io/types.js"; import { PolicyDenied } from "../../../../policy/errors.js"; import { decodeText, encodeText, utf8Locale } from "../../../../shell/bytes.js"; import { ArithError } from "../../../../shell/errors.js"; import { concat } from "../../../../utils/bytes.js"; import { assignElement } from "../../../session/elements.js"; import { envSnapshot, sessionView } from "../../../session/state.js"; import { ExecutionNode } from "../../../types.js"; import { TARGET_RE } from "../constants.js"; import { builtinUsage, fail, result } from "../shared.js"; import { printfOutput, runPrintf } from "./format.js"; /** * Print formatted output, honoring GNU printf's format-reuse rules. * * Supports `%s %c %b %q`, the integer conversions `%d %i %o %u %x %X`, * the float conversions `%f %F %e %E %g %G %a %A`, and `%%`, with * `- + 0 # (space)` flags, numeric or `*` width/precision, and backslash * escapes (including `\u`/`\U`) interpreted once in the same scan. When * arguments remain after one pass the format is reused until they are * exhausted; a missing argument renders as the empty string / `0`. * Integers saturate at 64-bit limits; negative unsigned values wrap. `%a` * formats at IEEE double precision. The shared Rust stream owns conversions. * * With `-v NAME` the formatted text is stored in the shell variable * `NAME` (or the array element `NAME[idx]`) instead of written to * stdout, matching bash's builtin. An unusable `NAME` is rejected before * the format runs (status 2); a readonly name or an out-of-range subscript * still reports the format's own errors first, then fails with status 1 * and leaves the variable untouched. bash stores the bytes the format * produced, so a `\x` run that is valid UTF-8 is stored as its characters, * and up to the first NUL, which no variable holds (`printf -v n '1\0002'` * stores 1). `-v` is the builtin's alone: run as a program (`find -exec * printf`, which execvp answers with coreutils printf) the word is the * format, and a format that takes no argument warns about the ones it * drops. */ export async function handlePrintf(args, session, view, signal) { const program = isProgramInvocation(session); let target = null; let parsed = null; if (args.length >= 2 && args[0] === '-v' && !program) { target = args[1] ?? ''; args = args.slice(2); parsed = TARGET_RE.exec(target); if (parsed === null) { // bash validates the name before formatting, so a bad name // suppresses the conversion errors the format would report. return fail('printf', `bash: printf: \`${target}': not a valid identifier\n`, 2); } } const first = args[0]; if (first !== undefined && !program) { if (first === '--') { args = args.slice(1); if (args.length === 0) { // `--` ends the options and the FORMAT is still required, so the line // is bash's usage error rather than an empty one (bash 5.2.21: // `printf --` is exit 2 with the usage, where `printf -- --zzz` prints // `--zzz`). return fail('printf', builtinUsage('printf'), 2); } } else if (first.startsWith('-') && first.length > 1 && first !== '-v') { // bash's option scan takes single letters only, so it reports the // first character it does not know spelled with ONE dash: a long // spelling answers for its second dash and its text never reaches the // message, which is why `printf --zzz`, `printf --hel` and // `printf --zzz=é` are all `printf: --: invalid option` (bash 5.2.21). // The coreutils binary is lenient here and prints the word, but mirage // ships printf as a builtin, so the builtin governs. A bare `-v` short // of its NAME is left to the format path, where bash's own `option // requires an argument` is a separate change. return fail('printf', `bash: printf: -${first[1] ?? ''}: invalid option\n${builtinUsage('printf')}`, 2); } } else if (first === '--') { // coreutils printf takes one leading `--` as the end of its options. args = args.slice(1); } if (args.length === 0) { // A format is required: bash's usage error, `printf -v x` too. if (program) return fail('printf', `printf: missing operand\n${usageHint('printf')}\n`); return fail('printf', builtinUsage('printf'), 2); } const posix = program && Object.hasOwn(envSnapshot(session), 'POSIXLY_CORRECT'); const utf8 = utf8Locale(envSnapshot(session)); if (target === null || parsed === null) { return printed(args[0] ?? '', args.slice(1), program, posix, utf8, signal); } const [output, stderr, exitCode] = await runPrintf(args[0] ?? '', args.slice(1), program, posix, utf8, signal); const errors = decodeText(stderr); const base = parsed[1] ?? ''; const text = decodeText(output).split('\0', 1)[0] ?? ''; let status; try { status = await assignElement(session, view ?? null, base, parsed[2] ?? null, text); } catch (err) { if (err instanceof ArithError) { // The target carries `-i` and the formatted text does not // evaluate, which ends the shell as any `-i` value does. const signal = err.signal('printf', true); signal.stderr = concat([encodeText(errors), signal.stderr]); throw signal; } if (!(err instanceof PolicyDenied)) throw err; return fail('printf', errors + `bash: ${err.message}\n`); } if (status === 'readonly') return fail('printf', errors + `bash: ${base}: readonly variable\n`); if (status === 'denied') return fail('printf', errors + `bash: ${base}: permission denied\n`); if (status !== 'ok') return fail('printf', errors + `bash: ${target}: bad array subscript\n`); return result('printf', { exitCode, stderr: errors }); } /** * `printf` to stdout: the output streams as the engine drains it, and the * status and stderr settle once it ends. Mirrors Python's _printed. */ function printed(fmt, args, program, posix, utf8, signal) { const io = new IOResult(); const node = new ExecutionNode({ command: 'printf' }); async function* output() { try { yield* printfOutput(fmt, args, io, program, posix, utf8, signal); } finally { node.exitCode = io.exitCode; node.stderr = io.stderr instanceof Uint8Array ? io.stderr : new Uint8Array(); } } return [output(), io, node]; } /** The `printf` arm. */ export async function printfBuiltin(call) { return handlePrintf([...call.argv.args], call.context.session, sessionView(call.context.session, call.registry.policies, call.context.frame.diagnostics), call.signal); } //# sourceMappingURL=printf.js.map