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:
Raven Scott
2026-04-27 08:41:43 -04:00
parent dd5890b98c
commit d5535cdb65
17 changed files with 117 additions and 10 deletions
+4
View File
@@ -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 {
+5 -2
View File
@@ -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 &&
+53
View File
@@ -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 })
})
+3
View File
@@ -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)) {
+6
View File
@@ -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
+3
View File
@@ -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