Skip to content

Commit 5bda5fa

Browse files
DannyNemerFishrock123
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 <[email protected]>
1 parent df69d95 commit 5bda5fa

File tree

3 files changed

+73
-0
lines changed

3 files changed

+73
-0
lines changed

doc/api/readline.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ changes:
370370
`crlfDelay` milliseconds, both `\r` and `\n` will be treated as separate
371371
end-of-line input. Default to `100` milliseconds.
372372
`crlfDelay` will be coerced to `[100, 2000]` range.
373+
* `deDupeHistory` {boolean} If `true`, when a new input line added to the
374+
history list duplicates an older one, this removes the older line from the
375+
list. Defaults to `false`.
373376

374377
The `readline.createInterface()` method creates a new `readline.Interface`
375378
instance.

lib/readline.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ function Interface(input, output, completer, terminal) {
6060

6161
EventEmitter.call(this);
6262
var historySize;
63+
var deDupeHistory = false;
6364
let crlfDelay;
6465
let prompt = '> ';
6566

@@ -69,6 +70,7 @@ function Interface(input, output, completer, terminal) {
6970
completer = input.completer;
7071
terminal = input.terminal;
7172
historySize = input.historySize;
73+
deDupeHistory = input.deDupeHistory;
7274
if (input.prompt !== undefined) {
7375
prompt = input.prompt;
7476
}
@@ -101,6 +103,7 @@ function Interface(input, output, completer, terminal) {
101103
this.output = output;
102104
this.input = input;
103105
this.historySize = historySize;
106+
this.deDupeHistory = !!deDupeHistory;
104107
this.crlfDelay = Math.max(kMincrlfDelay,
105108
Math.min(kMaxcrlfDelay, crlfDelay >>> 0));
106109

@@ -278,6 +281,12 @@ Interface.prototype._addHistory = function() {
278281
if (this.line.trim().length === 0) return this.line;
279282

280283
if (this.history.length === 0 || this.history[0] !== this.line) {
284+
if (this.deDupeHistory) {
285+
// Remove older history line if identical to new one
286+
const dupIndex = this.history.indexOf(this.line);
287+
if (dupIndex !== -1) this.history.splice(dupIndex, 1);
288+
}
289+
281290
this.history.unshift(this.line);
282291

283292
// Only store so many

test/parallel/test-readline-interface.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,67 @@ function isWarned(emitter) {
326326
return false;
327327
});
328328

329+
// duplicate lines are removed from history when `options.deDupeHistory`
330+
// is `true`
331+
fi = new FakeInput();
332+
rli = new readline.Interface({
333+
input: fi,
334+
output: fi,
335+
terminal: true,
336+
deDupeHistory: true
337+
});
338+
expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat'];
339+
callCount = 0;
340+
rli.on('line', function(line) {
341+
assert.strictEqual(line, expectedLines[callCount]);
342+
callCount++;
343+
});
344+
fi.emit('data', expectedLines.join('\n') + '\n');
345+
assert.strictEqual(callCount, expectedLines.length);
346+
fi.emit('keypress', '.', { name: 'up' }); // 'bat'
347+
assert.strictEqual(rli.line, expectedLines[--callCount]);
348+
fi.emit('keypress', '.', { name: 'up' }); // 'bar'
349+
assert.notStrictEqual(rli.line, expectedLines[--callCount]);
350+
assert.strictEqual(rli.line, expectedLines[--callCount]);
351+
fi.emit('keypress', '.', { name: 'up' }); // 'baz'
352+
assert.strictEqual(rli.line, expectedLines[--callCount]);
353+
fi.emit('keypress', '.', { name: 'up' }); // 'foo'
354+
assert.notStrictEqual(rli.line, expectedLines[--callCount]);
355+
assert.strictEqual(rli.line, expectedLines[--callCount]);
356+
assert.strictEqual(callCount, 0);
357+
rli.close();
358+
359+
// duplicate lines are not removed from history when `options.deDupeHistory`
360+
// is `false`
361+
fi = new FakeInput();
362+
rli = new readline.Interface({
363+
input: fi,
364+
output: fi,
365+
terminal: true,
366+
deDupeHistory: false
367+
});
368+
expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat'];
369+
callCount = 0;
370+
rli.on('line', function(line) {
371+
assert.strictEqual(line, expectedLines[callCount]);
372+
callCount++;
373+
});
374+
fi.emit('data', expectedLines.join('\n') + '\n');
375+
assert.strictEqual(callCount, expectedLines.length);
376+
fi.emit('keypress', '.', { name: 'up' }); // 'bat'
377+
assert.strictEqual(rli.line, expectedLines[--callCount]);
378+
fi.emit('keypress', '.', { name: 'up' }); // 'bar'
379+
assert.notStrictEqual(rli.line, expectedLines[--callCount]);
380+
assert.strictEqual(rli.line, expectedLines[--callCount]);
381+
fi.emit('keypress', '.', { name: 'up' }); // 'baz'
382+
assert.strictEqual(rli.line, expectedLines[--callCount]);
383+
fi.emit('keypress', '.', { name: 'up' }); // 'bar'
384+
assert.strictEqual(rli.line, expectedLines[--callCount]);
385+
fi.emit('keypress', '.', { name: 'up' }); // 'foo'
386+
assert.strictEqual(rli.line, expectedLines[--callCount]);
387+
assert.strictEqual(callCount, 0);
388+
rli.close();
389+
329390
// sending a multi-byte utf8 char over multiple writes
330391
const buf = Buffer.from('☮', 'utf8');
331392
fi = new FakeInput();

0 commit comments

Comments
 (0)