Skip to content

Commit 8bd6ab7

Browse files
DannyNemerMylesBorins
authored andcommitted
readline: add option to stop duplicates in history
Adds `options.deDupeHistory` for `readline.createInterface(options)`. If `options.deDupeHistory` is `true`, when a new input line being added to the history list duplicates an older one, removes the older line from the list. Defaults to `false`. Many users would appreciate this option, as it is a common setting in shells. This option certainly should not be default behavior, as it would be problematic in applications such as the `repl`, which inherits from the readline `Interface`. Extends documentation to reflect this API addition. Adds tests for when `options.deDupeHistory` is truthy, and when `options.deDupeHistory` is falsey. PR-URL: #2982 Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
1 parent 62a8f47 commit 8bd6ab7

File tree

3 files changed

+73
-0
lines changed

3 files changed

+73
-0
lines changed

doc/api/readline.md

+3
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,9 @@ added: v0.1.98
363363
`crlfDelay` milliseconds, both `\r` and `\n` will be treated as separate
364364
end-of-line input. Default to `100` milliseconds.
365365
`crlfDelay` will be coerced to `[100, 2000]` range.
366+
* `deDupeHistory` {boolean} If `true`, when a new input line added to the
367+
history list duplicates an older one, this removes the older line from the
368+
list. Defaults to `false`.
366369

367370
The `readline.createInterface()` method creates a new `readline.Interface`
368371
instance.

lib/readline.js

+9
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function Interface(input, output, completer, terminal) {
3939

4040
EventEmitter.call(this);
4141
var historySize;
42+
var deDupeHistory = false;
4243
let crlfDelay;
4344
let prompt = '> ';
4445

@@ -48,6 +49,7 @@ function Interface(input, output, completer, terminal) {
4849
completer = input.completer;
4950
terminal = input.terminal;
5051
historySize = input.historySize;
52+
deDupeHistory = input.deDupeHistory;
5153
if (input.prompt !== undefined) {
5254
prompt = input.prompt;
5355
}
@@ -80,6 +82,7 @@ function Interface(input, output, completer, terminal) {
8082
this.output = output;
8183
this.input = input;
8284
this.historySize = historySize;
85+
this.deDupeHistory = !!deDupeHistory;
8386
this.crlfDelay = Math.max(kMincrlfDelay,
8487
Math.min(kMaxcrlfDelay, crlfDelay >>> 0));
8588

@@ -249,6 +252,12 @@ Interface.prototype._addHistory = function() {
249252
if (this.line.trim().length === 0) return this.line;
250253

251254
if (this.history.length === 0 || this.history[0] !== this.line) {
255+
if (this.deDupeHistory) {
256+
// Remove older history line if identical to new one
257+
const dupIndex = this.history.indexOf(this.line);
258+
if (dupIndex !== -1) this.history.splice(dupIndex, 1);
259+
}
260+
252261
this.history.unshift(this.line);
253262

254263
// Only store so many

test/parallel/test-readline-interface.js

+61
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,67 @@ function isWarned(emitter) {
303303
return false;
304304
});
305305

306+
// duplicate lines are removed from history when `options.deDupeHistory`
307+
// is `true`
308+
fi = new FakeInput();
309+
rli = new readline.Interface({
310+
input: fi,
311+
output: fi,
312+
terminal: true,
313+
deDupeHistory: true
314+
});
315+
expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat'];
316+
callCount = 0;
317+
rli.on('line', function(line) {
318+
assert.strictEqual(line, expectedLines[callCount]);
319+
callCount++;
320+
});
321+
fi.emit('data', expectedLines.join('\n') + '\n');
322+
assert.strictEqual(callCount, expectedLines.length);
323+
fi.emit('keypress', '.', { name: 'up' }); // 'bat'
324+
assert.strictEqual(rli.line, expectedLines[--callCount]);
325+
fi.emit('keypress', '.', { name: 'up' }); // 'bar'
326+
assert.notStrictEqual(rli.line, expectedLines[--callCount]);
327+
assert.strictEqual(rli.line, expectedLines[--callCount]);
328+
fi.emit('keypress', '.', { name: 'up' }); // 'baz'
329+
assert.strictEqual(rli.line, expectedLines[--callCount]);
330+
fi.emit('keypress', '.', { name: 'up' }); // 'foo'
331+
assert.notStrictEqual(rli.line, expectedLines[--callCount]);
332+
assert.strictEqual(rli.line, expectedLines[--callCount]);
333+
assert.strictEqual(callCount, 0);
334+
rli.close();
335+
336+
// duplicate lines are not removed from history when `options.deDupeHistory`
337+
// is `false`
338+
fi = new FakeInput();
339+
rli = new readline.Interface({
340+
input: fi,
341+
output: fi,
342+
terminal: true,
343+
deDupeHistory: false
344+
});
345+
expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat'];
346+
callCount = 0;
347+
rli.on('line', function(line) {
348+
assert.strictEqual(line, expectedLines[callCount]);
349+
callCount++;
350+
});
351+
fi.emit('data', expectedLines.join('\n') + '\n');
352+
assert.strictEqual(callCount, expectedLines.length);
353+
fi.emit('keypress', '.', { name: 'up' }); // 'bat'
354+
assert.strictEqual(rli.line, expectedLines[--callCount]);
355+
fi.emit('keypress', '.', { name: 'up' }); // 'bar'
356+
assert.notStrictEqual(rli.line, expectedLines[--callCount]);
357+
assert.strictEqual(rli.line, expectedLines[--callCount]);
358+
fi.emit('keypress', '.', { name: 'up' }); // 'baz'
359+
assert.strictEqual(rli.line, expectedLines[--callCount]);
360+
fi.emit('keypress', '.', { name: 'up' }); // 'bar'
361+
assert.strictEqual(rli.line, expectedLines[--callCount]);
362+
fi.emit('keypress', '.', { name: 'up' }); // 'foo'
363+
assert.strictEqual(rli.line, expectedLines[--callCount]);
364+
assert.strictEqual(callCount, 0);
365+
rli.close();
366+
306367
// sending a multi-byte utf8 char over multiple writes
307368
const buf = Buffer.from('☮', 'utf8');
308369
fi = new FakeInput();

0 commit comments

Comments
 (0)