Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/tui.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>") + ash(" · raw ") + acid("/start <name>"));
for (const e of engineStatus()) {
Expand Down Expand Up @@ -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;
}
Expand Down
25 changes: 25 additions & 0 deletions test/tui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <cmd>` and `!<cmd>` 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));
});
Loading