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 = shell.Eval("var x = 1; Print(x);") ;
result = shell.Eval("var x = 1; Print(x); var y = 2");
result = shell.Eval("var Foo = function() { ... }") ;
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 ;
if (function != null)
{
var dlg = (DelegateType)delegate(T1 p1, T2 p2, T3 p3... )
{
function.Invoke(@this, new {p1, p2, p3, ...});
} ;
}
Sample Projects
- Creates a simple shell with 3 operations,
Help
, Print
and Exit
. Allows custom output, by default Console.Out
. LocalTest
: Uses previous shell from the local command prompt. Defines a couple of imports and global variables. 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.
- VSA Scripting in .NET
- System.CodeDom Namespace
- Microsoft.JScript Namespace
- Microsoft.JScript.ScriptFunction class
- Embedding JavaScript into C# with Rhino and IKVM
- .NET Script Editor (C#, VB.NET Mini IDE)