Skip to content

Commit 691882c

Browse files
mattiasrungefoxxyz
authored andcommitted
readline: add history event and option to set initial history
Add a history event which is emitted when the history has been changed. This enables persisting of the history in some way but also to allows a listener to alter the history. One use-case could be to prevent passwords from ending up in the history. A constructor option is also added to allow for setting an initial history list when creating a Readline interface. PR-URL: nodejs#33662 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent f1bfb64 commit 691882c

File tree

3 files changed

+106
-32
lines changed

3 files changed

+106
-32
lines changed

doc/api/readline.md

+32-3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,28 @@ rl.on('line', (input) => {
8888
});
8989
```
9090

91+
### Event: `'history'`
92+
<!-- YAML
93+
added: REPLACEME
94+
-->
95+
96+
The `'history'` event is emitted whenever the history array has changed.
97+
98+
The listener function is called with an array containing the history array.
99+
It will reflect all changes, added lines and removed lines due to
100+
`historySize` and `removeHistoryDuplicates`.
101+
102+
The primary purpose is to allow a listener to persist the history.
103+
It is also possible for the listener to change the history object. This
104+
could be useful to prevent certain lines to be added to the history, like
105+
a password.
106+
107+
```js
108+
rl.on('history', (history) => {
109+
console.log(`Received: ${history}`);
110+
});
111+
```
112+
91113
### Event: `'pause'`
92114
<!-- YAML
93115
added: v0.7.5
@@ -479,6 +501,9 @@ the current position of the cursor down.
479501
<!-- YAML
480502
added: v0.1.98
481503
changes:
504+
- version: REPLACEME
505+
pr-url: https://github.com/nodejs/node/pull/33662
506+
description: The `history` option is supported now.
482507
- version: v13.9.0
483508
pr-url: https://github.com/nodejs/node/pull/31318
484509
description: The `tabSize` option is supported now.
@@ -507,21 +532,25 @@ changes:
507532
* `terminal` {boolean} `true` if the `input` and `output` streams should be
508533
treated like a TTY, and have ANSI/VT100 escape codes written to it.
509534
**Default:** checking `isTTY` on the `output` stream upon instantiation.
535+
* `history` {string[]} Initial list of history lines. This option makes sense
536+
only if `terminal` is set to `true` by the user or by an internal `output`
537+
check, otherwise the history caching mechanism is not initialized at all.
538+
**Default:** `[]`.
510539
* `historySize` {number} Maximum number of history lines retained. To disable
511540
the history set this value to `0`. This option makes sense only if
512541
`terminal` is set to `true` by the user or by an internal `output` check,
513542
otherwise the history caching mechanism is not initialized at all.
514543
**Default:** `30`.
544+
* `removeHistoryDuplicates` {boolean} If `true`, when a new input line added
545+
to the history list duplicates an older one, this removes the older line
546+
from the list. **Default:** `false`.
515547
* `prompt` {string} The prompt string to use. **Default:** `'> '`.
516548
* `crlfDelay` {number} If the delay between `\r` and `\n` exceeds
517549
`crlfDelay` milliseconds, both `\r` and `\n` will be treated as separate
518550
end-of-line input. `crlfDelay` will be coerced to a number no less than
519551
`100`. It can be set to `Infinity`, in which case `\r` followed by `\n`
520552
will always be considered a single newline (which may be reasonable for
521553
[reading files][] with `\r\n` line delimiter). **Default:** `100`.
522-
* `removeHistoryDuplicates` {boolean} If `true`, when a new input line added
523-
to the history list duplicates an older one, this removes the older line
524-
from the list. **Default:** `false`.
525554
* `escapeCodeTimeout` {number} The duration `readline` will wait for a
526555
character (when reading an ambiguous key sequence in milliseconds one that
527556
can both form a complete key sequence using the input read so far and can

lib/readline.js

+20-2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const {
6868
ERR_INVALID_OPT_VALUE
6969
} = require('internal/errors').codes;
7070
const {
71+
validateArray,
7172
validateString,
7273
validateUint32,
7374
} = require('internal/validators');
@@ -133,6 +134,7 @@ function Interface(input, output, completer, terminal) {
133134
this.tabSize = 8;
134135

135136
FunctionPrototypeCall(EventEmitter, this,);
137+
let history;
136138
let historySize;
137139
let removeHistoryDuplicates = false;
138140
let crlfDelay;
@@ -143,6 +145,7 @@ function Interface(input, output, completer, terminal) {
143145
output = input.output;
144146
completer = input.completer;
145147
terminal = input.terminal;
148+
history = input.history;
146149
historySize = input.historySize;
147150
if (input.tabSize !== undefined) {
148151
validateUint32(input.tabSize, 'tabSize', true);
@@ -170,6 +173,12 @@ function Interface(input, output, completer, terminal) {
170173
throw new ERR_INVALID_OPT_VALUE('completer', completer);
171174
}
172175

176+
if (history === undefined) {
177+
history = [];
178+
} else {
179+
validateArray(history, 'history');
180+
}
181+
173182
if (historySize === undefined) {
174183
historySize = kHistorySize;
175184
}
@@ -191,6 +200,7 @@ function Interface(input, output, completer, terminal) {
191200
this[kSubstringSearch] = null;
192201
this.output = output;
193202
this.input = input;
203+
this.history = history;
194204
this.historySize = historySize;
195205
this.removeHistoryDuplicates = !!removeHistoryDuplicates;
196206
this.crlfDelay = crlfDelay ?
@@ -280,7 +290,6 @@ function Interface(input, output, completer, terminal) {
280290
// Cursor position on the line.
281291
this.cursor = 0;
282292

283-
this.history = [];
284293
this.historyIndex = -1;
285294

286295
if (output !== null && output !== undefined)
@@ -396,7 +405,16 @@ Interface.prototype._addHistory = function() {
396405
}
397406

398407
this.historyIndex = -1;
399-
return this.history[0];
408+
409+
// The listener could change the history object, possibly
410+
// to remove the last added entry if it is sensitive and should
411+
// not be persisted in the history, like a password
412+
const line = this.history[0];
413+
414+
// Emit history event to notify listeners of update
415+
this.emit('history', this.history);
416+
417+
return line;
400418
};
401419

402420

test/parallel/test-readline-interface.js

+54-27
Original file line numberDiff line numberDiff line change
@@ -116,35 +116,30 @@ function assertCursorRowsAndCols(rli, rows, cols) {
116116
code: 'ERR_INVALID_OPT_VALUE'
117117
});
118118

119-
// Constructor throws if historySize is not a positive number
120-
assert.throws(() => {
121-
readline.createInterface({
122-
input,
123-
historySize: 'not a number'
124-
});
125-
}, {
126-
name: 'RangeError',
127-
code: 'ERR_INVALID_OPT_VALUE'
128-
});
129-
130-
assert.throws(() => {
131-
readline.createInterface({
132-
input,
133-
historySize: -1
119+
// Constructor throws if history is not an array
120+
['not an array', 123, 123n, {}, true, Symbol(), null].forEach((history) => {
121+
assert.throws(() => {
122+
readline.createInterface({
123+
input,
124+
history,
125+
});
126+
}, {
127+
name: 'TypeError',
128+
code: 'ERR_INVALID_ARG_TYPE'
134129
});
135-
}, {
136-
name: 'RangeError',
137-
code: 'ERR_INVALID_OPT_VALUE'
138130
});
139131

140-
assert.throws(() => {
141-
readline.createInterface({
142-
input,
143-
historySize: NaN
132+
// Constructor throws if historySize is not a positive number
133+
['not a number', -1, NaN, {}, true, Symbol(), null].forEach((historySize) => {
134+
assert.throws(() => {
135+
readline.createInterface({
136+
input,
137+
historySize,
138+
});
139+
}, {
140+
name: 'RangeError',
141+
code: 'ERR_INVALID_OPT_VALUE'
144142
});
145-
}, {
146-
name: 'RangeError',
147-
code: 'ERR_INVALID_OPT_VALUE'
148143
});
149144

150145
// Check for invalid tab sizes.
@@ -239,6 +234,38 @@ function assertCursorRowsAndCols(rli, rows, cols) {
239234
rli.close();
240235
}
241236

237+
// Adding history lines should emit the history event with
238+
// the history array
239+
{
240+
const [rli, fi] = getInterface({ terminal: true });
241+
const expectedLines = ['foo', 'bar', 'baz', 'bat'];
242+
rli.on('history', common.mustCall((history) => {
243+
const expectedHistory = expectedLines.slice(0, history.length).reverse();
244+
assert.deepStrictEqual(history, expectedHistory);
245+
}, expectedLines.length));
246+
for (const line of expectedLines) {
247+
fi.emit('data', `${line}\n`);
248+
}
249+
rli.close();
250+
}
251+
252+
// Altering the history array in the listener should not alter
253+
// the line being processed
254+
{
255+
const [rli, fi] = getInterface({ terminal: true });
256+
const expectedLine = 'foo';
257+
rli.on('history', common.mustCall((history) => {
258+
assert.strictEqual(history[0], expectedLine);
259+
history.shift();
260+
}));
261+
rli.on('line', common.mustCall((line) => {
262+
assert.strictEqual(line, expectedLine);
263+
assert.strictEqual(rli.history.length, 0);
264+
}));
265+
fi.emit('data', `${expectedLine}\n`);
266+
rli.close();
267+
}
268+
242269
// Duplicate lines are removed from history when
243270
// `options.removeHistoryDuplicates` is `true`
244271
{
@@ -774,7 +801,7 @@ for (let i = 0; i < 12; i++) {
774801
assert.strictEqual(rli.historySize, 0);
775802

776803
fi.emit('data', 'asdf\n');
777-
assert.deepStrictEqual(rli.history, terminal ? [] : undefined);
804+
assert.deepStrictEqual(rli.history, []);
778805
rli.close();
779806
}
780807

@@ -784,7 +811,7 @@ for (let i = 0; i < 12; i++) {
784811
assert.strictEqual(rli.historySize, 30);
785812

786813
fi.emit('data', 'asdf\n');
787-
assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : undefined);
814+
assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : []);
788815
rli.close();
789816
}
790817

0 commit comments

Comments
 (0)