Permalink
Browse files

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>
  • Loading branch information...
DannyNemer authored and Fishrock123 committed Sep 21, 2015
1 parent df69d95 commit 5bda5faffdeefa448965159e524eacdd14490436
Showing with 73 additions and 0 deletions.
  1. +3 −0 doc/api/readline.md
  2. +9 −0 lib/readline.js
  3. +61 −0 test/parallel/test-readline-interface.js
View
@@ -370,6 +370,9 @@ changes:
`crlfDelay` milliseconds, both `\r` and `\n` will be treated as separate
end-of-line input. Default to `100` milliseconds.
`crlfDelay` will be coerced to `[100, 2000]` range.
+ * `deDupeHistory` {boolean} If `true`, when a new input line added to the
+ history list duplicates an older one, this removes the older line from the
+ list. Defaults to `false`.
The `readline.createInterface()` method creates a new `readline.Interface`
instance.
View
@@ -60,6 +60,7 @@ function Interface(input, output, completer, terminal) {
EventEmitter.call(this);
var historySize;
+ var deDupeHistory = false;
let crlfDelay;
let prompt = '> ';
@@ -69,6 +70,7 @@ function Interface(input, output, completer, terminal) {
completer = input.completer;
terminal = input.terminal;
historySize = input.historySize;
+ deDupeHistory = input.deDupeHistory;
if (input.prompt !== undefined) {
prompt = input.prompt;
}
@@ -101,6 +103,7 @@ function Interface(input, output, completer, terminal) {
this.output = output;
this.input = input;
this.historySize = historySize;
+ this.deDupeHistory = !!deDupeHistory;
this.crlfDelay = Math.max(kMincrlfDelay,
Math.min(kMaxcrlfDelay, crlfDelay >>> 0));
@@ -278,6 +281,12 @@ Interface.prototype._addHistory = function() {
if (this.line.trim().length === 0) return this.line;
if (this.history.length === 0 || this.history[0] !== this.line) {
+ if (this.deDupeHistory) {
+ // Remove older history line if identical to new one
+ const dupIndex = this.history.indexOf(this.line);
+ if (dupIndex !== -1) this.history.splice(dupIndex, 1);
+ }
+
this.history.unshift(this.line);
// Only store so many
@@ -326,6 +326,67 @@ function isWarned(emitter) {
return false;
});
+ // duplicate lines are removed from history when `options.deDupeHistory`
+ // is `true`
+ fi = new FakeInput();
+ rli = new readline.Interface({
+ input: fi,
+ output: fi,
+ terminal: true,
+ deDupeHistory: true
+ });
+ expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat'];
+ callCount = 0;
+ rli.on('line', function(line) {
+ assert.strictEqual(line, expectedLines[callCount]);
+ callCount++;
+ });
+ fi.emit('data', expectedLines.join('\n') + '\n');
+ assert.strictEqual(callCount, expectedLines.length);
+ fi.emit('keypress', '.', { name: 'up' }); // 'bat'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'bar'
+ assert.notStrictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'baz'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'foo'
+ assert.notStrictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(callCount, 0);
+ rli.close();
+
+ // duplicate lines are not removed from history when `options.deDupeHistory`
+ // is `false`
+ fi = new FakeInput();
+ rli = new readline.Interface({
+ input: fi,
+ output: fi,
+ terminal: true,
+ deDupeHistory: false
+ });
+ expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat'];
+ callCount = 0;
+ rli.on('line', function(line) {
+ assert.strictEqual(line, expectedLines[callCount]);
+ callCount++;
+ });
+ fi.emit('data', expectedLines.join('\n') + '\n');
+ assert.strictEqual(callCount, expectedLines.length);
+ fi.emit('keypress', '.', { name: 'up' }); // 'bat'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'bar'
+ assert.notStrictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'baz'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'bar'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit('keypress', '.', { name: 'up' }); // 'foo'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(callCount, 0);
+ rli.close();
+
// sending a multi-byte utf8 char over multiple writes
const buf = Buffer.from('', 'utf8');
fi = new FakeInput();

0 comments on commit 5bda5fa

Please sign in to comment.