From a8dd917ce7756c5f91b0ec5ea27e339d9da7179c Mon Sep 17 00:00:00 2001 From: Evan Lucas Date: Wed, 14 Jan 2015 22:12:03 -0600 Subject: [PATCH] repl: add history support for REPL Adds support for saving command history for REPL. When the binary is run without any arguments or with the `-i` flag, it will default to saving the command history to ~/.node_history. Otherwise, it is disabled by default. The path to which the history is saved can be changed by setting the `NODE_HISTORY_PATH` environment variable. Saving history can be disabled by setting the `NODE_DISABLE_HISTORY` environment variable. --- doc/api/repl.markdown | 7 +++ lib/repl.js | 54 +++++++++++++++++++--- src/node.js | 6 ++- test/parallel/test-repl-history.js | 58 ++++++++++++++++++++++++ test/parallel/test-repl-options.js | 1 + test/parallel/test-repl-timeout-throw.js | 1 + test/parallel/test-repl.js | 3 +- 7 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 test/parallel/test-repl-history.js diff --git a/doc/api/repl.markdown b/doc/api/repl.markdown index e5081c3c8db4eb..45d1d06c3299d5 100644 --- a/doc/api/repl.markdown +++ b/doc/api/repl.markdown @@ -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=`. + 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`. @@ -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: diff --git a/lib/repl.js b/lib/repl.js index b9ce053c915d7f..fc4be0011a4c84 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -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; @@ -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 { @@ -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; + } // just for backwards compat, see github.com/joyent/node/pull/7127 self.rli = this; @@ -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'); }); @@ -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); @@ -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; }; diff --git a/src/node.js b/src/node.js index e6056797b2a344..209fc109b3c755 100644 --- a/src/node.js +++ b/src/node.js @@ -119,7 +119,8 @@ // REPL var opts = { useGlobal: true, - ignoreUndefined: false + ignoreUndefined: false, + useHistory: true }; if (parseInt(process.env['NODE_NO_READLINE'], 10)) { opts.terminal = false; @@ -127,6 +128,9 @@ 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(); diff --git a/test/parallel/test-repl-history.js b/test/parallel/test-repl-history.js new file mode 100644 index 00000000000000..ce1d1dccb88827 --- /dev/null +++ b/test/parallel/test-repl-history.js @@ -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(); diff --git a/test/parallel/test-repl-options.js b/test/parallel/test-repl-options.js index e58f459393ecba..c2f97e8134b172 100644 --- a/test/parallel/test-repl-options.js +++ b/test/parallel/test-repl-options.js @@ -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); diff --git a/test/parallel/test-repl-timeout-throw.js b/test/parallel/test-repl-timeout-throw.js index 2febf2e3ce0565..64bf3f043758a0 100644 --- a/test/parallel/test-repl-timeout-throw.js +++ b/test/parallel/test-repl-timeout-throw.js @@ -1,3 +1,4 @@ +process.env.NODE_DISABLE_HISTORY = 1; var assert = require('assert'); var common = require('../common.js'); diff --git a/test/parallel/test-repl.js b/test/parallel/test-repl.js index 5f775b4094d769..16e059b9fa9311 100644 --- a/test/parallel/test-repl.js +++ b/test/parallel/test-repl.js @@ -258,7 +258,8 @@ function unix_test() { prompt: prompt_unix, input: socket, output: socket, - useGlobal: true + useGlobal: true, + useHistory: false }).context.message = message; });