Click here to Skip to main content
Click here to Skip to main content

A JavaScript Shell

By , 18 Nov 2010
Rate this:
Please Sign up or sign in to vote.

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.

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.

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.

 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.

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...

 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:

 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)

About the Author

Erich Ledesma
Software Developer SunHotels
Spain Spain
I Received a Bachelor's Degree in Computer Science at the Mathematics and Computer Science Faculty, University of Havana, Cuba.
 
Programming Languages I use:
C#, C++, Javascript, Delphi, Java, PHP, and others (less frequently).

Comments and Discussions

 
GeneralOr you can use Jint Pinmemberneon-np18-Nov-10 11:53 
GeneralRe: Or you can use Jint PinmemberErich Ledesma18-Nov-10 12:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140421.2 | Last Updated 18 Nov 2010
Article Copyright 2010 by Erich Ledesma
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid