From 8bd6ab787063ac2d43a9b7e3924dbeabbfa550dc Mon Sep 17 00:00:00 2001 From: Danny Nemer Date: Mon, 21 Sep 2015 10:08:36 -0400 Subject: [PATCH] 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: https://github.com/nodejs/node/pull/2982 Reviewed-By: Jeremiah Senkpiel --- doc/api/readline.md | 3 ++ lib/readline.js | 9 ++++ test/parallel/test-readline-interface.js | 61 ++++++++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/doc/api/readline.md b/doc/api/readline.md index bafb007144..0251ef178a 100644 --- a/doc/api/readline.md +++ b/doc/api/readline.md @@ -363,6 +363,9 @@ added: v0.1.98 `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. diff --git a/lib/readline.js b/lib/readline.js index bb186e6dd3..c2c52c8b73 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -39,6 +39,7 @@ function Interface(input, output, completer, terminal) { EventEmitter.call(this); var historySize; + var deDupeHistory = false; let crlfDelay; let prompt = '> '; @@ -48,6 +49,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; } @@ -80,6 +82,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)); @@ -249,6 +252,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 diff --git a/test/parallel/test-readline-interface.js b/test/parallel/test-readline-interface.js index 0542f83d49..35b9b39324 100644 --- a/test/parallel/test-readline-interface.js +++ b/test/parallel/test-readline-interface.js @@ -303,6 +303,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();