doc: improvements to console.markdown copy
[platform/upstream/nodejs.git] / lib / repl.js
1 /* A repl library that you can include in your own code to get a runtime
2  * interface to your program.
3  *
4  *   var repl = require("repl");
5  *   // start repl on stdin
6  *   repl.start("prompt> ");
7  *
8  *   // listen for unix socket connections and start repl on them
9  *   net.createServer(function(socket) {
10  *     repl.start("node via Unix socket> ", socket);
11  *   }).listen("/tmp/node-repl-sock");
12  *
13  *   // listen for TCP socket connections and start repl on them
14  *   net.createServer(function(socket) {
15  *     repl.start("node via TCP socket> ", socket);
16  *   }).listen(5001);
17  *
18  *   // expose foo to repl context
19  *   repl.start("node > ").context.foo = "stdin is fun";
20  */
21
22 'use strict';
23
24 const util = require('util');
25 const inherits = util.inherits;
26 const Stream = require('stream');
27 const vm = require('vm');
28 const path = require('path');
29 const fs = require('fs');
30 const rl = require('readline');
31 const Console = require('console').Console;
32 const domain = require('domain');
33 const debug = util.debuglog('repl');
34
35 const replMap = new WeakMap();
36
37 try {
38   // hack for require.resolve("./relative") to work properly.
39   module.filename = path.resolve('repl');
40 } catch (e) {
41   // path.resolve('repl') fails when the current working directory has been
42   // deleted.  Fall back to the directory name of the (absolute) executable
43   // path.  It's not really correct but what are the alternatives?
44   const dirname = path.dirname(process.execPath);
45   module.filename = path.resolve(dirname, 'repl');
46 }
47
48 // hack for repl require to work properly with node_modules folders
49 module.paths = require('module')._nodeModulePaths(module.filename);
50
51 // If obj.hasOwnProperty has been overridden, then calling
52 // obj.hasOwnProperty(prop) will break.
53 // See: https://github.com/joyent/node/issues/1707
54 function hasOwnProperty(obj, prop) {
55   return Object.prototype.hasOwnProperty.call(obj, prop);
56 }
57
58
59 // Can overridden with custom print functions, such as `probe` or `eyes.js`.
60 // This is the default "writer" value if none is passed in the REPL options.
61 exports.writer = util.inspect;
62
63 exports._builtinLibs = ['assert', 'buffer', 'child_process', 'cluster',
64   'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https', 'net',
65   'os', 'path', 'punycode', 'querystring', 'readline', 'stream',
66   'string_decoder', 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'zlib'];
67
68
69 const BLOCK_SCOPED_ERROR = 'Block-scoped declarations (let, ' +
70     'const, function, class) not yet supported outside strict mode';
71
72
73 class LineParser {
74
75   constructor() {
76     this.reset();
77   }
78
79   reset() {
80     this._literal = null;
81     this.shouldFail = false;
82     this.blockComment = false;
83   }
84
85   parseLine(line) {
86     var previous = null;
87     this.shouldFail = false;
88     const wasWithinStrLiteral = this._literal !== null;
89
90     for (const current of line) {
91       if (previous === '\\') {
92         // valid escaping, skip processing. previous doesn't matter anymore
93         previous = null;
94         continue;
95       }
96
97       if (!this._literal) {
98         if (previous === '*' && current === '/') {
99           if (this.blockComment) {
100             this.blockComment = false;
101             previous = null;
102             continue;
103           } else {
104             this.shouldFail = true;
105             break;
106           }
107         }
108
109         // ignore rest of the line if `current` and `previous` are `/`s
110         if (previous === current && previous === '/' && !this.blockComment) {
111           break;
112         }
113
114         if (previous === '/' && current === '*') {
115           this.blockComment = true;
116           previous = null;
117         }
118       }
119
120       if (this.blockComment) continue;
121
122       if (current === this._literal) {
123         this._literal = null;
124       } else if (current === '\'' || current === '"') {
125         this._literal = this._literal || current;
126       }
127
128       previous = current;
129     }
130
131     const isWithinStrLiteral = this._literal !== null;
132
133     if (!wasWithinStrLiteral && !isWithinStrLiteral) {
134       // Current line has nothing to do with String literals, trim both ends
135       line = line.trim();
136     } else if (wasWithinStrLiteral && !isWithinStrLiteral) {
137       // was part of a string literal, but it is over now, trim only the end
138       line = line.trimRight();
139     } else if (isWithinStrLiteral && !wasWithinStrLiteral) {
140       // was not part of a string literal, but it is now, trim only the start
141       line = line.trimLeft();
142     }
143
144     const lastChar = line.charAt(line.length - 1);
145
146     this.shouldFail = this.shouldFail ||
147                       ((!this._literal && lastChar === '\\') ||
148                       (this._literal   && lastChar !== '\\'));
149
150     return line;
151   }
152 }
153
154
155 function REPLServer(prompt,
156                     stream,
157                     eval_,
158                     useGlobal,
159                     ignoreUndefined,
160                     replMode) {
161   if (!(this instanceof REPLServer)) {
162     return new REPLServer(prompt,
163                           stream,
164                           eval_,
165                           useGlobal,
166                           ignoreUndefined,
167                           replMode);
168   }
169
170   var options, input, output, dom;
171   if (prompt !== null && typeof prompt === 'object') {
172     // an options object was given
173     options = prompt;
174     stream = options.stream || options.socket;
175     input = options.input;
176     output = options.output;
177     eval_ = options.eval;
178     useGlobal = options.useGlobal;
179     ignoreUndefined = options.ignoreUndefined;
180     prompt = options.prompt;
181     dom = options.domain;
182     replMode = options.replMode;
183   } else if (typeof prompt !== 'string') {
184     throw new Error('An options Object, or a prompt String are required');
185   } else {
186     options = {};
187   }
188
189   var self = this;
190
191   self._domain = dom || domain.create();
192
193   self.useGlobal = !!useGlobal;
194   self.ignoreUndefined = !!ignoreUndefined;
195   self.replMode = replMode || exports.REPL_MODE_SLOPPY;
196
197   self._inTemplateLiteral = false;
198
199   // just for backwards compat, see github.com/joyent/node/pull/7127
200   self.rli = this;
201
202   const savedRegExMatches = ['', '', '', '', '', '', '', '', '', ''];
203   const sep = '\u0000\u0000\u0000';
204   const regExMatcher = new RegExp(`^${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
205                                   `${sep}(.*)${sep}(.*)${sep}(.*)${sep}(.*)` +
206                                   `${sep}(.*)$`);
207
208   eval_ = eval_ || defaultEval;
209
210   function defaultEval(code, context, file, cb) {
211     var err, result, retry = false;
212     // first, create the Script object to check the syntax
213     while (true) {
214       try {
215         if (!/^\s*$/.test(code) &&
216             (self.replMode === exports.REPL_MODE_STRICT || retry)) {
217           // "void 0" keeps the repl from returning "use strict" as the
218           // result value for let/const statements.
219           code = `'use strict'; void 0; ${code}`;
220         }
221         var script = vm.createScript(code, {
222           filename: file,
223           displayErrors: false
224         });
225       } catch (e) {
226         debug('parse error %j', code, e);
227         if (self.replMode === exports.REPL_MODE_MAGIC &&
228             e.message === BLOCK_SCOPED_ERROR &&
229             !retry) {
230           retry = true;
231           continue;
232         }
233         if (isRecoverableError(e, self))
234           err = new Recoverable(e);
235         else
236           err = e;
237       }
238       break;
239     }
240
241     // This will set the values from `savedRegExMatches` to corresponding
242     // predefined RegExp properties `RegExp.$1`, `RegExp.$2` ... `RegExp.$9`
243     regExMatcher.test(savedRegExMatches.join(sep));
244
245     if (!err) {
246       try {
247         if (self.useGlobal) {
248           result = script.runInThisContext({ displayErrors: false });
249         } else {
250           result = script.runInContext(context, { displayErrors: false });
251         }
252       } catch (e) {
253         err = e;
254         if (err && process.domain) {
255           debug('not recoverable, send to domain');
256           process.domain.emit('error', err);
257           process.domain.exit();
258           return;
259         }
260       }
261     }
262
263     // After executing the current expression, store the values of RegExp
264     // predefined properties back in `savedRegExMatches`
265     for (let idx = 1; idx < savedRegExMatches.length; idx += 1) {
266       savedRegExMatches[idx] = RegExp[`$${idx}`];
267     }
268
269     cb(err, result);
270   }
271
272   self.eval = self._domain.bind(eval_);
273
274   self._domain.on('error', function(e) {
275     debug('domain error');
276     const top = replMap.get(self);
277     top.outputStream.write((e.stack || e) + '\n');
278     top.lineParser.reset();
279     top.bufferedCommand = '';
280     top.lines.level = [];
281     top.displayPrompt();
282   });
283
284   if (!input && !output) {
285     // legacy API, passing a 'stream'/'socket' option
286     if (!stream) {
287       // use stdin and stdout as the default streams if none were given
288       stream = process;
289     }
290     if (stream.stdin && stream.stdout) {
291       // We're given custom object with 2 streams, or the `process` object
292       input = stream.stdin;
293       output = stream.stdout;
294     } else {
295       // We're given a duplex readable/writable Stream, like a `net.Socket`
296       input = stream;
297       output = stream;
298     }
299   }
300
301   self.inputStream = input;
302   self.outputStream = output;
303
304   self.resetContext();
305   self.lineParser = new LineParser();
306   self.bufferedCommand = '';
307   self.lines.level = [];
308
309   function complete(text, callback) {
310     self.complete(text, callback);
311   }
312
313   rl.Interface.call(this, {
314     input: self.inputStream,
315     output: self.outputStream,
316     completer: complete,
317     terminal: options.terminal,
318     historySize: options.historySize
319   });
320
321   self.setPrompt(prompt !== undefined ? prompt : '> ');
322
323   this.commands = Object.create(null);
324   defineDefaultCommands(this);
325
326   // figure out which "writer" function to use
327   self.writer = options.writer || exports.writer;
328
329   if (options.useColors === undefined) {
330     options.useColors = self.terminal;
331   }
332   self.useColors = !!options.useColors;
333
334   if (self.useColors && self.writer === util.inspect) {
335     // Turn on ANSI coloring.
336     self.writer = function(obj, showHidden, depth) {
337       return util.inspect(obj, showHidden, depth, true);
338     };
339   }
340
341   self.setPrompt(self._prompt);
342
343   self.on('close', function() {
344     self.emit('exit');
345   });
346
347   var sawSIGINT = false;
348   self.on('SIGINT', function() {
349     var empty = self.line.length === 0;
350     self.clearLine();
351
352     if (!(self.bufferedCommand && self.bufferedCommand.length > 0) && empty) {
353       if (sawSIGINT) {
354         self.close();
355         sawSIGINT = false;
356         return;
357       }
358       self.output.write('(To exit, press ^C again or type .exit)\n');
359       sawSIGINT = true;
360     } else {
361       sawSIGINT = false;
362     }
363
364     self.lineParser.reset();
365     self.bufferedCommand = '';
366     self.lines.level = [];
367     self.displayPrompt();
368   });
369
370   self.on('line', function(cmd) {
371     debug('line %j', cmd);
372     sawSIGINT = false;
373     var skipCatchall = false;
374
375     // leading whitespaces in template literals should not be trimmed.
376     if (self._inTemplateLiteral) {
377       self._inTemplateLiteral = false;
378     } else {
379       cmd = self.lineParser.parseLine(cmd);
380     }
381
382     // Check to see if a REPL keyword was used. If it returns true,
383     // display next prompt and return.
384     if (cmd && cmd.charAt(0) === '.' && isNaN(parseFloat(cmd))) {
385       var matches = cmd.match(/^\.([^\s]+)\s*(.*)$/);
386       var keyword = matches && matches[1];
387       var rest = matches && matches[2];
388       if (self.parseREPLKeyword(keyword, rest) === true) {
389         return;
390       } else {
391         self.outputStream.write('Invalid REPL keyword\n');
392         skipCatchall = true;
393       }
394     }
395
396     if (!skipCatchall && (cmd || (!cmd && self.bufferedCommand))) {
397       var evalCmd = self.bufferedCommand + cmd;
398       if (/^\s*\{/.test(evalCmd) && /\}\s*$/.test(evalCmd)) {
399         // It's confusing for `{ a : 1 }` to be interpreted as a block
400         // statement rather than an object literal.  So, we first try
401         // to wrap it in parentheses, so that it will be interpreted as
402         // an expression.
403         evalCmd = '(' + evalCmd + ')\n';
404       } else {
405         // otherwise we just append a \n so that it will be either
406         // terminated, or continued onto the next expression if it's an
407         // unexpected end of input.
408         evalCmd = evalCmd + '\n';
409       }
410
411       debug('eval %j', evalCmd);
412       self.eval(evalCmd, self.context, 'repl', finish);
413     } else {
414       finish(null);
415     }
416
417     function finish(e, ret) {
418       debug('finish', e, ret);
419       self.memory(cmd);
420
421       if (e && !self.bufferedCommand && cmd.trim().match(/^npm /)) {
422         self.outputStream.write('npm should be run outside of the ' +
423                                 'node repl, in your normal shell.\n' +
424                                 '(Press Control-D to exit.)\n');
425         self.lineParser.reset();
426         self.bufferedCommand = '';
427         self.displayPrompt();
428         return;
429       }
430
431       // If error was SyntaxError and not JSON.parse error
432       if (e) {
433         if (e instanceof Recoverable && !self.lineParser.shouldFail) {
434           // Start buffering data like that:
435           // {
436           // ...  x: 1
437           // ... }
438           self.bufferedCommand += cmd + '\n';
439           self.displayPrompt();
440           return;
441         } else {
442           self._domain.emit('error', e.err || e);
443         }
444       }
445
446       // Clear buffer if no SyntaxErrors
447       self.lineParser.reset();
448       self.bufferedCommand = '';
449
450       // If we got any output - print it (if no error)
451       if (!e &&
452           // When an invalid REPL command is used, error message is printed
453           // immediately. We don't have to print anything else. So, only when
454           // the second argument to this function is there, print it.
455           arguments.length === 2 &&
456           (!self.ignoreUndefined || ret !== undefined)) {
457         self.context._ = ret;
458         self.outputStream.write(self.writer(ret) + '\n');
459       }
460
461       // Display prompt again
462       self.displayPrompt();
463     }
464   });
465
466   self.on('SIGCONT', function() {
467     self.displayPrompt(true);
468   });
469
470   self.displayPrompt();
471 }
472 inherits(REPLServer, rl.Interface);
473 exports.REPLServer = REPLServer;
474
475 exports.REPL_MODE_SLOPPY = Symbol('repl-sloppy');
476 exports.REPL_MODE_STRICT = Symbol('repl-strict');
477 exports.REPL_MODE_MAGIC = Symbol('repl-magic');
478
479 // prompt is a string to print on each line for the prompt,
480 // source is a stream to use for I/O, defaulting to stdin/stdout.
481 exports.start = function(prompt,
482                          source,
483                          eval_,
484                          useGlobal,
485                          ignoreUndefined,
486                          replMode) {
487   var repl = new REPLServer(prompt,
488                             source,
489                             eval_,
490                             useGlobal,
491                             ignoreUndefined,
492                             replMode);
493   if (!exports.repl) exports.repl = repl;
494   replMap.set(repl, repl);
495   return repl;
496 };
497
498 REPLServer.prototype.createContext = function() {
499   var context;
500   if (this.useGlobal) {
501     context = global;
502   } else {
503     context = vm.createContext();
504     for (var i in global) context[i] = global[i];
505     context.console = new Console(this.outputStream);
506     context.global = context;
507     context.global.global = context;
508   }
509
510   context.module = module;
511   context.require = require;
512
513   this.lines = [];
514   this.lines.level = [];
515
516   // make built-in modules available directly
517   // (loaded lazily)
518   exports._builtinLibs.forEach(function(name) {
519     Object.defineProperty(context, name, {
520       get: function() {
521         var lib = require(name);
522         context._ = context[name] = lib;
523         return lib;
524       },
525       // allow the creation of other globals with this name
526       set: function(val) {
527         delete context[name];
528         context[name] = val;
529       },
530       configurable: true
531     });
532   });
533
534   return context;
535 };
536
537 REPLServer.prototype.resetContext = function() {
538   this.context = this.createContext();
539
540   // Allow REPL extensions to extend the new context
541   this.emit('reset', this.context);
542 };
543
544 REPLServer.prototype.displayPrompt = function(preserveCursor) {
545   var prompt = this._initialPrompt;
546   if (this.bufferedCommand.length) {
547     prompt = '...';
548     var levelInd = new Array(this.lines.level.length).join('..');
549     prompt += levelInd + ' ';
550   }
551
552   // Do not overwrite `_initialPrompt` here
553   REPLServer.super_.prototype.setPrompt.call(this, prompt);
554   this.prompt(preserveCursor);
555 };
556
557 // When invoked as an API method, overwrite _initialPrompt
558 REPLServer.prototype.setPrompt = function setPrompt(prompt) {
559   this._initialPrompt = prompt;
560   REPLServer.super_.prototype.setPrompt.call(this, prompt);
561 };
562
563 // A stream to push an array into a REPL
564 // used in REPLServer.complete
565 function ArrayStream() {
566   Stream.call(this);
567
568   this.run = function(data) {
569     var self = this;
570     data.forEach(function(line) {
571       self.emit('data', line + '\n');
572     });
573   };
574 }
575 util.inherits(ArrayStream, Stream);
576 ArrayStream.prototype.readable = true;
577 ArrayStream.prototype.writable = true;
578 ArrayStream.prototype.resume = function() {};
579 ArrayStream.prototype.write = function() {};
580
581 const requireRE = /\brequire\s*\(['"](([\w\.\/-]+\/)?([\w\.\/-]*))/;
582 const simpleExpressionRE =
583     /(([a-zA-Z_$](?:\w|\$)*)\.)*([a-zA-Z_$](?:\w|\$)*)\.?$/;
584
585 function intFilter(item) {
586   // filters out anything not starting with A-Z, a-z, $ or _
587   return /^[A-Za-z_$]/.test(item);
588 }
589
590 function filteredOwnPropertyNames(obj) {
591   if (!obj) return [];
592   return Object.getOwnPropertyNames(obj).filter(intFilter);
593 }
594
595 // Provide a list of completions for the given leading text. This is
596 // given to the readline interface for handling tab completion.
597 //
598 // Example:
599 //  complete('var foo = util.')
600 //    -> [['util.print', 'util.debug', 'util.log', 'util.inspect', 'util.pump'],
601 //        'util.' ]
602 //
603 // Warning: This eval's code like "foo.bar.baz", so it will run property
604 // getter code.
605 REPLServer.prototype.complete = function(line, callback) {
606   // There may be local variables to evaluate, try a nested REPL
607   if (this.bufferedCommand !== undefined && this.bufferedCommand.length) {
608     // Get a new array of inputed lines
609     var tmp = this.lines.slice();
610     // Kill off all function declarations to push all local variables into
611     // global scope
612     this.lines.level.forEach(function(kill) {
613       if (kill.isFunction) {
614         tmp[kill.line] = '';
615       }
616     });
617     var flat = new ArrayStream();         // make a new "input" stream
618     var magic = new REPLServer('', flat); // make a nested REPL
619     magic.context = magic.createContext();
620     flat.run(tmp);                        // eval the flattened code
621     // all this is only profitable if the nested REPL
622     // does not have a bufferedCommand
623     if (!magic.bufferedCommand) {
624       replMap.set(magic, replMap.get(this));
625       return magic.complete(line, callback);
626     }
627   }
628
629   var completions;
630
631   // list of completion lists, one for each inheritance "level"
632   var completionGroups = [];
633
634   var completeOn, match, filter, i, group, c;
635
636   // REPL commands (e.g. ".break").
637   var match = null;
638   match = line.match(/^\s*(\.\w*)$/);
639   if (match) {
640     completionGroups.push(Object.keys(this.commands));
641     completeOn = match[1];
642     if (match[1].length > 1) {
643       filter = match[1];
644     }
645
646     completionGroupsLoaded();
647   } else if (match = line.match(requireRE)) {
648     // require('...<Tab>')
649     var exts = Object.keys(require.extensions);
650     var indexRe = new RegExp('^index(' + exts.map(regexpEscape).join('|') +
651                              ')$');
652
653     completeOn = match[1];
654     var subdir = match[2] || '';
655     var filter = match[1];
656     var dir, files, f, name, base, ext, abs, subfiles, s;
657     group = [];
658     var paths = module.paths.concat(require('module').globalPaths);
659     for (i = 0; i < paths.length; i++) {
660       dir = path.resolve(paths[i], subdir);
661       try {
662         files = fs.readdirSync(dir);
663       } catch (e) {
664         continue;
665       }
666       for (f = 0; f < files.length; f++) {
667         name = files[f];
668         ext = path.extname(name);
669         base = name.slice(0, -ext.length);
670         if (base.match(/-\d+\.\d+(\.\d+)?/) || name === '.npm') {
671           // Exclude versioned names that 'npm' installs.
672           continue;
673         }
674         if (exts.indexOf(ext) !== -1) {
675           if (!subdir || base !== 'index') {
676             group.push(subdir + base);
677           }
678         } else {
679           abs = path.resolve(dir, name);
680           try {
681             if (fs.statSync(abs).isDirectory()) {
682               group.push(subdir + name + '/');
683               subfiles = fs.readdirSync(abs);
684               for (s = 0; s < subfiles.length; s++) {
685                 if (indexRe.test(subfiles[s])) {
686                   group.push(subdir + name);
687                 }
688               }
689             }
690           } catch (e) {}
691         }
692       }
693     }
694     if (group.length) {
695       completionGroups.push(group);
696     }
697
698     if (!subdir) {
699       completionGroups.push(exports._builtinLibs);
700     }
701
702     completionGroupsLoaded();
703
704   // Handle variable member lookup.
705   // We support simple chained expressions like the following (no function
706   // calls, etc.). That is for simplicity and also because we *eval* that
707   // leading expression so for safety (see WARNING above) don't want to
708   // eval function calls.
709   //
710   //   foo.bar<|>     # completions for 'foo' with filter 'bar'
711   //   spam.eggs.<|>  # completions for 'spam.eggs' with filter ''
712   //   foo<|>         # all scope vars with filter 'foo'
713   //   foo.<|>        # completions for 'foo' with filter ''
714   } else if (line.length === 0 || line[line.length - 1].match(/\w|\.|\$/)) {
715     match = simpleExpressionRE.exec(line);
716     if (line.length === 0 || match) {
717       var expr;
718       completeOn = (match ? match[0] : '');
719       if (line.length === 0) {
720         filter = '';
721         expr = '';
722       } else if (line[line.length - 1] === '.') {
723         filter = '';
724         expr = match[0].slice(0, match[0].length - 1);
725       } else {
726         var bits = match[0].split('.');
727         filter = bits.pop();
728         expr = bits.join('.');
729       }
730
731       // Resolve expr and get its completions.
732       var memberGroups = [];
733       if (!expr) {
734         // If context is instance of vm.ScriptContext
735         // Get global vars synchronously
736         if (this.useGlobal || vm.isContext(this.context)) {
737           var contextProto = this.context;
738           while (contextProto = Object.getPrototypeOf(contextProto)) {
739             completionGroups.push(filteredOwnPropertyNames(contextProto));
740           }
741           completionGroups.push(filteredOwnPropertyNames(this.context));
742           addStandardGlobals(completionGroups, filter);
743           completionGroupsLoaded();
744         } else {
745           this.eval('.scope', this.context, 'repl', function(err, globals) {
746             if (err || !Array.isArray(globals)) {
747               addStandardGlobals(completionGroups, filter);
748             } else if (Array.isArray(globals[0])) {
749               // Add grouped globals
750               globals.forEach(function(group) {
751                 completionGroups.push(group);
752               });
753             } else {
754               completionGroups.push(globals);
755               addStandardGlobals(completionGroups, filter);
756             }
757             completionGroupsLoaded();
758           });
759         }
760       } else {
761         this.eval(expr, this.context, 'repl', function(e, obj) {
762           // if (e) console.log(e);
763
764           if (obj != null) {
765             if (typeof obj === 'object' || typeof obj === 'function') {
766               try {
767                 memberGroups.push(filteredOwnPropertyNames(obj));
768               } catch (ex) {
769                 // Probably a Proxy object without `getOwnPropertyNames` trap.
770                 // We simply ignore it here, as we don't want to break the
771                 // autocompletion. Fixes the bug
772                 // https://github.com/nodejs/node/issues/2119
773               }
774             }
775             // works for non-objects
776             try {
777               var sentinel = 5;
778               var p;
779               if (typeof obj === 'object' || typeof obj === 'function') {
780                 p = Object.getPrototypeOf(obj);
781               } else {
782                 p = obj.constructor ? obj.constructor.prototype : null;
783               }
784               while (p !== null) {
785                 memberGroups.push(filteredOwnPropertyNames(p));
786                 p = Object.getPrototypeOf(p);
787                 // Circular refs possible? Let's guard against that.
788                 sentinel--;
789                 if (sentinel <= 0) {
790                   break;
791                 }
792               }
793             } catch (e) {
794               //console.log("completion error walking prototype chain:" + e);
795             }
796           }
797
798           if (memberGroups.length) {
799             for (i = 0; i < memberGroups.length; i++) {
800               completionGroups.push(memberGroups[i].map(function(member) {
801                 return expr + '.' + member;
802               }));
803             }
804             if (filter) {
805               filter = expr + '.' + filter;
806             }
807           }
808
809           completionGroupsLoaded();
810         });
811       }
812     } else {
813       completionGroupsLoaded();
814     }
815   } else {
816     completionGroupsLoaded();
817   }
818
819   // Will be called when all completionGroups are in place
820   // Useful for async autocompletion
821   function completionGroupsLoaded(err) {
822     if (err) throw err;
823
824     // Filter, sort (within each group), uniq and merge the completion groups.
825     if (completionGroups.length && filter) {
826       var newCompletionGroups = [];
827       for (i = 0; i < completionGroups.length; i++) {
828         group = completionGroups[i].filter(function(elem) {
829           return elem.indexOf(filter) == 0;
830         });
831         if (group.length) {
832           newCompletionGroups.push(group);
833         }
834       }
835       completionGroups = newCompletionGroups;
836     }
837
838     if (completionGroups.length) {
839       var uniq = {};  // unique completions across all groups
840       completions = [];
841       // Completion group 0 is the "closest"
842       // (least far up the inheritance chain)
843       // so we put its completions last: to be closest in the REPL.
844       for (i = completionGroups.length - 1; i >= 0; i--) {
845         group = completionGroups[i];
846         group.sort();
847         for (var j = 0; j < group.length; j++) {
848           c = group[j];
849           if (!hasOwnProperty(uniq, c)) {
850             completions.push(c);
851             uniq[c] = true;
852           }
853         }
854         completions.push(''); // separator btwn groups
855       }
856       while (completions.length && completions[completions.length - 1] === '') {
857         completions.pop();
858       }
859     }
860
861     callback(null, [completions || [], completeOn]);
862   }
863 };
864
865
866 /**
867  * Used to parse and execute the Node REPL commands.
868  *
869  * @param {keyword} keyword The command entered to check.
870  * @return {Boolean} If true it means don't continue parsing the command.
871  */
872 REPLServer.prototype.parseREPLKeyword = function(keyword, rest) {
873   var cmd = this.commands[keyword];
874   if (cmd) {
875     cmd.action.call(this, rest);
876     return true;
877   }
878   return false;
879 };
880
881
882 REPLServer.prototype.defineCommand = function(keyword, cmd) {
883   if (typeof cmd === 'function') {
884     cmd = {action: cmd};
885   } else if (typeof cmd.action !== 'function') {
886     throw new Error('bad argument, action must be a function');
887   }
888   this.commands[keyword] = cmd;
889 };
890
891 REPLServer.prototype.memory = function memory(cmd) {
892   var self = this;
893
894   self.lines = self.lines || [];
895   self.lines.level = self.lines.level || [];
896
897   // save the line so I can do magic later
898   if (cmd) {
899     // TODO should I tab the level?
900     self.lines.push(new Array(self.lines.level.length).join('  ') + cmd);
901   } else {
902     // I don't want to not change the format too much...
903     self.lines.push('');
904   }
905
906   // I need to know "depth."
907   // Because I can not tell the difference between a } that
908   // closes an object literal and a } that closes a function
909   if (cmd) {
910     // going down is { and (   e.g. function() {
911     // going up is } and )
912     var dw = cmd.match(/{|\(/g);
913     var up = cmd.match(/}|\)/g);
914     up = up ? up.length : 0;
915     dw = dw ? dw.length : 0;
916     var depth = dw - up;
917
918     if (depth) {
919       (function workIt() {
920         if (depth > 0) {
921           // going... down.
922           // push the line#, depth count, and if the line is a function.
923           // Since JS only has functional scope I only need to remove
924           // "function() {" lines, clearly this will not work for
925           // "function()
926           // {" but nothing should break, only tab completion for local
927           // scope will not work for this function.
928           self.lines.level.push({
929             line: self.lines.length - 1,
930             depth: depth,
931             isFunction: /\s*function\s*/.test(cmd)
932           });
933         } else if (depth < 0) {
934           // going... up.
935           var curr = self.lines.level.pop();
936           if (curr) {
937             var tmp = curr.depth + depth;
938             if (tmp < 0) {
939               //more to go, recurse
940               depth += curr.depth;
941               workIt();
942             } else if (tmp > 0) {
943               //remove and push back
944               curr.depth += depth;
945               self.lines.level.push(curr);
946             }
947           }
948         }
949       }());
950     }
951
952     // it is possible to determine a syntax error at this point.
953     // if the REPL still has a bufferedCommand and
954     // self.lines.level.length === 0
955     // TODO? keep a log of level so that any syntax breaking lines can
956     // be cleared on .break and in the case of a syntax error?
957     // TODO? if a log was kept, then I could clear the bufferedComand and
958     // eval these lines and throw the syntax error
959   } else {
960     self.lines.level = [];
961   }
962 };
963
964 function addStandardGlobals(completionGroups, filter) {
965   // Global object properties
966   // (http://www.ecma-international.org/publications/standards/Ecma-262.htm)
967   completionGroups.push(['NaN', 'Infinity', 'undefined',
968     'eval', 'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'decodeURI',
969     'decodeURIComponent', 'encodeURI', 'encodeURIComponent',
970     'Object', 'Function', 'Array', 'String', 'Boolean', 'Number',
971     'Date', 'RegExp', 'Error', 'EvalError', 'RangeError',
972     'ReferenceError', 'SyntaxError', 'TypeError', 'URIError',
973     'Math', 'JSON']);
974   // Common keywords. Exclude for completion on the empty string, b/c
975   // they just get in the way.
976   if (filter) {
977     completionGroups.push(['break', 'case', 'catch', 'const',
978       'continue', 'debugger', 'default', 'delete', 'do', 'else',
979       'export', 'false', 'finally', 'for', 'function', 'if',
980       'import', 'in', 'instanceof', 'let', 'new', 'null', 'return',
981       'switch', 'this', 'throw', 'true', 'try', 'typeof', 'undefined',
982       'var', 'void', 'while', 'with', 'yield']);
983   }
984 }
985
986 function defineDefaultCommands(repl) {
987   repl.defineCommand('break', {
988     help: 'Sometimes you get stuck, this gets you out',
989     action: function() {
990       this.lineParser.reset();
991       this.bufferedCommand = '';
992       this.displayPrompt();
993     }
994   });
995
996   var clearMessage;
997   if (repl.useGlobal) {
998     clearMessage = 'Alias for .break';
999   } else {
1000     clearMessage = 'Break, and also clear the local context';
1001   }
1002   repl.defineCommand('clear', {
1003     help: clearMessage,
1004     action: function() {
1005       this.lineParser.reset();
1006       this.bufferedCommand = '';
1007       if (!this.useGlobal) {
1008         this.outputStream.write('Clearing context...\n');
1009         this.resetContext();
1010       }
1011       this.displayPrompt();
1012     }
1013   });
1014
1015   repl.defineCommand('exit', {
1016     help: 'Exit the repl',
1017     action: function() {
1018       this.close();
1019     }
1020   });
1021
1022   repl.defineCommand('help', {
1023     help: 'Show repl options',
1024     action: function() {
1025       var self = this;
1026       Object.keys(this.commands).sort().forEach(function(name) {
1027         var cmd = self.commands[name];
1028         self.outputStream.write(name + '\t' + (cmd.help || '') + '\n');
1029       });
1030       this.displayPrompt();
1031     }
1032   });
1033
1034   repl.defineCommand('save', {
1035     help: 'Save all evaluated commands in this REPL session to a file',
1036     action: function(file) {
1037       try {
1038         fs.writeFileSync(file, this.lines.join('\n') + '\n');
1039         this.outputStream.write('Session saved to:' + file + '\n');
1040       } catch (e) {
1041         this.outputStream.write('Failed to save:' + file + '\n');
1042       }
1043       this.displayPrompt();
1044     }
1045   });
1046
1047   repl.defineCommand('load', {
1048     help: 'Load JS from a file into the REPL session',
1049     action: function(file) {
1050       try {
1051         var stats = fs.statSync(file);
1052         if (stats && stats.isFile()) {
1053           var self = this;
1054           var data = fs.readFileSync(file, 'utf8');
1055           var lines = data.split('\n');
1056           this.displayPrompt();
1057           lines.forEach(function(line) {
1058             if (line) {
1059               self.write(line + '\n');
1060             }
1061           });
1062         } else {
1063           this.outputStream.write('Failed to load:' + file +
1064                                   ' is not a valid file\n');
1065         }
1066       } catch (e) {
1067         this.outputStream.write('Failed to load:' + file + '\n');
1068       }
1069       this.displayPrompt();
1070     }
1071   });
1072 }
1073
1074 function regexpEscape(s) {
1075   return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
1076 }
1077
1078
1079 /**
1080  * Converts commands that use var and function <name>() to use the
1081  * local exports.context when evaled. This provides a local context
1082  * on the REPL.
1083  *
1084  * @param {String} cmd The cmd to convert.
1085  * @return {String} The converted command.
1086  */
1087 REPLServer.prototype.convertToContext = function(cmd) {
1088   const scopeVar = /^\s*var\s*([_\w\$]+)(.*)$/m;
1089   const scopeFunc = /^\s*function\s*([_\w\$]+)/;
1090   var self = this, matches;
1091
1092   // Replaces: var foo = "bar";  with: self.context.foo = bar;
1093   matches = scopeVar.exec(cmd);
1094   if (matches && matches.length === 3) {
1095     return 'self.context.' + matches[1] + matches[2];
1096   }
1097
1098   // Replaces: function foo() {};  with: foo = function foo() {};
1099   matches = scopeFunc.exec(self.bufferedCommand);
1100   if (matches && matches.length === 2) {
1101     return matches[1] + ' = ' + self.bufferedCommand;
1102   }
1103
1104   return cmd;
1105 };
1106
1107
1108 // If the error is that we've unexpectedly ended the input,
1109 // then let the user try to recover by adding more input.
1110 function isRecoverableError(e, self) {
1111   if (e && e.name === 'SyntaxError') {
1112     var message = e.message;
1113     if (message === 'Unterminated template literal' ||
1114         message === 'Missing } in template expression') {
1115       self._inTemplateLiteral = true;
1116       return true;
1117     }
1118     return /^(Unexpected end of input|Unexpected token)/.test(message);
1119   }
1120   return false;
1121 }
1122
1123 function Recoverable(err) {
1124   this.err = err;
1125 }
1126 inherits(Recoverable, SyntaxError);