// A repl library that you can include in your own code to get a runtime // 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 > ").context.foo = "stdin is fun"; // expose foo to repl context var util = require('util'); var Script = process.binding('evals').Script; var evalcx = Script.runInContext; var path = require("path"); var fs = require("fs"); var rl = require('readline'); var context; var disableColors = process.env.NODE_DISABLE_COLORS ? true : false; // hack for require.resolve("./relative") to work properly. module.filename = process.cwd() + "/repl"; function resetContext() { context = Script.createContext(); for (var i in global) context[i] = global[i]; context.module = module; context.require = require; } // Can overridden with custom print functions, such as `probe` or `eyes.js` exports.writer = util.inspect; function REPLServer(prompt, stream) { var self = this; if (!context) resetContext(); if (!exports.repl) exports.repl = this; self.context = context; self.buffered_cmd = ''; self.stream = stream || process.openStdin(); self.prompt = prompt || "> "; var rli = self.rli = rl.createInterface(self.stream, function (text) { return self.complete(text); }); this.commands = {}; defineDefaultCommands(this); if (rli.enabled && !disableColors) { // Turn on ANSI coloring. exports.writer = function(obj, showHidden, depth) { return util.inspect(obj, showHidden, depth, true); }; } rli.setPrompt(self.prompt); rli.on("SIGINT", function () { if (self.buffered_cmd && self.buffered_cmd.length > 0) { rli.write("\n"); self.buffered_cmd = ''; self.displayPrompt(); } else { rli.close(); } }); self.stream.addListener("data", function (chunk) { rli.write(chunk); }); rli.addListener('line', function (cmd) { cmd = trimWhitespace(cmd); // Check to see if a REPL keyword was used. If it returns true, // display next prompt and return. if (cmd && cmd.charAt(0) === '.') { var matches = cmd.match(/^(\.[^\s]+)\s*(.*)$/); var keyword = matches && matches[1]; var rest = matches && matches[2]; if (self.parseREPLKeyword(keyword, rest) === true) return; } // The catchall for errors try { self.buffered_cmd += cmd; // This try is for determining if the command is complete, or should // continue onto the next line. try { // Use evalcx to supply the global context var ret = evalcx(self.buffered_cmd, context, "repl"); if (ret !== undefined) { context._ = ret; self.stream.write(exports.writer(ret) + "\n"); } self.buffered_cmd = ''; } catch (e) { // instanceof doesn't work across context switches. if (!(e && e.constructor && e.constructor.name === "SyntaxError")) { throw e; // It could also be an error from JSON.parse } else if(e && e.stack && e.stack.match('Unexpected token ILLEGAL') && e.stack.match(/Object.parse \(native\)/)) { throw e; } } } catch (e) { // On error: Print the error and clear the buffer if (e.stack) { self.stream.write(e.stack + "\n"); } else { self.stream.write(e.toString() + "\n"); } self.buffered_cmd = ''; } self.displayPrompt(); }); rli.addListener('close', function () { self.stream.destroy(); }); self.displayPrompt(); } 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) { return new REPLServer(prompt, source); }; REPLServer.prototype.displayPrompt = function () { this.rli.setPrompt(this.buffered_cmd.length ? '... ' : this.prompt); this.rli.prompt(); }; // read a line from the stream, then eval it REPLServer.prototype.readline = function (cmd) { }; /** * Provide a list of completions for the given leading text. This is * given to the readline interface for handling tab completion. * * @param {line} The text (preceding the cursor) to complete * @returns {Array} Two elements: (1) an array of completions; and * (2) the leading text completed. * * Example: * complete('var foo = util.') * -> [['util.print', 'util.debug', 'util.log', 'util.inspect', 'util.pump'], * 'util.' ] * * Warning: This eval's code like "foo.bar.baz", so it will run property * getter code. */ REPLServer.prototype.complete = function (line) { var completions, completionGroups = [], // list of completion lists, one for each inheritance "level" completeOn, match, filter, i, j, group, c; // REPL commands (e.g. ".break"). var match = null; match = line.match(/^\s*(\.\w*)$/); if (match) { completionGroups.push(Object.keys(this.commands)); completeOn = match[1]; if (match[1].length > 1) { filter = match[1]; } } // require('...') else if (match = line.match(/\brequire\s*\(['"](([\w\.\/-]+\/)?([\w\.\/-]*))/)) { //TODO: suggest require.exts be exposed to be introspec registered extensions? //TODO: suggest include the '.' in exts in internal repr: parity with `path.extname`. var exts = [".js", ".node"]; var indexRe = new RegExp('^index(' + exts.map(regexpEscape).join('|') + ')$'); completeOn = match[1]; var subdir = match[2] || ""; var filter = match[1]; var dir, files, f, name, base, ext, abs, subfiles, s; group = []; for (i = 0; i < require.paths.length; i++) { dir = require.paths[i]; if (subdir && subdir[0] === '/') { dir = subdir; } else if (subdir) { dir = path.join(dir, subdir); } try { files = fs.readdirSync(dir); } catch (e) { continue; } for (f = 0; f < files.length; f++) { name = files[f]; ext = path.extname(name); base = name.slice(0, -ext.length); if (base.match(/-\d+\.\d+(\.\d+)?/) || name === ".npm") { // Exclude versioned names that 'npm' installs. continue; } if (exts.indexOf(ext) !== -1) { if (!subdir || base !== "index") { group.push(subdir + base); } } else { abs = path.join(dir, name); try { if (fs.statSync(abs).isDirectory()) { group.push(subdir + name + '/'); subfiles = fs.readdirSync(abs); for (s = 0; s < subfiles.length; s++) { if (indexRe.test(subfiles[s])) { group.push(subdir + name); } } } } catch(e) {} } } } if (group.length) { completionGroups.push(group); } if (!subdir) { // Kind of lame that this needs to be updated manually. // Intentionally excluding moved modules: posix, utils. var builtinLibs = ['assert', 'buffer', 'child_process', 'crypto', 'dgram', 'dns', 'events', 'file', 'freelist', 'fs', 'http', 'net', 'path', 'querystring', 'readline', 'repl', 'string_decoder', 'util', 'tcp', 'url']; completionGroups.push(builtinLibs); } } // Handle variable member lookup. // We support simple chained expressions like the following (no function // calls, etc.). That is for simplicity and also because we *eval* that // leading expression so for safety (see WARNING above) don't want to // eval function calls. // // foo.bar<|> # completions for 'foo' with filter 'bar' // spam.eggs.<|> # completions for 'spam.eggs' with filter '' // foo<|> # all scope vars with filter 'foo' // foo.<|> # completions for 'foo' with filter '' else if (line.length === 0 || line[line.length-1].match(/\w|\.|\$/)) { var simpleExpressionPat = /(([a-zA-Z_$](?:\w|\$)*)\.)*([a-zA-Z_$](?:\w|\$)*)\.?$/; match = simpleExpressionPat.exec(line); if (line.length === 0 || match) { var expr; completeOn = (match ? match[0] : ""); if (line.length === 0) { filter = ""; expr = ""; } else if (line[line.length-1] === '.') { filter = ""; expr = match[0].slice(0, match[0].length-1); } else { var bits = match[0].split('.'); filter = bits.pop(); expr = bits.join('.'); } //console.log("expression completion: completeOn='"+completeOn+"' expr='"+expr+"'"); // Resolve expr and get its completions. var obj, memberGroups = []; if (!expr) { completionGroups.push(Object.getOwnPropertyNames(this.context)); // Global object properties // (http://www.ecma-international.org/publications/standards/Ecma-262.htm) completionGroups.push(["NaN", "Infinity", "undefined", "eval", "parseInt", "parseFloat", "isNaN", "isFinite", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "Object", "Function", "Array", "String", "Boolean", "Number", "Date", "RegExp", "Error", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", "Math", "JSON"]); // Common keywords. Exclude for completion on the empty string, b/c // they just get in the way. if (filter) { completionGroups.push(["break", "case", "catch", "const", "continue", "debugger", "default", "delete", "do", "else", "export", "false", "finally", "for", "function", "if", "import", "in", "instanceof", "let", "new", "null", "return", "switch", "this", "throw", "true", "try", "typeof", "undefined", "var", "void", "while", "with", "yield"]); } } else { try { obj = evalcx(expr, this.context, "repl"); } catch (e) { //console.log("completion eval error, expr='"+expr+"': "+e); } if (obj != null) { if (typeof obj === "object" || typeof obj === "function") { memberGroups.push(Object.getOwnPropertyNames(obj)); } // works for non-objects var p = obj.constructor ? obj.constructor.prototype : null; try { var sentinel = 5; while (p !== null) { memberGroups.push(Object.getOwnPropertyNames(p)); p = Object.getPrototypeOf(p); // Circular refs possible? Let's guard against that. sentinel--; if (sentinel <= 0) { break; } } } catch (e) { //console.log("completion error walking prototype chain:" + e); } } if (memberGroups.length) { for (i = 0; i < memberGroups.length; i++) { completionGroups.push(memberGroups[i].map(function(member) { return expr + '.' + member; })); } if (filter) { filter = expr + '.' + filter; } } } } } // Filter, sort (within each group), uniq and merge the completion groups. if (completionGroups.length && filter) { var newCompletionGroups = []; for (i = 0; i < completionGroups.length; i++) { group = completionGroups[i].filter(function(elem) { return elem.indexOf(filter) == 0; }); if (group.length) { newCompletionGroups.push(group); } } completionGroups = newCompletionGroups; } if (completionGroups.length) { var uniq = {}; // unique completions across all groups completions = []; // Completion group 0 is the "closest" (least far up the inheritance chain) // so we put its completions last: to be closest in the REPL. for (i = completionGroups.length - 1; i >= 0; i--) { group = completionGroups[i]; group.sort(); for (var j = 0; j < group.length; j++) { c = group[j]; if (!uniq.hasOwnProperty(c)) { completions.push(c); uniq[c] = true; } } completions.push(""); // separator btwn groups } while (completions.length && completions[completions.length-1] === "") { completions.pop(); } } return [completions || [], completeOn]; }; /** * 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 */ REPLServer.prototype.parseREPLKeyword = function (keyword, rest) { var cmd = this.commands[keyword]; if (cmd) { cmd.action.call(this, rest); return true; } return false; }; REPLServer.prototype.defineCommand = function(keyword, cmd) { if (typeof cmd === 'function') cmd = {action: cmd}; else if (typeof cmd.action !== 'function') { throw new Error('bad argument, action must be a function'); } this.commands['.' + keyword] = cmd; }; function defineDefaultCommands(repl) { // TODO remove me after 0.3.x repl.defineCommand('break', { help: 'Sometimes you get stuck, this gets you out', action: function() { this.buffered_cmd = ''; this.displayPrompt(); } }); repl.defineCommand('clear', { help: 'Break, and also clear the local context', action: function() { this.stream.write("Clearing context...\n"); this.buffered_cmd = ''; resetContext(); this.displayPrompt(); } }); repl.defineCommand('exit', { help: 'Exit the repl', action: function() { this.rli.close(); } }); repl.defineCommand('help', { help: 'Show repl options', action: function() { var self = this; Object.keys(this.commands).sort().forEach(function(name) { var cmd = self.commands[name]; self.stream.write(name + "\t" + (cmd.help || '') + "\n"); }); this.displayPrompt(); } }); } function trimWhitespace (cmd) { var trimmer = /^\s*(.+)\s*$/m, matches = trimmer.exec(cmd); if (matches && matches.length === 2) { return matches[1]; } } function regexpEscape(s) { return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); } /** * Converts commands that use var and function () to use the * local exports.context when evaled. This provides a local context * on the REPL. * * @param {String} cmd The cmd to convert * @returns {String} The converted command */ REPLServer.prototype.convertToContext = function (cmd) { var self = this, matches, scopeVar = /^\s*var\s*([_\w\$]+)(.*)$/m, scopeFunc = /^\s*function\s*([_\w\$]+)/; // Replaces: var foo = "bar"; with: self.context.foo = bar; matches = scopeVar.exec(cmd); if (matches && matches.length === 3) { return "self.context." + matches[1] + matches[2]; } // Replaces: function foo() {}; with: foo = function foo() {}; matches = scopeFunc.exec(self.buffered_cmd); if (matches && matches.length === 2) { return matches[1] + " = " + self.buffered_cmd; } return cmd; };