250 lines
6.9 KiB
JavaScript
250 lines
6.9 KiB
JavaScript
/**
|
|
* 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<string, unknown>} 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<string, unknown>} 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<string, unknown>} 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<string, unknown>} 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<string, string>} 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<string, string>} 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<string, unknown>} 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 <<DELIM`.
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} execLine
|
|
* @param {(ctx: Record<string, unknown>) => 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 }
|
|
}
|