From 1e0444db07922c354e003cb19aa001b337a8eed1 Mon Sep 17 00:00:00 2001 From: Diyon18 Date: Sat, 13 Jun 2026 22:26:23 +0800 Subject: [PATCH 1/2] repl: handle dot commands in multiline input --- lib/repl.js | 27 +++++++++++++++- test/parallel/test-repl-multiline.js | 46 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/lib/repl.js b/lib/repl.js index 17aab1c409beca..af32106d006525 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -76,6 +76,7 @@ const { StringPrototypeCharAt, StringPrototypeEndsWith, StringPrototypeIncludes, + StringPrototypeLastIndexOf, StringPrototypeRepeat, StringPrototypeSlice, StringPrototypeStartsWith, @@ -802,7 +803,31 @@ class REPLServer extends Interface { } // Check REPL keywords and empty lines against a trimmed line input. - const trimmedCmd = StringPrototypeTrim(cmd); + let trimmedCmd = StringPrototypeTrim(cmd); + + // TTY multiline input is stored as one editable line by readline. Check + // the newest physical line so dot-commands work from a continuation + // prompt, while keeping the previous lines buffered. + if (self.terminal && + !self[kBufferedCommandSymbol] && + StringPrototypeIncludes(cmd, '\n')) { + const lastNewlineIndex = StringPrototypeLastIndexOf(cmd, '\n'); + const commandLine = StringPrototypeSlice(cmd, lastNewlineIndex + 1); + const trimmedCommandLine = StringPrototypeTrim(commandLine); + + if (StringPrototypeCharAt(trimmedCommandLine, 0) === '.' && + StringPrototypeCharAt(trimmedCommandLine, 1) !== '.' && + NumberIsNaN(NumberParseFloat(trimmedCommandLine))) { + const matches = RegExpPrototypeExec(/^\.([^\s]+)\s*(.*)$/, trimmedCommandLine); + const keyword = matches?.[1]; + + if (self.commands[keyword]) { + self[kBufferedCommandSymbol] = + StringPrototypeSlice(cmd, 0, lastNewlineIndex + 1); + trimmedCmd = trimmedCommandLine; + } + } + } // Check to see if a REPL keyword was used. If it returns true, // display next prompt and return. diff --git a/test/parallel/test-repl-multiline.js b/test/parallel/test-repl-multiline.js index 6aecb670114484..a05fd5be958aa8 100644 --- a/test/parallel/test-repl-multiline.js +++ b/test/parallel/test-repl-multiline.js @@ -4,6 +4,8 @@ const assert = require('assert'); const { startNewREPLServer } = require('../common/repl'); const input = ['const foo = {', '};', 'foo']; +const dotCommandSyntaxError = + /Uncaught SyntaxError: Unexpected token '\.'/; function run({ useColors }) { const { replServer, output } = startNewREPLServer({ useColors }); @@ -27,3 +29,47 @@ function run({ useColors }) { run({ useColors: true }); run({ useColors: false }); + +function runDotCommandAfterRecoverable(command, validate) { + const { replServer, output } = startNewREPLServer(); + + replServer.on('exit', common.mustCall()); + replServer.write('function a() {\n'); + replServer.write(`${command}\n`); + + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); + validate(replServer, output); + + replServer.close(); +} + +runDotCommandAfterRecoverable('.break', (replServer, output) => { + replServer.write('1 + 1\n'); + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); + assert.match(output.accumulator, /2\n/); +}); + +runDotCommandAfterRecoverable('.help', (replServer, output) => { + assert.match(output.accumulator, /\.break\s+Sometimes you get stuck/); + assert.match(output.accumulator, /\.help\s+Print this help message/); + + replServer.write('.break\n'); + replServer.write('1 + 1\n'); + + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); + assert.match(output.accumulator, /2\n/); +}); + +{ + const { replServer, output } = startNewREPLServer(); + let exited = false; + + replServer.on('exit', common.mustCall(() => { + exited = true; + })); + replServer.write('function a() {\n'); + replServer.write('.exit\n'); + + assert.strictEqual(exited, true); + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); +} From 1e9aaf07b509979b022611a18a48a5c469b6b203 Mon Sep 17 00:00:00 2001 From: Diyon18 Date: Sun, 14 Jun 2026 18:04:54 +0800 Subject: [PATCH 2/2] repl: handle dot commands in buffered multiline input --- lib/repl.js | 10 +++---- test/parallel/test-repl-multiline.js | 43 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/lib/repl.js b/lib/repl.js index af32106d006525..b5b25fb534f980 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -808,9 +808,7 @@ class REPLServer extends Interface { // TTY multiline input is stored as one editable line by readline. Check // the newest physical line so dot-commands work from a continuation // prompt, while keeping the previous lines buffered. - if (self.terminal && - !self[kBufferedCommandSymbol] && - StringPrototypeIncludes(cmd, '\n')) { + if (self.terminal && StringPrototypeIncludes(cmd, '\n')) { const lastNewlineIndex = StringPrototypeLastIndexOf(cmd, '\n'); const commandLine = StringPrototypeSlice(cmd, lastNewlineIndex + 1); const trimmedCommandLine = StringPrototypeTrim(commandLine); @@ -822,8 +820,10 @@ class REPLServer extends Interface { const keyword = matches?.[1]; if (self.commands[keyword]) { - self[kBufferedCommandSymbol] = - StringPrototypeSlice(cmd, 0, lastNewlineIndex + 1); + if (!self[kBufferedCommandSymbol]) { + self[kBufferedCommandSymbol] = + StringPrototypeSlice(cmd, 0, lastNewlineIndex + 1); + } trimmedCmd = trimmedCommandLine; } } diff --git a/test/parallel/test-repl-multiline.js b/test/parallel/test-repl-multiline.js index a05fd5be958aa8..8c531e7d178a3d 100644 --- a/test/parallel/test-repl-multiline.js +++ b/test/parallel/test-repl-multiline.js @@ -60,6 +60,35 @@ runDotCommandAfterRecoverable('.help', (replServer, output) => { assert.match(output.accumulator, /2\n/); }); +function runBufferedDotCommandAfterRecoverable(command, validate) { + const { replServer, input, output } = + startNewREPLServer({ terminal: false }); + + replServer.on('exit', common.mustCall()); + input.run(['function a() {', command]); + + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); + validate(input, output); + + replServer.close(); +} + +runBufferedDotCommandAfterRecoverable('.break', (input, output) => { + input.run(['1 + 1']); + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); + assert.match(output.accumulator, /2\n/); +}); + +runBufferedDotCommandAfterRecoverable('.help', (input, output) => { + assert.match(output.accumulator, /\.break\s+Sometimes you get stuck/); + assert.match(output.accumulator, /\.help\s+Print this help message/); + + input.run(['.break', '1 + 1']); + + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); + assert.match(output.accumulator, /2\n/); +}); + { const { replServer, output } = startNewREPLServer(); let exited = false; @@ -73,3 +102,17 @@ runDotCommandAfterRecoverable('.help', (replServer, output) => { assert.strictEqual(exited, true); assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); } + +{ + const { replServer, input, output } = + startNewREPLServer({ terminal: false }); + let exited = false; + + replServer.on('exit', common.mustCall(() => { + exited = true; + })); + input.run(['function a() {', '.exit']); + + assert.strictEqual(exited, true); + assert.doesNotMatch(output.accumulator, dotCommandSyntaxError); +}