580 lines
18 KiB
JavaScript
580 lines
18 KiB
JavaScript
/**
|
|
* Shell parameter / arithmetic word expansion (`$VAR`, `${…}`, `$((…))`)
|
|
* and command-substitution / pathname expansion of tokenized words.
|
|
*/
|
|
import { findArithmeticClose } from './shell-lex.js'
|
|
import { shellCheckUnboundParam } from './shell-nounset.js'
|
|
import { bareOsEvalArithmeticExpr } from './shell-arithmetic.js'
|
|
import { BARE_OS_EXIT_STATUS_ENV } from './shell-runtime.js'
|
|
import { pathnameExpandShellWord } from './shell-glob.js'
|
|
|
|
/** @typedef {{ q: 'u' | 's' | 'd', t: string }} ShellWordPart */
|
|
|
|
/**
|
|
* @param {string} inner
|
|
* @param {Record<string, string>} env
|
|
*/
|
|
function expandParamBracedInner(inner, env, depth = 0) {
|
|
const expandNested = (s) => {
|
|
const txt = String(s ?? '')
|
|
if (!txt.includes('$')) return txt
|
|
return expandWord(txt, env, depth + 1)
|
|
}
|
|
const tr = inner.trim()
|
|
const lenParam = /^#([A-Za-z_][A-Za-z0-9_]*)$/.exec(tr)
|
|
if (lenParam) {
|
|
shellCheckUnboundParam(lenParam[1], env)
|
|
return String(String(env[lenParam[1]] ?? '').length)
|
|
}
|
|
const indirectOn =
|
|
env &&
|
|
(env.BARE_OS_SHELL_INDIRECT_EXPANSION === '1' ||
|
|
env.BARE_OS_SHELL_INDIRECT_EXPANSION === 'true')
|
|
const indirectName = /^!([A-Za-z_][A-Za-z0-9_]*)$/.exec(tr)
|
|
if (indirectOn && indirectName) {
|
|
const ref = String(env[indirectName[1]] ?? '')
|
|
shellCheckUnboundParam(ref, env)
|
|
return String(env[ref] ?? '')
|
|
}
|
|
const paramV2 =
|
|
env &&
|
|
(env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === '1' ||
|
|
env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === 'true')
|
|
const paramV3 =
|
|
env &&
|
|
(env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === '1' ||
|
|
env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === 'true')
|
|
|
|
const errIdx = inner.indexOf(':?')
|
|
if (paramV3 && errIdx > 0) {
|
|
const name = inner.slice(0, errIdx).trim()
|
|
const msg = inner.slice(errIdx + 2)
|
|
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
const v = env[name]
|
|
if (v == null || String(v) === '') {
|
|
throw new Error(expandNested(msg) || 'parameter null or unset')
|
|
}
|
|
return String(v)
|
|
}
|
|
}
|
|
|
|
const assignIdx = inner.indexOf(':=')
|
|
if (paramV2 && assignIdx > 0) {
|
|
const name = inner.slice(0, assignIdx).trim()
|
|
const alt = inner.slice(assignIdx + 2)
|
|
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
let v = env[name]
|
|
if (v == null || String(v) === '') {
|
|
const ex = expandNested(alt)
|
|
env[name] = ex
|
|
v = ex
|
|
}
|
|
return String(v ?? '')
|
|
}
|
|
}
|
|
|
|
const posixUnsetOnly =
|
|
env &&
|
|
(env.BARE_OS_SHELL_POSIX_UNSET_ONLY_DEFAULT === '1' ||
|
|
env.BARE_OS_SHELL_POSIX_UNSET_ONLY_DEFAULT === 'true')
|
|
if (posixUnsetOnly) {
|
|
const hy = inner.indexOf('-')
|
|
if (
|
|
hy > 0 &&
|
|
inner.slice(hy - 1, hy + 1) !== ':-' &&
|
|
!inner.includes(':')
|
|
) {
|
|
const m = /^([A-Za-z_][A-Za-z0-9_]*)-(.+)$/.exec(inner)
|
|
if (m && m[1] && m[2] != null) {
|
|
const name = m[1]
|
|
const alt = m[2]
|
|
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
if (!Object.prototype.hasOwnProperty.call(env, name))
|
|
return expandNested(alt)
|
|
return String(env[name] ?? '')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const idx = inner.indexOf(':-')
|
|
if (idx > 0) {
|
|
const name = inner.slice(0, idx).trim()
|
|
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
const alt = inner.slice(idx + 2)
|
|
const v = env[name]
|
|
if (v != null && String(v) !== '') return String(v)
|
|
return expandNested(alt)
|
|
}
|
|
}
|
|
|
|
const plusIdx = inner.indexOf(':+')
|
|
if (paramV3 && plusIdx > 0) {
|
|
const name = inner.slice(0, plusIdx).trim()
|
|
const alt = inner.slice(plusIdx + 2)
|
|
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
const v = env[name]
|
|
if (v != null && String(v) !== '') return expandNested(alt)
|
|
return ''
|
|
}
|
|
}
|
|
|
|
if (paramV3) {
|
|
const sliceRe = /^([A-Za-z_][A-Za-z0-9_]*):(\d+)(?::(\d+))?$/.exec(tr)
|
|
if (sliceRe) {
|
|
const name = sliceRe[1]
|
|
const off = Number.parseInt(sliceRe[2], 10)
|
|
const ln =
|
|
sliceRe[3] != null ? Number.parseInt(sliceRe[3], 10) : undefined
|
|
const v = String(env[name] ?? '')
|
|
let out = Number.isFinite(off) ? v.slice(off) : v
|
|
if (ln != null && Number.isFinite(ln)) out = out.slice(0, ln)
|
|
return out
|
|
}
|
|
const globalRepl = /^([A-Za-z_][A-Za-z0-9_]*)\/\/(.*)\/(.*)$/.exec(tr)
|
|
if (globalRepl && globalRepl[2].length <= 256 && globalRepl[3].length <= 512) {
|
|
const name = globalRepl[1]
|
|
let v = String(env[name] ?? '')
|
|
const pat = globalRepl[2]
|
|
const rep = expandNested(globalRepl[3])
|
|
try {
|
|
const re = new RegExp(pat, 'g')
|
|
v = v.replace(re, rep)
|
|
} catch {
|
|
/* invalid regex — leave value */
|
|
}
|
|
return v
|
|
}
|
|
}
|
|
|
|
if (paramV2) {
|
|
const longPref = /^([A-Za-z_][A-Za-z0-9_]*)##(.+)$/.exec(inner)
|
|
if (longPref && longPref[2].length > 0 && longPref[2].length <= 128) {
|
|
const v = String(env[longPref[1]] ?? '')
|
|
const pat = longPref[2]
|
|
if (pat === '*/') {
|
|
const i = v.lastIndexOf('/')
|
|
return i >= 0 ? v.slice(i + 1) : v
|
|
}
|
|
let end = -1
|
|
for (let i = 0; i <= v.length - pat.length; i++) {
|
|
if (v.slice(i, i + pat.length) === pat) end = i + pat.length
|
|
}
|
|
return end >= 0 ? v.slice(end) : v
|
|
}
|
|
const shortPref = /^([A-Za-z_][A-Za-z0-9_]*)#(.+)$/.exec(inner)
|
|
if (shortPref && shortPref[2].length > 0 && shortPref[2].length <= 128) {
|
|
const v = String(env[shortPref[1]] ?? '')
|
|
const pat = shortPref[2]
|
|
if (pat === '*/') {
|
|
const i = v.indexOf('/')
|
|
return i >= 0 ? v.slice(i + 1) : v
|
|
}
|
|
const i = v.indexOf(pat)
|
|
return i >= 0 ? v.slice(i + pat.length) : v
|
|
}
|
|
const longSuf = /^([A-Za-z_][A-Za-z0-9_]*)%%(.+)$/.exec(inner)
|
|
if (longSuf && longSuf[2].length > 0 && longSuf[2].length <= 128) {
|
|
const v = String(env[longSuf[1]] ?? '')
|
|
const pat = longSuf[2]
|
|
if (!/[?*[]/.test(pat) && v.endsWith(pat))
|
|
return v.slice(0, v.length - pat.length)
|
|
if (pat.includes('*') && !pat.includes('[') && !pat.includes('?')) {
|
|
const parts = pat.split('*')
|
|
if (parts.length === 2) {
|
|
const a = parts[0]
|
|
const b = parts[1]
|
|
let best = -1
|
|
for (let len = 1; len <= v.length; len++) {
|
|
const suf = v.slice(v.length - len)
|
|
if (
|
|
suf.startsWith(a) &&
|
|
suf.endsWith(b) &&
|
|
suf.length >= a.length + b.length
|
|
) {
|
|
if (best < 0 || len > best) best = len
|
|
}
|
|
}
|
|
if (best > 0) return v.slice(0, v.length - best)
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
const shortSuf = /^([A-Za-z_][A-Za-z0-9_]*)%(.+)$/.exec(inner)
|
|
if (shortSuf && shortSuf[2].length > 0 && shortSuf[2].length <= 128) {
|
|
const v = String(env[shortSuf[1]] ?? '')
|
|
const pat = shortSuf[2]
|
|
if (!/[?*[]/.test(pat) && v.endsWith(pat))
|
|
return v.slice(0, v.length - pat.length)
|
|
if (pat.includes('*') && !pat.includes('[') && !pat.includes('?')) {
|
|
const parts = pat.split('*')
|
|
if (parts.length === 2) {
|
|
const a = parts[0]
|
|
const b = parts[1]
|
|
let best = -1
|
|
for (let len = 1; len <= v.length; len++) {
|
|
const suf = v.slice(v.length - len)
|
|
if (
|
|
suf.startsWith(a) &&
|
|
suf.endsWith(b) &&
|
|
suf.length >= a.length + b.length
|
|
) {
|
|
if (best < 0 || len < best) best = len
|
|
}
|
|
}
|
|
if (best > 0) return v.slice(0, v.length - best)
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
}
|
|
|
|
const hash = inner.indexOf('#')
|
|
if (hash > 0) {
|
|
const name = inner.slice(0, hash).trim()
|
|
const pref = inner.slice(hash + 1)
|
|
if (
|
|
/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) &&
|
|
pref.length > 0 &&
|
|
pref.length <= 128
|
|
) {
|
|
const v = String(env[name] ?? '')
|
|
return v.startsWith(pref) ? v.slice(pref.length) : v
|
|
}
|
|
}
|
|
const keySimple = inner.trim()
|
|
shellCheckUnboundParam(keySimple, env)
|
|
return env[keySimple] ?? ''
|
|
}
|
|
|
|
/**
|
|
* @param {string} s
|
|
* @param {Record<string, string>} env
|
|
*/
|
|
export function expandWord(s, env, depth = 0) {
|
|
const maxDepthRaw = Number.parseInt(
|
|
String(env?.BARE_OS_SHELL_EXPANSION_MAX_DEPTH || '32'),
|
|
10
|
|
)
|
|
const maxDepth =
|
|
Number.isFinite(maxDepthRaw) && maxDepthRaw > 0 ? Math.min(maxDepthRaw, 256) : 32
|
|
if (depth > maxDepth) {
|
|
throw new Error(`shell: expansion recursion too deep (max ${maxDepth})`)
|
|
}
|
|
const paramExpOn =
|
|
env &&
|
|
(env.BARE_OS_SHELL_PARAM_EXPANSION === '1' ||
|
|
env.BARE_OS_SHELL_PARAM_EXPANSION === 'true')
|
|
const paramV2 =
|
|
env &&
|
|
(env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === '1' ||
|
|
env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === 'true')
|
|
const paramV3 =
|
|
env &&
|
|
(env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === '1' ||
|
|
env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === 'true')
|
|
let out = ''
|
|
let j = 0
|
|
while (j < s.length) {
|
|
if (s[j] === '$') {
|
|
if (s[j + 1] === '(' && s[j + 2] === '(') {
|
|
const close = findArithmeticClose(s, j + 3)
|
|
if (close < 0) {
|
|
out += s.slice(j)
|
|
break
|
|
}
|
|
const inner = s.slice(j + 3, close)
|
|
try {
|
|
out += bareOsEvalArithmeticExpr(inner, env)
|
|
} catch (e) {
|
|
if (
|
|
env?.BARE_OS_SHELL_POSIX_MODE === '1' ||
|
|
env?.BARE_OS_SHELL_POSIX_MODE === 'true'
|
|
) {
|
|
throw new Error(
|
|
'shell: arithmetic: invalid token (POSIX mode strict arithmetic)'
|
|
)
|
|
}
|
|
throw e
|
|
}
|
|
j = close + 2
|
|
continue
|
|
}
|
|
if (s[j + 1] === '{') {
|
|
const end = s.indexOf('}', j + 2)
|
|
if (end === -1) {
|
|
out += s.slice(j)
|
|
break
|
|
}
|
|
const inner = s.slice(j + 2, end)
|
|
const tr0 = inner.trim()
|
|
const indirectOnBr =
|
|
env?.BARE_OS_SHELL_INDIRECT_EXPANSION === '1' ||
|
|
env?.BARE_OS_SHELL_INDIRECT_EXPANSION === 'true'
|
|
if (inner === '?') {
|
|
out += env[BARE_OS_EXIT_STATUS_ENV] ?? '0'
|
|
} else if (
|
|
/^#[A-Za-z_][A-Za-z0-9_]*$/.test(tr0) ||
|
|
(indirectOnBr && /^![A-Za-z_][A-Za-z0-9_]*$/.test(tr0)) ||
|
|
(paramExpOn &&
|
|
(inner.includes(':-') ||
|
|
(paramV3 && (inner.includes(':+') || inner.includes(':?'))) ||
|
|
(paramV3 &&
|
|
(/^[A-Za-z_][A-Za-z0-9_]*:\d/.test(tr0) ||
|
|
/^[A-Za-z_][A-Za-z0-9_]*\/\//.test(inner))) ||
|
|
(paramV2 &&
|
|
(inner.includes(':=') ||
|
|
/^[A-Za-z_][A-Za-z0-9_]*##/.test(inner) ||
|
|
/^[A-Za-z_][A-Za-z0-9_]*%%/.test(inner) ||
|
|
/^[A-Za-z_][A-Za-z0-9_]*%[^%]/.test(inner) ||
|
|
/^[A-Za-z_][A-Za-z0-9_]*#[^#]/.test(inner))) ||
|
|
(/^[A-Za-z_][A-Za-z0-9_]*#/.test(inner) && inner.includes('#'))))
|
|
) {
|
|
out += expandParamBracedInner(inner, env, depth + 1)
|
|
} else {
|
|
const ik = inner.trim()
|
|
shellCheckUnboundParam(ik, env)
|
|
out += env[ik] ?? ''
|
|
}
|
|
j = end + 1
|
|
continue
|
|
}
|
|
if (s[j + 1] === '?') {
|
|
out += env[BARE_OS_EXIT_STATUS_ENV] ?? '0'
|
|
j += 2
|
|
continue
|
|
}
|
|
if (/[0-9]/.test(s[j + 1] ?? '')) {
|
|
const pn = s[j + 1]
|
|
shellCheckUnboundParam(pn, env)
|
|
out += env[pn] ?? ''
|
|
j += 2
|
|
continue
|
|
}
|
|
let k = j + 1
|
|
while (k < s.length && /[A-Za-z0-9_]/.test(s[k])) k++
|
|
const name = s.slice(j + 1, k)
|
|
if (name) {
|
|
shellCheckUnboundParam(name, env)
|
|
out += env[name] ?? ''
|
|
j = k
|
|
} else {
|
|
out += '$'
|
|
j++
|
|
}
|
|
continue
|
|
}
|
|
out += s[j++]
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* @param {string} s
|
|
* @param {number} dollarIdx index of `$` in a `$(…)` command substitution
|
|
*/
|
|
function findCmdSubstCloseParen(s, dollarIdx) {
|
|
if (s[dollarIdx] !== '$' || s[dollarIdx + 1] !== '(') return -1
|
|
let depth = 1
|
|
for (let j = dollarIdx + 2; j < s.length; j++) {
|
|
if (s[j] === '(') depth++
|
|
else if (s[j] === ')') {
|
|
depth--
|
|
if (depth === 0) return j
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
/**
|
|
* Next `$(` that is command substitution (skip `$((` arithmetic regions).
|
|
* @param {string} s
|
|
* @param {number} from
|
|
*/
|
|
function findNextCmdSubstParen(s, from) {
|
|
let i = from
|
|
while (i < s.length) {
|
|
const j = s.indexOf('$(', i)
|
|
if (j < 0) return -1
|
|
if (j + 2 < s.length && s[j + 2] === '(') {
|
|
const ac = findArithmeticClose(s, j + 3)
|
|
if (ac < 0) return -1
|
|
i = ac + 2
|
|
continue
|
|
}
|
|
return j
|
|
}
|
|
return -1
|
|
}
|
|
|
|
/**
|
|
* @param {string} s
|
|
* @param {number} from scan from here (first char inside / after opener)
|
|
*/
|
|
function findBacktickClose(s, from) {
|
|
return s.indexOf('`', from)
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} inner
|
|
* @param {Record<string, string>} env
|
|
*/
|
|
async function expandCmdsubstEmbedded(ctx, inner, env) {
|
|
const ex = typeof ctx.execLine === 'function' ? ctx.execLine : null
|
|
if (!ex) throw new Error('shell: cmdsubst: execLine unavailable')
|
|
const maxLen =
|
|
Number.parseInt(env.BARE_OS_SHELL_CMDSUBST_MAX_BYTES || '8192', 10) || 8192
|
|
const lines = []
|
|
const prev = ctx.console.log
|
|
ctx.console.log = (...a) => {
|
|
lines.push(a.map(String).join(' '))
|
|
}
|
|
try {
|
|
await ex(inner.trim())
|
|
} finally {
|
|
ctx.console.log = prev
|
|
}
|
|
let out = lines.join('\n').replace(/\n+$/, '')
|
|
if (out.length > maxLen) out = out.slice(0, maxLen)
|
|
const totalBudgetRaw = Number.parseInt(
|
|
String(env.BARE_OS_SHELL_EXPANSION_MAX_BYTES || '262144'),
|
|
10
|
|
)
|
|
const totalBudget =
|
|
Number.isFinite(totalBudgetRaw) && totalBudgetRaw > 0
|
|
? Math.min(totalBudgetRaw, 8 * 1024 * 1024)
|
|
: 262144
|
|
if (out.length > totalBudget) {
|
|
throw new Error(
|
|
`shell: expansion exceeds BARE_OS_SHELL_EXPANSION_MAX_BYTES (${totalBudget})`
|
|
)
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Optional trace hook for expansion ordering.
|
|
* Order is: parameter/command/arithmetic, then split/glob.
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {Record<string, string>} env
|
|
* @param {Record<string, unknown>} row
|
|
*/
|
|
function maybeTraceShellExpansion(ctx, env, row) {
|
|
const on =
|
|
env.BARE_OS_SHELL_EXPANSION_TRACE === '1' ||
|
|
env.BARE_OS_SHELL_EXPANSION_TRACE === 'true'
|
|
if (!on) return
|
|
if (!Array.isArray(ctx.shellExpansionTrace)) ctx.shellExpansionTrace = []
|
|
ctx.shellExpansionTrace.push({
|
|
ts: Date.now(),
|
|
...row
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} s
|
|
* @param {Record<string, string>} env
|
|
* @param {number} [depth]
|
|
*/
|
|
async function expandWordWithCmdSubst(ctx, s, env, depth = 0) {
|
|
const maxDepth = 2
|
|
if (depth > maxDepth) throw new Error('shell: cmdsubst: nesting too deep')
|
|
const rawCmdOn = String(env.BARE_OS_SHELL_CMDSUBST || '').trim().toLowerCase()
|
|
const cmdOn = !(rawCmdOn === '0' || rawCmdOn === 'false' || rawCmdOn === 'off')
|
|
if (!cmdOn) return expandWord(s, env)
|
|
|
|
const tick = findBacktickClose(s, 0)
|
|
const dol = findNextCmdSubstParen(s, 0)
|
|
|
|
/** @type {'tick'|'dol'|null} */
|
|
let kind = null
|
|
let pos = -1
|
|
if (tick >= 0 && (dol < 0 || tick < dol)) {
|
|
kind = 'tick'
|
|
pos = tick
|
|
} else if (dol >= 0) {
|
|
kind = 'dol'
|
|
pos = dol
|
|
} else {
|
|
return expandWord(s, env)
|
|
}
|
|
|
|
if (kind === 'dol') {
|
|
const closeParen = findCmdSubstCloseParen(s, pos)
|
|
if (closeParen < 0) return expandWord(s, env)
|
|
const inner = s.slice(pos + 2, closeParen)
|
|
const pre = s.slice(0, pos)
|
|
const post = s.slice(closeParen + 1)
|
|
const mid = await expandCmdsubstEmbedded(ctx, inner, env)
|
|
const merged = pre + mid + post
|
|
return expandWordWithCmdSubst(ctx, merged, env, depth + 1)
|
|
}
|
|
|
|
const closeTick = findBacktickClose(s, pos + 1)
|
|
if (closeTick < 0) return expandWord(s, env)
|
|
const inner = s.slice(pos + 1, closeTick)
|
|
const pre = s.slice(0, pos)
|
|
const post = s.slice(closeTick + 1)
|
|
const mid = await expandCmdsubstEmbedded(ctx, inner, env)
|
|
const merged = pre + mid + post
|
|
return expandWordWithCmdSubst(ctx, merged, env, depth + 1)
|
|
}
|
|
|
|
/**
|
|
* Param / command-substitution expansion per quote segment, then pathname expansion.
|
|
* Expansion ordering (declared profile): parameter/command/arithmetic -> word split -> glob.
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {{ type: 'word', value: string, parts?: ShellWordPart[] }} wordTok
|
|
* @param {Record<string, string>} env
|
|
* @param {{ redirect?: boolean, disablePathnameExpansion?: boolean }} [globOpts]
|
|
* @returns {Promise<string[]>}
|
|
*/
|
|
export async function expandShellWordTokens(ctx, wordTok, env, globOpts) {
|
|
const parts =
|
|
wordTok.parts && wordTok.parts.length
|
|
? wordTok.parts
|
|
: [{ q: /** @type {'u'} */ ('u'), t: wordTok.value }]
|
|
/** @type {ShellWordPart[]} */
|
|
const ep = []
|
|
for (const p of parts) {
|
|
if (p.q === 's') ep.push(p)
|
|
else {
|
|
maybeTraceShellExpansion(ctx, env, {
|
|
stage: 'expand-pre',
|
|
quote: p.q,
|
|
input: p.t
|
|
})
|
|
const s = await expandWordWithCmdSubst(ctx, p.t, env, 0)
|
|
maybeTraceShellExpansion(ctx, env, {
|
|
stage: 'expand-post',
|
|
quote: p.q,
|
|
output: s
|
|
})
|
|
ep.push({ q: p.q, t: s })
|
|
}
|
|
}
|
|
const out = await pathnameExpandShellWord(ctx, ep, env, globOpts || {})
|
|
maybeTraceShellExpansion(ctx, env, {
|
|
stage: 'split-glob',
|
|
outputCount: out.length,
|
|
output: out.slice(0, 8)
|
|
})
|
|
const budgetRaw = Number.parseInt(
|
|
String(env.BARE_OS_SHELL_EXPANSION_MAX_BYTES || '262144'),
|
|
10
|
|
)
|
|
const budget =
|
|
Number.isFinite(budgetRaw) && budgetRaw > 0
|
|
? Math.min(budgetRaw, 8 * 1024 * 1024)
|
|
: 262144
|
|
const bytes = out.reduce((n, s) => n + String(s).length, 0)
|
|
if (bytes > budget) {
|
|
throw new Error(
|
|
`shell: expansion exceeds BARE_OS_SHELL_EXPANSION_MAX_BYTES (${budget})`
|
|
)
|
|
}
|
|
return out
|
|
}
|