Orginize
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Shell builtin name table and optional POSIX `read` builtin.
|
||||
*/
|
||||
|
||||
export const SHELL_BUILTINS = new Set([
|
||||
'alias',
|
||||
'unalias',
|
||||
'barerc',
|
||||
'cd',
|
||||
'export',
|
||||
'unset',
|
||||
'readonly',
|
||||
'umask',
|
||||
'set',
|
||||
':',
|
||||
'command',
|
||||
'type',
|
||||
'logout',
|
||||
'exit',
|
||||
'jobs',
|
||||
'fg',
|
||||
'bg',
|
||||
'wait',
|
||||
'suspend-job',
|
||||
'disown',
|
||||
'trap',
|
||||
'test',
|
||||
'['
|
||||
])
|
||||
|
||||
/**
|
||||
* Optional POSIX-style **`read`** builtin (bounded line, IFS split). Off by default.
|
||||
* @param {Record<string, string | undefined> | null | undefined} env
|
||||
*/
|
||||
export function bareOsShellReadBuiltinEnabled(env) {
|
||||
const o = env && typeof env === 'object' ? env : {}
|
||||
return o.BARE_OS_SHELL_READ_BUILTIN === '1' || o.BARE_OS_SHELL_READ_BUILTIN === 'true'
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorted list of shell builtin command names for completion / UX.
|
||||
* Includes **`read`** only when {@link bareOsShellReadBuiltinEnabled} is true.
|
||||
* @param {Record<string, string | undefined> | null | undefined} env
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function listBareOsShellBuiltins(env) {
|
||||
const out = [...SHELL_BUILTINS]
|
||||
if (bareOsShellReadBuiltinEnabled(env)) out.push('read')
|
||||
out.sort()
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} cmd
|
||||
* @param {Record<string, string | undefined> | null | undefined} env
|
||||
*/
|
||||
export function isShellBuiltin(cmd, env) {
|
||||
if (SHELL_BUILTINS.has(cmd)) return true
|
||||
if (cmd === 'read' && bareOsShellReadBuiltinEnabled(env)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
* @param {string} ifs
|
||||
* @param {number} nNames
|
||||
*/
|
||||
export function bareOsShellReadSplitFields(line, ifs, nNames) {
|
||||
const sep = ifs.length ? ifs[0] : ' '
|
||||
if (nNames <= 1) return [line]
|
||||
const out = []
|
||||
let rest = line
|
||||
for (let i = 0; i < nNames - 1; i++) {
|
||||
const idx = rest.indexOf(sep)
|
||||
if (idx === -1) {
|
||||
out.push(rest)
|
||||
rest = ''
|
||||
break
|
||||
}
|
||||
out.push(rest.slice(0, idx))
|
||||
rest = rest.slice(idx + sep.length)
|
||||
}
|
||||
while (out.length < nNames - 1) out.push('')
|
||||
out.push(rest)
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {number} maxBytes
|
||||
* @param {(m: string) => void} errFn
|
||||
* @returns {Promise<string | null>} null = EOF / error
|
||||
*/
|
||||
export async function bareOsShellReadOneLine(ctx, maxBytes, errFn, opts = {}) {
|
||||
const delimiter = typeof opts.delimiter === 'string' ? opts.delimiter : '\n'
|
||||
const timeoutMs = Number.isFinite(opts.timeoutMs) ? Number(opts.timeoutMs) : 0
|
||||
if (typeof ctx.shellStdin === 'string') {
|
||||
const raw = ctx.shellStdin
|
||||
const idx = delimiter ? raw.indexOf(delimiter) : -1
|
||||
const line = idx === -1 ? raw : raw.slice(0, idx)
|
||||
ctx.shellStdin = idx === -1 ? '' : raw.slice(idx + delimiter.length)
|
||||
if (line.length > maxBytes) {
|
||||
errFn(`read: line exceeds BARE_OS_SHELL_READ_MAX_BYTES (${maxBytes})`)
|
||||
return null
|
||||
}
|
||||
return line
|
||||
}
|
||||
const rl = ctx.readLine
|
||||
if (typeof rl === 'function') {
|
||||
const readP = rl('')
|
||||
const ln =
|
||||
timeoutMs > 0
|
||||
? await Promise.race([
|
||||
readP,
|
||||
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
|
||||
])
|
||||
: await readP
|
||||
if (ln == null) return null
|
||||
if (ln.length > maxBytes) {
|
||||
errFn(`read: line exceeds BARE_OS_SHELL_READ_MAX_BYTES (${maxBytes})`)
|
||||
return null
|
||||
}
|
||||
return ln
|
||||
}
|
||||
errFn(
|
||||
'read: no input (redirect stdin, use a pipeline, or interactive readLine)'
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string[]} argv
|
||||
* @param {Record<string, string>} env
|
||||
* @param {(m: string) => void} origErr
|
||||
*/
|
||||
export async function runShellReadBuiltin(ctx, argv, env, origErr) {
|
||||
let i = 1
|
||||
let rawMode = false
|
||||
let delimiter = '\n'
|
||||
let timeoutMs = 0
|
||||
while (i < argv.length && argv[i].startsWith('-')) {
|
||||
const a = argv[i]
|
||||
if (a === '-r') {
|
||||
rawMode = true
|
||||
}
|
||||
else if (a === '-d') {
|
||||
const d = argv[i + 1]
|
||||
if (d == null) {
|
||||
origErr.call(ctx.console, 'read: option requires an argument -- d')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
delimiter = String(d).slice(0, 1)
|
||||
i++
|
||||
}
|
||||
else if (a === '-t') {
|
||||
const v = argv[i + 1]
|
||||
if (v == null) {
|
||||
origErr.call(ctx.console, 'read: option requires an argument -- t')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
const n = Number.parseFloat(String(v))
|
||||
if (!Number.isFinite(n) || n < 0) {
|
||||
origErr.call(ctx.console, 'read: invalid timeout: ' + String(v))
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
timeoutMs = Math.min(Math.floor(n * 1000), 120000)
|
||||
i++
|
||||
}
|
||||
else if (a === '--') {
|
||||
i++
|
||||
break
|
||||
} else {
|
||||
origErr.call(ctx.console, 'read: unsupported option: ' + a)
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
i++
|
||||
}
|
||||
const names = argv.slice(i).filter(Boolean)
|
||||
if (!names.length) names.push('REPLY')
|
||||
const maxRaw = env.BARE_OS_SHELL_READ_MAX_BYTES
|
||||
const maxParsed =
|
||||
maxRaw != null && String(maxRaw).trim() !== ''
|
||||
? Number.parseInt(String(maxRaw), 10)
|
||||
: 65536
|
||||
const maxBytes =
|
||||
Number.isFinite(maxParsed) && maxParsed > 0
|
||||
? Math.min(maxParsed, 2_000_000)
|
||||
: 65536
|
||||
let line = await bareOsShellReadOneLine(
|
||||
ctx,
|
||||
maxBytes,
|
||||
(m) => origErr.call(ctx.console, m),
|
||||
{ delimiter, timeoutMs }
|
||||
)
|
||||
if (line === null) {
|
||||
for (const n of names) env[n] = ''
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (!rawMode) {
|
||||
line = line.replace(/\\(.)/g, '$1')
|
||||
}
|
||||
const ifs =
|
||||
env.IFS !== undefined && env.IFS !== null ? String(env.IFS) : ' \t\n'
|
||||
const fields = bareOsShellReadSplitFields(line, ifs, names.length)
|
||||
for (let j = 0; j < names.length; j++) {
|
||||
const k = names[j]
|
||||
if (
|
||||
ctx.shellReadonlyVars instanceof Set &&
|
||||
ctx.shellReadonlyVars.has(k)
|
||||
) {
|
||||
origErr.call(ctx.console, k + ': readonly variable')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
env[k] = fields[j] ?? ''
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
}
|
||||
Reference in New Issue
Block a user