Jump To …

repl.coffee

A very simple Read-Eval-Print-Loop. Compiles one line at a time to JavaScript and evaluates it. Good for simple tests, or poking around the Node.js API. Using it looks like this:

coffee> console.log "#{num} bottles of beer" for num in [99..1]

Start by opening up stdin and stdout.

stdin = process.openStdin()
stdout = process.stdout

Require the coffee-script module to get access to the compiler.

CoffeeScript = require './coffee-script'
readline     = require 'readline'
{inspect}    = require 'util'
{Script}     = require 'vm'
Module       = require 'module'

REPL Setup

Config

REPL_PROMPT = 'coffee> '
REPL_PROMPT_MULTILINE = '------> '
REPL_PROMPT_CONTINUATION = '......> '
enableColours = no
unless process.platform is 'win32'
  enableColours = not process.env.NODE_DISABLE_COLORS

Log an error.

error = (err) ->
  stdout.write (err.stack or err.toString()) + '\n'

Autocompletion

Regexes to match complete-able bits of text.

ACCESSOR  = /\s*([\w\.]+)(?:\.(\w*))$/
SIMPLEVAR = /(\w+)$/i

Returns a list of completions, and the completed text.

autocomplete = (text) ->
  completeAttribute(text) or completeVariable(text) or [[], text]

Attempt to autocomplete a chained dotted attribute: one.two.three.

completeAttribute = (text) ->
  if match = text.match ACCESSOR
    [all, obj, prefix] = match
    try obj = Script.runInThisContext obj
    catch e
      return
    return unless obj?
    obj = Object obj
    candidates = Object.getOwnPropertyNames obj
    while obj = Object.getPrototypeOf obj
      for key in Object.getOwnPropertyNames obj when key not in candidates
        candidates.push key
    completions = getCompletions prefix, candidates
    [completions, prefix]

Attempt to autocomplete an in-scope free variable: one.

completeVariable = (text) ->
  free = text.match(SIMPLEVAR)?[1]
  free = "" if text is ""
  if free?
    vars = Script.runInThisContext 'Object.getOwnPropertyNames(Object(this))'
    keywords = (r for r in CoffeeScript.RESERVED when r[..1] isnt '__')
    candidates = vars
    for key in keywords when key not in candidates
      candidates.push key
    completions = getCompletions free, candidates
    [completions, free]

Return elements of candidates for which prefix is a prefix.

getCompletions = (prefix, candidates) ->
  el for el in candidates when 0 is el.indexOf prefix

Make sure that uncaught exceptions don't kill the REPL.

process.on 'uncaughtException', error

The current backlog of multi-line code.

backlog = ''

The main REPL function. run is called every time a line of code is entered. Attempt to evaluate the command. If there's an exception, print it out instead of exiting.

run = (buffer) ->

remove single-line comments

  buffer = buffer.replace /(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, "$1$2$3"

remove trailing newlines

  buffer = buffer.replace /[\r\n]+$/, ""
  if multilineMode
    backlog += "#{buffer}\n"
    repl.setPrompt REPL_PROMPT_CONTINUATION
    repl.prompt()
    return
  if !buffer.toString().trim() and !backlog
    repl.prompt()
    return
  code = backlog += buffer
  if code[code.length - 1] is '\\'
    backlog = "#{backlog[...-1]}\n"
    repl.setPrompt REPL_PROMPT_CONTINUATION
    repl.prompt()
    return
  repl.setPrompt REPL_PROMPT
  backlog = ''
  try
    _ = global._
    returnValue = CoffeeScript.eval "_=(#{code}\n)", {
      filename: 'repl'
      modulename: 'repl'
    }
    if returnValue is undefined
      global._ = _
    repl.output.write "#{inspect returnValue, no, 2, enableColours}\n"
  catch err
    error err
  repl.prompt()

if stdin.readable and stdin.isRaw

handle piped input

  pipedInput = ''
  repl =
    prompt: -> stdout.write @_prompt
    setPrompt: (p) -> @_prompt = p
    input: stdin
    output: stdout
    on: ->
  stdin.on 'data', (chunk) ->
    pipedInput += chunk
    return unless /\n/.test pipedInput
    lines = pipedInput.split "\n"
    pipedInput = lines[lines.length - 1]
    for line in lines[...-1] when line
      stdout.write "#{line}\n"
      run line
    return
  stdin.on 'end', ->
    for line in pipedInput.trim().split "\n" when line
      stdout.write "#{line}\n"
      run line
    stdout.write '\n'
    process.exit 0
else

Create the REPL by listening to stdin.

  if readline.createInterface.length < 3
    repl = readline.createInterface stdin, autocomplete
    stdin.on 'data', (buffer) -> repl.write buffer
  else
    repl = readline.createInterface stdin, stdout, autocomplete

multilineMode = off

Handle multi-line mode switch

repl.input.on 'keypress', (char, key) ->

test for Ctrl-v

  return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v'
  cursorPos = repl.cursor
  repl.output.cursorTo 0
  repl.output.clearLine 1
  multilineMode = not multilineMode
  repl._line() if not multilineMode and backlog
  backlog = ''
  repl.setPrompt (newPrompt = if multilineMode then REPL_PROMPT_MULTILINE else REPL_PROMPT)
  repl.prompt()
  repl.output.cursorTo newPrompt.length + (repl.cursor = cursorPos)

Handle Ctrl-d press at end of last line in multiline mode

repl.input.on 'keypress', (char, key) ->
  return unless multilineMode and repl.line

test for Ctrl-d

  return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'd'
  multilineMode = off
  repl._line()

repl.on 'attemptClose', ->
  if multilineMode
    multilineMode = off
    repl.output.cursorTo 0
    repl.output.clearLine 1
    repl._onLine repl.line
    return
  if backlog or repl.line
    backlog = ''
    repl.historyIndex = -1
    repl.setPrompt REPL_PROMPT
    repl.output.write '\n(^C again to quit)'
    repl._line (repl.line = '')
  else
    repl.close()

repl.on 'close', ->
  repl.output.write '\n'
  repl.input.destroy()

repl.on 'line', run

repl.setPrompt REPL_PROMPT
repl.prompt()