Skip to content

Commit 4c22d6e

Browse files
lanceaddaleax
authored andcommitted
repl: add repl.setupHistory for programmatic repl
Adds a `repl.setupHistory()` instance method so that programmatic REPLs can also write history to a file. This change also refactors all of the history file management to `lib/internal/repl/history.js`, cleaning up and simplifying `lib/internal/repl.js`. PR-URL: #25895 Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com>
1 parent 896962f commit 4c22d6e

File tree

6 files changed

+422
-158
lines changed

6 files changed

+422
-158
lines changed

doc/api/repl.md

+16
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,22 @@ deprecated: v9.0.0
448448
An internal method used to parse and execute `REPLServer` keywords.
449449
Returns `true` if `keyword` is a valid keyword, otherwise `false`.
450450

451+
### replServer.setupHistory(historyPath, callback)
452+
<!-- YAML
453+
added: REPLACEME
454+
-->
455+
456+
* `historyPath` {string} the path to the history file
457+
* `callback` {Function} called when history writes are ready or upon error
458+
* `err` {Error}
459+
* `repl` {repl.REPLServer}
460+
461+
Initializes a history log file for the REPL instance. When executing the
462+
Node.js binary and using the command line REPL, a history file is initialized
463+
by default. However, this is not the case when creating a REPL
464+
programmatically. Use this method to initialize a history log file when working
465+
with REPL instances programmatically.
466+
451467
## repl.start([options])
452468
<!-- YAML
453469
added: v0.1.91

lib/internal/repl.js

+2-158
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,10 @@
11
'use strict';
22

3-
const { Interface } = require('readline');
43
const REPL = require('repl');
5-
const path = require('path');
6-
const fs = require('fs');
7-
const os = require('os');
8-
const util = require('util');
9-
const debug = util.debuglog('repl');
4+
105
module.exports = Object.create(REPL);
116
module.exports.createInternalRepl = createRepl;
127

13-
// XXX(chrisdickinson): The 15ms debounce value is somewhat arbitrary.
14-
// The debounce is to guard against code pasted into the REPL.
15-
const kDebounceHistoryMS = 15;
16-
17-
function _writeToOutput(repl, message) {
18-
repl._writeToOutput(message);
19-
repl._refreshLine();
20-
}
21-
228
function createRepl(env, opts, cb) {
239
if (typeof opts === 'function') {
2410
cb = opts;
@@ -55,151 +41,9 @@ function createRepl(env, opts, cb) {
5541
if (!Number.isNaN(historySize) && historySize > 0) {
5642
opts.historySize = historySize;
5743
} else {
58-
// XXX(chrisdickinson): set here to avoid affecting existing applications
59-
// using repl instances.
6044
opts.historySize = 1000;
6145
}
6246

6347
const repl = REPL.start(opts);
64-
if (opts.terminal) {
65-
return setupHistory(repl, env.NODE_REPL_HISTORY, cb);
66-
}
67-
68-
repl._historyPrev = _replHistoryMessage;
69-
cb(null, repl);
70-
}
71-
72-
function setupHistory(repl, historyPath, ready) {
73-
// Empty string disables persistent history
74-
if (typeof historyPath === 'string')
75-
historyPath = historyPath.trim();
76-
77-
if (historyPath === '') {
78-
repl._historyPrev = _replHistoryMessage;
79-
return ready(null, repl);
80-
}
81-
82-
if (!historyPath) {
83-
try {
84-
historyPath = path.join(os.homedir(), '.node_repl_history');
85-
} catch (err) {
86-
_writeToOutput(repl, '\nError: Could not get the home directory.\n' +
87-
'REPL session history will not be persisted.\n');
88-
89-
debug(err.stack);
90-
repl._historyPrev = _replHistoryMessage;
91-
return ready(null, repl);
92-
}
93-
}
94-
95-
var timer = null;
96-
var writing = false;
97-
var pending = false;
98-
repl.pause();
99-
// History files are conventionally not readable by others:
100-
// https://github.com/nodejs/node/issues/3392
101-
// https://github.com/nodejs/node/pull/3394
102-
fs.open(historyPath, 'a+', 0o0600, oninit);
103-
104-
function oninit(err, hnd) {
105-
if (err) {
106-
// Cannot open history file.
107-
// Don't crash, just don't persist history.
108-
_writeToOutput(repl, '\nError: Could not open history file.\n' +
109-
'REPL session history will not be persisted.\n');
110-
debug(err.stack);
111-
112-
repl._historyPrev = _replHistoryMessage;
113-
repl.resume();
114-
return ready(null, repl);
115-
}
116-
fs.close(hnd, onclose);
117-
}
118-
119-
function onclose(err) {
120-
if (err) {
121-
return ready(err);
122-
}
123-
fs.readFile(historyPath, 'utf8', onread);
124-
}
125-
126-
function onread(err, data) {
127-
if (err) {
128-
return ready(err);
129-
}
130-
131-
if (data) {
132-
repl.history = data.split(/[\n\r]+/, repl.historySize);
133-
} else {
134-
repl.history = [];
135-
}
136-
137-
fs.open(historyPath, 'r+', onhandle);
138-
}
139-
140-
function onhandle(err, hnd) {
141-
if (err) {
142-
return ready(err);
143-
}
144-
fs.ftruncate(hnd, 0, (err) => {
145-
repl._historyHandle = hnd;
146-
repl.on('line', online);
147-
148-
// Reading the file data out erases it
149-
repl.once('flushHistory', function() {
150-
repl.resume();
151-
ready(null, repl);
152-
});
153-
flushHistory();
154-
});
155-
}
156-
157-
// ------ history listeners ------
158-
function online() {
159-
repl._flushing = true;
160-
161-
if (timer) {
162-
clearTimeout(timer);
163-
}
164-
165-
timer = setTimeout(flushHistory, kDebounceHistoryMS);
166-
}
167-
168-
function flushHistory() {
169-
timer = null;
170-
if (writing) {
171-
pending = true;
172-
return;
173-
}
174-
writing = true;
175-
const historyData = repl.history.join(os.EOL);
176-
fs.write(repl._historyHandle, historyData, 0, 'utf8', onwritten);
177-
}
178-
179-
function onwritten(err, data) {
180-
writing = false;
181-
if (pending) {
182-
pending = false;
183-
online();
184-
} else {
185-
repl._flushing = Boolean(timer);
186-
if (!repl._flushing) {
187-
repl.emit('flushHistory');
188-
}
189-
}
190-
}
191-
}
192-
193-
194-
function _replHistoryMessage() {
195-
if (this.history.length === 0) {
196-
_writeToOutput(
197-
this,
198-
'\nPersistent history support disabled. ' +
199-
'Set the NODE_REPL_HISTORY environment\nvariable to ' +
200-
'a valid, user-writable path to enable.\n'
201-
);
202-
}
203-
this._historyPrev = Interface.prototype._historyPrev;
204-
return this._historyPrev();
48+
repl.setupHistory(opts.terminal ? env.NODE_REPL_HISTORY : '', cb);
20549
}

lib/internal/repl/history.js

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
'use strict';
2+
3+
const { Interface } = require('readline');
4+
const path = require('path');
5+
const fs = require('fs');
6+
const os = require('os');
7+
const util = require('util');
8+
const debug = util.debuglog('repl');
9+
10+
// XXX(chrisdickinson): The 15ms debounce value is somewhat arbitrary.
11+
// The debounce is to guard against code pasted into the REPL.
12+
const kDebounceHistoryMS = 15;
13+
14+
module.exports = setupHistory;
15+
16+
function _writeToOutput(repl, message) {
17+
repl._writeToOutput(message);
18+
repl._refreshLine();
19+
}
20+
21+
function setupHistory(repl, historyPath, ready) {
22+
// Empty string disables persistent history
23+
if (typeof historyPath === 'string')
24+
historyPath = historyPath.trim();
25+
26+
if (historyPath === '') {
27+
repl._historyPrev = _replHistoryMessage;
28+
return ready(null, repl);
29+
}
30+
31+
if (!historyPath) {
32+
try {
33+
historyPath = path.join(os.homedir(), '.node_repl_history');
34+
} catch (err) {
35+
_writeToOutput(repl, '\nError: Could not get the home directory.\n' +
36+
'REPL session history will not be persisted.\n');
37+
38+
debug(err.stack);
39+
repl._historyPrev = _replHistoryMessage;
40+
return ready(null, repl);
41+
}
42+
}
43+
44+
var timer = null;
45+
var writing = false;
46+
var pending = false;
47+
repl.pause();
48+
// History files are conventionally not readable by others:
49+
// https://github.com/nodejs/node/issues/3392
50+
// https://github.com/nodejs/node/pull/3394
51+
fs.open(historyPath, 'a+', 0o0600, oninit);
52+
53+
function oninit(err, hnd) {
54+
if (err) {
55+
// Cannot open history file.
56+
// Don't crash, just don't persist history.
57+
_writeToOutput(repl, '\nError: Could not open history file.\n' +
58+
'REPL session history will not be persisted.\n');
59+
debug(err.stack);
60+
61+
repl._historyPrev = _replHistoryMessage;
62+
repl.resume();
63+
return ready(null, repl);
64+
}
65+
fs.close(hnd, onclose);
66+
}
67+
68+
function onclose(err) {
69+
if (err) {
70+
return ready(err);
71+
}
72+
fs.readFile(historyPath, 'utf8', onread);
73+
}
74+
75+
function onread(err, data) {
76+
if (err) {
77+
return ready(err);
78+
}
79+
80+
if (data) {
81+
repl.history = data.split(/[\n\r]+/, repl.historySize);
82+
} else {
83+
repl.history = [];
84+
}
85+
86+
fs.open(historyPath, 'r+', onhandle);
87+
}
88+
89+
function onhandle(err, hnd) {
90+
if (err) {
91+
return ready(err);
92+
}
93+
fs.ftruncate(hnd, 0, (err) => {
94+
repl._historyHandle = hnd;
95+
repl.on('line', online);
96+
97+
// Reading the file data out erases it
98+
repl.once('flushHistory', function() {
99+
repl.resume();
100+
ready(null, repl);
101+
});
102+
flushHistory();
103+
});
104+
}
105+
106+
// ------ history listeners ------
107+
function online(line) {
108+
repl._flushing = true;
109+
110+
if (timer) {
111+
clearTimeout(timer);
112+
}
113+
114+
timer = setTimeout(flushHistory, kDebounceHistoryMS);
115+
}
116+
117+
function flushHistory() {
118+
timer = null;
119+
if (writing) {
120+
pending = true;
121+
return;
122+
}
123+
writing = true;
124+
const historyData = repl.history.join(os.EOL);
125+
fs.write(repl._historyHandle, historyData, 0, 'utf8', onwritten);
126+
}
127+
128+
function onwritten(err, data) {
129+
writing = false;
130+
if (pending) {
131+
pending = false;
132+
online();
133+
} else {
134+
repl._flushing = Boolean(timer);
135+
if (!repl._flushing) {
136+
repl.emit('flushHistory');
137+
}
138+
}
139+
}
140+
}
141+
142+
function _replHistoryMessage() {
143+
if (this.history.length === 0) {
144+
_writeToOutput(
145+
this,
146+
'\nPersistent history support disabled. ' +
147+
'Set the NODE_REPL_HISTORY environment\nvariable to ' +
148+
'a valid, user-writable path to enable.\n'
149+
);
150+
}
151+
this._historyPrev = Interface.prototype._historyPrev;
152+
return this._historyPrev();
153+
}

lib/repl.js

+5
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ const {
8383
startSigintWatchdog,
8484
stopSigintWatchdog
8585
} = internalBinding('util');
86+
const history = require('internal/repl/history');
8687

8788
// Lazy-loaded.
8889
let processTopLevelAwait;
@@ -761,6 +762,10 @@ exports.start = function(prompt,
761762
return repl;
762763
};
763764

765+
REPLServer.prototype.setupHistory = function setupHistory(historyFile, cb) {
766+
history(this, historyFile, cb);
767+
};
768+
764769
REPLServer.prototype.clearBufferedCommand = function clearBufferedCommand() {
765770
this[kBufferedCommandSymbol] = '';
766771
};

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@
173173
'lib/internal/readline.js',
174174
'lib/internal/repl.js',
175175
'lib/internal/repl/await.js',
176+
'lib/internal/repl/history.js',
176177
'lib/internal/repl/recoverable.js',
177178
'lib/internal/socket_list.js',
178179
'lib/internal/test/binding.js',

0 commit comments

Comments
 (0)