Skip to content

Commit 0450ce7

Browse files
repl: add mode detection, cli persistent history
this creates a new internal module responsible for providing the repl created via "iojs" or "iojs -i," and adds the following options to the readline and repl subsystems: * "repl mode" - determine whether a repl is strict mode, sloppy mode, or auto-detect mode. * historySize - determine the maximum number of lines a repl will store as history. The built-in repl gains persistent history support when the NODE_REPL_HISTORY_FILE environment variable is set. This functionality is not exposed to userland repl instances. PR-URL: #1513 Reviewed-By: Fedor Indutny <[email protected]>
1 parent a5dcff8 commit 0450ce7

File tree

10 files changed

+394
-61
lines changed

10 files changed

+394
-61
lines changed

doc/api/readline.markdown

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ the following values:
3939
treated like a TTY, and have ANSI/VT100 escape codes written to it.
4040
Defaults to checking `isTTY` on the `output` stream upon instantiation.
4141

42+
- `historySize` - maximum number of history lines retained. Defaults to `30`.
43+
4244
The `completer` function is given the current line entered by the user, and
4345
is supposed to return an Array with 2 entries:
4446

doc/api/repl.markdown

+20
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ For example, you could add this to your bashrc file:
2929

3030
alias iojs="env NODE_NO_READLINE=1 rlwrap iojs"
3131

32+
The built-in repl (invoked by running `iojs` or `iojs -i`) may be controlled
33+
via the following environment variables:
34+
35+
- `NODE_REPL_HISTORY_FILE` - if given, must be a path to a user-writable,
36+
user-readable file. When a valid path is given, persistent history support
37+
is enabled: REPL history will persist across `iojs` repl sessions.
38+
- `NODE_REPL_HISTORY_SIZE` - defaults to `1000`. In conjunction with
39+
`NODE_REPL_HISTORY_FILE`, controls how many lines of history will be
40+
persisted. Must be a positive number.
41+
- `NODE_REPL_MODE` - may be any of `sloppy`, `strict`, or `magic`. Defaults
42+
to `magic`, which will automatically run "strict mode only" statements in
43+
strict mode.
3244

3345
## repl.start(options)
3446

@@ -64,6 +76,14 @@ the following values:
6476
returns the formatting (including coloring) to display. Defaults to
6577
`util.inspect`.
6678

79+
- `replMode` - controls whether the repl runs all commands in strict mode,
80+
default mode, or a hybrid mode ("magic" mode.) Acceptable values are:
81+
* `repl.REPL_MODE_SLOPPY` - run commands in sloppy mode.
82+
* `repl.REPL_MODE_STRICT` - run commands in strict mode. This is equivalent to
83+
prefacing every repl statement with `'use strict'`.
84+
* `repl.REPL_MODE_MAGIC` - attempt to run commands in default mode. If they
85+
fail to parse, re-try in strict mode.
86+
6787
You can use your own `eval` function if it has following signature:
6888

6989
function eval(cmd, context, filename, callback) {

lib/internal/repl.js

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
'use strict';
2+
3+
module.exports = {createRepl: createRepl};
4+
5+
const Interface = require('readline').Interface;
6+
const REPL = require('repl');
7+
const path = require('path');
8+
9+
// XXX(chrisdickinson): The 15ms debounce value is somewhat arbitrary.
10+
// The debounce is to guard against code pasted into the REPL.
11+
const kDebounceHistoryMS = 15;
12+
13+
try {
14+
// hack for require.resolve("./relative") to work properly.
15+
module.filename = path.resolve('repl');
16+
} catch (e) {
17+
// path.resolve('repl') fails when the current working directory has been
18+
// deleted. Fall back to the directory name of the (absolute) executable
19+
// path. It's not really correct but what are the alternatives?
20+
const dirname = path.dirname(process.execPath);
21+
module.filename = path.resolve(dirname, 'repl');
22+
}
23+
24+
// hack for repl require to work properly with node_modules folders
25+
module.paths = require('module')._nodeModulePaths(module.filename);
26+
27+
function createRepl(env, cb) {
28+
const opts = {
29+
useGlobal: true,
30+
ignoreUndefined: false
31+
};
32+
33+
if (parseInt(env.NODE_NO_READLINE)) {
34+
opts.terminal = false;
35+
}
36+
if (parseInt(env.NODE_DISABLE_COLORS)) {
37+
opts.useColors = false;
38+
}
39+
40+
opts.replMode = {
41+
'strict': REPL.REPL_MODE_STRICT,
42+
'sloppy': REPL.REPL_MODE_SLOPPY,
43+
'magic': REPL.REPL_MODE_MAGIC
44+
}[String(env.NODE_REPL_MODE).toLowerCase().trim()];
45+
46+
if (opts.replMode === undefined) {
47+
opts.replMode = REPL.REPL_MODE_MAGIC;
48+
}
49+
50+
const historySize = Number(env.NODE_REPL_HISTORY_SIZE);
51+
if (!isNaN(historySize) && historySize > 0) {
52+
opts.historySize = historySize;
53+
} else {
54+
// XXX(chrisdickinson): set here to avoid affecting existing applications
55+
// using repl instances.
56+
opts.historySize = 1000;
57+
}
58+
59+
const repl = REPL.start(opts);
60+
if (env.NODE_REPL_HISTORY_PATH) {
61+
return setupHistory(repl, env.NODE_REPL_HISTORY_PATH, cb);
62+
}
63+
repl._historyPrev = _replHistoryMessage;
64+
cb(null, repl);
65+
}
66+
67+
function setupHistory(repl, historyPath, ready) {
68+
const fs = require('fs');
69+
var timer = null;
70+
var writing = false;
71+
var pending = false;
72+
repl.pause();
73+
fs.open(historyPath, 'a+', oninit);
74+
75+
function oninit(err, hnd) {
76+
if (err) {
77+
return ready(err);
78+
}
79+
fs.close(hnd, onclose);
80+
}
81+
82+
function onclose(err) {
83+
if (err) {
84+
return ready(err);
85+
}
86+
fs.readFile(historyPath, 'utf8', onread);
87+
}
88+
89+
function onread(err, data) {
90+
if (err) {
91+
return ready(err);
92+
}
93+
94+
if (data) {
95+
try {
96+
repl.history = JSON.parse(data);
97+
if (!Array.isArray(repl.history)) {
98+
throw new Error('Expected array, got ' + typeof repl.history);
99+
}
100+
repl.history.slice(-repl.historySize);
101+
} catch (err) {
102+
return ready(
103+
new Error(`Could not parse history data in ${historyPath}.`));
104+
}
105+
}
106+
107+
fs.open(historyPath, 'w', onhandle);
108+
}
109+
110+
function onhandle(err, hnd) {
111+
if (err) {
112+
return ready(err);
113+
}
114+
repl._historyHandle = hnd;
115+
repl.on('line', online);
116+
repl.resume();
117+
return ready(null, repl);
118+
}
119+
120+
// ------ history listeners ------
121+
function online() {
122+
repl._flushing = true;
123+
124+
if (timer) {
125+
clearTimeout(timer);
126+
}
127+
128+
timer = setTimeout(flushHistory, kDebounceHistoryMS);
129+
}
130+
131+
function flushHistory() {
132+
timer = null;
133+
if (writing) {
134+
pending = true;
135+
return;
136+
}
137+
writing = true;
138+
const historyData = JSON.stringify(repl.history, null, 2);
139+
fs.write(repl._historyHandle, historyData, 0, 'utf8', onwritten);
140+
}
141+
142+
function onwritten(err, data) {
143+
writing = false;
144+
if (pending) {
145+
pending = false;
146+
online();
147+
} else {
148+
repl._flushing = Boolean(timer);
149+
if (!repl._flushing) {
150+
repl.emit('flushHistory');
151+
}
152+
}
153+
}
154+
}
155+
156+
157+
function _replHistoryMessage() {
158+
if (this.history.length === 0) {
159+
this._writeToOutput(
160+
'\nPersistent history support disabled. ' +
161+
'Set the NODE_REPL_HISTORY_PATH environment variable to ' +
162+
'a valid, user-writable path to enable.\n'
163+
);
164+
this._refreshLine();
165+
}
166+
this._historyPrev = Interface.prototype._historyPrev;
167+
return this._historyPrev();
168+
}

lib/module.js

+12-9
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,17 @@ Module._load = function(request, parent, isMain) {
273273
debug('Module._load REQUEST ' + (request) + ' parent: ' + parent.id);
274274
}
275275

276+
// REPL is a special case, because it needs the real require.
277+
if (request === 'internal/repl' || request === 'repl') {
278+
if (Module._cache[request]) {
279+
return Module._cache[request];
280+
}
281+
var replModule = new Module(request);
282+
replModule._compile(NativeModule.getSource(request), `${request}.js`);
283+
NativeModule._cache[request] = replModule;
284+
return replModule.exports;
285+
}
286+
276287
var filename = Module._resolveFilename(request, parent);
277288

278289
var cachedModule = Module._cache[filename];
@@ -281,14 +292,6 @@ Module._load = function(request, parent, isMain) {
281292
}
282293

283294
if (NativeModule.nonInternalExists(filename)) {
284-
// REPL is a special case, because it needs the real require.
285-
if (filename == 'repl') {
286-
var replModule = new Module('repl');
287-
replModule._compile(NativeModule.getSource('repl'), 'repl.js');
288-
NativeModule._cache.repl = replModule;
289-
return replModule.exports;
290-
}
291-
292295
debug('load native module ' + request);
293296
return NativeModule.require(filename);
294297
}
@@ -502,7 +505,7 @@ Module._initPaths = function() {
502505

503506
// bootstrap repl
504507
Module.requireRepl = function() {
505-
return Module._load('repl', '.');
508+
return Module._load('internal/repl', '.');
506509
};
507510

508511
Module._initPaths();

lib/readline.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,30 @@ function Interface(input, output, completer, terminal) {
3535
this._sawReturn = false;
3636

3737
EventEmitter.call(this);
38+
var historySize;
3839

3940
if (arguments.length === 1) {
4041
// an options object was given
4142
output = input.output;
4243
completer = input.completer;
4344
terminal = input.terminal;
45+
historySize = input.historySize;
4446
input = input.input;
4547
}
48+
historySize = historySize || kHistorySize;
4649

4750
completer = completer || function() { return []; };
4851

4952
if (typeof completer !== 'function') {
5053
throw new TypeError('Argument \'completer\' must be a function');
5154
}
5255

56+
if (typeof historySize !== 'number' ||
57+
isNaN(historySize) ||
58+
historySize < 0) {
59+
throw new TypeError('Argument \'historySize\' must be a positive number');
60+
}
61+
5362
// backwards compat; check the isTTY prop of the output stream
5463
// when `terminal` was not specified
5564
if (terminal === undefined && !(output === null || output === undefined)) {
@@ -60,6 +69,7 @@ function Interface(input, output, completer, terminal) {
6069

6170
this.output = output;
6271
this.input = input;
72+
this.historySize = historySize;
6373

6474
// Check arity, 2 - for async, 1 for sync
6575
this.completer = completer.length === 2 ? completer : function(v, callback) {
@@ -214,7 +224,7 @@ Interface.prototype._addHistory = function() {
214224
this.history.unshift(this.line);
215225

216226
// Only store so many
217-
if (this.history.length > kHistorySize) this.history.pop();
227+
if (this.history.length > this.historySize) this.history.pop();
218228
}
219229

220230
this.historyIndex = -1;

0 commit comments

Comments
 (0)