Skip to content

Commit 60f18ed

Browse files
isaacsTooTallNate
authored andcommitted
readline: treat bare \r as a line ending
Fixes #3305
1 parent 9bd9c54 commit 60f18ed

File tree

2 files changed

+56
-4
lines changed

2 files changed

+56
-4
lines changed

lib/readline.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ function Interface(input, output, completer, terminal) {
4949
return new Interface(input, output, completer, terminal);
5050
}
5151

52+
this._sawReturn = false;
53+
5254
EventEmitter.call(this);
5355

5456
if (arguments.length === 1) {
@@ -292,18 +294,27 @@ Interface.prototype.write = function(d, key) {
292294
this.terminal ? this._ttyWrite(d, key) : this._normalWrite(d);
293295
};
294296

297+
// \r\n, \n, or \r followed by something other than \n
298+
var lineEnding = /\r?\n|\r(?!\n)/;
295299
Interface.prototype._normalWrite = function(b) {
296300
if (b === undefined) {
297301
return;
298302
}
299303
var string = this._decoder.write(b);
304+
if (this._sawReturn) {
305+
string = string.replace(/^\n/, '');
306+
this._sawReturn = false;
307+
}
308+
300309
if (this._line_buffer) {
301310
string = this._line_buffer + string;
302311
this._line_buffer = null;
303312
}
304-
if (string.indexOf('\n') !== -1) {
313+
if (lineEnding.test(string)) {
314+
this._sawReturn = /\r$/.test(string);
315+
305316
// got one or more newlines; process into "line" events
306-
var lines = string.split(/\r?\n/);
317+
var lines = string.split(lineEnding);
307318
// either '' or (concievably) the unfinished portion of the next line
308319
string = lines.pop();
309320
this._line_buffer = string;
@@ -733,11 +744,23 @@ Interface.prototype._ttyWrite = function(s, key) {
733744
} else {
734745
/* No modifier keys used */
735746

747+
// \r bookkeeping is only relevant if a \n comes right after.
748+
if (this._sawReturn && key.name !== 'enter')
749+
this._sawReturn = false;
750+
736751
switch (key.name) {
737-
case 'enter':
752+
case 'return': // carriage return, i.e. \r
753+
this._sawReturn = true;
738754
this._line();
739755
break;
740756

757+
case 'enter':
758+
if (this._sawReturn)
759+
this._sawReturn = false
760+
else
761+
this._line();
762+
break;
763+
741764
case 'backspace':
742765
this._deleteLeft();
743766
break;
@@ -758,7 +781,6 @@ Interface.prototype._ttyWrite = function(s, key) {
758781
this._moveCursor(+1);
759782
break;
760783

761-
case 'return': // carriage return, i.e. \r
762784
case 'home':
763785
this._moveCursor(-Infinity);
764786
break;

test/simple/test-readline-interface.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,36 @@ FakeInput.prototype.end = function() {};
126126
assert.equal(callCount, expectedLines.length - 1);
127127
rli.close();
128128

129+
// \r\n should emit one line event when split across multiple writes.
130+
fi = new FakeInput();
131+
rli = new readline.Interface({ input: fi, output: fi, terminal: terminal });
132+
expectedLines = ['foo', 'bar', 'baz', 'bat'];
133+
callCount = 0;
134+
rli.on('line', function(line) {
135+
assert.equal(line, expectedLines[callCount]);
136+
callCount++;
137+
});
138+
expectedLines.forEach(function(line) {
139+
fi.emit('data', line + '\r');
140+
fi.emit('data', '\n');
141+
});
142+
assert.equal(callCount, expectedLines.length);
143+
rli.close();
144+
145+
// \r should behave like \n when alone
146+
fi = new FakeInput();
147+
rli = new readline.Interface({ input: fi, output: fi, terminal: true });
148+
expectedLines = ['foo', 'bar', 'baz', 'bat'];
149+
callCount = 0;
150+
rli.on('line', function(line) {
151+
assert.equal(line, expectedLines[callCount]);
152+
callCount++;
153+
});
154+
fi.emit('data', expectedLines.join('\r'));
155+
assert.equal(callCount, expectedLines.length - 1);
156+
rli.close();
157+
158+
129159
// sending a multi-byte utf8 char over multiple writes
130160
var buf = Buffer('☮', 'utf8');
131161
fi = new FakeInput();

0 commit comments

Comments
 (0)