Skip to content

Commit 8ab26cf

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 <[email protected]>
1 parent 17d16e8 commit 8ab26cf

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
@@ -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

+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

@@ -257,6 +260,12 @@ Interface.prototype._addHistory = function() {
257260
if (this.line.trim().length === 0) return this.line;
258261

259262
if (this.history.length === 0 || this.history[0] !== this.line) {
263+
if (this.deDupeHistory) {
264+
// Remove older history line if identical to new one
265+
const dupIndex = this.history.indexOf(this.line);
266+
if (dupIndex !== -1) this.history.splice(dupIndex, 1);
267+
}
268+
260269
this.history.unshift(this.line);
261270

262271
// Only store so many

test/parallel/test-readline-interface.js

+61
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,67 @@ function isWarned(emitter) {
305305
return false;
306306
});
307307

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

0 commit comments

Comments
 (0)