RunNET

Sometimes I just wish I could use the power of the .NET Framework for scripting. I use cmd files a lot for automating daily tasks, like making backups. However convoluted their syntax, they're actually quite powerful. And if you make an effort, you can keep them quite readable. Using cmd files has its advantages: they're really easy to update (i.e. if you want to add a directory to the backup), they're kind of debuggable with Shell Runner and they're sort of self-configuring:

Set SRC=C:\Data
Set DEST=D:\Backups\Data
For %%D in (Photos Music Stuff) Do XCOPY %SRC%\%%D bla bla

You get the drift.

But, as mentioned before: the syntax is truly horrible.

Now, of course, I could write a nice VB.NET console application to do the same, but I'd have to make an executable file to run it, which makes me lose two advantages: I can't inspect the program without the source code, let alone update it. Of course, updating should not be that necessary if I make the source and destination directories either arguments to the application, or store them in some configuration file. But then I'm making a real application - parsing arguments, reporting errors, reading configuration - while all I want to do is create, edit and run some code!

This wouldn't be the Home of Tools if we hadn't found a solution. In this case, it's called RunNET. It's a console application that you run like this:

RunNET test.vb

where test.vb looks like this:

Imports System

Public Class Program
  ' It can be either a Sub or a function returning Integer
  Public Shared Function Main() As Integer
    Console.WriteLine("Hello world from VB.NET!")
    Return 1234
  End Function
End Class

The result is exactly what you would expect: Hello world on the console. The fun thing, of course, is that you have the entire .NET Framework at your disposal: System.IO, System.Xml, System.Data, etc, plus redirection and so forth from the command line.

The heavy lifting is done by a class called CodeRunner which, in turn, relies excusively on the built-in code compilation abilities of the .NET Framework. Basically, it compiles the code snippet into an in-memory assembly, finds the Program class in that assembly, locates its Main() method, and calls it, supplying arguments if necessary. It then returns the value returned by Main(), which is a nullable integer.

This approach makes the VB-code run as if it's interpreted. That's not true, of course: an assembly is in fact generated, just never saved to disk. The overhead necessary to accomplish this is - again, of course - huge. The source code is actually compiled by the .NET compiler, just like Visual Studio would make an executable for it. But you know what? On my development machine it takes so little time I don't notice it at all! And another advantage is that the entire program is compiled before it's run, so there are no nasty syntax errors that only crop up in certain run-time situations.

So there you are: RunNET runs .NET programs like scripts.

Adding references

If you take a look at CodeRunner (all code is on CodePlex, as usual) you'll notice first of all that it's not that much code at all. That's because .NET's built-in compiler support does all the hard work. That's also used to link a source file's extension to a language (actually, a CodeDomProvider).

Second, the whole compile process is parameterized using the CompilerParameters class. The only thing we can't pass in automatically is any external references the code may need. These need to be specified in the parameters, so how do we get to them? The simple solution would be to require arguments for RunNET, that it can pass on to CodeRunner. But that's not a good idea at all - how can I remember (or even determine!) which references I need to pass? I just want to run the code snippet!

So I decided to encode the references into the code snippet itself. The main RunNET code examines the snippet using a regular expression that finds "url's" of the type

ref://System.Data.dll

in the code. So if you add the name of the DLL you would like to reference somewhere in the snippet, RunNET will pick them up and pass them to CodeRunner. Here's an example that needs a reference to System.Xml.dll:

using System;
using System.Xml; // ref://System.Xml.dll

public class Program
{
  public static void Main()
  {
    Console.WriteLine(XmlNodeOrder.Before.ToString());
  }
}

Use RunNET's /v option (see below) to resolve reference issues - it will tell you which references were found in the code snippet.

Usage

When you run RunNET without arguments, it will report:

RunNET: RunNET v1.0.0 (64-bit)
Copyright © MOBZystems, 2011
http://www.mobzystems.com/tools/runnet

Run .NET code as a console application.

Usage: RunNET [/v] source.ext [arg [arg ...]]
Options:
  /v  - verbose (show result of call to Program.Main and exit code)
  /l  - show supported languages and extensions on this computer
source.ext   - source code to run. Must have a valid language extension.
               The static/Shared method Main() in the public class Program is called,
               and its result is returned as exit code for RunNET.
               Main() must return an int/Integer, or return void/be a Sub.
               It must accept no arguments, or an array of string, specifying arguments
arg          - argument(s) passed to Program.Main()

Normally, you specify a source file to run, plus any arguments you need for it:

RunNET AddDays.vb 2011-09-21 16

The /v option will show more information.

Language support

When the /l option is used, RunNET will display all language providers installed on your PC. Mine reports:

Provider: CSharpCodeProvider
Supported languages: "c#", "cs", "csharp"
Supported extensions: ".cs"
Provider: VBCodeProvider
Supported languages: "vb", "vbs", "visualbasic", "vbscript"
Supported extensions: ".vb"
Provider: JScriptCodeProvider
Supported languages: "js", "jscript", "javascript"
Supported extensions: ".js"
The CodeDom provider type "(...)VJSharpCodeProvider(...)" could not be located.
Provider: CppCodeProvider
Supported languages: "c++", "mc", "cpp"
Supported extensions: ".h"

This indicates I have code providers for VB.NET, C#, JScript, VJ#, and C++. VB.NET and C# work out of the box, but the VJ# provider cannot be loaded (I haven't installed it, so that fits), but I have other problems with JScript. JScript does not support classes, so I can't create a Program class in code. Besides, there is a jscript interpreter already, so it doesn't make much sense using RunNET on it. C++ is yet another story: the file extension .cpp is not supported by the CppCodeProvider - only .h. That means RunNET will not be able to run .cpp snippets. But somehow I don't see C++ as a scripting language anyway - weird, huh?

Example: a message box

The following 'script' will display a message box using parameters on the command line:

Option Strict On
Option Explicit On

Imports System
Imports System.Windows.Forms ' ref://System.Windows.Forms.dll

Public Class Program
  Public Shared Function Main(arguments() As String) As Integer
    ' We need at least 2 arguments
    If arguments.Length < 2 Then
      ' Show usage
      Console.WriteLine("Usage: MsgBox <title> <prompt> [buttons [icon]]")
      Console.WriteLine()
      ' Show possible values for the button argument
      Console.WriteLine("Possible values for buttons (default is OKCancel):")
      For Each mbb As String In [Enum].GetNames(GetType(MessageBoxButtons))
        Console.WriteLine("- " + mbb)
      Next
      ' And for the icon argument
      Console.WriteLine("Possible values for icon (default is Question):")
      For Each mbi As String In [Enum].GetNames(GetType(MessageBoxIcon))
        Console.WriteLine("- " + mbi)
      Next

      Console.WriteLine()
      Console.WriteLine("Return value is:")
      For Each dr As DialogResult In [Enum].GetValues(GetType(DialogResult))
        If dr <> DialogResult.None Then
          Console.WriteLine(String.Format("- {0} ({1})", dr, CInt(dr)))
        End If
      Next
      Return -1
    Else
      ' Get the first two arguments
      Dim title As String = arguments(0)
      Dim prompt As String = arguments(1)

      ' Get the button argument
      Dim buttons As MessageBoxButtons = MessageBoxButtons.OKCancel
      If arguments.Length > 2 Then
        buttons = DirectCast(
          [Enum].Parse(GetType(MessageBoxButtons), arguments(2), True), 
          MessageBoxButtons
        )
      End If

      ' Get the icon argument
      Dim icon As MessageBoxIcon = MessageBoxIcon.Question
      If arguments.Length > 3 Then
        icon = DirectCast(
          [Enum].Parse(GetType(MessageBoxIcon), arguments(3), True), 
          MessageBoxIcon
        )
      End If

      ' Do the work!
      Return MessageBox.Show(Nothing, prompt, title, buttons, icon)
    End If
  End Function
End Class

The result is returned as an integer and available to the caller in the cmd variable %ERRORLEVEL%.

Download RunNET

Download RunNET Download RunNET 1.0.0 (September 21, 2011) from CodePlex or browse source code

32/64-bit executable

Comments? Bugs? Suggestions? Feature requests? Let us know! Alternatively, you can create a work item on Codeplex.