Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / JScript.NET

A JavaScript Shell

4.25/5 (4 votes)
18 Nov 2010CPOL4 min read 31.3K   739  
Bases to create your own JavaScript shell for your application

Introduction

How many times have you been in need of, or at least could use the help of a shell in your application? How many of those times have you written your own parser? For me, it’s been a couple of times each.

Some weeks ago, I was using MongoDB, looking at its great JavaScript shell when it struck me: I want my own!!

Since I want it for .NET, I didn’t use Mongo’s code (it’s in C++ and I don’t know about the third part dependencies it might be using and so). I didn’t want either, for the n-th time, to spend much time building a parser; so I used existing Microsoft.JScript and Microsoft.Vsa which are both quite obscure, poor documented and inextensible. But, they delivered the job beautifully.

In this document, I won’t explain much (if at all) about CodeDom or VSA, refer to my References or Helpful links sections for that matter.

Using the Code

First of all, let's see how easy it would be using a shell. The following example is a test program: LocalShell.

C#
using (var shell = ShellFactory<SampleShell>.Default.CreateShell())
{   
    shell.OpenShell();  
    shell.AddGlobalObject("tokens", new List<string>());
    shell.AddGlobalObject("globals", new Dictionary<string, object>());
    do 
    { 
        Console.Write(" > "); 
        try 
        { 
            shell.Eval(Console.ReadLine());
        } 
        catch (Exception ex) 
        { 
            Console.WriteLine(ex); 
        } 
} while (shell.Running);  

Type SampleShell defines global functions available from the prompt. There would be 2 global vars available: tokens a globals, a list and a dictionary respectively. The shell instance itself would let us know when it exits.

Looking at the shell console:

> for(var i = 0; i < 5; i++) tokens.Add(i.ToString("D8"))
> Print(tokens[0])
00000000
> Print(tokens[1])
00000001
> for(var t in tokens) Print(t)
00000000
00000001
00000002
00000003
00000004
> Exit()

In the previous example, we used the global variable tokens and the global function Print. Methods in shells type would work as the global functions.

Creating a Shell

Clients would interact with shells using Eval and CreateMethodHandler, both with pretty clear names, anyway explained later. To create a shell, you just have to define your class shell extending from Scaredfinger.JSShell.Shell and later use Scaredfinger.JSShell.ShellFactory<TShell> to create shell instances. There’re some operations performed behind the curtains, pretty dark ones, that's the reason shell instances are created with a Factory.

C#
namespace SampleShell
{ 
    public class SampleShell : Shell 
    {   
        public TextWriter Output { get; set; }   

        [ShellOperation]   
        public void Help()  { … }   

        [ShellOperation]   
        public void Print(string text)  { … }   

        [ShellOperation]   
        public void Exit() { … } 
    }
}

The complete implementation for SampleShell can be found in a sample project with the same name.

CreateMethodHandler Method

A use I could think of this shell, is, programming, on runtime, event listeners for events triggered by my application.

C#
 EventHandler<EventArgType> handler = shell.CreateEventHandler<EventArgs>(jsBody);
...
application.SomeEvent += handler ;      

Wasn’t that simple? Now let’s take a look at jsBody’s code, which has some tricks.

C#
string jsBody = @"
// Any javascript statement, as many as you’d need
// this, sender and e are available. No globals, not vars nor functions
// Print(globals) ; 
// Error, actually a compile time one, but in our case, there is no difference
this.Print(this.globals); 
// Now, we’re talking
";

No function declaration, no curlies. The shell would add them. There is also a problem with the scope. Inside the function, shell’s scope doesn’t work (Unfortunately VSA is far to obscure to fix this in an easy elegant way). But, I could specify any object as this, so this, will be the a reference to shell’s scope, if you change something inside, is going to be changed when you get back outside. Yeah, I known: it doesn’t make sense printing a dictionary. I’ve never been very good with examples.

Eval Method

Most common method, at least for me. Executes a code block. You can include as many statements as you like separated with a semicolon, as usual. This method also returns a result object, which will be the last expression with a value or null if no expression returned a value.

There is also a very special return...

C#
 result = shell.Eval("Print(1)") ; 
// result is null

result = shell.Eval("var x = 1; Print(x);") ; 
// result is 1

result = shell.Eval("var x = 1; Print(x); var y = 2"); 
// result is 2

// Now
result = shell.Eval("var Foo = function() { ... }") ; 
// result is a function, kind of a delegate.  

Remember that very special return I was talking about, it would be the last one. It returns a ScriptFunction which is a ready to use object.

This is the mechanism I use to create event handlers, and you could use to create any kind of delegates. I didn’t make general delegate creation cause it would make necessary generating Delegate types for each result, and really didn’t wanted to complicate things around.

To create a specific delegate:

C#
 var function = shell.Eval("var __F = function() {...}") as ScriptFunction;
var @this = null ; // Or any value you like to make as the this inside the function body

if (function != null)
{ 
    var dlg = (DelegateType)delegate(T1 p1, T2 p2, T3 p3... ) 
    {   
        function.Invoke(@this, new {p1, p2, p3, ...}); 
    } ;
}

Sample Projects

  1. Creates a simple shell with 3 operations, Help, Print and Exit. Allows custom output, by default Console.Out.
  2. LocalTest: Uses previous shell from the local command prompt. Defines a couple of imports and global variables.
  3. ShellServer: A telnet server using SampleShell instead of bash.

Security Issues

There are many dangers involved in the use of such a flexible shell. So use it at your own risk.

For instance: If you allow remote access to a locally running shell, even if you have just referenced System.dll, reflection could always be used to load more harmful packages. Theoretically, you shouldn’t be able to use reflection if no “import System.Reflection” is used in the shell definition, nevertheless, hackers usually find the unthinkable workarounds.

References

  1. VSA Scripting in .NET
  2. System.CodeDom Namespace
  3. Microsoft.JScript Namespace
  4. Microsoft.JScript.ScriptFunction class

Helpful Links

  1. Embedding JavaScript into C# with Rhino and IKVM
  2. .NET Script Editor (C#, VB.NET Mini IDE)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)