Skip to content

Commit 7512429

Browse files
mattiasrungetargos
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: #33662 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 2cfe795 commit 7512429

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
@@ -522,6 +544,9 @@ the current position of the cursor down.
522544
<!-- YAML
523545
added: v0.1.98
524546
changes:
547+
- version: REPLACEME
548+
pr-url: https://github.com/nodejs/node/pull/33662
549+
description: The `history` option is supported now.
525550
- version: v13.9.0
526551
pr-url: https://github.com/nodejs/node/pull/31318
527552
description: The `tabSize` option is supported now.
@@ -550,21 +575,25 @@ changes:
550575
* `terminal` {boolean} `true` if the `input` and `output` streams should be
551576
treated like a TTY, and have ANSI/VT100 escape codes written to it.
552577
**Default:** checking `isTTY` on the `output` stream upon instantiation.
578+
* `history` {string[]} Initial list of history lines. This option makes sense
579+
only if `terminal` is set to `true` by the user or by an internal `output`
580+
check, otherwise the history caching mechanism is not initialized at all.
581+
**Default:** `[]`.
553582
* `historySize` {number} Maximum number of history lines retained. To disable
554583
the history set this value to `0`. This option makes sense only if
555584
`terminal` is set to `true` by the user or by an internal `output` check,
556585
otherwise the history caching mechanism is not initialized at all.
557586
**Default:** `30`.
587+
* `removeHistoryDuplicates` {boolean} If `true`, when a new input line added
588+
to the history list duplicates an older one, this removes the older line
589+
from the list. **Default:** `false`.
558590
* `prompt` {string} The prompt string to use. **Default:** `'> '`.
559591
* `crlfDelay` {number} If the delay between `\r` and `\n` exceeds
560592
`crlfDelay` milliseconds, both `\r` and `\n` will be treated as separate
561593
end-of-line input. `crlfDelay` will be coerced to a number no less than
562594
`100`. It can be set to `Infinity`, in which case `\r` followed by `\n`
563595
will always be considered a single newline (which may be reasonable for
564596
[reading files][] with `\r\n` line delimiter). **Default:** `100`.
565-
* `removeHistoryDuplicates` {boolean} If `true`, when a new input line added
566-
to the history list duplicates an older one, this removes the older line
567-
from the list. **Default:** `false`.
568597
* `escapeCodeTimeout` {number} The duration `readline` will wait for a
569598
character (when reading an ambiguous key sequence in milliseconds one that
570599
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
@@ -73,6 +73,7 @@ const {
7373
ERR_INVALID_CURSOR_POS,
7474
} = codes;
7575
const {
76+
validateArray,
7677
validateCallback,
7778
validateString,
7879
validateUint32,
@@ -142,6 +143,7 @@ function Interface(input, output, completer, terminal) {
142143
this.tabSize = 8;
143144

144145
FunctionPrototypeCall(EventEmitter, this,);
146+
let history;
145147
let historySize;
146148
let removeHistoryDuplicates = false;
147149
let crlfDelay;
@@ -152,6 +154,7 @@ function Interface(input, output, completer, terminal) {
152154
output = input.output;
153155
completer = input.completer;
154156
terminal = input.terminal;
157+
history = input.history;
155158
historySize = input.historySize;
156159
if (input.tabSize !== undefined) {
157160
validateUint32(input.tabSize, 'tabSize', true);
@@ -179,6 +182,12 @@ function Interface(input, output, completer, terminal) {
179182
throw new ERR_INVALID_ARG_VALUE('completer', completer);
180183
}
181184

185+
if (history === undefined) {
186+
history = [];
187+
} else {
188+
validateArray(history, 'history');
189+
}
190+
182191
if (historySize === undefined) {
183192
historySize = kHistorySize;
184193
}
@@ -201,6 +210,7 @@ function Interface(input, output, completer, terminal) {
201210
this[kSubstringSearch] = null;
202211
this.output = output;
203212
this.input = input;
213+
this.history = history;
204214
this.historySize = historySize;
205215
this.removeHistoryDuplicates = !!removeHistoryDuplicates;
206216
this.crlfDelay = crlfDelay ?
@@ -292,7 +302,6 @@ function Interface(input, output, completer, terminal) {
292302
// Cursor position on the line.
293303
this.cursor = 0;
294304

295-
this.history = [];
296305
this.historyIndex = -1;
297306

298307
if (output !== null && output !== undefined)
@@ -439,7 +448,16 @@ Interface.prototype._addHistory = function() {
439448
}
440449

441450
this.historyIndex = -1;
442-
return this.history[0];
451+
452+
// The listener could change the history object, possibly
453+
// to remove the last added entry if it is sensitive and should
454+
// not be persisted in the history, like a password
455+
const line = this.history[0];
456+
457+
// Emit history event to notify listeners of update
458+
this.emit('history', this.history);
459+
460+
return line;
443461
};
444462

445463

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_ARG_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_ARG_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_ARG_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_ARG_VALUE'
144142
});
145-
}, {
146-
name: 'RangeError',
147-
code: 'ERR_INVALID_ARG_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)