Skip to content

Commit

Permalink
repl: add mode detection, cli persistent history
Browse files Browse the repository at this point in the 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 <fedor@indutny.com>
  • Loading branch information
chrisdickinson committed May 1, 2015
1 parent a5dcff8 commit 0450ce7
Show file tree
Hide file tree
Showing 10 changed files with 394 additions and 61 deletions.
2 changes: 2 additions & 0 deletions doc/api/readline.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ the following values:
treated like a TTY, and have ANSI/VT100 escape codes written to it.
Defaults to checking `isTTY` on the `output` stream upon instantiation.

- `historySize` - maximum number of history lines retained. Defaults to `30`.

The `completer` function is given the current line entered by the user, and
is supposed to return an Array with 2 entries:

Expand Down
20 changes: 20 additions & 0 deletions doc/api/repl.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ For example, you could add this to your bashrc file:

alias iojs="env NODE_NO_READLINE=1 rlwrap iojs"

The built-in repl (invoked by running `iojs` or `iojs -i`) may be controlled
via the following environment variables:

- `NODE_REPL_HISTORY_FILE` - if given, must be a path to a user-writable,
user-readable file. When a valid path is given, persistent history support
is enabled: REPL history will persist across `iojs` repl sessions.
- `NODE_REPL_HISTORY_SIZE` - defaults to `1000`. In conjunction with
`NODE_REPL_HISTORY_FILE`, controls how many lines of history will be
persisted. Must be a positive number.
- `NODE_REPL_MODE` - may be any of `sloppy`, `strict`, or `magic`. Defaults
to `magic`, which will automatically run "strict mode only" statements in
strict mode.

## repl.start(options)

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

- `replMode` - controls whether the repl runs all commands in strict mode,
default mode, or a hybrid mode ("magic" mode.) Acceptable values are:
* `repl.REPL_MODE_SLOPPY` - run commands in sloppy mode.
* `repl.REPL_MODE_STRICT` - run commands in strict mode. This is equivalent to
prefacing every repl statement with `'use strict'`.
* `repl.REPL_MODE_MAGIC` - attempt to run commands in default mode. If they
fail to parse, re-try in strict mode.

You can use your own `eval` function if it has following signature:

function eval(cmd, context, filename, callback) {
Expand Down
168 changes: 168 additions & 0 deletions lib/internal/repl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
'use strict';

module.exports = {createRepl: createRepl};

const Interface = require('readline').Interface;
const REPL = require('repl');
const path = require('path');

// XXX(chrisdickinson): The 15ms debounce value is somewhat arbitrary.
// The debounce is to guard against code pasted into the REPL.
const kDebounceHistoryMS = 15;

try {
// hack for require.resolve("./relative") to work properly.
module.filename = path.resolve('repl');
} catch (e) {
// path.resolve('repl') fails when the current working directory has been
// deleted. Fall back to the directory name of the (absolute) executable
// path. It's not really correct but what are the alternatives?
const dirname = path.dirname(process.execPath);
module.filename = path.resolve(dirname, 'repl');
}

// hack for repl require to work properly with node_modules folders
module.paths = require('module')._nodeModulePaths(module.filename);

function createRepl(env, cb) {
const opts = {
useGlobal: true,
ignoreUndefined: false
};

if (parseInt(env.NODE_NO_READLINE)) {
opts.terminal = false;
}
if (parseInt(env.NODE_DISABLE_COLORS)) {
opts.useColors = false;
}

opts.replMode = {
'strict': REPL.REPL_MODE_STRICT,
'sloppy': REPL.REPL_MODE_SLOPPY,
'magic': REPL.REPL_MODE_MAGIC
}[String(env.NODE_REPL_MODE).toLowerCase().trim()];

if (opts.replMode === undefined) {
opts.replMode = REPL.REPL_MODE_MAGIC;
}

const historySize = Number(env.NODE_REPL_HISTORY_SIZE);
if (!isNaN(historySize) && historySize > 0) {
opts.historySize = historySize;
} else {
// XXX(chrisdickinson): set here to avoid affecting existing applications
// using repl instances.
opts.historySize = 1000;
}

const repl = REPL.start(opts);
if (env.NODE_REPL_HISTORY_PATH) {
return setupHistory(repl, env.NODE_REPL_HISTORY_PATH, cb);
}
repl._historyPrev = _replHistoryMessage;
cb(null, repl);
}

function setupHistory(repl, historyPath, ready) {
const fs = require('fs');
var timer = null;
var writing = false;
var pending = false;
repl.pause();
fs.open(historyPath, 'a+', oninit);

function oninit(err, hnd) {
if (err) {
return ready(err);
}
fs.close(hnd, onclose);
}

function onclose(err) {
if (err) {
return ready(err);
}
fs.readFile(historyPath, 'utf8', onread);
}

function onread(err, data) {
if (err) {
return ready(err);
}

if (data) {
try {
repl.history = JSON.parse(data);
if (!Array.isArray(repl.history)) {
throw new Error('Expected array, got ' + typeof repl.history);
}
repl.history.slice(-repl.historySize);
} catch (err) {
return ready(
new Error(`Could not parse history data in ${historyPath}.`));
}
}

fs.open(historyPath, 'w', onhandle);
}

function onhandle(err, hnd) {
if (err) {
return ready(err);
}
repl._historyHandle = hnd;
repl.on('line', online);
repl.resume();
return ready(null, repl);
}

// ------ history listeners ------
function online() {
repl._flushing = true;

if (timer) {
clearTimeout(timer);
}

timer = setTimeout(flushHistory, kDebounceHistoryMS);
}

function flushHistory() {
timer = null;
if (writing) {
pending = true;
return;
}
writing = true;
const historyData = JSON.stringify(repl.history, null, 2);
fs.write(repl._historyHandle, historyData, 0, 'utf8', onwritten);
}

function onwritten(err, data) {
writing = false;
if (pending) {
pending = false;
online();
} else {
repl._flushing = Boolean(timer);
if (!repl._flushing) {
repl.emit('flushHistory');
}
}
}
}


function _replHistoryMessage() {
if (this.history.length === 0) {
this._writeToOutput(
'\nPersistent history support disabled. ' +
'Set the NODE_REPL_HISTORY_PATH environment variable to ' +
'a valid, user-writable path to enable.\n'
);
this._refreshLine();
}
this._historyPrev = Interface.prototype._historyPrev;
return this._historyPrev();
}
21 changes: 12 additions & 9 deletions lib/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,17 @@ Module._load = function(request, parent, isMain) {
debug('Module._load REQUEST ' + (request) + ' parent: ' + parent.id);
}

// REPL is a special case, because it needs the real require.
if (request === 'internal/repl' || request === 'repl') {
if (Module._cache[request]) {
return Module._cache[request];
}
var replModule = new Module(request);
replModule._compile(NativeModule.getSource(request), `${request}.js`);
NativeModule._cache[request] = replModule;
return replModule.exports;
}

var filename = Module._resolveFilename(request, parent);

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

if (NativeModule.nonInternalExists(filename)) {
// REPL is a special case, because it needs the real require.
if (filename == 'repl') {
var replModule = new Module('repl');
replModule._compile(NativeModule.getSource('repl'), 'repl.js');
NativeModule._cache.repl = replModule;
return replModule.exports;
}

debug('load native module ' + request);
return NativeModule.require(filename);
}
Expand Down Expand Up @@ -502,7 +505,7 @@ Module._initPaths = function() {

// bootstrap repl
Module.requireRepl = function() {
return Module._load('repl', '.');
return Module._load('internal/repl', '.');
};

Module._initPaths();
Expand Down
12 changes: 11 additions & 1 deletion lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,30 @@ function Interface(input, output, completer, terminal) {
this._sawReturn = false;

EventEmitter.call(this);
var historySize;

if (arguments.length === 1) {
// an options object was given
output = input.output;
completer = input.completer;
terminal = input.terminal;
historySize = input.historySize;
input = input.input;
}
historySize = historySize || kHistorySize;

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

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

if (typeof historySize !== 'number' ||
isNaN(historySize) ||
historySize < 0) {
throw new TypeError('Argument \'historySize\' must be a positive number');
}

// backwards compat; check the isTTY prop of the output stream
// when `terminal` was not specified
if (terminal === undefined && !(output === null || output === undefined)) {
Expand All @@ -60,6 +69,7 @@ function Interface(input, output, completer, terminal) {

this.output = output;
this.input = input;
this.historySize = historySize;

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

// Only store so many
if (this.history.length > kHistorySize) this.history.pop();
if (this.history.length > this.historySize) this.history.pop();
}

this.historyIndex = -1;
Expand Down
Loading

0 comments on commit 0450ce7

Please sign in to comment.