A JScript interactive interpreter shell for the Windows Script Host

A few weeks ago a friend of mine who was starting to code WSH scripts asked me if there was an interactive shell for it. I told him I didn’t know of one, but it got me thinking. Python users are well aware of its interactive shell, and indeed, take it for granted. The ability to execute statements immediately, one line at a time, is pretty fundamental. Yet, Windows Script Host offers no such built-in functionality.

As a new WSH script writer, I would often find myself in a cycle of Edit, Save, Run, quite similar to the Edit, Save, Compile cycle of native code, with small snippets or even one-liners. As my scripts exceeded a certain threshold of complexity, I would find myself using the script debugger. Unfortunately, since it provides a read-only view of the debugged script, when I wanted to make my changes and test them, I would have to switch to the editor window, make my changes and start the session all over again.

I recall one of my first programming experiences, around the age of 9. I was toying with my brother’s old Atari 800XL, initially in BASIC. The machine had a BASIC interpreter built-in to its ROM and had a measly 64K of memory. When it was turned on, a friendly “READY” banner written in white over a blue background greeted you to the BASIC interpreter. The ability to interpret statements for rapid modeling was considered so fundamental it was this, not a disk operating system, that was the core of the machine.

Fast forward back to the present. My friend’s question had me searching for a solution. I did not find an interpreter targeting WSH, but I did find a variety of JavaScript shells for the web browser, like this one. These are good candidates for brushing up on the HTML DOM, but are less useful to those using WSH. For instance, attempting to model automation controllers quickly brings you into the realm of warnings and denials from the browser’s security apparatus. In the case of the specific shell in question, its approach of having the user use Shift-Enter for multi-line entry was inconvenient, since you had to keep doing so until your code block was complete.

JScript lends itself well for implementing a self-hosted shell through the “eval” keyword. As I was examining the input mechanisms available to a command-line WSH script, I saw that the WScript.StdIn object was a TextStream, only supporting newline-terminated input. This means I could not implement the same Shift-Enter based approach for multi-line input used by the browser hosted shell mentioned above.

During my search, I also found two JavaScript shells that are not browser-based but do not target the Windows Scripting Host. One was a part of Spidermonkey, which is Mozilla’s classic JavaScript implementation (which is set to be retired and replaced by the JIT-based Tamarin, the open source version of Adobe Flash’s ActionScript VM, in future versions of Firefox). The other was a part of Rhino, an implementation of JavaScript in Java.

I examined their source to determine what was their approach to multi-line input. It appeared that both the Spidermonkey and the Rhino shells used the underlying script language implementation’s functionality for determining whether a given string is a “compilable entity.” They would keep on reading lines until that condition was met.

Unfortunately, it did not seem like I could adopt a similar approach. Calling “eval” repeatedly until successful is problematic. Even if I were to implement the shell in native code using the Active Scripting hosting interfaces instead, it did not appear as though IActiveScript or the related interfaces provided a similar “compile testing” method.

Defeated, I opted for a simple approach where a blank line initiates multi-line input and two consecutive blank lines terminate it.

Pardon me for the coarse, unpolished illustration code:

function hex(n) {
    if (n >= 0) {
        return n.toString(16);
    } else {
        n += 0x100000000;
        return n.toString(16);
    }
}
var scriptText;
var previousLine;
var line;
var result;
while(true) {
    WScript.StdOut.Write("jscript> ");
    if (WScript.StdIn.AtEndOfStream) {
        WScript.Echo("Bye.");
        break;
    }
    line = WScript.StdIn.ReadLine();
    scriptText = line + "\n";
    if (line === "") {
        WScript.Echo(
            "Enter two consecutive blank lines to terminate multi-line input.");
        do {
            if (WScript.StdIn.AtEndOfStream) {
                break;
            }
            previousLine = line;
            line = WScript.StdIn.ReadLine();
            line += "\n";
            scriptText += line;
        } while(previousLine != "\n" || line != "\n");
    }
    try {
        result = eval(scriptText);
    } catch (error) {
        WScript.Echo("0x" + hex(error.number) + " " + error.name + ": " +
            error.message);
    }
    if (result) {
        try {
            WScript.Echo(result);
        } catch (error) {
            WScript.Echo("<<<unprintable>>>");
        }
    }
    result = null;
}

This is simple enough and is quite useful for the majority of cases. It does have its disadvantages, however. Notably, the surrounding code of the shell is leaked into the namespace accessible by the interpreted snippets. For example, typing “hex” exposes the error code conversion function. However, for my needs, I found this quite satisfactory.

If anyone can offer an improved implementation, I’d be happy to see it in the comments.

Save this code to a file, like shell.js, and use “cscript shell.js” to start it. Multi-line input is performed as described above. Ctrl-Z can be used to quit.

A nice stunt you can pull with this is wrap the shell in a .WSF referencing your favorite type libraries. For example, consider this shell.wsf:

<job>
    <reference object="Scripting.FileSystemObject" />
    <script language="JScript" src="shell.js" />
</job>

If you start a shell with “cscript shell.wsf”, the shell instance will have access to type library constants like “ForReading”, “ForAppending” and so forth.

Although I’m not much of a VBScript fan, I considered doing something similar for it, since it could be quite handy for testing those pesky automation objects that take SAFEARRAYs and are thus not that JScript friendly. However, VBScript’s distinction between expressions and statements (and its Eval function vs. the Execute & ExecuteGlobal keywords) make such a thing a bit more complicated. It is also not clear whether the interpreter should opt for executing statements using Execute or ExecuteGlobal, and in what cases. If anyone is up for implementing this, I’d love to see it.

Have fun.

About these ads

18 thoughts on “A JScript interactive interpreter shell for the Windows Script Host

  1. I added this to the top of the script so starting with wscript will just spawn cscript

    try {WScript.StdOut.Write(“\n”);}catch(error){
    var WshShell = new ActiveXObject(“WScript.Shell”);
    var sh=WshShell.ExpandEnvironmentStrings(“%COMSPEC%”);if(!sh.indexOf(“%”))sh=”cmd.exe”;
    WshShell.Run(sh+” /K cscript.exe /nologo \””+WScript.ScriptFullName+”\””);WScript.Quit();}

  2. A nice addition, Anders.

    Beyond the obvious lack of a built-in interpreter shell addressed in the post, I always found the inability to choose whether to run with wscript or cscript from within a script a great deficiency of WSH.

  3. now that I actually looked it up in the helpfile, WScript.FullName contains the path to the interpreter, so you could just do indexOf(“cscript.exe”) on it. DOH (atleast the try code should be able to handle 3rd party script hosts aswell)

  4. Pingback: More on testing JavaScript code « News from MathTran

  5. ‘** Dave Cason’s VBScript Interactive Shell
    ‘** Updates will be available @ http://davecason.com
    ‘** …when the website is up and running.
    ‘** Version 1.00.2008.09.16
    ‘** Inspired by Koby Kahane’s (KK’s) JScript version
    ‘** http://kobyk.wordpress.com/2007/09/14/
    ‘** … a-jscript-interactive-interpreter-shell-for-the-windows-script-host/
    ‘** You must open this script using CScript or with the command prompt.
    Option Explicit
    dim scriptText, previousLine, line, result
    while(true)
    With WScript
    .StdOut.Write(“vbscript> “)
    if (.StdIn.AtEndOfStream) then
    .Echo(“Bye.”)
    .Quit
    end if
    line = .StdIn.ReadLine()
    scriptText = line + vbNewLine
    if (line=””) then
    .Echo(“Enter two consecutive blank lines to ” & _
    “terminate multi-line input.”)
    previousLine = line
    line = .StdIn.ReadLine()
    line = line & vbNewLine
    scriptText = scriptText & line
    if not (previousLine vbNewLine and line vbNewLine) then
    .Echo(“Bye.”)
    .Quit
    end if
    end if
    on error resume next
    err.clear
    result = execute(scriptText)
    if err.number = 0 then
    .echo “…by Execute.”
    else
    err.clear
    result = eval(scriptText)
    if (result) then
    err.clear
    .Echo(“By Eval: ” & result)
    elseif err.number 0 then
    .Echo(Join(Array(“Eval Error”, _
    err.number,err.source,err.description),”:”))
    err.clear
    else
    .Echo(“Error:Undefined Error(Sorry!)”)
    end if
    end if
    result = Empty
    err.clear
    on error goto 0
    End With
    wend

  6. Notes:
    ** You can use “Open With Command Prompt” to launch the script directly with CScript if you have that powertoy installed.
    ** For the VBScript version, here are some things you can do:
    dim i
    i = 1
    for i = i to 10:.echo i:next
    set shell = CreateObject(“WScript.Shell”)
    shell.popup “Hello World!”,i,”Closes in ” & i & ” seconds.”,vbInformation

  7. I just noticed all of my left and right chevrons disappeared. I will repost an html escaped version:

    ‘** Dave Cason’s VBScript Interactive Shell
    ‘** Updates will be available @ http://davecason.com
    ‘** …when the website is up and running.
    ‘** Version 1.00.2008.09.16
    ‘** Inspired by Koby Kahane’s (KK’s) JScript version
    ‘** http://kobyk.wordpress.com/2007/09/14/
    ‘** … a-jscript-interactive-interpreter-shell …
    ‘** … -for-the-windows-script-host/
    ‘** Open this script using CScript or with the command prompt.
    Option Explicit
    dim scriptText, previousLine, line, result
    while(true)
    With WScript
    .StdOut.Write(“vbscript> “)
    if (.StdIn.AtEndOfStream) then
    .Echo(“Bye.”)
    .Quit
    end if
    line = .StdIn.ReadLine()
    scriptText = line + vbNewLine
    if (line=””) then
    .Echo(“Enter two consecutive blank lines to ” & _
    “terminate multi-line input.”)
    previousLine = line
    line = .StdIn.ReadLine()
    line = line & vbNewLine
    scriptText = scriptText & line
    if not (previousLine <> vbNewLine and line <> vbNewLine) then
    .Echo(“Bye.”)
    .Quit
    end if
    end if
    on error resume next
    err.clear
    result = execute(scriptText)
    if err.number = 0 then
    .echo “…by Execute.”
    else
    err.clear
    result = eval(scriptText)
    if (result) then
    err.clear
    .Echo(“By Eval: ” & result)
    elseif err.number <> 0 then
    .Echo(Join(Array(“Eval Error”, _
    err.number,err.source,err.description),”:”))
    err.clear
    else
    .Echo(“Error:Undefined Error(Sorry!)”)
    end if
    end if
    result = Empty
    err.clear
    on error goto 0
    End With
    wend

  8. One more comment:
    if you copy/paste the above vbscript, you will need to find and replace the three types of special quotes with regular ones: Left double, right double, and single.

  9. >>> Notably, the surrounding code of the shell is leaked into the namespace accessible by the interpreted snippets.
    I have moved all the variables and the function hex into the global variable GSHELL.
    The function hex is now only exposed through GSHELL.

    (function () {

    var GSHELL = {};

    GSHELL.hex = function (n) {
    if (n >= 0) {
    return n.toString(16);
    } else {
    n += 0×100000000;
    return n.toString(16);
    }
    };

    while(true) {
    WScript.StdOut.Write(“jscript> “);
    if (WScript.StdIn.AtEndOfStream) {
    WScript.Echo(“Bye.”);
    break;
    }
    GSHELL.line = WScript.StdIn.ReadLine();
    GSHELL.scriptText = GSHELL.line + “\n”;
    if (GSHELL.line === “”) {
    WScript.Echo(
    “Enter two consecutive blank lines to terminate multi-line input.”);
    do {
    if (WScript.StdIn.AtEndOfStream) {
    break;
    }
    GSHELL.previousLine = GSHELL.line;
    GSHELL.line = WScript.StdIn.ReadLine();
    GSHELL.line += “\n”;
    GSHELL.scriptText += GSHELL.line;
    } while(GSHELL.previousLine != “\n” || GSHELL.line != “\n”);
    }
    try {
    GSHELL.result = eval(GSHELL.scriptText);
    } catch (error) {
    WScript.Echo(“0x” + GSHELL.hex(error.number) + ” ” + error.name + “: ” +
    error.message);
    }
    if (GSHELL.result) {
    try {
    WScript.Echo(GSHELL.result);
    } catch (error) {
    WScript.Echo(“<<>>”);
    }
    }
    GSHELL.result = null;
    }
    }());

    I created a shell.wsf which contains as first file util.js
    All the stuff I put into util.js is now avalaible within the console.
    For instance a print function :
    var print = function(something) {WScript.Echo(something)}

  10. I started a new project at http://code.google.com/p/jscriptshell/ to implement a jscript interactive shell like your post demonstrated. I try to add support of multi-line mode. Currently you need type ctrl-z to force termination of a bad input, but it may be possible to analyze syntax error by generate and test (trial by error) — aka write a script to file system and call cscript.exe which can return the line number and char position of the error. If the position is not same as the the position of the final significant character of the script file we generated, we can suppose such syntax error is not recoverable by read more lines, then we can terminate the multi-line input and output the error msg.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s