diff --git a/src/tui.mjs b/src/tui.mjs index 1e80582..e5374af 100644 --- a/src/tui.mjs +++ b/src/tui.mjs @@ -105,6 +105,15 @@ export function splitCommandLine(line) { return parts; } +// Everything after the first word of a command line, exactly as typed. `/shell` +// hands this straight to `$SHELL -c`, the same way `!cmd` does: the shell does +// its own parsing, so re-joining the tokenized parts would strip the user's +// quotes and escapes and silently split `-m "two words"` into two arguments. +function commandRemainder(line) { + const firstWord = /^\s*\S+\s*/.exec(String(line)); + return firstWord ? String(line).slice(firstWord[0].length).trim() : ""; +} + function printEngines() { console.log(bone(" engines") + ash(" — autonomous ") + acid("/agents ") + ash(" · raw ") + acid("/start ")); for (const e of engineStatus()) { @@ -361,8 +370,9 @@ export async function tui() { continue; } if (cmd === "shell" || cmd === "sh") { + const rawCmd = commandRemainder(line); rl.close(); - await openShell(rest.length ? rest.join(" ") : null); + await openShell(rawCmd || null); rl = mkrl(); continue; } diff --git a/test/tui.test.mjs b/test/tui.test.mjs index bb09a13..a3a4de9 100644 --- a/test/tui.test.mjs +++ b/test/tui.test.mjs @@ -80,3 +80,28 @@ test("TUI /run passes positional args through to moshscript argv", () => { assert.match(result.stdout, /alpha/); assert.match(result.stdout, /two words/); }); + +// `/shell ` and `!` are the same feature — both hand the command to +// `$SHELL -c`, which does its own parsing. Re-joining the tokenized parts drops +// the user's quotes, so `-m "two words"` reaches the shell as two arguments. +const posixShell = process.platform === "win32" ? { skip: "needs a POSIX shell" } : {}; + +test("TUI /shell hands the raw command line to the shell, quoting intact", posixShell, async () => { + const result = await runTui('/shell printf "[%s]\\n" "two words"\n/quit\n'); + + assert.equal(result.status, 0); + assert.match(result.stdout, /\[two words\]/); + assert.doesNotMatch(result.stdout, /\[two\]/); +}); + +test("TUI /shell and !cmd run an identical command identically", posixShell, async () => { + const command = 'printf "[%s]\\n" "two words"'; + const bracketed = (out) => out.match(/\[[^\]\n]*\]/g) || []; + + const viaSlash = await runTui(`/shell ${command}\n/quit\n`); + const viaBang = await runTui(`!${command}\n/quit\n`); + + assert.equal(viaSlash.status, 0); + assert.equal(viaBang.status, 0); + assert.deepEqual(bracketed(viaSlash.stdout), bracketed(viaBang.stdout)); +});