This commit is contained in:
Raven Scott
2026-04-26 16:12:58 -04:00
parent 20907247cc
commit d3001aba9c
3 changed files with 54 additions and 0 deletions
+22
View File
@@ -740,6 +740,17 @@ export function tokenize(line) {
i++ i++
let inner = '' let inner = ''
while (i < line.length && line[i] !== '"') { while (i < line.length && line[i] !== '"') {
if (line[i] === '$' && line[i + 1] === '(' && line[i + 2] === '(') {
const close = findArithmeticClose(line, i + 3)
if (close < 0) {
inner += line.slice(i)
i = line.length
break
}
inner += line.slice(i, close + 2)
i = close + 2
continue
}
if (line[i] === '\\' && i + 1 < line.length) { if (line[i] === '\\' && i + 1 < line.length) {
i++ i++
inner += line[i++] inner += line[i++]
@@ -751,6 +762,17 @@ export function tokenize(line) {
parts.push({ q: 'd', t: inner }) parts.push({ q: 'd', t: inner })
continue continue
} }
if (ch === '$' && line[i + 1] === '(' && line[i + 2] === '(') {
const close = findArithmeticClose(line, i + 3)
if (close < 0) {
cur.t += line.slice(i)
i = line.length
break
}
cur.t += line.slice(i, close + 2)
i = close + 2
continue
}
if ( if (
/\s/.test(ch) || /\s/.test(ch) ||
ch === '|' || ch === '|' ||
+23
View File
@@ -3012,6 +3012,15 @@ test('tokenize records quote parts for glob (literal * in quotes)', async (t) =>
t.is(lit.parts[0].t, '*') t.is(lit.parts[0].t, '*')
}) })
test('tokenize keeps arithmetic expansion in one word token', async (t) => {
const toks = tokenize('n=$((n+1)); echo "$((n+2))"')
const words = toks.filter((x) => x.type === 'word').map((x) => x.value)
t.ok(words.includes('n=$((n+1))'))
t.ok(words.includes('$((n+2))'))
t.is(toks.filter((x) => x.type === 'op' && x.value === '(').length, 0)
t.is(toks.filter((x) => x.type === 'op' && x.value === ')').length, 0)
})
test('execShellLine pathname glob and noglob', async (t) => { test('execShellLine pathname glob and noglob', async (t) => {
const dir = testCorestoreDir('shglob') const dir = testCorestoreDir('shglob')
const store = new Corestore(dir) const store = new Corestore(dir)
@@ -4499,6 +4508,10 @@ async function run(ctx) {
lines.length = 0 lines.length = 0
await execShellLine(ctx, 'if false; then echo x; fi') await execShellLine(ctx, 'if false; then echo x; fi')
t.ok(!lines.some((l) => l.includes('x'))) t.ok(!lines.some((l) => l.includes('x')))
ctx.exitCode = 0
await execShellLine(ctx, 'n=0; if true; then n=$((n+1)); fi; echo $n')
t.is(ctx.exitCode, 0)
t.ok(lines.some((l) => l.includes('1')))
await store.close() await store.close()
rmSync(dir, { recursive: true, force: true }) rmSync(dir, { recursive: true, force: true })
}) })
@@ -6323,6 +6336,16 @@ test('tier-1 sh -c executes command and sets positional args', async (t) => {
await runBinCommand(ctx, ['sh', '-c', 'false']) await runBinCommand(ctx, ['sh', '-c', 'false'])
t.is(ctx.exitCode, 1) t.is(ctx.exitCode, 1)
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, [
'sh',
'-c',
'n=0; if true; then n=$((n+1)); fi; echo $n'
])
t.is(ctx.exitCode, 0)
t.ok(lines.includes('1'))
await store.close() await store.close()
rmSync(dir, { recursive: true, force: true }) rmSync(dir, { recursive: true, force: true })
}) })
@@ -95,6 +95,15 @@ test('run_command wraps compound shell with sh -c before capture redirection', a
t.ok(seen[0].startsWith("sh -c 'for x in a b; do if true; then echo $x; fi; done'")) t.ok(seen[0].startsWith("sh -c 'for x in a b; do if true; then echo $x; fi; done'"))
}) })
test('run_command wraps combined if + arithmetic expansion probe', async (t) => {
const { ctx, seen } = makeCtx()
const probe = 'n=0; if true; then n=$((n+1)); fi; echo $n'
const out = await callRunCommand(ctx, probe)
t.ok(out.ok)
t.ok(seen.length >= 1)
t.ok(seen[0].startsWith(`sh -c '${probe}'`))
})
test('run_command keeps simple command direct with capture redirection', async (t) => { test('run_command keeps simple command direct with capture redirection', async (t) => {
const { ctx, seen } = makeCtx() const { ctx, seen } = makeCtx()
const out = await callRunCommand(ctx, 'echo hi') const out = await callRunCommand(ctx, 'echo hi')