Fixed command substitution behavior in shell expansion:
packages/bare-os-booter/lib/shell.js $(...)/backticks are now enabled by default unless explicitly disabled via BARE_OS_SHELL_CMDSUBST=0|false|off. Fixed sh -c exit/status propagation: packages/bare-os-booter/lib/shell.js Syncs BARE_OS_EXIT_STATUS after each executed statement so later commands in the same line (like echo $?) see the immediately previous status. packages/bare-os-coreutils/src/sh.js In -c mode, reads final BARE_OS_EXIT_STATUS back into ctx.exitCode for consistent result propagation. Implemented trap behavior for requested scope (EXIT, INT, TERM): packages/bare-os-booter/lib/shell.js exit builtin now runs EXIT trap handler before requesting booter exit. packages/bare-os-booter/index.js Signal delivery for shell PID now triggers trap dispatch for INT/TERM. Hardened ulimit -f invalid diagnostics: packages/bare-os-coreutils/src/ulimit.js Explicit invalid-value error for malformed -f setter input, with nonzero exit. Supported-but-unimplemented setter values still return explicit unsupported-setter error.
This commit is contained in:
@@ -118,6 +118,7 @@ import {
|
||||
} from './lib/bare-os-hrpc-route-table.js'
|
||||
import {
|
||||
execShellLine,
|
||||
dispatchShellTrapSignal,
|
||||
syncBareOsExitStatusEnv,
|
||||
getBareOsPipelineLimits
|
||||
} from './lib/shell.js'
|
||||
@@ -3933,6 +3934,9 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
}
|
||||
}
|
||||
const atMs = Date.now()
|
||||
if (pid === 3 && (sig === 'INT' || sig === 'TERM')) {
|
||||
Promise.resolve(dispatchShellTrapSignal(ctx, sig)).catch(() => {})
|
||||
}
|
||||
bareOsVirtualSignalState.set(pid, { signal: sig, atMs })
|
||||
if (typeof globalThis.process?.emit === 'function') {
|
||||
try {
|
||||
|
||||
@@ -1630,8 +1630,8 @@ function maybeTraceShellExpansion(ctx, env, row) {
|
||||
async function expandWordWithCmdSubst(ctx, s, env, depth = 0) {
|
||||
const maxDepth = 2
|
||||
if (depth > maxDepth) throw new Error('shell: cmdsubst: nesting too deep')
|
||||
const cmdOn =
|
||||
env.BARE_OS_SHELL_CMDSUBST === '1' || env.BARE_OS_SHELL_CMDSUBST === 'true'
|
||||
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)
|
||||
@@ -2844,6 +2844,8 @@ async function execParsedPipeline(ctx, pipeline) {
|
||||
const n = Number.parseInt(argv[1], 10)
|
||||
ec = Number.isFinite(n) ? n : 0
|
||||
}
|
||||
await dispatchShellTrapSignal(ctx, 'EXIT')
|
||||
ctx.exitCode = ec
|
||||
if (typeof ctx.requestBooterExit === 'function') {
|
||||
ctx.requestBooterExit(ec)
|
||||
}
|
||||
@@ -4137,6 +4139,7 @@ async function execShellLineInner(ctx, rawTrimmed) {
|
||||
syncBareOsExitStatusEnv(ctx)
|
||||
return 'exit'
|
||||
}
|
||||
syncBareOsExitStatusEnv(ctx)
|
||||
const shE = ctx.vfs?.env
|
||||
if (
|
||||
shE &&
|
||||
|
||||
@@ -3687,6 +3687,33 @@ test('execShellLine $\'…\' echo and cmdsubst $(…) / backticks when enabled',
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine command substitution is enabled by default', async (t) => {
|
||||
const dir = testCorestoreDir('shcmdsub-default')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pcmdsubdef'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put(
|
||||
'/bin/echo',
|
||||
b4a.from(`async function run(ctx, argv) {
|
||||
ctx.console.log(argv.slice(1).join(' '))
|
||||
}
|
||||
`)
|
||||
)
|
||||
const logs = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
delete ctx.vfs.env.BARE_OS_SHELL_CMDSUBST
|
||||
ctx.console = {
|
||||
log: (...a) => logs.push(a.join(' ')),
|
||||
error: (...a) => logs.push('e:' + a.join(' '))
|
||||
}
|
||||
await execShellLine(ctx, 'echo "$(echo hi)"')
|
||||
t.ok(logs.some((l) => l.includes('hi')), logs.join('|'))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('expandWord supports ${#var} length and ${var:offset} when v3', (t) => {
|
||||
const env = {
|
||||
X: 'abcde',
|
||||
@@ -4392,6 +4419,26 @@ test('execShellLine trap normalizes signals and prints handlers', async (t) => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine trap EXIT runs before exit', async (t) => {
|
||||
const dir = testCorestoreDir('sh-trap-exit')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pshtrapexit'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const logs = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.console = {
|
||||
log: (m) => logs.push(String(m)),
|
||||
error: (...a) => logs.push(a.map(String).join(' '))
|
||||
}
|
||||
await execShellLine(ctx, "trap 'trap -l' EXIT; exit 1")
|
||||
t.ok(logs.some((l) => l.includes('HUP INT KILL TERM')))
|
||||
t.is(ctx.exitCode, 1)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine BARE_OS_SHELL_NOUNSET rejects unbound variable', async (t) => {
|
||||
const dir = testCorestoreDir('nounset')
|
||||
const store = new Corestore(dir)
|
||||
@@ -7853,6 +7900,12 @@ test('tier-1 sh -c executes command and sets positional args', async (t) => {
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(!lines.some((l) => l.startsWith('e:')))
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['sh', '-c', 'false; echo $?'])
|
||||
t.ok(lines.some((l) => String(l).trim() === '1'), lines.join('|'))
|
||||
t.is(ctx.exitCode, 0)
|
||||
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
@@ -62,6 +62,9 @@ async function run(ctx, argv) {
|
||||
}
|
||||
try {
|
||||
await ctx.execLine(command)
|
||||
const v = ctx.vfs?.env?.BARE_OS_EXIT_STATUS
|
||||
const n = Number.parseInt(String(v ?? ''), 10)
|
||||
if (Number.isFinite(n)) ctx.exitCode = n
|
||||
} finally {
|
||||
if (saved && env) {
|
||||
for (const [k, v] of Object.entries(saved)) {
|
||||
|
||||
@@ -61,6 +61,12 @@ async function run(ctx, argv) {
|
||||
}
|
||||
if (args[0] === '-f') {
|
||||
if (args.length > 1) {
|
||||
const raw = String(args[1] ?? '').trim()
|
||||
if (!/^(unlimited|[0-9]+[kKmMgG]?)$/.test(raw)) {
|
||||
ctx.console.error('ulimit: invalid file size: ' + raw)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
ctx.console.error('ulimit: setting file size limit is not supported in Bare OS')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
|
||||
@@ -63,8 +63,9 @@ function makeCtx() {
|
||||
const m = />\s*'([^']+)'\s*2>&1/.exec(String(cmd))
|
||||
const outPath = m ? m[1] : '/tmp/agent.out'
|
||||
await ctx.vfs.writeFile(outPath, b4a.from('ok\n'))
|
||||
ctx.exitCode = 0
|
||||
ctx.vfs.env.BARE_OS_EXIT_STATUS = '0'
|
||||
const fail = String(cmd).includes('false')
|
||||
ctx.exitCode = fail ? 1 : 0
|
||||
ctx.vfs.env.BARE_OS_EXIT_STATUS = fail ? '1' : '0'
|
||||
}
|
||||
}
|
||||
return { ctx, seen }
|
||||
@@ -111,3 +112,10 @@ test('run_command keeps simple command direct with capture redirection', async (
|
||||
t.ok(seen.length >= 1)
|
||||
t.ok(seen[0].startsWith('echo hi > '))
|
||||
})
|
||||
|
||||
test('run_command captures nonzero status through sh -c wrapper', async (t) => {
|
||||
const { ctx } = makeCtx()
|
||||
const out = await callRunCommand(ctx, 'false; echo $?')
|
||||
t.ok(out.ok)
|
||||
t.ok(String(out.stdout_stderr || '').includes('EXIT:1'))
|
||||
})
|
||||
|
||||
@@ -88,6 +88,18 @@ test('ulimit -f setter reports unsupported', async (t) => {
|
||||
t.ok(ctx._errs.join('\n').includes('not supported'))
|
||||
})
|
||||
|
||||
test('ulimit -f invalid reports explicit diagnostic', async (t) => {
|
||||
const run = await loadBin('ulimit')
|
||||
const ctx = mkCtx({
|
||||
async readFile() {
|
||||
return null
|
||||
}
|
||||
})
|
||||
await run(ctx, ['ulimit', '-f', 'invalid'])
|
||||
t.is(ctx.exitCode, 1)
|
||||
t.ok(ctx._errs.join('\n').includes('invalid file size'))
|
||||
})
|
||||
|
||||
test('xargs -P0 maps to bounded parallel mode', async (t) => {
|
||||
const run = await loadBin('xargs')
|
||||
let calls = 0
|
||||
|
||||
@@ -151,6 +151,9 @@ async function run(ctx, argv) {
|
||||
}
|
||||
try {
|
||||
await ctx.execLine(command)
|
||||
const v = ctx.vfs?.env?.BARE_OS_EXIT_STATUS
|
||||
const n = Number.parseInt(String(v ?? ''), 10)
|
||||
if (Number.isFinite(n)) ctx.exitCode = n
|
||||
} finally {
|
||||
if (saved && env) {
|
||||
for (const [k, v] of Object.entries(saved)) {
|
||||
|
||||
@@ -150,6 +150,12 @@ async function run(ctx, argv) {
|
||||
}
|
||||
if (args[0] === '-f') {
|
||||
if (args.length > 1) {
|
||||
const raw = String(args[1] ?? '').trim()
|
||||
if (!/^(unlimited|[0-9]+[kKmMgG]?)$/.test(raw)) {
|
||||
ctx.console.error('ulimit: invalid file size: ' + raw)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
ctx.console.error('ulimit: setting file size limit is not supported in Bare OS')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema": 2,
|
||||
"profileId": "bare-os-posix-like",
|
||||
"generatedAt": "2026-04-27T12:30:48.768Z",
|
||||
"generatedAt": "2026-04-27T12:41:02.630Z",
|
||||
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||
"commandIndex": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"atMs": 1777293048767,
|
||||
"atMs": 1777293662629,
|
||||
"commands": [
|
||||
"agent",
|
||||
"appctl",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user