Click here to Skip to main content
15,036,145 members
Articles / Database Development / NoSQL
Article
Posted 4 Aug 2021

Tagged as

Stats

1.9K views
24 downloads
6 bookmarked

Teaching Coding with mscript and Metastrings

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
4 Aug 2021CPOL9 min read
See what it takes to build an IDE for beginners, powered by a homegrown NoSQL database
Learn about developing a dynamic NoSQL database on top of SQLite, and what the internals .NET implementation of the innards of a basic IDE look like

Introduction

This article lifts the covers off mscript, a simple, free, open source IDE for beginning programmers...

...and metastrings, the dynamic, open source NoSQL database that powers it all.

It's all free and open source, for the benefit of the developer community.

In fact, I created mscript specifically for this community, for us to teach others to code, better.

There's an introductory video; I recommend that you start there.

The mscript Programming Language

Besides being a simple IDE for beginners, mscript is a quirky programming language, somewhere between C and VBScript... and those are different!

Data Types

  1. number, a double
  2. string
  3. bool
  4. list: a collection of items; internally a List<object>
  5. index: a collection of pairs of items, mapping keys to values, internally an metastrings.ListDictionary<object, object>
  6. and, sort of, the special value null

Statement Types

! line comment, can be indented, but must start at beginning of line, 
! not at end of line

/*
block
comment
*/

Print statements:

>> print verbatim, anything on the line

> "print value of expression" + " like this"

{>>
anything in here
is printed 
verbatim,
useful for <script> and <style> blocks
>>}

Variable declaration with required initial value:
$ variable = "some value"

Variable assignment to a new value:
& variable = "some new value"

If = 10, else if > 10, else:
? x = 10
    > "not quite"
? x > 10
    > "yep, there it is"
<>
    > "nowhere close"
}

Foreach:
@ n : list(1, 2, 3)
    > n
}
...Prints 1 2 3 on separate lines

For (int n = ...
# n : 10 -> 8
    > n
}
...Prints 10 9 8 on separate lines

Define our own function to return twice its input:
f my_func(n)
	<- n * 2
}

Call a function:
> my_func(2)
...prints 4

For code like DB UPSERTs where you don not care about the return value, 
use the * statement:
* do_something("foo", "bar")

Here is an example of mscript using the Fibonacci series...recursion is fine...
f fib (n)
    ? n <= 1 
        <- 1
    <>
        <- fib(n - 2) + fib(n - 1)
    }
}

# n : 1 -> 10
    > fib(n)
}
...prints  the first 10 numbers in the series

mscript facilitates basic web development 
with its < and <{ statements, like so:

! get the user's chosen email address using the input function which,
! when part of the overall IDE system, reads from the query string
$ email = input("email")
> "Entered email: " + htmlEncode(email)
>> <br><br>

! output the user interface
<{ form
	>> Enter your email address:
	< input index("type", "email", "name", "email", "value", email)
	< input index("type", "submit", "value", "Enter")
}

And that's it!

Top to Bottom

The mscript IDE is an Electron.js Windows app, powered by a .NET API server, all powered by the metastrings NoSQL database, built on SQLite.

The electron JavaScript UI code uses AJAX to call into a local .NET System.Net.HttpListener server to do the real work.

This arrangement may seem strange, but I like using HTML/CSS/JS for UI, and I like using C# for program logic, and metastrings is C#, hence the unholy marriage.

The only problem I had with this arrangement is that I was unable to get the combined code onto the Apple Mac Store. It's all open source, so if you've got a Mac, give it a try! Maybe you'll have better luck...

Here's the compelling argument for using metastrings as your database of choice for small projects where ease-of-use is paramount:

C#
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.IO;

namespace metastrings
{
    class Program
    {
        static async Task Main()
        {
            // Let's keep it simple and use SQLite
            // We just need to define a path to the database
            // If the file does not exist, an empty database is automatically created
            string dbFilePath = 
                Path.Combine(Environment.GetFolderPath
                            (Environment.SpecialFolder.MyDocuments), "cars.db");

            // Create the Context object and pass in our database file path
            // This creates the SQLite database file if needed, 
            // and opens the database connection
            using (var ctxt = new Context(dbFilePath))
            {
                // Add database records using a helper function...so many cars...
                Console.WriteLine("Adding cars...");
                await AddCarAsync(ctxt, 1983, "Toyota", "Tercel");
                await AddCarAsync(ctxt, 1998, "Toyota", "Tacoma");
                await AddCarAsync(ctxt, 2001, "Nissan", "Xterra");
                await AddCarAsync(ctxt, 1987, "Nissan", "Pathfinder");
                //...

                // Select data out of the database using a basic dialect of SQL
                // No JOINs
                // All WHERE criteria must use parameters
                // All ORDER BY colums must be in SELECT column list
                // Here, we gather the "value" pseudo-column, 
                // the row ID GUID created by the helper function
                Console.WriteLine("Getting old cars...");
                var oldCarGuids = new List<object>();
                var select = 
                    Sql.Parse("SELECT value, year, make, 
                    model FROM cars WHERE year < @year ORDER BY year ASC");
                select.AddParam("@year", 1990);
                using (var reader = await ctxt.ExecSelectAsync(select))
                {
                    while (reader.Read())
                    {
                        oldCarGuids.Add(reader.GetValue(0));
                        Console.WriteLine(reader.GetDouble(1) + 
                        ": " + reader.GetString(2) + " - " + reader.GetString(3));
                    }
                }

                // Use the list of row IDs to delete some rows
                Console.WriteLine("Deleting old cars...");
                await ctxt.Cmd.DeleteAsync("cars", oldCarGuids);

                // Drop the table to keep things clean for the new run
                Console.WriteLine("Cleaning up...");
                await ctxt.Cmd.DropAsync("cars");

                Console.WriteLine("All done.");
            }
        }

        // Given info about a car, add it to the database using the Context object
        static async Task AddCarAsync(Context ctxt, int year, string make, string model)
        {
            // The Define class is used to UPSERT
            // No need to create tables, just refer to them and the database takes care of it
            // Same goes for columns, just add data into the columns and it just works
            // The second parameter to the Define constructor is the row ID
            // This would be a natural primary key, but lacking that we use a GUID
            var define = new Define("cars", Guid.NewGuid().ToString());
            define.Set("year", year);
            define.Set("make", make);
            define.Set("model", model);
            await ctxt.Cmd.DefineAsync(define);
        }
    }
}

Imagine how much SQL that would have taken!

Or integrating with another database that supports SQLite's basic dependency and DB file simplicity.

The mscript API Server

The mscript API server is a .NET System.Net.HttpListener application, defined in the mscript-server solution, in the scriptsvr project, Program.cs. The electron frontend spawns an instance of this server, waits for it to say "I'm ready", then does AJAX with it all day to get the real work of the application done.

Not too many dependencies...

C#
using System;
using System.Threading.Tasks;
using System.Text;
using System.Net;
using System.IO;
using System.Diagnostics;

namespace metascript
{
    /// <summary>
    /// Web server class based on HttpListener.
    /// </summary>
    class Program
    {
        static void Main(string[] args)
        {

HttpState is like System.Web.HttpContext, with extra mscript helper functions and specifics. There is one database connection string per application, a reasonable restriction for this application, so we set this global string to a friendly location on the user's hard drive, in Documents. [UserRoaming] is also supported, but is a really bad idea for a Microsoft Store app, as all files in that directory are deleted when the store app is uninstalled.

C#
// Put the DB somewhere friendly.  Roaming is a bad choice for a store app.
HttpState.DbConnStr = "Data Source=[MyDocuments]/mscript/mscript.db";

We allow callers to set the API port and whether to allow connections from other computers. This flexibility is currently unused, but you might find it useful.

C#
int port = 16914;
bool localOnly = true;
for (int a = 0; a < args.Length; ++a)
{
    string cur = args[a].TrimStart('-');
    string next = (a + 1) < args.Length ? args[a + 1] : null;
    switch (cur)
    {
        case "port":
            port = int.Parse(next);
            ++a;
            break;

        case "allowRemoteAccess":
            localOnly = false;
            break;
    }
}
Console.WriteLine("Port: {0}", port);

We kill off all other instances of this. This makes for clean startups of the IDE after unclean shutdowns. The program exits if this does not pan out, as having multiple instances of the server running is nothing but trouble.

C#
// Kill off any processes already running.
// There can be only one!
try
{
    Console.Write("Getting processes...");
    var processes = Process.GetProcesses();
    Console.WriteLine($" {processes.Length} found");
    foreach (var process in processes)
    {
        if (Path.GetFileNameWithoutExtension(process.ProcessName) == "scriptsvr")
        {
            if (process.Id != Process.GetCurrentProcess().Id)
            {
                Console.WriteLine($"Killing process {process.Id}");
                process.Kill(true);
            }
        }
    }
}
catch (Exception exp)
{
    Console.WriteLine("Killing existing processes failed, bailing");
    Console.WriteLine($"{exp.GetType().FullName}: {exp.Message}");
    return;
}

The API serving starts here. This is the code for setting up an HttpListener to listen on a port.

C#
// Start listening for requests.
Console.WriteLine("Listening...");
HttpListener listener = new HttpListener();
listener.Prefixes.Add($"http://localhost:{port}/");
try
{
    listener.Start();
}
catch (Exception exp)
{
    Console.WriteLine("Starting listening failed, bailing");
    Console.WriteLine($"{exp.GetType().FullName}: {exp.Message}");
    return;
}

Here is the request processing loop. We get a context object, then pass this onto a new Task to do the async / await networking magic.

C#
    // Process requets.
    Console.WriteLine("Processing...");
    while (true)
    {
        var ctxt = listener.GetContext();
        if (localOnly && !ctxt.Request.IsLocal) // a little security
        {
            Console.WriteLine("Ignoring non-local request");
            continue;
        }

        // Fire off a task to handle the request.
        Task.Run(async () => await HandleClientAsync(ctxt).ConfigureAwait(false));
    }
}

Here is the request processor function. It takes the HttpListener context object, creates a disposable HttpState object with that context, and updates the response headers. Take note of the Access-Control-Allow-... headers. These magical incantations control what clients are allowed to access this server. We respond with the same headers for every request, allowing all clients. If it's an OPTIONS request, we know that that's all the client cares about and we bail.

C#
        private static async Task HandleClientAsync(HttpListenerContext httpCtxt)
        {
#if !DEBUG
            try
#endif
            {
                using (var state = new HttpState(httpCtxt))
                {
                    httpCtxt.Response.ContentEncoding = Encoding.UTF8;
                    httpCtxt.Response.Headers.Add("Cache-Control", "no-store");
                    httpCtxt.Response.Headers.Add("Pragma", "no-cache");
                    httpCtxt.Response.Headers.Add("Content-Type", "text/html");

                    httpCtxt.Response.AppendHeader("Access-Control-Allow-Origin", "*");
                    httpCtxt.Response.AppendHeader
                    ("Access-Control-Allow-Methods", "GET, POST");
                    httpCtxt.Response.AppendHeader
                    ("Access-Control-Allow-Headers", "Authorization, Ajax-Cookies");

                    if (httpCtxt.Request.HttpMethod == "OPTIONS") // CORS OPTIONS 
                                                                  // request completes
                        return;

Here we dissect the request path and switch on it to decide which handler class to handle the request. Each handler class implements the IPage interface, which is simply:

C#
Task HandleRequestAsync(HttpState state)

essentially, "You're async, handle this request by way of HttpState." The switch statement finalizes the page handler object.

C#
string path;
{
    path = httpCtxt.Request.Url.PathAndQuery.TrimStart('/');
    int question = path.IndexOf('?');
    if (question > 0)
        path = path.Substring(0, question);
    path = path.ToLower();
}
Console.WriteLine("Request: {0} {1}",
                  state.HttpCtxt.Request.HttpMethod,
                  path);

IPage page;
switch (path)
{
    case "savescript":
        page = new SaveScript();
        break;

    case "execute":
        page = new ExecuteScript();
        break;

    case "getscripttext":
        page = new GetScriptText();
        break;

    case "getscriptnames":
        page = new GetScriptNames();
        break;

    case "starter":
        page = new StarterScript();
        break;

    case "deletescript":
        page = new DeleteScript();
        break;

    case "renamescript":
        page = new RenameScript();
        break;

    case "geterrorlog":
        page = new GetErrorLog();
        break;

    default:
        await ErrorLog.LogAsync
        (state.MsCtxt, $"Page not found: {path}").ConfigureAwait(false);
        httpCtxt.Response.StatusCode = 404;
        return;
}

Here, we call into the page object to handle the request, and we handle exceptions. It's can be a good idea to have an exception type for ending the page like this PageFinishException. This isn't good for performance, as part of the normal flow of things, don't use exceptions for that, but this application is not performance sensitive, and it makes it easy for code to call it quits early, even deep in request processing code.

C#
                    Exception capturedException;
                    try
                    {
                        await page.HandleRequestAsync(state).ConfigureAwait(false);
                        Console.WriteLine("Handler completes");
                        return;
                    }
                    catch (PageFinishException)
                    {
                        Console.WriteLine("Handler finishes");
                        return;
                    }
                    catch (Exception pageExp)
                    {
                        Console.WriteLine("Handler ERROR");
                        capturedException = pageExp;
                    }
                    await Errors.HandleErrorAsync
                    (state, capturedException).ConfigureAwait(false);
                }
            }
#if !DEBUG
            catch (Exception exp)
            {
                if (!(exp is PageFinishException))
                    Console.WriteLine("Unhandled EXCEPTION: " + exp);
            }
#endif
        }
    }
}

An IPage Example

The page classes are really small, just shuttling requests into API code and delivering return values out to the response. In this example, we want to rename a script, so we just take the old and new names and call into the Script class with the HttpState and parameters, and it does the deed. This class is in the mscript-server solution, in the scriptsvr project.

C#
using System;
using System.Threading.Tasks;

namespace metascript
{
    class RenameScript : IPage
    {
        public async Task HandleRequestAsync(HttpState state)
        {
            string oldName = state.HttpCtxt.Request.QueryString["oldName"];
            string newName = state.HttpCtxt.Request.QueryString["newName"];
            await Script.RenameScriptAsync(state, oldName, newName).ConfigureAwait(false);
        }
    }
}

The Script Class

Script is a static class, and most of its members take a HttpState parameter for taking care of their business.

Script is controller code. The metastrings DB is the model. The electron app is the view. MVC, it's going to be huge!

Here is the Script.RenameScriptAsync function called by the RenameScript page handler class. You find it in the mscript-server solution's scriptlib project, Script.js.

This incantation - state.MsCtxt.GetRowIdAsync("scripts", oldName) - asks the HttpState class to get the metastrings context object (MsCtxt, a metastrings.Context object), to get the database row ID from the metastrings database for the script (in the "scripts" table, go figure) with the primary key value oldName.

Then we use the script object by calling GetScriptAsync with the metastrings context object and the row ID we just got. Then we make the name change in memory, and call SaveScriptAsync to commit the change. Finally, we delete any script with the old name.

C#
public static async Task RenameScriptAsync(HttpState state, string oldName, string newName)
{
	if (oldName == newName)
		throw new UserException("You cannot set a script title to its current value.");

	long rowId = await state.MsCtxt.GetRowIdAsync("scripts", oldName).ConfigureAwait(false);
	var script = await GetScriptAsync(state.MsCtxt, rowId).ConfigureAwait(false);
	if (script == null)
		throw new UserException("Script to rename not found: " + oldName);

	script.name = newName;
	await SaveScriptAsync(state, script).ConfigureAwait(false);

	await DeleteScriptAsync(state, oldName).ConfigureAwait(false);
}

GetScript is an object getter function that uses a metastrings DB query to get the script name from the row ID, and gets the script text from a separate metastrings API for long strings.

C#
public static async Task<Script> GetScriptAsync(Context ctxt, long scriptId)
{
	var select = Sql.Parse($"SELECT name FROM scripts WHERE id = @scriptId");
	select.AddParam("@scriptId", scriptId);
	Script script;
	using (var reader = await ctxt.ExecSelectAsync(select).ConfigureAwait(false))
	{
		if (!await reader.ReadAsync().ConfigureAwait(false))
			throw new MException("Script not found: " + scriptId);

		script =
			new Script()
			{
				id = scriptId,
				name = reader.GetString(0)
			};
	}

	script.text =
		await ctxt.Cmd.GetLongStringAsync
		(
			new LongStringOp()
			{
				table = "scripts",
				fieldName = "text",
				itemId = scriptId
			}
		).ConfigureAwait(false);

	return script;
}

SaveScript uses a metastrings UPSERT to define a row with the script name, and the long strings API to store the script text.

C#
public static async Task SaveScriptAsync(HttpState state, Script script)
{
	var define = new Define("scripts", script.name);
	define.Set("name", script.name);
	await state.MsCtxt.Cmd.DefineAsync(define).ConfigureAwait(false);

	script.id = await state.MsCtxt.GetRowIdAsync("scripts", script.name).ConfigureAwait(false);
	if (script.text != null)
	{
		await state.MsCtxt.Cmd.PutLongStringAsync
		(
			new LongStringPut()
			{
				table = "scripts",
				fieldName = "text",
				itemId = script.id,
				longString = script.text
			}
		).ConfigureAwait(false);
	}
}

DeleteScript uses a metastrings NoSQL API call to delete the script by name. NOTE: Bug here, the script text is not cleaned up. The metastrings DeleteAsync function should probably handle this internally. Gotta love writing about code!

C#
public static async Task DeleteScriptAsync(HttpState state, string name)
{
	string key = name;
	await state.MsCtxt.Cmd.DeleteAsync("scripts", key).ConfigureAwait(false);
}

The mscript Language Implementation

You've seen the mscript server, in the mscript-server solution. Let's move to mscript-core for a tour of the implementation of the mscript programming language. The main classes are Expression and ScriptProcessor.

Expression

Expression implements the all important string parsing and expression evaluation to turn pieces of script text into values.

C#
Task<object> EvaluateAsync(string expStr)

In order to parse and evaluate expressions, when you construct the Expression, you pass in the symbol table of variable names and values, and an interface for working with externally defined functions.

Expression implements function execution, the entire mscript runtime, in...

C#
Task<object> ExecuteFunctionAsync(string function, list paramList)

...and has special logic for making things look object-oriented. mscript is a procedural language. But some of its primitives - strings, lists, and indexes - are objects, and working with them should look object-oriented. Here's the trick in the code, inside ExecuteFunctionAsync, in the default clause of the big "which function" switch:

C#
int dotIndex = function.IndexOf('.');
if (dotIndex > 0) // somelist.reversed()
{
	string symbol = function.Substring(0, dotIndex);
	function = function.Substring(dotIndex + 1);

	object value;
	if (m_symbols.TryGet(symbol, out value))
	{
		list newVals = new list(paramList.Count + 1);
		newVals.Add(value);
		newVals.AddRange(paramList);
		return await ExecuteFunctionAsync(function, newVals).ConfigureAwait(false);
	}
}
else // someindex("some key") or somelist(914)
{
	object value;
	if (m_symbols.TryGet(function, out value))
	{
		list newVals = new list(paramList.Count + 1);
		newVals.Add(value);
		newVals.AddRange(paramList);
		return await ExecuteFunctionAsync("get", newVals).ConfigureAwait(false);
	}
}
throw new ScriptException("Function not defined: " + function);

So, if it looks like "object.member_function(914)", it calls "member_function(object, 914)". And if it looks like "object(914)", it calls "get(object, 914)" to do string/list-index or dictionary-key-lookup.

ScriptProcessor

ScriptProcessor takes the entire script body and executes it, line by line. A late addition was to make a pass through the script collecting function definitions so that when executing the code the processor can call functions before they are declared, allowing for freedom in writing scripts. Global variables still have to be declared before they are used.

C#
Task<object> ProcessAsync(int startLine, int endLine, 
             ProcessOutcome outcome, int indentLevel, int callDepth)

...is the main script processing function. The startLine and endLine parameters bound what lines to process. This is for processing the bodies of loops or functions, for example. The outcome parameter is for indicating to the caller "how things went", specifically whether there was a continue, break, or return. The indentLevel is used for pretty HTML output. The callDepth is used to enforce that functions only be declared at depth zero, not within some random if-else somewhere.

All mscript block statements end with a } So finding }'s is really important. Hence FindMatchingEnd...

C#
public static int FindMatchingEnd(string[] lines, int startIndex, int endIndex)
{
	int blockCount = 0;
	bool lastBlockBeginWasWhen = false;
	for (int i = startIndex; i <= endIndex; ++i)
	{
		string line = lines[i].Trim();
		if (line == "}")
		{
			--blockCount;
			lastBlockBeginWasWhen = false;
		}
		else if (IsLineBlockBegin(line))
		{
			bool isWhen = line.StartsWith("? ", StringComparison.Ordinal);
			if (!(isWhen && lastBlockBeginWasWhen))
			{
				++blockCount;
				lastBlockBeginWasWhen = isWhen;
			}
		}

		if (blockCount < 0)
			throw new ScriptException("Too many } found");

		if (blockCount == 0)
			return i;
	}

	throw new ScriptException("End of statement not found");
}

if / else if / else statements in mscript seem odd. They end up looking more like switch / case statements, or, true to influence, SQL's CASE WHEN syntax. So in mscript, when you see:

? x = 1
	...
? x = 3
	...
<>
	...
}

you need to read that as:

if (x == 1) 
{
	...
} 
else if (x == 3) 
{
	...
} 
else 
{
	...
}

not as:

if (x == 1) 
{
	...
}

if (x == 3) 
{
	...
} 
else 
{
	...
}

This was how I could get cascading if-else without closing statements per clause or a separate symbol for else. I had to add the { / } statement, the scope block in C/C++/C#, so you could nest ?'s inside { / }'s.

Sorting out the ifs and if-elses and elses can be tricky business! This function returns the line indexes of all the top level ?s and <>s in the given lines. It took a while to get this one right. Great interview question...I know I'd fail!

C#
public static List<int> FindElses(string[] lines, int startIndex, int endIndex)
{
	var retVal = new List<int>();

	List<string> blockingLines = new List<string>();
	for (int i = startIndex; i <= endIndex; ++i)
	{
		string line = lines[i].Trim();
		bool isWhenBegin =
			line == "<>"
			||
			line.StartsWith("? ", StringComparison.Ordinal);

		bool isEnd = line == "}";

		string blockIn = blockingLines.LastOrDefault();
		bool inWhenBlock =
			!string.IsNullOrEmpty(blockIn)
			&&
			(
				blockIn == "<>"
				||
				blockIn.StartsWith("? ", StringComparison.Ordinal)
			);

		if (IsLineBlockBegin(line))
		{
			if (!(inWhenBlock && isWhenBegin))
				blockingLines.Add(line);
		}
		else if (isEnd)
		{
			blockingLines.RemoveAt(blockingLines.Count - 1);
		}

		if (blockingLines.Count == 1)
		{
			if (isWhenBegin)
				retVal.Add(i);
		}
		else if (blockingLines.Count == 0)
		{
			if (isEnd)
			{
				retVal.Add(i);
				return retVal;
			}
		}
	}

	throw new ScriptException("End of statement not found");
}

private static bool IsLineBlockBegin(string line)
{
	line = line.Trim();

	if (line == "O" || line == "{")
		return true;
	
	foreach (string begin in sm_blockBeginnings)
	{
		if (line.StartsWith(begin, StringComparison.Ordinal))
			return true;
	}
	return false;
}

private static string[] sm_blockBeginnings = 
    new[] { "<{", "?", "@", "#", "f" };

"execute"

I have not said much of anything about how the Electron UI code interacts with this mscript API server.

I'll offer this up.

In the IDE, the panel on the left has your script, and the panel on the right is where you see the web page you are building. The panel on the right is an IFRAME. When you press the "Run it!" button, the IDE directs the IFRAME to load http://localhost:port/execute?script=nameOfTheCurrentScript. And all the FORMs you create with <{s, they get method=get and action=execute, and a hidden form field is added with name "script" and the name of the current script, so that you invisibly get postback-type functionality.

Here's the "page" for executing scripts, bringing it all together:

C#
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.IO;

namespace metascript
{
    class ExecuteScript : IPage
    {
        public async Task HandleRequestAsync(HttpState state)
        {
            string scriptName = state.HttpCtxt.Request.QueryString["script"];
            if (string.IsNullOrWhiteSpace(scriptName))
                throw new UserException("Specify the script you want to run");
            
            var scriptText = await Script.GetScriptTextAsync
                             (state, scriptName).ConfigureAwait(false);
            if (scriptText == null) // empty is okay
                throw new UserException("Sorry, the script was not found");

            var symbols = new SymbolTable();
            using
            (
                var processor =
                    new ScriptProcessor
                    (
                        scriptText,
                        symbols,
                        state.HttpCtxt.Response.OutputStream,
                        scriptName,
                        "execute",
                        state,
                        sm_scriptFunctions
                    )
                )
            {
                ScriptException collectedExp;
                try
                {
                    await processor.ProcessAsync().ConfigureAwait(false);
                    return;
                }
                catch (ScriptException exp)
                {
                    collectedExp = exp;
                }
                using (var stream = new StreamWriter
                (state.HttpCtxt.Response.OutputStream, leaveOpen: true))
                {
                    await stream.WriteAsync
                    (
                        $"\n" +
                        $"ERROR: {collectedExp.Message}\n" +
                        $"Line {collectedExp.LineNumber}\n" +
                        $"{collectedExp.Line}"
                    ).ConfigureAwait(false);
                }
            }
        }

        private static readonly Dictionary<string, IScriptContextFunction> 
            sm_scriptFunctions = ScriptFunctions.GetScriptFunctions();
    }
}

Building and Running

The attached ZIP contains the four projects that mscript is composed of:

  1. metastrings, the database
  2. mscript-core, Expression and ScriptProcessor
  3. mscript-server, the API server
  4. mscript-ide, the Electron IDE

To get up and running...

  1. load the solution file in mscript-server, it brings in everything but the IDE
  2. get everything to build
  3. get all tests to pass
  4. open a cmd prompt and cd to the scriptcli project's output folder
  5. run scriptcli test.ms, it should output Hello world!

Conclusion and Points of Interest

I hope you've enjoyed learning about mscript. Maybe you know somebody who would like to learn programming. You can use mscript to get them going.

Leaving the classroom, maybe extend scriptcli with a "system" function, hit that sweet spot between batch files and Python scripts...? Just dreaming.

History

  • 4th August, 2021: Initial version

License

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

Share

About the Author

Michael Sydney Balloni
Software Developer
United States United States
Michael Balloni is a manager of software development at a cybersecurity software and services provider.

Check out https://www.michaelballoni.com for all the programming fun he's done over the years.

He has been developing software since 1994, back when Mosaic was the web browser of choice. IE 4.0 changed the world, and Michael rode that wave for five years at a .com that was a cloud storage system before the term "cloud" meant anything. He moved on to a medical imaging gig for seven years, working up and down the architecture of a million-lines-code C++ system.

Michael has been at his current cybersecurity gig for six years, making his way into management. He still loves to code, so he sneaks in as much as he can at work and at home.

Comments and Discussions

 
-- There are no messages in this forum --