From b7441040f87ff5e0e7b4575852996b6a64e0ea8f Mon Sep 17 00:00:00 2001 From: Matt Ranney Date: Sun, 11 Apr 2010 16:13:32 -0700 Subject: [PATCH] REPL can be run from multiple different streams. e.g. from UNIX sockets with socat. --- lib/repl.js | 191 +++++++++++++++++++++------------------ test/simple/test-repl.js | 138 ++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+), 88 deletions(-) create mode 100644 test/simple/test-repl.js diff --git a/lib/repl.js b/lib/repl.js index 6188d7a3b3..a092e47048 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -1,153 +1,168 @@ // A repl library that you can include in your own code to get a runtime -// interface to your program. Just require("/repl.js"). +// interface to your program. +// +// var repl = require("/repl.js"); +// repl.start("prompt> "); // start repl on stdin +// net.createServer(function (socket) { // listen for unix socket connections and start repl on them +// repl.start("node via Unix socket> ", socket); +// }).listen("/tmp/node-repl-sock"); +// net.createServer(function (socket) { // listen for TCP socket connections and start repl on them +// repl.start("node via TCP socket> ", socket); +// }).listen(5001); + +// repl.start("node > ").scope.foo = "stdin is fun"; // expose foo to repl scope var sys = require('sys'); -var buffered_cmd = ''; -var trimmer = /^\s*(.+)\s*$/m; -var scopedVar = /^\s*var\s*([_\w\$]+)(.*)$/m; -var scopeFunc = /^\s*function\s*([_\w\$]+)/; - -exports.scope = {}; -exports.prompt = "node> "; // Can overridden with custom print functions, such as `probe` or `eyes.js` -exports.writer = sys.p; +exports.writer = sys.inspect; + +function REPLServer(prompt, stream) { + var self = this; + + self.scope = {}; + self.buffered_cmd = ''; + self.prompt = prompt || "node> "; + self.stream = stream || process.openStdin(); + self.stream.setEncoding('utf8'); + self.stream.addListener("data", function (chunk) { + self.readline.call(self, chunk); + }); + self.displayPrompt(); +} +exports.REPLServer = REPLServer; -var stdin; +// 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) { + return new REPLServer(prompt, source); +}; -exports.start = function (prompt) { - if (prompt !== undefined) { - exports.prompt = prompt; - } +REPLServer.prototype.displayPrompt = function () { + var self = this; + self.stream.write(self.buffered_cmd.length ? '... ' : self.prompt); +}; - stdin = process.openStdin(); - stdin.setEncoding('utf8'); - stdin.addListener("data", readline); - displayPrompt(); -} - -/** - * The main REPL function. This is called everytime the user enters - * data on the command line. - */ -function readline (cmd) { - cmd = trimWhitespace(cmd); +// read a line from the stream, then eval it +REPLServer.prototype.readline = function (cmd) { + var self = this; + cmd = self.trimWhitespace(cmd); + // Check to see if a REPL keyword was used. If it returns true, // display next prompt and return. - if (parseREPLKeyword(cmd) === true) { + if (self.parseREPLKeyword(cmd) === true) { return; } - + // The catchall for errors try { - buffered_cmd += "\n" + cmd; + self.buffered_cmd += cmd; // This try is for determining if the command is complete, or should // continue onto the next line. try { - buffered_cmd = convertToScope(buffered_cmd); - - // Scope the readline with exports.scope to provide "local" vars - with (exports.scope) { - var ret = eval(buffered_cmd); + self.buffered_cmd = self.convertToScope(self.buffered_cmd); + + // Scope the readline with self.scope to provide "local" vars and make Douglas Crockford cry + with (self.scope) { + var ret = eval(self.buffered_cmd); if (ret !== undefined) { - exports.scope['_'] = ret; - exports.writer(ret); + self.scope['_'] = ret; + self.stream.write(exports.writer(ret) + "\n"); } } - - buffered_cmd = ''; + + self.buffered_cmd = ''; } catch (e) { if (!(e instanceof SyntaxError)) throw e; } } catch (e) { // On error: Print the error and clear the buffer if (e.stack) { - sys.puts(e.stack); + self.stream.write(e.stack + "\n"); } else { - sys.puts(e.toString()); + self.stream.write(e.toString() + "\n"); } - buffered_cmd = ''; + self.buffered_cmd = ''; } - - displayPrompt(); -} - - -/** - * Used to display the prompt. - */ -function displayPrompt () { - sys.print(buffered_cmd.length ? '... ' : exports.prompt); -} + + self.displayPrompt(); +}; /** * Used to parse and execute the Node REPL commands. - * + * * @param {cmd} cmd The command entered to check - * @returns {Boolean} If true it means don't continue parsing the command + * @returns {Boolean} If true it means don't continue parsing the command */ -function parseREPLKeyword (cmd) { + +REPLServer.prototype.parseREPLKeyword = function (cmd) { + var self = this; + switch (cmd) { case ".break": - buffered_cmd = ''; - displayPrompt(); + self.buffered_cmd = ''; + self.displayPrompt(); return true; case ".clear": - sys.puts("Clearing Scope..."); - buffered_cmd = ''; - exports.scope = {}; - displayPrompt(); + self.stream.write("Clearing Scope...\n"); + self.buffered_cmd = ''; + self.scope = {}; + self.displayPrompt(); return true; case ".exit": - stdin.close(); + self.stream.close(); return true; case ".help": - sys.puts(".break\tSometimes you get stuck in a place you can't get out... This will get you out."); - sys.puts(".clear\tBreak, and also clear the local scope."); - sys.puts(".exit\tExit the prompt"); - sys.puts(".help\tShow repl options"); - displayPrompt(); + self.stream.write(".break\tSometimes you get stuck in a place you can't get out... This will get you out.\n"); + self.stream.write(".clear\tBreak, and also clear the local scope.\n"); + self.stream.write(".exit\tExit the prompt\n"); + self.stream.write(".help\tShow repl options\n"); + self.displayPrompt(); return true; } return false; -} +}; /** * Trims Whitespace from a line. - * + * * @param {String} cmd The string to trim the whitespace from - * @returns {String} The trimmed string + * @returns {String} The trimmed string */ -function trimWhitespace (cmd) { - var matches = trimmer.exec(cmd); - if (matches && matches.length == 2) { +REPLServer.prototype.trimWhitespace = function (cmd) { + var trimmer = /^\s*(.+)\s*$/m, + matches = trimmer.exec(cmd); + + if (matches && matches.length === 2) { return matches[1]; } -} +}; /** * Converts commands that use var and function () to use the * local exports.scope when evaled. This provides a local scope * on the REPL. - * + * * @param {String} cmd The cmd to convert * @returns {String} The converted command */ -function convertToScope (cmd) { - var matches; - - // Replaces: var foo = "bar"; with: exports.scope.foo = bar; - matches = scopedVar.exec(cmd); - if (matches && matches.length == 3) { - return "exports.scope." + matches[1] + matches[2]; +REPLServer.prototype.convertToScope = function (cmd) { + var self = this, matches, + scopeVar = /^\s*var\s*([_\w\$]+)(.*)$/m, + scopeFunc = /^\s*function\s*([_\w\$]+)/; + + // Replaces: var foo = "bar"; with: self.scope.foo = bar; + matches = scopeVar.exec(cmd); + if (matches && matches.length === 3) { + return "self.scope." + matches[1] + matches[2]; } - + // Replaces: function foo() {}; with: foo = function foo() {}; - matches = scopeFunc.exec(buffered_cmd); - if (matches && matches.length == 2) { - return matches[1] + " = " + buffered_cmd; + matches = scopeFunc.exec(self.buffered_cmd); + if (matches && matches.length === 2) { + return matches[1] + " = " + self.buffered_cmd; } - + return cmd; -} +}; diff --git a/test/simple/test-repl.js b/test/simple/test-repl.js new file mode 100644 index 0000000000..54511ce3df --- /dev/null +++ b/test/simple/test-repl.js @@ -0,0 +1,138 @@ +require("../common"); +var sys = require("sys"), + net = require("net"), + repl = require("repl"), + message = "Read, Eval, Print Loop", + unix_socket_path = "/tmp/node-repl-sock", + prompt_unix = "node via Unix socket> ", + prompt_tcp = "node via TCP socket> ", + server_tcp, server_unix, client_tcp, client_unix, timer; + +debug('repl test'); + +// function for REPL to run +invoke_me = function (arg) { + return "invoked " + arg; +}; + +function send_expect(list) { + if (list.length > 0) { + var cur = list.shift(); + + cur.client.expect = cur.expect; + cur.client.list = list; + if (cur.send.length > 0) { + cur.client.write(cur.send); + } + } +} + +function tcp_test() { + server_tcp = net.createServer(function (socket) { + assert.strictEqual(server_tcp, socket.server); + assert.strictEqual(server_tcp.type, 'tcp4'); + + socket.addListener("end", function () { + socket.end(); + }); + + repl.start(prompt_tcp, socket); + }); + + server_tcp.addListener('listening', function () { + client_tcp = net.createConnection(PORT); + + client_tcp.addListener('connect', function () { + assert.equal(true, client_tcp.readable); + assert.equal(true, client_tcp.writable); + + send_expect([ + { client: client_tcp, send: "", expect: prompt_tcp }, + { client: client_tcp, send: "invoke_me(333)", expect: ('\'' + "invoked 333" + '\'\n' + prompt_tcp) }, + { client: client_tcp, send: "a += 1", expect: ("12346" + '\n' + prompt_tcp) } + ]); + }); + + client_tcp.addListener('data', function (data) { + var data_str = data.asciiSlice(0, data.length); + sys.puts("TCP data: " + data_str + ", compare to " + client_tcp.expect); + assert.strictEqual(client_tcp.expect, data_str); + if (client_tcp.list && client_tcp.list.length > 0) { + send_expect(client_tcp.list); + } + else { + sys.puts("End of TCP test."); + client_tcp.end(); + client_unix.end(); + clearTimeout(timer); + } + }); + + client_tcp.addListener("error", function (e) { + throw e; + }); + + client_tcp.addListener("close", function () { + server_tcp.close(); + }); + }); + + server_tcp.listen(PORT); +} + +function unix_test() { + server_unix = net.createServer(function (socket) { + assert.strictEqual(server_unix, socket.server); + assert.strictEqual(server_unix.type, 'unix'); + + socket.addListener("end", function () { + socket.end(); + }); + + repl.start(prompt_unix, socket).scope.message = message; + }); + + server_unix.addListener('listening', function () { + client_unix = net.createConnection(unix_socket_path); + + client_unix.addListener('connect', function () { + assert.equal(true, client_unix.readable); + assert.equal(true, client_unix.writable); + + send_expect([ + { client: client_unix, send: "", expect: prompt_unix }, + { client: client_unix, send: "message", expect: ('\'' + message + '\'\n' + prompt_unix) }, + { client: client_unix, send: "invoke_me(987)", expect: ('\'' + "invoked 987" + '\'\n' + prompt_unix) }, + { client: client_unix, send: "a = 12345", expect: ("12345" + '\n' + prompt_unix) } + ]); + }); + + client_unix.addListener('data', function (data) { + var data_str = data.asciiSlice(0, data.length); + sys.puts("Unix data: " + data_str + ", compare to " + client_unix.expect); + assert.strictEqual(client_unix.expect, data_str); + if (client_unix.list && client_unix.list.length > 0) { + send_expect(client_unix.list); + } + else { + sys.puts("End of Unix test, running TCP test."); + tcp_test(); + } + }); + + client_unix.addListener("error", function (e) { + throw e; + }); + + client_unix.addListener("close", function () { + server_unix.close(); + }); + }); + + server_unix.listen(unix_socket_path); +} + +unix_test(); +timer = setTimeout(function () { + assert.fail("Timeout"); +}, 1000);