diff --git a/lib/repl.js b/lib/repl.js index 206fde66078f2c..00021f7e3850be 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -1,7 +1,7 @@ /* A repl library that you can include in your own code to get a runtime * interface to your program. * - * var repl = require("repl"); + * const repl = require("repl"); * // start repl on stdin * repl.start("prompt> "); * @@ -96,7 +96,7 @@ class LineParser { } parseLine(line) { - var previous = null; + let previous = null; this.shouldFail = false; const wasWithinStrLiteral = this._literal !== null; @@ -189,7 +189,7 @@ function REPLServer(prompt, replMode); } - var options, input, output, dom, breakEvalOnSigint; + let options, input, output, dom, breakEvalOnSigint; if (prompt !== null && typeof prompt === 'object') { // an options object was given options = prompt; @@ -213,21 +213,19 @@ function REPLServer(prompt, throw new Error('Cannot specify both breakEvalOnSigint and eval for REPL'); } - var self = this; + this._domain = dom || domain.create(); - self._domain = dom || domain.create(); - - self.useGlobal = !!useGlobal; - self.ignoreUndefined = !!ignoreUndefined; - self.replMode = replMode || exports.REPL_MODE_SLOPPY; - self.underscoreAssigned = false; - self.last = undefined; - self.breakEvalOnSigint = !!breakEvalOnSigint; + this.useGlobal = !!useGlobal; + this.ignoreUndefined = !!ignoreUndefined; + this.replMode = replMode || exports.REPL_MODE_SLOPPY; + this.underscoreAssigned = false; + this.last = undefined; + this.breakEvalOnSigint = !!breakEvalOnSigint; - self._inTemplateLiteral = false; + this._inTemplateLiteral = false; // just for backwards compat, see github.com/joyent/node/pull/7127 - self.rli = this; + this.rli = this; const savedRegExMatches = ['', '', '', '', '', '', '', '', '', '']; const sep = '\u0000\u0000\u0000'; @@ -238,7 +236,8 @@ function REPLServer(prompt, eval_ = eval_ || defaultEval; function defaultEval(code, context, file, cb) { - var err, result, retry = false, input = code, wrappedErr; + let err, result, retry = false, wrappedErr, script; + const input = code; // first, create the Script object to check the syntax if (code === '\n') @@ -247,22 +246,22 @@ function REPLServer(prompt, while (true) { try { if (!/^\s*$/.test(code) && - (self.replMode === exports.REPL_MODE_STRICT || retry)) { + (this.replMode === exports.REPL_MODE_STRICT || retry)) { // "void 0" keeps the repl from returning "use strict" as the // result value for let/const statements. code = `'use strict'; void 0;\n${code}`; } - var script = vm.createScript(code, { + script = vm.createScript(code, { filename: file, displayErrors: false }); } catch (e) { debug('parse error %j', code, e); - if (self.replMode === exports.REPL_MODE_MAGIC && + if (this.replMode === exports.REPL_MODE_MAGIC && e.message === BLOCK_SCOPED_ERROR && - !retry || self.wrappedCmd) { - if (self.wrappedCmd) { - self.wrappedCmd = false; + !retry || this.wrappedCmd) { + if (this.wrappedCmd) { + this.wrappedCmd = false; // unwrap and try again code = `${input.substring(1, input.length - 2)}\n`; wrappedErr = e; @@ -273,7 +272,7 @@ function REPLServer(prompt, } // preserve original error for wrapped command const error = wrappedErr || e; - if (isRecoverableError(error, self)) + if (isRecoverableError(error, this)) err = new Recoverable(error); else err = error; @@ -288,34 +287,34 @@ function REPLServer(prompt, if (!err) { // Unset raw mode during evaluation so that Ctrl+C raises a signal. let previouslyInRawMode; - if (self.breakEvalOnSigint) { + if (this.breakEvalOnSigint) { // Start the SIGINT watchdog before entering raw mode so that a very // quick Ctrl+C doesn’t lead to aborting the process completely. utilBinding.startSigintWatchdog(); - previouslyInRawMode = self._setRawMode(false); + previouslyInRawMode = this._setRawMode(false); } try { try { const scriptOptions = { displayErrors: false, - breakOnSigint: self.breakEvalOnSigint + breakOnSigint: this.breakEvalOnSigint }; - if (self.useGlobal) { + if (this.useGlobal) { result = script.runInThisContext(scriptOptions); } else { result = script.runInContext(context, scriptOptions); } } finally { - if (self.breakEvalOnSigint) { + if (this.breakEvalOnSigint) { // Reset terminal mode to its previous value. - self._setRawMode(previouslyInRawMode); + this._setRawMode(previouslyInRawMode); // Returns true if there were pending SIGINTs *after* the script // has terminated without being interrupted itself. if (utilBinding.stopSigintWatchdog()) { - self.emit('SIGINT'); + this.emit('SIGINT'); } } } @@ -344,13 +343,13 @@ function REPLServer(prompt, cb(err, result); } - self.eval = self._domain.bind(eval_); + this.eval = this._domain.bind(eval_); - self._domain.on('error', function(e) { + this._domain.on('error', (e) => { debug('domain error'); - const top = replMap.get(self); + const top = replMap.get(this); internalUtil.decorateErrorStack(e); - if (e.stack && self.replMode === exports.REPL_MODE_STRICT) { + if (e.stack && this.replMode === exports.REPL_MODE_STRICT) { e.stack = e.stack.replace(/(\s+at\s+repl:)(\d+)/, (_, pre, line) => pre + (line - 1)); } @@ -378,22 +377,18 @@ function REPLServer(prompt, } } - self.inputStream = input; - self.outputStream = output; - - self.resetContext(); - self.lineParser = new LineParser(); - self.bufferedCommand = ''; - self.lines.level = []; + this.inputStream = input; + this.outputStream = output; - function complete(text, callback) { - self.complete(text, callback); - } + this.resetContext(); + this.lineParser = new LineParser(); + this.bufferedCommand = ''; + this.lines.level = []; Interface.call(this, { - input: self.inputStream, - output: self.outputStream, - completer: complete, + input: this.inputStream, + output: this.outputStream, + completer: this.complete, terminal: options.terminal, historySize: options.historySize, prompt @@ -403,81 +398,132 @@ function REPLServer(prompt, defineDefaultCommands(this); // figure out which "writer" function to use - self.writer = options.writer || exports.writer; + this.writer = options.writer || exports.writer; if (options.useColors === undefined) { - options.useColors = self.terminal; + options.useColors = this.terminal; } - self.useColors = !!options.useColors; + this.useColors = !!options.useColors; - if (self.useColors && self.writer === util.inspect) { + if (this.useColors && this.writer === util.inspect) { // Turn on ANSI coloring. - self.writer = function(obj, showHidden, depth) { + this.writer = function(obj, showHidden, depth) { return util.inspect(obj, showHidden, depth, true); }; } - self.on('close', function() { - self.emit('exit'); - }); + this.on('close', () => this.emit('exit')); - var sawSIGINT = false; - self.on('SIGINT', function() { - var empty = self.line.length === 0; - self.clearLine(); + let sawSIGINT = false; + this.on('SIGINT', () => { + const empty = this.line.length === 0; + this.clearLine(); - if (!(self.bufferedCommand && self.bufferedCommand.length > 0) && empty) { + if (!(this.bufferedCommand && this.bufferedCommand.length > 0) && empty) { if (sawSIGINT) { - self.close(); + this.close(); sawSIGINT = false; return; } - self.output.write('(To exit, press ^C again or type .exit)\n'); + this.output.write('(To exit, press ^C again or type .exit)\n'); sawSIGINT = true; } else { sawSIGINT = false; } - self.lineParser.reset(); - self.bufferedCommand = ''; - self.lines.level = []; - self.displayPrompt(); + this.lineParser.reset(); + this.bufferedCommand = ''; + this.lines.level = []; + this.displayPrompt(); }); - self.on('line', function(cmd) { + this.on('line', (cmd) => { debug('line %j', cmd); sawSIGINT = false; + const finish = (function(_repl) { + return function finish(e, ret) { + debug('finish', e, ret); + _repl.memory(cmd); + + _repl.wrappedCmd = false; + if (e && !_repl.bufferedCommand && cmd.trim().startsWith('npm ')) { + _repl.outputStream.write('npm should be run outside of the ' + + 'node repl, in your normal shell.\n' + + '(Press Control-D to exit.)\n'); + _repl.lineParser.reset(); + _repl.bufferedCommand = ''; + _repl.displayPrompt(); + return; + } + + // If error was SyntaxError and not JSON.parse error + if (e) { + if (e instanceof Recoverable && !_repl.lineParser.shouldFail) { + // Start buffering data like that: + // { + // ... x: 1 + // ... } + _repl.bufferedCommand += cmd + '\n'; + _repl.displayPrompt(); + return; + } else { + _repl._domain.emit('error', e.err || e); + } + } + + // Clear buffer if no SyntaxErrors + _repl.lineParser.reset(); + _repl.bufferedCommand = ''; + + // If we got any output - print it (if no error) + if (!e && + // When an invalid REPL command is used, error message is printed + // immediately. We don't have to print anything else. So, only when + // the second argument to this function is there, print it. + arguments.length === 2 && + (!_repl.ignoreUndefined || ret !== undefined)) { + if (!_repl.underscoreAssigned) { + _repl.last = ret; + } + _repl.outputStream.write(_repl.writer(ret) + '\n'); + } + + // Display prompt again + _repl.displayPrompt(); + }; + }(this)); + // leading whitespaces in template literals should not be trimmed. - if (self._inTemplateLiteral) { - self._inTemplateLiteral = false; + if (this._inTemplateLiteral) { + this._inTemplateLiteral = false; } else { - cmd = self.lineParser.parseLine(cmd); + cmd = this.lineParser.parseLine(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) { + const matches = cmd.match(/^\.([^\s]+)\s*(.*)$/); + const keyword = matches && matches[1]; + const rest = matches && matches[2]; + if (this.parseREPLKeyword(keyword, rest) === true) { return; - } else if (!self.bufferedCommand) { - self.outputStream.write('Invalid REPL keyword\n'); + } else if (!this.bufferedCommand) { + this.outputStream.write('Invalid REPL keyword\n'); finish(null); return; } } - var evalCmd = self.bufferedCommand + cmd; + let evalCmd = this.bufferedCommand + cmd; if (/^\s*\{/.test(evalCmd) && /\}\s*$/.test(evalCmd)) { // It's confusing for `{ a : 1 }` to be interpreted as a block // statement rather than an object literal. So, we first try // to wrap it in parentheses, so that it will be interpreted as // an expression. evalCmd = '(' + evalCmd + ')\n'; - self.wrappedCmd = true; + this.wrappedCmd = true; } else { // otherwise we just append a \n so that it will be either // terminated, or continued onto the next expression if it's an @@ -486,65 +532,12 @@ function REPLServer(prompt, } debug('eval %j', evalCmd); - self.eval(evalCmd, self.context, 'repl', finish); - - function finish(e, ret) { - debug('finish', e, ret); - self.memory(cmd); - - self.wrappedCmd = false; - if (e && !self.bufferedCommand && cmd.trim().startsWith('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.lineParser.reset(); - self.bufferedCommand = ''; - self.displayPrompt(); - return; - } - - // If error was SyntaxError and not JSON.parse error - if (e) { - if (e instanceof Recoverable && !self.lineParser.shouldFail) { - // Start buffering data like that: - // { - // ... x: 1 - // ... } - self.bufferedCommand += cmd + '\n'; - self.displayPrompt(); - return; - } else { - self._domain.emit('error', e.err || e); - } - } - - // Clear buffer if no SyntaxErrors - self.lineParser.reset(); - self.bufferedCommand = ''; - - // If we got any output - print it (if no error) - if (!e && - // When an invalid REPL command is used, error message is printed - // immediately. We don't have to print anything else. So, only when - // the second argument to this function is there, print it. - arguments.length === 2 && - (!self.ignoreUndefined || ret !== undefined)) { - if (!self.underscoreAssigned) { - self.last = ret; - } - self.outputStream.write(self.writer(ret) + '\n'); - } - // Display prompt again - self.displayPrompt(); - } - }); - - self.on('SIGCONT', function() { - self.displayPrompt(true); + this.eval(evalCmd, this.context, 'repl', finish); }); - self.displayPrompt(); + this.on('SIGCONT', () => this.displayPrompt(true)); + this.displayPrompt(); } inherits(REPLServer, Interface); exports.REPLServer = REPLServer; @@ -561,7 +554,7 @@ exports.start = function(prompt, useGlobal, ignoreUndefined, replMode) { - var repl = new REPLServer(prompt, + const repl = new REPLServer(prompt, source, eval_, useGlobal, @@ -587,7 +580,7 @@ REPLServer.prototype.close = function replClose() { }; REPLServer.prototype.createContext = function() { - var context; + let context; if (this.useGlobal) { context = global; } else { @@ -671,11 +664,8 @@ REPLServer.prototype.setPrompt = function setPrompt(prompt) { function ArrayStream() { Stream.call(this); - this.run = function(data) { - var self = this; - data.forEach(function(line) { - self.emit('data', line + '\n'); - }); + this.run = (data) => { + data.forEach((line) => this.emit('data', line + '\n')); }; } util.inherits(ArrayStream, Stream); @@ -712,18 +702,18 @@ 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(); + const lineArray = 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] = ''; + lineArray[kill.line] = ''; } }); - var flat = new ArrayStream(); // make a new "input" stream - var magic = new REPLServer('', flat); // make a nested REPL + const flat = new ArrayStream(); // make a new "input" stream + const magic = new REPLServer('', flat); // make a nested REPL magic.context = magic.createContext(); - flat.run(tmp); // eval the flattened code + flat.run(lineArray); // eval the flattened code // all this is only profitable if the nested REPL // does not have a bufferedCommand if (!magic.bufferedCommand) { @@ -732,15 +722,15 @@ REPLServer.prototype.complete = function(line, callback) { } } - var completions; + let completions; // list of completion lists, one for each inheritance "level" - var completionGroups = []; + let completionGroups = []; - var completeOn, i, group, c; + let completeOn, i, group, c; // REPL commands (e.g. ".break"). - var match = null; + let match = null, filter; match = line.match(/^\s*\.(\w*)$/); if (match) { completionGroups.push(Object.keys(this.commands)); @@ -753,15 +743,15 @@ REPLServer.prototype.complete = function(line, callback) { } else if (match = line.match(requireRE)) { // require('...') const exts = Object.keys(this.context.require.extensions); - var indexRe = new RegExp('^index(' + exts.map(regexpEscape).join('|') + + const 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); + filter = match[1]; + completeOn = match[1]; + const subdir = match[2] || ''; + const paths = module.paths.concat(require('module').globalPaths); + let dir, files, f, name, base, ext, abs, subfiles, s; for (i = 0; i < paths.length; i++) { dir = path.resolve(paths[i], subdir); try { @@ -996,19 +986,17 @@ REPLServer.prototype.defineCommand = function(keyword, cmd) { }; REPLServer.prototype.memory = function memory(cmd) { - var self = this; - - self.lines = self.lines || []; - self.lines.level = self.lines.level || []; + this.lines = this.lines || []; + this.lines.level = this.lines.level || []; // save the line so I can do magic later if (cmd) { // TODO should I tab the level? - const len = self.lines.level.length ? self.lines.level.length - 1 : 0; - self.lines.push(' '.repeat(len) + cmd); + const len = this.lines.level.length ? this.lines.level.length - 1 : 0; + this.lines.push(' '.repeat(len) + cmd); } else { // I don't want to not change the format too much... - self.lines.push(''); + this.lines.push(''); } // I need to know "depth." @@ -1024,7 +1012,7 @@ REPLServer.prototype.memory = function memory(cmd) { var depth = dw - up; if (depth) { - (function workIt() { + (function workIt(_repl) { if (depth > 0) { // going... down. // push the line#, depth count, and if the line is a function. @@ -1033,39 +1021,39 @@ REPLServer.prototype.memory = function memory(cmd) { // "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, + _repl.lines.level.push({ + line: _repl.lines.length - 1, depth: depth, isFunction: /\s*function\s*/.test(cmd) }); } else if (depth < 0) { // going... up. - var curr = self.lines.level.pop(); + var curr = _repl.lines.level.pop(); if (curr) { var tmp = curr.depth + depth; if (tmp < 0) { //more to go, recurse depth += curr.depth; - workIt(); + workIt(_repl); } else if (tmp > 0) { //remove and push back curr.depth += depth; - self.lines.level.push(curr); + _repl.lines.level.push(curr); } } } - }()); + }(this)); } // it is possible to determine a syntax error at this point. // if the REPL still has a bufferedCommand and - // self.lines.level.length === 0 + // this.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 bufferedCommand and // eval these lines and throw the syntax error } else { - self.lines.level = []; + this.lines.level = []; } }; @@ -1124,10 +1112,9 @@ function defineDefaultCommands(repl) { 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'); + Object.keys(this.commands).sort().forEach((name) => { + var cmd = this.commands[name]; + this.outputStream.write(name + '\t' + (cmd.help || '') + '\n'); }); this.displayPrompt(); } @@ -1152,13 +1139,12 @@ function defineDefaultCommands(repl) { 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) { + lines.forEach((line) => { if (line) { - self.write(line + '\n'); + this.write(line + '\n'); } }); } else { @@ -1191,10 +1177,10 @@ REPLServer.prototype.convertToContext = function(cmd) { const scopeFunc = /^\s*function\s*([_\w\$]+)/; var matches; - // Replaces: var foo = "bar"; with: self.context.foo = bar; + // Replaces: var foo = "bar"; with: this.context.foo = bar; matches = scopeVar.exec(cmd); if (matches && matches.length === 3) { - return 'self.context.' + matches[1] + matches[2]; + return 'this.context.' + matches[1] + matches[2]; } // Replaces: function foo() {}; with: foo = function foo() {}; @@ -1214,12 +1200,12 @@ function bailOnIllegalToken(parser) { // If the error is that we've unexpectedly ended the input, // then let the user try to recover by adding more input. -function isRecoverableError(e, self) { +function isRecoverableError(e, _repl) { if (e && e.name === 'SyntaxError') { var message = e.message; if (message === 'Unterminated template literal' || message === 'Missing } in template expression') { - self._inTemplateLiteral = true; + _repl._inTemplateLiteral = true; return true; } @@ -1229,7 +1215,7 @@ function isRecoverableError(e, self) { return true; if (message === 'Invalid or unexpected token') - return !bailOnIllegalToken(self.lineParser); + return !bailOnIllegalToken(_repl.lineParser); } return false; }