~ubuntu-branches/ubuntu/precise/nodejs/precise

« back to all changes in this revision

Viewing changes to lib/repl.js

  • Committer: Bazaar Package Importer
  • Author(s): Jérémy Lal
  • Date: 2010-08-20 11:49:04 UTC
  • mfrom: (7.1.6 sid)
  • Revision ID: james.westby@ubuntu.com-20100820114904-lz22w6fkth7yh179
Tags: 0.2.0-1
New upstream release

Show diffs side-by-side

added added

removed removed

Lines of Context:
10
10
//   repl.start("node via TCP socket> ", socket);
11
11
// }).listen(5001);
12
12
 
13
 
// repl.start("node > ").scope.foo = "stdin is fun";  // expose foo to repl scope
 
13
// repl.start("node > ").context.foo = "stdin is fun";  // expose foo to repl context
14
14
 
15
15
var sys = require('sys');
16
 
var evalcx = process.binding('evals').Script.runInNewContext;
 
16
var Script = process.binding('evals').Script;
 
17
var evalcx = Script.runInContext;
17
18
var path = require("path");
18
 
var scope;
 
19
var fs = require("fs");
 
20
var rl = require('readline');
 
21
var context;
19
22
 
20
23
function cwdRequire (id) {
21
24
  if (id.match(/^\.\.\//) || id.match(/^\.\//)) {
23
26
  }
24
27
  return require(id);
25
28
}
 
29
Object.keys(require).forEach(function (k) {
 
30
  cwdRequire[k] = require[k];
 
31
});
26
32
 
27
 
function setScope (self) {
28
 
  scope = {};
29
 
  for (var i in global) scope[i] = global[i];
30
 
  scope.module = module;
31
 
  scope.require = cwdRequire;
 
33
function resetContext() {
 
34
  context = Script.createContext();
 
35
  for (var i in global) context[i] = global[i];
 
36
  context.module = module;
 
37
  context.require = cwdRequire;
32
38
}
33
39
 
34
40
 
37
43
 
38
44
function REPLServer(prompt, stream) {
39
45
  var self = this;
40
 
  if (!scope) setScope();
41
 
  self.scope = scope;
 
46
  if (!context) resetContext();
 
47
  self.context = context;
42
48
  self.buffered_cmd = '';
 
49
 
 
50
  self.stream = stream || process.openStdin();
43
51
  self.prompt = prompt || "node> ";
44
 
  self.stream = stream || process.openStdin();
45
 
  self.stream.setEncoding('utf8');
 
52
 
 
53
  var rli = self.rli = rl.createInterface(self.stream, function (text) {
 
54
    return self.complete(text);
 
55
  });
 
56
  rli.setPrompt(self.prompt);
 
57
 
46
58
  self.stream.addListener("data", function (chunk) {
47
 
    self.readline.call(self, chunk);
48
 
  });
 
59
    rli.write(chunk);
 
60
  });
 
61
 
 
62
  rli.addListener('line', function (cmd) {
 
63
    cmd = trimWhitespace(cmd);
 
64
 
 
65
    var flushed = true;
 
66
 
 
67
    // Check to see if a REPL keyword was used. If it returns true,
 
68
    // display next prompt and return.
 
69
    if (self.parseREPLKeyword(cmd) === true) return;
 
70
 
 
71
    // The catchall for errors
 
72
    try {
 
73
      self.buffered_cmd += cmd;
 
74
      // This try is for determining if the command is complete, or should
 
75
      // continue onto the next line.
 
76
      try {
 
77
        // Use evalcx to supply the global context
 
78
        var ret = evalcx(self.buffered_cmd, context, "repl");
 
79
        if (ret !== undefined) {
 
80
          context._ = ret;
 
81
          flushed = self.stream.write(exports.writer(ret) + "\n");
 
82
        }
 
83
 
 
84
        self.buffered_cmd = '';
 
85
      } catch (e) {
 
86
        // instanceof doesn't work across context switches.
 
87
        if (!(e && e.constructor && e.constructor.name === "SyntaxError")) {
 
88
          throw e;
 
89
        }
 
90
      }
 
91
    } catch (e) {
 
92
      // On error: Print the error and clear the buffer
 
93
      if (e.stack) {
 
94
        flushed = self.stream.write(e.stack + "\n");
 
95
      } else {
 
96
        flushed = self.stream.write(e.toString() + "\n");
 
97
      }
 
98
      self.buffered_cmd = '';
 
99
    }
 
100
 
 
101
    // need to make sure the buffer is flushed before displaying the prompt
 
102
    // again. This is really ugly. Need to have callbacks from
 
103
    // net.Stream.write()
 
104
    if (flushed) {
 
105
      self.displayPrompt();
 
106
    } else {
 
107
      self.displayPromptOnDrain = true;
 
108
    }
 
109
  });
 
110
 
 
111
  self.stream.addListener('drain', function () {
 
112
    if (self.displayPromptOnDrain) {
 
113
      self.displayPrompt();
 
114
      self.displayPromptOnDrain = false;
 
115
    }
 
116
  });
 
117
 
 
118
  rli.addListener('close', function () {
 
119
    self.stream.destroy();
 
120
  });
 
121
 
49
122
  self.displayPrompt();
50
123
}
51
124
exports.REPLServer = REPLServer;
57
130
};
58
131
 
59
132
REPLServer.prototype.displayPrompt = function () {
60
 
  var self = this;
61
 
  self.stream.write(self.buffered_cmd.length ? '...   ' : self.prompt);
 
133
  this.rli.setPrompt(this.buffered_cmd.length ? '...   ' : this.prompt);
 
134
  this.rli.prompt();
62
135
};
63
136
 
64
137
// read a line from the stream, then eval it
65
138
REPLServer.prototype.readline = function (cmd) {
66
 
  var self = this;
67
 
 
68
 
  cmd = self.trimWhitespace(cmd);
69
 
  
70
 
  // Check to see if a REPL keyword was used. If it returns true,
71
 
  // display next prompt and return.
72
 
  if (self.parseREPLKeyword(cmd) === true) {
73
 
    return;
74
 
  }
75
 
  
76
 
  // The catchall for errors
77
 
  try {
78
 
    self.buffered_cmd += cmd;
79
 
    // This try is for determining if the command is complete, or should
80
 
    // continue onto the next line.
81
 
    try {
82
 
      // Scope the readline with self.scope
83
 
      // with(){} and eval() are considered bad.
84
 
      var ret = evalcx(self.buffered_cmd, scope, "repl");
85
 
      if (ret !== undefined) {
86
 
        scope._ = ret;
87
 
        self.stream.write(exports.writer(ret) + "\n");
88
 
      }
89
 
 
90
 
      self.buffered_cmd = '';
91
 
    } catch (e) {
92
 
      // instanceof doesn't work across context switches.
93
 
      if (!(e && e.constructor && e.constructor.name === "SyntaxError")) {
94
 
        throw e;
95
 
      }
96
 
    }
97
 
  } catch (e) {
98
 
    // On error: Print the error and clear the buffer
99
 
    if (e.stack) {
100
 
      self.stream.write(e.stack + "\n");
101
 
    } else {
102
 
      self.stream.write(e.toString() + "\n");
103
 
    }
104
 
    self.buffered_cmd = '';
105
 
  }
106
 
  
107
 
  self.displayPrompt();
 
139
};
 
140
 
 
141
/**
 
142
 * Provide a list of completions for the given leading text. This is
 
143
 * given to the readline interface for handling tab completion.
 
144
 *
 
145
 * @param {line} The text (preceding the cursor) to complete
 
146
 * @returns {Array} Two elements: (1) an array of completions; and
 
147
 *    (2) the leading text completed.
 
148
 *
 
149
 * Example:
 
150
 *  complete('var foo = sys.')
 
151
 *    -> [['sys.print', 'sys.debug', 'sys.log', 'sys.inspect', 'sys.pump'],
 
152
 *        'sys.' ]
 
153
 *
 
154
 * Warning: This eval's code like "foo.bar.baz", so it will run property
 
155
 * getter code.
 
156
 */
 
157
 
 
158
REPLServer.prototype.complete = function (line) {
 
159
  var completions,
 
160
      completionGroups = [],  // list of completion lists, one for each inheritance "level"
 
161
      completeOn,
 
162
      match, filter, i, j, group, c;
 
163
 
 
164
  // REPL commands (e.g. ".break").
 
165
  var match = null;
 
166
  match = line.match(/^\s*(\.\w*)$/);
 
167
  if (match) {
 
168
    completionGroups.push(['.break', '.clear', '.exit', '.help']);
 
169
    completeOn = match[1];
 
170
    if (match[1].length > 1) {
 
171
      filter = match[1];
 
172
    }
 
173
  }
 
174
 
 
175
  // require('...<Tab>')
 
176
  else if (match = line.match(/\brequire\s*\(['"](([\w\.\/-]+\/)?([\w\.\/-]*))/)) {
 
177
    //TODO: suggest require.exts be exposed to be introspec registered extensions?
 
178
    //TODO: suggest include the '.' in exts in internal repr: parity with `path.extname`.
 
179
    var exts = [".js", ".node"];
 
180
    var indexRe = new RegExp('^index(' + exts.map(regexpEscape).join('|') + ')$');
 
181
 
 
182
    completeOn = match[1];
 
183
    var subdir = match[2] || "";
 
184
    var filter = match[1];
 
185
    var dir, files, f, name, base, ext, abs, subfiles, s;
 
186
    group = [];
 
187
    for (i = 0; i < require.paths.length; i++) {
 
188
      dir = require.paths[i];
 
189
      if (subdir && subdir[0] === '/') {
 
190
        dir = subdir;
 
191
      } else if (subdir) {
 
192
        dir = path.join(dir, subdir);
 
193
      }
 
194
      try {
 
195
        files = fs.readdirSync(dir);
 
196
      } catch (e) {
 
197
        continue;
 
198
      }
 
199
      for (f = 0; f < files.length; f++) {
 
200
        name = files[f];
 
201
        ext = path.extname(name);
 
202
        base = name.slice(0, -ext.length);
 
203
        if (base.match(/-\d+\.\d+(\.\d+)?/) || name === ".npm") {
 
204
          // Exclude versioned names that 'npm' installs.
 
205
          continue;
 
206
        }
 
207
        if (exts.indexOf(ext) !== -1) {
 
208
          if (!subdir || base !== "index") {
 
209
            group.push(subdir + base);
 
210
          }
 
211
        } else {
 
212
          abs = path.join(dir, name);
 
213
          try {
 
214
            if (fs.statSync(abs).isDirectory()) {
 
215
              group.push(subdir + name + '/');
 
216
              subfiles = fs.readdirSync(abs);
 
217
              for (s = 0; s < subfiles.length; s++) {
 
218
                if (indexRe.test(subfiles[s])) {
 
219
                  group.push(subdir + name);
 
220
                }
 
221
              }
 
222
            }
 
223
          } catch(e) {}
 
224
        }
 
225
      }
 
226
    }
 
227
    if (group.length) {
 
228
      completionGroups.push(group);
 
229
    }
 
230
 
 
231
    if (!subdir) {
 
232
      // Kind of lame that this needs to be updated manually.
 
233
      // Intentionally excluding moved modules: posix, utils.
 
234
      var builtinLibs = ['assert', 'buffer', 'child_process', 'crypto', 'dgram',
 
235
        'dns', 'events', 'file', 'freelist', 'fs', 'http', 'net', 'path',
 
236
        'querystring', 'readline', 'repl', 'string_decoder', 'sys', 'tcp', 'url'];
 
237
      completionGroups.push(builtinLibs);
 
238
    }
 
239
  }
 
240
 
 
241
  // Handle variable member lookup.
 
242
  // We support simple chained expressions like the following (no function
 
243
  // calls, etc.). That is for simplicity and also because we *eval* that
 
244
  // leading expression so for safety (see WARNING above) don't want to
 
245
  // eval function calls.
 
246
  //
 
247
  //   foo.bar<|>     # completions for 'foo' with filter 'bar'
 
248
  //   spam.eggs.<|>  # completions for 'spam.eggs' with filter ''
 
249
  //   foo<|>         # all scope vars with filter 'foo'
 
250
  //   foo.<|>        # completions for 'foo' with filter ''
 
251
  else if (line.length === 0 || line[line.length-1].match(/\w|\./)) {
 
252
    var simpleExpressionPat = /(([a-zA-Z_]\w*)\.)*([a-zA-Z_]\w*)\.?$/;
 
253
    match = simpleExpressionPat.exec(line);
 
254
    if (line.length === 0 || match) {
 
255
      var expr;
 
256
      completeOn = (match ? match[0] : "");
 
257
      if (line.length === 0) {
 
258
        filter = "";
 
259
        expr = "";
 
260
      } else if (line[line.length-1] === '.') {
 
261
        filter = "";
 
262
        expr = match[0].slice(0, match[0].length-1);
 
263
      } else {
 
264
        var bits = match[0].split('.');
 
265
        filter = bits.pop();
 
266
        expr = bits.join('.');
 
267
      }
 
268
      //console.log("expression completion: completeOn='"+completeOn+"' expr='"+expr+"'");
 
269
 
 
270
      // Resolve expr and get its completions.
 
271
      var obj, memberGroups = [];
 
272
      if (!expr) {
 
273
        completionGroups.push(Object.getOwnPropertyNames(this.context));
 
274
        // Global object properties
 
275
        // (http://www.ecma-international.org/publications/standards/Ecma-262.htm)
 
276
        completionGroups.push(["NaN", "Infinity", "undefined",
 
277
          "eval", "parseInt", "parseFloat", "isNaN", "isFinite", "decodeURI",
 
278
          "decodeURIComponent", "encodeURI", "encodeURIComponent",
 
279
          "Object", "Function", "Array", "String", "Boolean", "Number",
 
280
          "Date", "RegExp", "Error", "EvalError", "RangeError",
 
281
          "ReferenceError", "SyntaxError", "TypeError", "URIError",
 
282
          "Math", "JSON"]);
 
283
        // Common keywords. Exclude for completion on the empty string, b/c
 
284
        // they just get in the way.
 
285
        if (filter) {
 
286
          completionGroups.push(["break", "case", "catch", "const",
 
287
            "continue", "debugger", "default", "delete", "do", "else", "export",
 
288
            "false", "finally", "for", "function", "if", "import", "in",
 
289
            "instanceof", "let", "new", "null", "return", "switch", "this",
 
290
            "throw", "true", "try", "typeof", "undefined", "var", "void",
 
291
            "while", "with", "yield"])
 
292
        }
 
293
      } else {
 
294
        try {
 
295
          obj = evalcx(expr, this.context, "repl");
 
296
        } catch (e) {
 
297
          //console.log("completion eval error, expr='"+expr+"': "+e);
 
298
        }
 
299
        if (obj != null) {
 
300
          if (typeof obj === "object" || typeof obj === "function") {
 
301
            memberGroups.push(Object.getOwnPropertyNames(obj));
 
302
          }
 
303
          var p = obj.constructor.prototype; // works for non-objects
 
304
          try {
 
305
            var sentinel = 5;
 
306
            while (p !== null) {
 
307
              memberGroups.push(Object.getOwnPropertyNames(p));
 
308
              p = Object.getPrototypeOf(p);
 
309
              // Circular refs possible? Let's guard against that.
 
310
              sentinel--;
 
311
              if (sentinel <= 0) {
 
312
                break;
 
313
              }
 
314
            }
 
315
          } catch (e) {
 
316
            //console.log("completion error walking prototype chain:" + e);
 
317
          }
 
318
        }
 
319
 
 
320
        if (memberGroups.length) {
 
321
          for (i = 0; i < memberGroups.length; i++) {
 
322
            completionGroups.push(memberGroups[i].map(function(member) {
 
323
              return expr + '.' + member;
 
324
            }));
 
325
          }
 
326
          if (filter) {
 
327
            filter = expr + '.' + filter;
 
328
          }
 
329
        }
 
330
      }
 
331
    }
 
332
  }
 
333
 
 
334
  // Filter, sort (within each group), uniq and merge the completion groups.
 
335
  if (completionGroups.length && filter) {
 
336
    var newCompletionGroups = [];
 
337
    for (i = 0; i < completionGroups.length; i++) {
 
338
      group = completionGroups[i].filter(function(elem) {
 
339
        return elem.indexOf(filter) == 0;
 
340
      });
 
341
      if (group.length) {
 
342
        newCompletionGroups.push(group);
 
343
      }
 
344
    }
 
345
    completionGroups = newCompletionGroups;
 
346
  }
 
347
  if (completionGroups.length) {
 
348
    var uniq = {};  // unique completions across all groups
 
349
    completions = [];
 
350
    // Completion group 0 is the "closest" (least far up the inheritance chain)
 
351
    // so we put its completions last: to be closest in the REPL.
 
352
    for (i = completionGroups.length - 1; i >= 0; i--) {
 
353
      group = completionGroups[i];
 
354
      group.sort();
 
355
      for (var j = 0; j < group.length; j++) {
 
356
        c = group[j];
 
357
        if (!uniq.hasOwnProperty(c)) {
 
358
          completions.push(c);
 
359
          uniq[c] = true;
 
360
        }
 
361
      }
 
362
      completions.push(""); // separator btwn groups
 
363
    }
 
364
    while (completions.length && completions[completions.length-1] === "") {
 
365
      completions.pop();
 
366
    }
 
367
  }
 
368
 
 
369
  return [completions || [], completeOn];
108
370
};
109
371
 
110
372
/**
111
373
 * Used to parse and execute the Node REPL commands.
112
 
 * 
 
374
 *
113
375
 * @param {cmd} cmd The command entered to check
114
 
 * @returns {Boolean} If true it means don't continue parsing the command 
 
376
 * @returns {Boolean} If true it means don't continue parsing the command
115
377
 */
116
378
 
117
379
REPLServer.prototype.parseREPLKeyword = function (cmd) {
118
380
  var self = this;
119
 
  
 
381
 
120
382
  switch (cmd) {
121
383
  case ".break":
122
384
    self.buffered_cmd = '';
123
385
    self.displayPrompt();
124
386
    return true;
125
387
  case ".clear":
126
 
    self.stream.write("Clearing Scope...\n");
 
388
    self.stream.write("Clearing context...\n");
127
389
    self.buffered_cmd = '';
128
 
    setScope();
 
390
    resetContext();
129
391
    self.displayPrompt();
130
392
    return true;
131
393
  case ".exit":
133
395
    return true;
134
396
  case ".help":
135
397
    self.stream.write(".break\tSometimes you get stuck in a place you can't get out... This will get you out.\n");
136
 
    self.stream.write(".clear\tBreak, and also clear the local scope.\n");
 
398
    self.stream.write(".clear\tBreak, and also clear the local context.\n");
137
399
    self.stream.write(".exit\tExit the prompt\n");
138
400
    self.stream.write(".help\tShow repl options\n");
139
401
    self.displayPrompt();
142
404
  return false;
143
405
};
144
406
 
145
 
/**
146
 
 * Trims Whitespace from a line.
147
 
 * 
148
 
 * @param {String} cmd The string to trim the whitespace from
149
 
 * @returns {String} The trimmed string 
150
 
 */
151
 
REPLServer.prototype.trimWhitespace = function (cmd) {
152
 
  var trimmer = /^\s*(.+)\s*$/m, 
 
407
function trimWhitespace (cmd) {
 
408
  var trimmer = /^\s*(.+)\s*$/m,
153
409
    matches = trimmer.exec(cmd);
154
410
 
155
411
  if (matches && matches.length === 2) {
156
412
    return matches[1];
157
413
  }
158
 
};
 
414
}
 
415
 
 
416
function regexpEscape(s) {
 
417
  return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
 
418
}
159
419
 
160
420
/**
161
421
 * Converts commands that use var and function <name>() to use the
162
 
 * local exports.scope when evaled. This provides a local scope
 
422
 * local exports.context when evaled. This provides a local context
163
423
 * on the REPL.
164
 
 * 
 
424
 *
165
425
 * @param {String} cmd The cmd to convert
166
426
 * @returns {String} The converted command
167
427
 */
168
 
REPLServer.prototype.convertToScope = function (cmd) {
 
428
REPLServer.prototype.convertToContext = function (cmd) {
169
429
  var self = this, matches,
170
430
    scopeVar = /^\s*var\s*([_\w\$]+)(.*)$/m,
171
431
    scopeFunc = /^\s*function\s*([_\w\$]+)/;
172
 
  
173
 
  // Replaces: var foo = "bar";  with: self.scope.foo = bar;
 
432
 
 
433
  // Replaces: var foo = "bar";  with: self.context.foo = bar;
174
434
  matches = scopeVar.exec(cmd);
175
435
  if (matches && matches.length === 3) {
176
 
    return "self.scope." + matches[1] + matches[2];
 
436
    return "self.context." + matches[1] + matches[2];
177
437
  }
178
 
  
 
438
 
179
439
  // Replaces: function foo() {};  with: foo = function foo() {};
180
440
  matches = scopeFunc.exec(self.buffered_cmd);
181
441
  if (matches && matches.length === 2) {
182
442
    return matches[1] + " = " + self.buffered_cmd;
183
443
  }
184
 
  
 
444
 
185
445
  return cmd;
186
446
};