Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

repl: add history support for REPL #434

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/api/repl.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ dropped into the REPL. It has simplistic emacs line-editing.
2
3

By default, it will save your history to `~/.node_history`. To specify a
different path, start node with the environment variable
`NODE_HISTORY_PATH=<the new history path>`.

For advanced line-editors, start node with the environmental variable
`NODE_NO_READLINE=1`. This will start the main and debugger REPL in canonical
terminal settings which will allow you to use with `rlwrap`.
Expand Down Expand Up @@ -63,6 +67,9 @@ the following values:
- `writer` - the function to invoke for each command that gets evaluated which
returns the formatting (including coloring) to display. Defaults to
`util.inspect`.

- `useHistory` - a boolean which specifies whether or not to save the history
of the current session. Defaults to `false`.

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

Expand Down
54 changes: 48 additions & 6 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,22 @@ exports._builtinLibs = ['assert', 'buffer', 'child_process', 'cluster',
'string_decoder', 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'zlib',
'smalloc'];


function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {
var historyPath = path.join(process.env.HOME || process.env.USERPROFILE,
'.node_history');

function REPLServer(prompt,
stream,
eval_,
useGlobal,
ignoreUndefined,
useHistory) {
if (!(this instanceof REPLServer)) {
return new REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined);
return new REPLServer(prompt,
stream,
eval_,
useGlobal,
ignoreUndefined,
useHistory);
}

var options, input, output, dom;
Expand All @@ -74,6 +86,7 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {
ignoreUndefined = options.ignoreUndefined;
prompt = options.prompt;
dom = options.domain;
useHistory = options.useHistory;
} else if (!util.isString(prompt)) {
throw new Error('An options Object, or a prompt String are required');
} else {
Expand All @@ -86,7 +99,10 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {

self.useGlobal = !!useGlobal;
self.ignoreUndefined = !!ignoreUndefined;

self.useHistory = !!useHistory;
if (self.useHistory) {
self.historyPath = process.env['NODE_HISTORY_PATH'] || historyPath;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be dangerous with setuid binaries. (Hopefully no one does that but...)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repl readme contains examples of how to hook up a REPL to a net.Socket for remote inspection – I'm not sure if anyone does this in practice but I feel like it could certainly run afoul of the setuid problem.

A module that's only run when the REPL would be started on the CLI, which only handled things like auto-loading of native modules and persistent history, would sidestep these problems.

}
// just for backwards compat, see github.com/joyent/node/pull/7127
self.rli = this;

Expand Down Expand Up @@ -197,6 +213,13 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {
self.setPrompt(self._prompt);

self.on('close', function() {
if (self.useHistory) {
try {
fs.writeFileSync(self.historyPath, self.history.join('\n'), 'utf8');
}
// ignore errors
catch (err) {}
}
self.emit('exit');
});

Expand Down Expand Up @@ -309,6 +332,15 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {
self.displayPrompt(true);
});

if (this.useHistory) {
try {
this.history = fs.readFileSync(self.historyPath, 'utf8').split('\n');
this.historyIndex = -1;
}
// ignore errors since we create on exit
catch (err) {}
}

self.displayPrompt();
}
inherits(REPLServer, rl.Interface);
Expand All @@ -317,8 +349,18 @@ exports.REPLServer = REPLServer;

// prompt is a string to print on each line for the prompt,
// source is a stream to use for I/O, defaulting to stdin/stdout.
exports.start = function(prompt, source, eval_, useGlobal, ignoreUndefined) {
var repl = new REPLServer(prompt, source, eval_, useGlobal, ignoreUndefined);
exports.start = function(prompt,
source,
eval_,
useGlobal,
ignoreUndefined,
useHistory) {
var repl = new REPLServer(prompt,
source,
eval_,
useGlobal,
ignoreUndefined,
useHistory);
if (!exports.repl) exports.repl = repl;
return repl;
};
Expand Down
6 changes: 5 additions & 1 deletion src/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,18 @@
// REPL
var opts = {
useGlobal: true,
ignoreUndefined: false
ignoreUndefined: false,
useHistory: true
};
if (parseInt(process.env['NODE_NO_READLINE'], 10)) {
opts.terminal = false;
}
if (parseInt(process.env['NODE_DISABLE_COLORS'], 10)) {
opts.useColors = false;
}
if (parseInt(process.env['NODE_DISABLE_HISTORY'], 10)) {
opts.useHistory = false;
}
var repl = Module.requireRepl().start(opts);
repl.on('exit', function() {
process.exit();
Expand Down
58 changes: 58 additions & 0 deletions test/parallel/test-repl-history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
var common = require('../common');
var assert = require('assert');
var Stream = require('stream');
var repl = require('repl');
var fs = require('fs');
var path = require('path');

process.env.NODE_HISTORY_PATH = path.join('..', 'fixtures', '.node_history');

// create a dummy stream that does nothing
var stream = new Stream();
stream.write = stream.pause = stream.resume = function(){};
stream.readable = stream.writable = true;

function testTerminalMode() {
var r1 = repl.start({
terminal: true,
useHistory: true
});

process.stdin.write('blahblah');

process.nextTick(function() {
process.stdin.end();
});

r1.on('exit', function() {
console.log('r1 exit')
var contents = fs.readFileSync(process.env.NODE_HISTORY_PATH, 'utf8');
fs.unlinkSync(process.env.NODE_HISTORY_PATH);
assert.equal(contents, 'blahblah');
testRegularMode();
});
}

function testRegularMode() {
var r2 = repl.start({
input: stream,
output: stream,
terminal: false,
useHistory: true
});

stream.write('blahblah');

process.nextTick(function() {
stream.emit('end');
});

r2.on('exit', function() {
console.log('r2 exit')
var contents = fs.readFileSync(process.env.NODE_HISTORY_PATH, 'utf8');
fs.unlinkSync(process.env.NODE_HISTORY_PATH);
assert.equal(contents, 'blahblah');
});
}

testTerminalMode();
1 change: 1 addition & 0 deletions test/parallel/test-repl-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ assert.equal(r1.terminal, true);
assert.equal(r1.useColors, r1.terminal);
assert.equal(r1.useGlobal, false);
assert.equal(r1.ignoreUndefined, false);
assert.equal(r1.useHistory, false);

// test r1 for backwards compact
assert.equal(r1.rli.input, stream);
Expand Down
1 change: 1 addition & 0 deletions test/parallel/test-repl-timeout-throw.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
process.env.NODE_DISABLE_HISTORY = 1;
var assert = require('assert');
var common = require('../common.js');

Expand Down
3 changes: 2 additions & 1 deletion test/parallel/test-repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ function unix_test() {
prompt: prompt_unix,
input: socket,
output: socket,
useGlobal: true
useGlobal: true,
useHistory: false
}).context.message = message;
});

Expand Down