Skip to content

Commit 26543f3

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 26543f3

File tree

3 files changed

+109
-36
lines changed

3 files changed

+109
-36
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: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -115,36 +115,29 @@ 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
133-
});
134-
}, {
135-
name: 'RangeError',
136-
code: 'ERR_INVALID_ARG_VALUE'
137-
});
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'
128+
})});
138129

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

149142
// Check for invalid tab sizes.
150143
assert.throws(
@@ -238,6 +231,38 @@ function assertCursorRowsAndCols(rli, rows, cols) {
238231
rli.close();
239232
}
240233

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

775800
fi.emit('data', 'asdf\n');
776-
assert.deepStrictEqual(rli.history, terminal ? [] : undefined);
801+
assert.deepStrictEqual(rli.history, []);
777802
rli.close();
778803
}
779804

@@ -783,7 +808,7 @@ for (let i = 0; i < 12; i++) {
783808
assert.strictEqual(rli.historySize, 30);
784809

785810
fi.emit('data', 'asdf\n');
786-
assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : undefined);
811+
assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : []);
787812
rli.close();
788813
}
789814

0 commit comments

Comments
 (0)