mirror of https://github.com/lukechilds/node.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
912 lines
28 KiB
912 lines
28 KiB
// Copyright Joyent, Inc. and other Node contributors.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
// persons to whom the Software is furnished to do so, subject to the
|
|
// following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included
|
|
// in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
/* A repl library that you can include in your own code to get a runtime
|
|
* interface to your program.
|
|
*
|
|
* var repl = require("/repl.js");
|
|
* // start repl on stdin
|
|
* repl.start("prompt> ");
|
|
*
|
|
* // listen for unix socket connections and start repl on them
|
|
* net.createServer(function(socket) {
|
|
* repl.start("node via Unix socket> ", socket);
|
|
* }).listen("/tmp/node-repl-sock");
|
|
*
|
|
* // listen for TCP socket connections and start repl on them
|
|
* net.createServer(function(socket) {
|
|
* repl.start("node via TCP socket> ", socket);
|
|
* }).listen(5001);
|
|
*
|
|
* // expose foo to repl context
|
|
* repl.start("node > ").context.foo = "stdin is fun";
|
|
*/
|
|
|
|
var util = require('util');
|
|
var inherits = require('util').inherits;
|
|
var Stream = require('stream');
|
|
var vm = require('vm');
|
|
var path = require('path');
|
|
var fs = require('fs');
|
|
var rl = require('readline');
|
|
var Console = require('console').Console;
|
|
var EventEmitter = require('events').EventEmitter;
|
|
|
|
// If obj.hasOwnProperty has been overridden, then calling
|
|
// obj.hasOwnProperty(prop) will break.
|
|
// See: https://github.com/joyent/node/issues/1707
|
|
function hasOwnProperty(obj, prop) {
|
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
}
|
|
|
|
|
|
// hack for require.resolve("./relative") to work properly.
|
|
module.filename = path.resolve('repl');
|
|
|
|
// hack for repl require to work properly with node_modules folders
|
|
module.paths = require('module')._nodeModulePaths(module.filename);
|
|
|
|
// Can overridden with custom print functions, such as `probe` or `eyes.js`.
|
|
// This is the default "writer" value if none is passed in the REPL options.
|
|
exports.writer = util.inspect;
|
|
|
|
exports._builtinLibs = ['assert', 'buffer', 'child_process', 'cluster',
|
|
'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https', 'net',
|
|
'os', 'path', 'punycode', 'querystring', 'readline', 'stream',
|
|
'string_decoder', 'tls', 'tty', 'url', 'util', 'vm', 'zlib'];
|
|
|
|
|
|
function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) {
|
|
if (!(this instanceof REPLServer)) {
|
|
return new REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined);
|
|
}
|
|
|
|
EventEmitter.call(this);
|
|
|
|
var options, input, output;
|
|
if (typeof prompt == 'object') {
|
|
// an options object was given
|
|
options = prompt;
|
|
stream = options.stream || options.socket;
|
|
input = options.input;
|
|
output = options.output;
|
|
eval_ = options.eval;
|
|
useGlobal = options.useGlobal;
|
|
ignoreUndefined = options.ignoreUndefined;
|
|
prompt = options.prompt;
|
|
} else if (typeof prompt != 'string') {
|
|
throw new Error('An options Object, or a prompt String are required');
|
|
} else {
|
|
options = {};
|
|
}
|
|
|
|
var self = this;
|
|
|
|
self.useGlobal = !!useGlobal;
|
|
self.ignoreUndefined = !!ignoreUndefined;
|
|
|
|
self.eval = eval_ || function(code, context, file, cb) {
|
|
var err, result;
|
|
try {
|
|
if (self.useGlobal) {
|
|
result = vm.runInThisContext(code, file);
|
|
} else {
|
|
result = vm.runInContext(code, context, file);
|
|
}
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
cb(err, result);
|
|
};
|
|
|
|
if (!input && !output) {
|
|
// legacy API, passing a 'stream'/'socket' option
|
|
if (!stream) {
|
|
// use stdin and stdout as the default streams if none were given
|
|
stream = process;
|
|
}
|
|
if (stream.stdin && stream.stdout) {
|
|
// We're given custom object with 2 streams, or the `process` object
|
|
input = stream.stdin;
|
|
output = stream.stdout;
|
|
} else {
|
|
// We're given a duplex readable/writable Stream, like a `net.Socket`
|
|
input = stream;
|
|
output = stream;
|
|
}
|
|
}
|
|
|
|
self.inputStream = input;
|
|
self.outputStream = output;
|
|
|
|
self.resetContext();
|
|
self.bufferedCommand = '';
|
|
|
|
self.prompt = (prompt != undefined ? prompt : '> ');
|
|
|
|
function complete(text, callback) {
|
|
self.complete(text, callback);
|
|
}
|
|
|
|
var rli = rl.createInterface({
|
|
input: self.inputStream,
|
|
output: self.outputStream,
|
|
completer: complete,
|
|
terminal: options.terminal
|
|
});
|
|
self.rli = rli;
|
|
|
|
this.commands = {};
|
|
defineDefaultCommands(this);
|
|
|
|
// figure out which "writer" function to use
|
|
self.writer = options.writer || exports.writer;
|
|
|
|
if (typeof options.useColors === 'undefined') {
|
|
options.useColors = rli.terminal;
|
|
}
|
|
self.useColors = !!options.useColors;
|
|
|
|
if (self.useColors && self.writer === util.inspect) {
|
|
// Turn on ANSI coloring.
|
|
self.writer = function(obj, showHidden, depth) {
|
|
return util.inspect(obj, showHidden, depth, true);
|
|
};
|
|
}
|
|
|
|
rli.setPrompt(self.prompt);
|
|
|
|
rli.on('close', function() {
|
|
self.emit('exit');
|
|
});
|
|
|
|
var sawSIGINT = false;
|
|
rli.on('SIGINT', function() {
|
|
var empty = rli.line.length === 0;
|
|
rli.clearLine();
|
|
|
|
if (!(self.bufferedCommand && self.bufferedCommand.length > 0) && empty) {
|
|
if (sawSIGINT) {
|
|
rli.close();
|
|
sawSIGINT = false;
|
|
return;
|
|
}
|
|
rli.output.write('(^C again to quit)\n');
|
|
sawSIGINT = true;
|
|
} else {
|
|
sawSIGINT = false;
|
|
}
|
|
|
|
self.bufferedCommand = '';
|
|
self.displayPrompt();
|
|
});
|
|
|
|
rli.on('line', function(cmd) {
|
|
sawSIGINT = false;
|
|
var skipCatchall = false;
|
|
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) === '.' && isNaN(parseFloat(cmd))) {
|
|
var matches = cmd.match(/^(\.[^\s]+)\s*(.*)$/);
|
|
var keyword = matches && matches[1];
|
|
var rest = matches && matches[2];
|
|
if (self.parseREPLKeyword(keyword, rest) === true) {
|
|
return;
|
|
} else {
|
|
self.outputStream.write('Invalid REPL keyword\n');
|
|
skipCatchall = true;
|
|
}
|
|
}
|
|
|
|
// Check if a builtin module name was used and then include it
|
|
// if there's no conflict.
|
|
if (!(cmd in self.context) && exports._builtinLibs.indexOf(cmd) !== -1) {
|
|
var lib = require(cmd);
|
|
self.context._ = self.context[cmd] = lib;
|
|
self.outputStream.write(self.writer(lib) + '\n');
|
|
self.displayPrompt();
|
|
return;
|
|
}
|
|
|
|
if (!skipCatchall) {
|
|
var evalCmd = self.bufferedCommand + cmd + '\n';
|
|
|
|
// This try is for determining if the command is complete, or should
|
|
// continue onto the next line.
|
|
// We try to evaluate both expressions e.g.
|
|
// '{ a : 1 }'
|
|
// and statements e.g.
|
|
// 'for (var i = 0; i < 10; i++) console.log(i);'
|
|
|
|
// First we attempt to eval as expression with parens.
|
|
// This catches '{a : 1}' properly.
|
|
self.eval('(' + evalCmd + ')',
|
|
self.context,
|
|
'repl',
|
|
function(e, ret) {
|
|
if (e && !isSyntaxError(e)) return finish(e);
|
|
|
|
if (typeof ret === 'function' &&
|
|
/^[\r\n\s]*function/.test(evalCmd) ||
|
|
e) {
|
|
// Now as statement without parens.
|
|
self.eval(evalCmd, self.context, 'repl', finish);
|
|
} else {
|
|
finish(null, ret);
|
|
}
|
|
});
|
|
|
|
} else {
|
|
finish(null);
|
|
}
|
|
|
|
function finish(e, ret) {
|
|
|
|
self.memory(cmd);
|
|
|
|
// If error was SyntaxError and not JSON.parse error
|
|
if (isSyntaxError(e)) {
|
|
if (!self.bufferedCommand && cmd.trim().match(/^npm /)) {
|
|
self.outputStream.write('npm should be run outside of the ' +
|
|
'node repl, in your normal shell.\n' +
|
|
'(Press Control-D to exit.)\n');
|
|
self.bufferedCommand = '';
|
|
self.displayPrompt();
|
|
return;
|
|
}
|
|
|
|
// Start buffering data like that:
|
|
// {
|
|
// ... x: 1
|
|
// ... }
|
|
self.bufferedCommand += cmd + '\n';
|
|
self.displayPrompt();
|
|
return;
|
|
} else if (e) {
|
|
self.outputStream.write((e.stack || e) + '\n');
|
|
}
|
|
|
|
// Clear buffer if no SyntaxErrors
|
|
self.bufferedCommand = '';
|
|
|
|
// If we got any output - print it (if no error)
|
|
if (!e && (!self.ignoreUndefined || ret !== undefined)) {
|
|
self.context._ = ret;
|
|
self.outputStream.write(self.writer(ret) + '\n');
|
|
}
|
|
|
|
// Display prompt again
|
|
self.displayPrompt();
|
|
};
|
|
});
|
|
|
|
rli.on('SIGCONT', function() {
|
|
self.displayPrompt(true);
|
|
});
|
|
|
|
self.displayPrompt();
|
|
}
|
|
inherits(REPLServer, EventEmitter);
|
|
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);
|
|
if (!exports.repl) exports.repl = repl;
|
|
return repl;
|
|
};
|
|
|
|
|
|
REPLServer.prototype.createContext = function() {
|
|
var context;
|
|
if (this.useGlobal) {
|
|
context = global;
|
|
} else {
|
|
context = vm.createContext();
|
|
for (var i in global) context[i] = global[i];
|
|
context.console = new Console(this.outputStream);
|
|
context.global = context;
|
|
context.global.global = context;
|
|
}
|
|
|
|
context.module = module;
|
|
context.require = require;
|
|
|
|
this.lines = [];
|
|
this.lines.level = [];
|
|
|
|
return context;
|
|
};
|
|
|
|
REPLServer.prototype.resetContext = function() {
|
|
this.context = this.createContext();
|
|
};
|
|
|
|
REPLServer.prototype.displayPrompt = function(preserveCursor) {
|
|
var prompt = this.prompt;
|
|
if (this.bufferedCommand.length) {
|
|
prompt = '...';
|
|
var levelInd = new Array(this.lines.level.length).join('..');
|
|
prompt += levelInd + ' ';
|
|
}
|
|
this.rli.setPrompt(prompt);
|
|
this.rli.prompt(preserveCursor);
|
|
};
|
|
|
|
|
|
// A stream to push an array into a REPL
|
|
// used in REPLServer.complete
|
|
function ArrayStream() {
|
|
Stream.call(this);
|
|
|
|
this.run = function(data) {
|
|
var self = this;
|
|
data.forEach(function(line) {
|
|
self.emit('data', line + '\n');
|
|
});
|
|
}
|
|
}
|
|
util.inherits(ArrayStream, Stream);
|
|
ArrayStream.prototype.readable = true;
|
|
ArrayStream.prototype.writable = true;
|
|
ArrayStream.prototype.resume = function() {};
|
|
ArrayStream.prototype.write = function() {};
|
|
|
|
var requireRE = /\brequire\s*\(['"](([\w\.\/-]+\/)?([\w\.\/-]*))/;
|
|
var simpleExpressionRE =
|
|
/(([a-zA-Z_$](?:\w|\$)*)\.)*([a-zA-Z_$](?:\w|\$)*)\.?$/;
|
|
|
|
|
|
// Provide a list of completions for the given leading text. This is
|
|
// given to the readline interface for handling tab completion.
|
|
//
|
|
// 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, callback) {
|
|
// There may be local variables to evaluate, try a nested REPL
|
|
if (this.bufferedCommand != undefined && this.bufferedCommand.length) {
|
|
// Get a new array of inputed lines
|
|
var tmp = this.lines.slice();
|
|
// Kill off all function declarations to push all local variables into
|
|
// global scope
|
|
this.lines.level.forEach(function(kill) {
|
|
if (kill.isFunction) {
|
|
tmp[kill.line] = '';
|
|
}
|
|
});
|
|
var flat = new ArrayStream(); // make a new "input" stream
|
|
var magic = new REPLServer('', flat); // make a nested REPL
|
|
magic.context = magic.createContext();
|
|
flat.run(tmp); // eval the flattened code
|
|
// all this is only profitable if the nested REPL
|
|
// does not have a bufferedCommand
|
|
if (!magic.bufferedCommand) {
|
|
return magic.complete(line, callback);
|
|
}
|
|
}
|
|
|
|
var completions;
|
|
|
|
// list of completion lists, one for each inheritance "level"
|
|
var completionGroups = [];
|
|
|
|
var 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];
|
|
}
|
|
|
|
completionGroupsLoaded();
|
|
} else if (match = line.match(requireRE)) {
|
|
// require('...<Tab>')
|
|
var exts = Object.keys(require.extensions);
|
|
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 = [];
|
|
var paths = module.paths.concat(require('module').globalPaths);
|
|
for (i = 0; i < paths.length; i++) {
|
|
dir = path.resolve(paths[i], 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.resolve(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) {
|
|
completionGroups.push(exports._builtinLibs);
|
|
}
|
|
|
|
completionGroupsLoaded();
|
|
|
|
// 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|\.|\$/)) {
|
|
match = simpleExpressionRE.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('.');
|
|
}
|
|
|
|
// Resolve expr and get its completions.
|
|
var obj, memberGroups = [];
|
|
if (!expr) {
|
|
// If context is instance of vm.ScriptContext
|
|
// Get global vars synchronously
|
|
if (this.useGlobal ||
|
|
this.context.constructor &&
|
|
this.context.constructor.name === 'Context') {
|
|
var contextProto = this.context;
|
|
while (contextProto = Object.getPrototypeOf(contextProto)) {
|
|
completionGroups.push(Object.getOwnPropertyNames(contextProto));
|
|
}
|
|
completionGroups.push(Object.getOwnPropertyNames(this.context));
|
|
addStandardGlobals(completionGroups, filter);
|
|
completionGroupsLoaded();
|
|
} else {
|
|
this.eval('.scope', this.context, 'repl', function(err, globals) {
|
|
if (err || !globals) {
|
|
addStandardGlobals(completionGroups, filter);
|
|
} else if (Array.isArray(globals[0])) {
|
|
// Add grouped globals
|
|
globals.forEach(function(group) {
|
|
completionGroups.push(group);
|
|
});
|
|
} else {
|
|
completionGroups.push(globals);
|
|
addStandardGlobals(completionGroups, filter);
|
|
}
|
|
completionGroupsLoaded();
|
|
});
|
|
}
|
|
} else {
|
|
this.eval(expr, this.context, 'repl', function(e, obj) {
|
|
// if (e) console.log(e);
|
|
|
|
if (obj != null) {
|
|
if (typeof obj === 'object' || typeof obj === 'function') {
|
|
memberGroups.push(Object.getOwnPropertyNames(obj));
|
|
}
|
|
// works for non-objects
|
|
try {
|
|
var sentinel = 5;
|
|
var p;
|
|
if (typeof obj === 'object' || typeof obj === 'function') {
|
|
p = Object.getPrototypeOf(obj);
|
|
} else {
|
|
p = obj.constructor ? obj.constructor.prototype : null;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
completionGroupsLoaded();
|
|
});
|
|
}
|
|
} else {
|
|
completionGroupsLoaded();
|
|
}
|
|
} else {
|
|
completionGroupsLoaded();
|
|
}
|
|
|
|
// Will be called when all completionGroups are in place
|
|
// Useful for async autocompletion
|
|
function completionGroupsLoaded(err) {
|
|
if (err) throw err;
|
|
|
|
// 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 (!hasOwnProperty(c)) {
|
|
completions.push(c);
|
|
uniq[c] = true;
|
|
}
|
|
}
|
|
completions.push(''); // separator btwn groups
|
|
}
|
|
while (completions.length && completions[completions.length - 1] === '') {
|
|
completions.pop();
|
|
}
|
|
}
|
|
|
|
callback(null, [completions || [], completeOn]);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Used to parse and execute the Node REPL commands.
|
|
*
|
|
* @param {keyword} keyword The command entered to check.
|
|
* @return {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;
|
|
};
|
|
|
|
REPLServer.prototype.memory = function memory(cmd) {
|
|
var self = this;
|
|
|
|
self.lines = self.lines || [];
|
|
self.lines.level = self.lines.level || [];
|
|
|
|
// save the line so I can do magic later
|
|
if (cmd) {
|
|
// TODO should I tab the level?
|
|
self.lines.push(new Array(self.lines.level.length).join(' ') + cmd);
|
|
} else {
|
|
// I don't want to not change the format too much...
|
|
self.lines.push('');
|
|
}
|
|
|
|
// I need to know "depth."
|
|
// Because I can not tell the difference between a } that
|
|
// closes an object literal and a } that closes a function
|
|
if (cmd) {
|
|
// going down is { and ( e.g. function() {
|
|
// going up is } and )
|
|
var dw = cmd.match(/{|\(/g);
|
|
var up = cmd.match(/}|\)/g);
|
|
up = up ? up.length : 0;
|
|
dw = dw ? dw.length : 0;
|
|
var depth = dw - up;
|
|
|
|
if (depth) {
|
|
(function workIt() {
|
|
if (depth > 0) {
|
|
// going... down.
|
|
// push the line#, depth count, and if the line is a function.
|
|
// Since JS only has functional scope I only need to remove
|
|
// "function() {" lines, clearly this will not work for
|
|
// "function()
|
|
// {" but nothing should break, only tab completion for local
|
|
// scope will not work for this function.
|
|
self.lines.level.push({
|
|
line: self.lines.length - 1,
|
|
depth: depth,
|
|
isFunction: /\s*function\s*/.test(cmd)
|
|
});
|
|
} else if (depth < 0) {
|
|
// going... up.
|
|
var curr = self.lines.level.pop();
|
|
if (curr) {
|
|
var tmp = curr.depth + depth;
|
|
if (tmp < 0) {
|
|
//more to go, recurse
|
|
depth += curr.depth;
|
|
workIt();
|
|
} else if (tmp > 0) {
|
|
//remove and push back
|
|
curr.depth += depth;
|
|
self.lines.level.push(curr);
|
|
}
|
|
}
|
|
}
|
|
}());
|
|
}
|
|
|
|
// it is possible to determine a syntax error at this point.
|
|
// if the REPL still has a bufferedCommand and
|
|
// self.lines.level.length === 0
|
|
// TODO? keep a log of level so that any syntax breaking lines can
|
|
// be cleared on .break and in the case of a syntax error?
|
|
// TODO? if a log was kept, then I could clear the bufferedComand and
|
|
// eval these lines and throw the syntax error
|
|
} else {
|
|
self.lines.level = [];
|
|
}
|
|
};
|
|
|
|
function addStandardGlobals(completionGroups, filter) {
|
|
// 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']);
|
|
}
|
|
}
|
|
|
|
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.bufferedCommand = '';
|
|
this.displayPrompt();
|
|
}
|
|
});
|
|
|
|
var clearMessage;
|
|
if (repl.useGlobal) {
|
|
clearMessage = 'Alias for .break';
|
|
} else {
|
|
clearMessage = 'Break, and also clear the local context';
|
|
}
|
|
repl.defineCommand('clear', {
|
|
help: clearMessage,
|
|
action: function() {
|
|
this.bufferedCommand = '';
|
|
if (!this.useGlobal) {
|
|
this.outputStream.write('Clearing context...\n');
|
|
this.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.outputStream.write(name + '\t' + (cmd.help || '') + '\n');
|
|
});
|
|
this.displayPrompt();
|
|
}
|
|
});
|
|
|
|
repl.defineCommand('save', {
|
|
help: 'Save all evaluated commands in this REPL session to a file',
|
|
action: function(file) {
|
|
try {
|
|
fs.writeFileSync(file, this.lines.join('\n') + '\n');
|
|
this.outputStream.write('Session saved to:' + file + '\n');
|
|
} catch (e) {
|
|
this.outputStream.write('Failed to save:' + file + '\n');
|
|
}
|
|
this.displayPrompt();
|
|
}
|
|
});
|
|
|
|
repl.defineCommand('load', {
|
|
help: 'Load JS from a file into the REPL session',
|
|
action: function(file) {
|
|
try {
|
|
var stats = fs.statSync(file);
|
|
if (stats && stats.isFile()) {
|
|
var self = this;
|
|
var data = fs.readFileSync(file, 'utf8');
|
|
var lines = data.split('\n');
|
|
this.displayPrompt();
|
|
lines.forEach(function(line) {
|
|
if (line) {
|
|
self.rli.write(line + '\n');
|
|
}
|
|
});
|
|
}
|
|
} catch (e) {
|
|
this.outputStream.write('Failed to load:' + file + '\n');
|
|
}
|
|
this.displayPrompt();
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function trimWhitespace(cmd) {
|
|
var trimmer = /^\s*(.+)\s*$/m,
|
|
matches = trimmer.exec(cmd);
|
|
|
|
if (matches && matches.length === 2) {
|
|
return matches[1];
|
|
}
|
|
return '';
|
|
}
|
|
|
|
|
|
function regexpEscape(s) {
|
|
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
}
|
|
|
|
|
|
/**
|
|
* Converts commands that use var and function <name>() to use the
|
|
* local exports.context when evaled. This provides a local context
|
|
* on the REPL.
|
|
*
|
|
* @param {String} cmd The cmd to convert.
|
|
* @return {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.bufferedCommand);
|
|
if (matches && matches.length === 2) {
|
|
return matches[1] + ' = ' + self.bufferedCommand;
|
|
}
|
|
|
|
return cmd;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns `true` if "e" is a SyntaxError, `false` otherwise.
|
|
* This function filters out false positives likes JSON.parse() errors and
|
|
* RegExp syntax errors.
|
|
*/
|
|
function isSyntaxError(e) {
|
|
// Convert error to string
|
|
e = e && (e.stack || e.toString());
|
|
return e && e.match(/^SyntaxError/) &&
|
|
// RegExp syntax error
|
|
!e.match(/^SyntaxError: Invalid regular expression/) &&
|
|
!e.match(/^SyntaxError: Invalid flags supplied to RegExp constructor/) &&
|
|
// JSON.parse() error
|
|
!(e.match(/^SyntaxError: Unexpected (token .*|end of input)/) &&
|
|
e.match(/\n at Object.parse \(native\)\n/));
|
|
}
|
|
|