/** * Small statement helpers: local/declare, limited `[[ ]]`, reserved-word * diagnostics, function positional env, and `case` pattern lists. */ import { expandWord } from './shell-expand.js' /** * @typedef {{ type: string, value: string }} ShellStmtToken */ /** * @param {Record} ctx * @param {ShellStmtToken[]} rest * @returns {Promise<'exit' | 'ok'>} */ export async function execShellLocalBuiltin(ctx, rest) { const vfs = ctx.vfs const env = vfs?.env if (!env || typeof env !== 'object') { ctx.exitCode = 0 return 'ok' } let n = 0 for (const t of rest) { if (++n > 48) break if (t.type !== 'word') continue const eq = t.value.indexOf('=') if (eq <= 0) continue const name = t.value.slice(0, eq).trim() if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue env[name] = expandWord(t.value.slice(eq + 1), env) } ctx.exitCode = 0 return 'ok' } /** * @param {Record} ctx * @param {ShellStmtToken[]} rest * @returns {'ok' | null} */ export function tryExecShellDeclareBuiltin(ctx, rest) { const vfs = ctx.vfs const env = vfs?.env if (!env || typeof env !== 'object') return 'ok' if (rest[0]?.type !== 'word' || rest[0].value !== '-r') return null let n = 0 for (let i = 1; i < rest.length; i++) { if (++n > 32) break const t = rest[i] if (t.type !== 'word') continue const eq = t.value.indexOf('=') if (eq <= 0) continue const name = t.value.slice(0, eq).trim() if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue env[name] = expandWord(t.value.slice(eq + 1), env) } ctx.exitCode = 0 return 'ok' } /** Reserved words that cannot begin a simple or compound statement (POSIX-style). */ export const BARE_OS_SHELL_MISPLACED_STATEMENT_START = new Set([ 'then', 'else', 'elif', 'fi', 'do', 'done', 'esac', 'in' ]) /** * @param {Record} ctx * @param {ShellStmtToken[]} stmt * @returns {boolean} true when an error was reported (caller should return) */ export function tryReportMisplacedReservedStatementStart(ctx, stmt) { const h = stmt[0] if (!h || h.type !== 'word') return false const w = h.value if (!BARE_OS_SHELL_MISPLACED_STATEMENT_START.has(w)) return false ctx.console.error( `shell: syntax error: reserved word '${w}' cannot start a statement` ) ctx.exitCode = 2 return true } /** * Minimal gated **`[[ … ]]`** — only **`[[ WORD == WORD ]]`** and **`[[ WORD != WORD ]]`**. * @param {Record} ctx * @param {ShellStmtToken[]} stmt * @returns {Promise<'exit' | 'ok'>} */ export async function execDoubleBracketLimited(ctx, stmt) { const vfs = ctx.vfs const env = vfs?.env && typeof vfs.env === 'object' ? vfs.env : {} const last = stmt[stmt.length - 1] if (!last || last.type !== 'word' || last.value !== ']]') { ctx.console.error('shell: [[: missing closing ]]') ctx.exitCode = 2 return 'ok' } const bodyStart = stmt[0]?.value === '[[' ? 1 : 2 const inner = stmt.slice(bodyStart, -1) if ( inner.length === 3 && inner[0].type === 'word' && inner[1].type === 'word' && inner[2].type === 'word' ) { const op = inner[1].value if (op === '==' || op === '!=') { const a = expandWord(inner[0].value, env) const b = expandWord(inner[2].value, env) ctx.exitCode = op === '==' ? (a === b ? 0 : 1) : a !== b ? 0 : 1 return 'ok' } } ctx.console.error( 'shell: [[: only `[[ WORD == WORD ]]` and `[[ WORD != WORD ]]` are supported' ) ctx.exitCode = 2 return 'ok' } /** * @param {Record} env * @param {string[]} argv */ export function assignShellFunctionPositionalEnv(env, argv) { env['0'] = argv[0] || '' const fnArgs = argv.slice(1) for (let i = 1; i <= 9; i++) env[String(i)] = fnArgs[i - 1] ?? '' env['#'] = String(fnArgs.length) } /** * @param {ShellStmtToken[]} toks * @param {Record} env * @returns {string[]} */ export function casePatternList(toks, env) { /** @type {string[]} */ const out = [] /** @type {ShellStmtToken[]} */ let cur = [] for (const t of toks) { if (t.type === 'op' && t.value === '|') { if (cur.length) { const s = cur.map((w) => w.value).join(' ') out.push(expandWord(s.trim(), env)) cur = [] } } else if (t.type === 'word') { cur.push(t) } } if (cur.length) { const s = cur.map((w) => w.value).join(' ') out.push(expandWord(s.trim(), env)) } return out.filter(Boolean) } /** * Clear simulated background jobs on guest ↔ unlocked transitions (POSIX session model). * @param {Record} ctx */ export function bareOsResetShellIdentityState(ctx) { if ( ctx.shellBackgroundJobs && typeof ctx.shellBackgroundJobs === 'object' && Array.isArray(ctx.shellBackgroundJobs.list) ) { ctx.shellBackgroundJobs.list.length = 0 ctx.shellBackgroundJobs.nextId = 1 } } /** * Consume an interactive here-document after `cmd <} ctx * @param {string} execLine * @param {(ctx: Record) => void} syncExit * @returns {Promise<{ ok: true, execLine: string } | { ok: false }>} */ export async function consumeInteractiveHeredoc(ctx, execLine, syncExit) { const readL = ctx.readLine if (typeof readL !== 'function') return { ok: true, execLine } const hm = execLine.match(/^(.*?)<<-?\s*(?:'([^']+)'|"([^"]+)"|(\S+))\s*$/) if (!hm) return { ok: true, execLine } const prefix = hm[1].trimEnd() if (!prefix) { ctx.console.error( 'shell: here-document requires a command before << on the same line' ) ctx.exitCode = 2 syncExit(ctx) return { ok: false } } const delim = hm[2] ?? hm[3] ?? hm[4] const singleQuoted = hm[2] != null const posixHeredocCap = (() => { if ( ctx.env?.BARE_OS_SHELL_POSIX_MODE !== '1' && ctx.env?.BARE_OS_SHELL_POSIX_MODE !== 'true' ) { return null } const raw = String(ctx.env.BARE_OS_SHELL_HEREDOC_MAX_BYTES || '').trim() const n = parseInt(raw, 10) if (Number.isFinite(n) && n > 0) return Math.min(2_000_000, n) return 262144 })() /** @type {string[]} */ const bodyLines = [] let heredocAcc = 0 for (;;) { const ln = await readL('> ') if (ln == null) break if (ln === delim) break if (posixHeredocCap != null) { heredocAcc += ln.length + 1 if (heredocAcc > posixHeredocCap) { ctx.console.error( 'shell: here-document exceeds BARE_OS_SHELL_HEREDOC_MAX_BYTES cap (POSIX mode)' ) ctx.exitCode = 2 syncExit(ctx) return { ok: false } } } bodyLines.push(ln) } const vfs = ctx.vfs const env = vfs?.env && typeof vfs.env === 'object' ? vfs.env : {} let body = bodyLines.join('\n') if (!singleQuoted) { body = bodyLines.map((l) => expandWord(l, env)).join('\n') } ctx.shellHeredocOnce = body if (body.length > 0 && !body.endsWith('\n')) { ctx.shellHeredocOnce += '\n' } return { ok: true, execLine: prefix } }