Listening To Reason

random musings about technologies by Andy Norris

04 March 2007

A Simple JavaScript Command Line Interpreter for Windows in JScript.Net

I've been playing around with JavaScript quite a bit lately, and one thing I enjoyed having was a web-based JavaScript shell for trying out JavaScript commands quickly. I was also thinking about the fact that my command-line skills have gotten a little rusty lately, and that I should get those back into shape. I remembered JScript.Net, which I've been meaning to read more about anyway as a possible solution for embedded scripting in .net applications. So I decided to write a simple interactive command-line interpreter, and see how far I got.

Working with JScript

The JScript compiler, jsc ships with the .Net SDK. If you Have Visual Studio (I've tested this casually with both 2003 and 2005), you can launch the Visual Studio command prompt and just type jsc foo.js to get it to compile jscript. The resulting executable will run against the .Net runtime, and unlike C# or VB.Net, it doesn't require you to create a class with a main function. In fact, here is "Hello World:"


print("Hello, World!");

That's all there is to it.

If you save it as hello.js and enter jsc hello.js, it will compile to hello.exe, and you can then type hello at the command line to run it. Dead simple.

That's the good news. The bad news is that the JScript.Net compiler is configured to be much more .Net-like than any other JavaScript compiler you're likely used to dealing with. For example, suppose you create the following program, and save it as bar.js:


function bar() { 
  print(5);
}

bar = function() {
  print(9);
}

Straightforward, right? It declares a function bound to bar, then changes bar to point to a new function. Well, if you compile this with jsc bar.js, it will throw an error at you:

bar.js(5,1) : error JS5040: 'bar' is read-only

In fact, there's enough annoying limitations that the obvious conclusion to draw is that JScript.Net is flatly broken.

Fortunately, there's a way around all the limitations. The reason it's so broken is that JScript is trying to produce code that will run reasonably quickly on the .Net CLR. And as many people have noticed by now, there aren't a lot of affordances in the CLR for dynamic languages. This is called "fast mode," and it's enabled by default. But if you care more about compatibility with the ECMAScript spec than speed, you can turn it off.

If you type jsc /fast- bar.js, then it will compile just fine, and pretty much all the features you expect will be there.

A Simple Interpreter

Once I figured out how to make the full JavaScript language compile, it was pretty easy to make an interactive interpreter. The core of one is ridiculously simple:


import System;

for ( ;; ) {
  Console.Write("%");  // prompt

  var input = Console.ReadLine();

  if(input == "quit" || input == "exit") { break; }

  try {
    Console.WriteLine(eval(input, "unsafe"));
  } catch(ex) {
    Console.WriteLine(ex.ToString());
  }
}

This is really all there is to a read-eval-print loop.

I refined it a little from there, with support for multiline statements (by ending with a backslash, which is a little cheesy, but I don't think I can support something like shift-enter without using a more elaborate IO methodology).

I also added a load command, so you can bring in libraries from file. The load has to be handled specially, so that the resulting code will be in the global namespace and not inside a closure, where the bindings will be lost on function exit. I think I can do better than the current implementation, however. load("baz.js") will load a library "baz.js" from the working directory. load("c:\\scripts\\baz.js") will do the obvious thing -- you should be able to use any path you choose to declare.

Finally, I added a cmd() function, that will execute its argument in the windows command shell, and return stdout and stderr to you. As with load, the argument will be evaluated like any other javascript expression, so if you do something like:

var xyzzy = "directory";
cmd(xyzzy.substr(0,3))

then xyzzy.substr(0,3) will evaluate to "dir", and the command will list the working directory. Of course, you can also do things like

%var dir = function() { return cmd("dir"); }
function() { return cmd("dir"); }
%dir()

which will then list your directory. So if you have common tasks like listing directories, listing file contents, etc., you can just map them to javascript functions for easy use. Obviously, you could also map ls(), if you prefer Unix-style commands. Of course, with the parentheses, it's more verbose than using a real shell -- or even a language like perl -- but it still makes general command line functionality fairly accessible.

There are obviously plenty of ways in which the code could be enhanced beyond this, but this seemed like enough to pass along to anyone who might be interested. If you have ideas for additional functionality, let me know. Or, for that matter, implement it yourself -- it's a simple code base. If you do anything cool, I'd love to hear about it.

The source

I don't have anywhere to post this for file download, and it's short, so I'm just going to post it here, inline. If you save this as "ijs.js", you can compile it with

jsc /fast- ijs.js

Then you can just run it at the command line with ijs.

Here's the source in full. It clocks in at under 100 lines at the moment:


// ijs.js -- an interactive javascript interpreter for Windows in JScript.Net
// Copyright 2007 Andrew Norris
// Anyone is free to use, alter, or redistribute this code for any purpose.

import System;
import System.Diagnostics;
import System.IO;

function loadFile(input) {
  // pull out what's inside the parens
  var begin = input.indexOf("(");
  if (begin == -1) {
    Console.WriteLine("syntax error: bad argument to load");
    return null;
  }
  var end = input.lastIndexOf(")");
  if (end == -1 || end < begin) {
    Console.WriteLine("syntax error: bad argument to load");
    return null;
  }
  var fileExpr = input.substring(begin+1, end);

  var fileName = "";
  try {
    fileName = eval(fileExpr, "unsafe");
  } catch(ex) {
    Console.WriteLine(
        "Exception evaluating file expression '" + fileExpr + "':\n" + ex.ToString());
    return null;
  }

  try {
    var file = new StreamReader(fileName);
    var result = file.ReadToEnd();
    file.Close();
    return result;
  } catch(ex) {
    Console.WriteLine(
        "Exception loading file '" + fileName + "':\n" + ex.ToString());
    return null;
  }
}

function cmd(cmdName) {
  var process = new Process();
  process.StartInfo.UseShellExecute = false;
  process.StartInfo.RedirectStandardOutput = true;
  process.StartInfo.RedirectStandardError = true;
  process.StartInfo.CreateNoWindow = true;
  process.StartInfo.FileName = "cmd.exe";
  process.StartInfo.Arguments = "/c " + cmdName;

  try {
    process.Start();
  } catch (ex) { 
    Console.WriteLine(ex.ToString());
  }
  Console.WriteLine(process.StandardOutput.ReadToEnd());
  Console.WriteLine(process.StandardError.ReadToEnd());
}

Console.WriteLine("Welcome to Interactive JavaScript.");

var cont = false;
for ( ;; ) {
  Console.Write("%");  // prompt

  var input = (cont) ? input + Console.ReadLine() : Console.ReadLine();

  // trim leading and trailing whitespace
  input = input.replace(/^[ \t]+/, "");
  input = input.replace(/[ \t]+$/, "");

  // end execution on quit
  if(input == "quit" || input == "exit") { break; }

  if (input.charAt(input.length-1) == '\\') {
    // lines ending in backslash continue on the next line
    input = input.substring(0,input.length-2);
    cont = true;
    continue;
  } else if(input.match(/^load\W/)) {
    // load file to eval
    input = loadFile(input);
    if (!input) { continue; }
  }
  cont=false;

  // eval the input
  try {
    Console.WriteLine(eval(input, "unsafe"));
  } catch(ex) {
    Console.WriteLine(ex.ToString());
  }
}

Labels: , , , ,