Skip to content

Commit aaeb463

Browse files
committed
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.
1 parent a35b32e commit aaeb463

File tree

3 files changed

+107
-32
lines changed

3 files changed

+107
-32
lines changed

doc/api/readline.md

Lines changed: 32 additions & 3 deletions
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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const {
3737
ArrayPrototypeReverse,
3838
ArrayPrototypeSplice,
3939
ArrayPrototypeUnshift,
40+
Array,
4041
DateNow,
4142
FunctionPrototypeBind,
4243
FunctionPrototypeCall,
@@ -67,6 +68,7 @@ const {
6768
ERR_INVALID_CURSOR_POS,
6869
} = require('internal/errors').codes;
6970
const {
71+
validateArray,
7072
validateCallback,
7173
validateString,
7274
validateUint32,
@@ -133,6 +135,7 @@ function Interface(input, output, completer, terminal) {
133135
this.tabSize = 8;
134136

135137
FunctionPrototypeCall(EventEmitter, this,);
138+
let history;
136139
let historySize;
137140
let removeHistoryDuplicates = false;
138141
let crlfDelay;
@@ -143,6 +146,7 @@ function Interface(input, output, completer, terminal) {
143146
output = input.output;
144147
completer = input.completer;
145148
terminal = input.terminal;
149+
history = input.history;
146150
historySize = input.historySize;
147151
if (input.tabSize !== undefined) {
148152
validateUint32(input.tabSize, 'tabSize', true);
@@ -170,6 +174,12 @@ function Interface(input, output, completer, terminal) {
170174
throw new ERR_INVALID_ARG_VALUE('completer', completer);
171175
}
172176

177+
if (history === undefined) {
178+
history = [];
179+
} else if (!(history instanceof Array)) {
180+
validateArray(history, 'history');
181+
}
182+
173183
if (historySize === undefined) {
174184
historySize = kHistorySize;
175185
}
@@ -191,6 +201,7 @@ function Interface(input, output, completer, terminal) {
191201
this[kSubstringSearch] = null;
192202
this.output = output;
193203
this.input = input;
204+
this.history = history;
194205
this.historySize = historySize;
195206
this.removeHistoryDuplicates = !!removeHistoryDuplicates;
196207
this.crlfDelay = crlfDelay ?
@@ -288,7 +299,6 @@ function Interface(input, output, completer, terminal) {
288299
// Cursor position on the line.
289300
this.cursor = 0;
290301

291-
this.history = [];
292302
this.historyIndex = -1;
293303

294304
if (output !== null && output !== undefined)
@@ -404,7 +414,16 @@ Interface.prototype._addHistory = function() {
404414
}
405415

406416
this.historyIndex = -1;
407-
return this.history[0];
417+
418+
// The listener could change the history object, possibly
419+
// to remove the last added entry if it is sensitive and should
420+
// not be persisted in the history, like a password
421+
const line = this.history[0];
422+
423+
// Emit history event to notify listeners of update
424+
this.emit('history', this.history);
425+
426+
return line;
408427
};
409428

410429

test/parallel/test-readline-interface.js

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -115,35 +115,30 @@ function assertCursorRowsAndCols(rli, rows, cols) {
115115
code: 'ERR_INVALID_ARG_VALUE'
116116
});
117117

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

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

149144
// Check for invalid tab sizes.
@@ -238,6 +233,38 @@ function assertCursorRowsAndCols(rli, rows, cols) {
238233
rli.close();
239234
}
240235

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

775802
fi.emit('data', 'asdf\n');
776-
assert.deepStrictEqual(rli.history, terminal ? [] : undefined);
803+
assert.deepStrictEqual(rli.history, []);
777804
rli.close();
778805
}
779806

@@ -783,7 +810,7 @@ for (let i = 0; i < 12; i++) {
783810
assert.strictEqual(rli.historySize, 30);
784811

785812
fi.emit('data', 'asdf\n');
786-
assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : undefined);
813+
assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : []);
787814
rli.close();
788815
}
789816

0 commit comments

Comments
 (0)