replace nasty batch files with simple mscripts

Code Project articles on development of mscript:

mscript is open source, and contributions are welcome, especially dll integrations

See GitHub for source code and a description of project layout:

questions or comments? [email protected]


installer: mscript.msi - standalone EXE (< 1 MB): mscript3.exe

For your use of mscript3.exe, these TERMS OF USE apply

The mscript.msi installer includes the terms of use


mscript is an open source project hosted by GitHub under the Apache 2.0 license

mscript relies on other open source libraries, each of which has its own license:

example script

/ Import the timestamp DLL that handles file timestamps
+ "mscript-timestamp.dll"

/ See if we have our one parameter, the path to the file to touch
? arguments.length() != 1
	>> Provide the path to the file to touch

/ Output the given path and its current timestamp
$ file_path = arguments.get(0)
> "File: " + file_path
> "Last Modified Be4: " + msts_last_modified(file_path)

/ Do the deed

/ Report the new timestamp
> "Last Modified Now: " + msts_last_modified(file_path)

For more samples, check out...

For those programmers familiar with the wonderful text processing program awk, not only is mscript fully capable as an awk replacement, it can, during processing, categorize to a database, which awk could only dream of

For those programmers that have tried to distribute their python code and failed, mscript is a stand-alone script processor that is simple to download, without any ugly setup required

version history






mscript is a line-based scripting language that uses symbols instead of keywords


exec(cmd_line, setttings)
 - This is the main function that gives mscript meaning in life
 - Build your command line, call exec, and get back an index with all you need to know: 
 -       success, exit_code, and output
 - pass in an index of settings:
         "ignore_errors" defaults to false
            any command not returning a zero exit code will raise an error
            set to true to tolerate errors
         "method" can be "popen" or "system"
            to get output from the command the default "popen" works
            to just get an exit code you can use "system"
 - Write all the script you want around calls to exec, and get a lot done

 - get the value of an environment variable

putEnv(name, value) 
 - Set the value of an environment value
 - This affects the environment inside mscript and in programs run by exec()

 - change the current working directory for all programs run with exec()

 - get the current working directory, with an optional drive letter

 - convert any value into JSON

 - take JSON and return any kind of object

 - raise an error, more on this later

 - return the path to the mscript EXE

 - return the four-part version number of any EXE or DLL

readFile(file_path, encoding)
 - read a text file into a string, using the specified encoding, either "ascii", "utf8", or "utf16"

readFileLines(file_path, encoding)
 - read the lines in a text file into a list strings

writeFile(file_path, file_contents, encoding)
 - write a string to a text file with an encoding

getIniString(file_path, section_name, setting_name, default_value)
getIniNumber(file_path, section_name, setting_name, default_value)
 - provide INI file support

 - return a string with substrings like %PATH% 
 - expanded to their environment variable values

 - get the last Win32 error number

 - get the string description and number of the last Win32 error
   like, "Access denied. (5)"
 - you can pass in an error number to get its error message

 - exit the script with an exit code

parseArgs(arguments_list, argument_specs_list)
 - pass the global string list variable arguments as the first parameter
 - pass a list of indexes defining the arguments your mscript takes
 - index fields are:
    flag: -i
    long-flag: --input
    description: Specify the input to this script
    takes: bool, if true means that a value for the flag is expected
    required: bool, if no value for the flag then an error is raised
    default: a default value for the flag if no value given for the flag
    numeric: bool, if true tries to treat the taken value as a number

 - the type of an object obj as a string

 - convert a string or bool into a number
 - convert anything into a string

list(item1, item2...)
 - create a list with the elements passed in

index(key1, value1, key2, value2...)
 - create an index with the pairs of keys and values passed in

 - deeply clone an object, including indexes containing list values, etc.

 - string or list length, or index pair count

obj.add(to_add1, to_add2...)
 - append to a string, add to a list, or add pairs to an index

obj.set(key, value)
 - set a character in a string, change the value at a key in a list or index

 - return character of string, element in list, or value for key in index

 - returns whether a string has a substring, a list has an item, or an index has key

 - index collection access

 - returns copy of obj with elements reversed, including keys of an index

 - returns a copy of obj with elements sorted, including index keys

 - join list items together into a string

 - split a string into a list of substrings

 - split a string into a list of line strings

 - return a copy of a string with any leading or trailing whitespace removed

str.toUpper(), str.toLower()
 - return a copy of a string in upper or lower case

str.replaced(from, to)
 - return a copy of a string with a substring replaced with another substring

str.fmt(parameter0, ...)
 - replace "{0}" with parameter0, "{1}" with parameter1, etc.

random(min, max)
 - return a random value in the range min -> max

obj.firstLocation(toFind), obj.lastLocation(toFind)
 - find the first or last location of an a substring in a string or item in a list

obj.subset(startIndex[, length])
 - get a substring of a string or a slice of a list, with an optional length

 - see if a string is a match for a regular expression
 - by default matches are looked for anywhere in the string
 - pass true as a second parameter to require a match against the full string

 - return a list of matches from a regular expression applied to a string
 - by default matches are looked for anywhere in the string
 - pass true as a second parameter to require a match against the full string

Standard math functions, for your math homework:
    abs asin acos atan ceil cos cosh exp floor 
    log log2 log10 round sin sinh sqrt tan tanh

 - pause the program for a number of seconds


mscript has a useful set of built-in functions, but adding significant new functionality to the built-in functions is undesirable. Instead, most new functionality will be developed in separate dll integrations. mscript ships with these useful DLLs:

  1. mscript-timestamp
  2. mscript-registry
  3. mscript-log
  4. mscript-db


This DLL let's you work with timestamps, in particular last modified times of files.  It exports a number of functions, notice the unique prefix of the function names:

    msts_build(year, month, day) or msts_build(year, month, day, hour, minute, second)
     - takes three date parameters or six date-time parameters, returning a string usable by the rest of the functions
    msts_add(timestamp string, part to add to string, and amount to add number)
     - adds a number of date units to a timestamp, returning the new timestamp
     - part to add can be "day", "hour", "minute", or "second"
    msts_diff(date1, data2, part)
     - returns the number of date units, date1 - date2
     - part can be "day", "hour", "minute", or "second"
    msts_format(timestamp, format_string)
     - format_string using put_time syntax
     - you can use this to get parts of the timestamp

     - get the current date-time, you can optionally pass in a bool to use UTC or local time
     - uses UTC by default

     - convert a local date-time to UTC
     - convert a UTC date-time to local

     - when was a file last modified?
     - when was a file created?
    - when was a file last accessed?

    msts_touch(file_path, optional_timestamp)
     - touch a file, marking its last modified timestamp to be now or optionally a given timestamp

To call these functions from your scripts, use a + statement, like so
+ "mscript-timestamp.dll"
> "Now: " + msts_now()


This simple DLL makes working with the registry a breeze...

     - ensure that a registry key exists

     - ensure that a registry key no longer exists
     - deltes the keys, its values, and all sub-keys and their values

     - get a list of the names of the sub-keys of a key
     - just the names of the sub-keys, not full keys

    msreg_put_settings(key, settings_index)
     - add settings to a key with the name-values in an index
     - you can send in number and string settings
     - to remove a setting, pass null as the index value

     - get the settings on a key in a name-values index
     - you only get back REG_DWORD and REG_SZ values
       no, multi-strings or expanded-strings


This DLL is a simplified logging library based on cx_Logging:

There is one global logging facility

You call mslog_start() with the filename to use and the log level to start with, and with an optional index of logging parameters, such as the prefix for each log line

You can call mslog_setlevel(log_level) to set what kinds of logging to write to the file.  Log levels from least to most are "NONE", "ERROR", "INFO", "DEBUG"

With logging set up, you call mslog_error(), mslog_info(), and mslog_debug() to write messages to the log

All routines return bool success; errors are only raised for invalid parameters

mslog_start(filename, log_level, options_index)
 - specify the log file path to use, and the initial log level
 - you can call mslog_setlevel() to set the level later on
 - options_index can contain the followings options:
    prefix - what to start each log line with
    maxFiles - how many numbered files to preserve
    maxFileSizeBytes - file size reached to start a new log file
    - see: online documentation
      for more information about these settings

 - turn off logging to the file entirely
 - set the logging level as a string, "NONE", "INFO", "ERROR", "DEBUG"

 - get the log level back out as a string

 - write a string message to the log


This DLL implements wrapper APIs for:
  1. SQLite
  2. 4db


SQLite is a file-based database engine that allows powerful database querying in an easy-to-use package. mscript-db.dll includes SQLite functionality, making it possible to do SQL in a very clean and simple fashion:

msdb_sql_init(db_name, db_file_path)
 - specify a name for the database, db_name, and connect to the database at db_file_path, creating the database if it does not already exist
 - you use the database name in all other API functions

 - close the database connection associated with the given db_name

msdb_sql_exec(db_name, sql_query, optional_query_parameters_index)
 - issue any sort of SQL statement, including things like CREATE TABLE
 - returns a list of lists, with the first list containing the column names

 - get the number of rows affected by the most recent SQL query, such as rows UPDATE'd or DELETE'd

 - get the autonumber row ID of the most recent SQL INSERT query


4db is a file-based NoSQL database engine. You develop the database schema based on the data you add

There are no CREATE TABLE statements. Instead you call the msdb_4db_define() function specifying a table name, a primary key value, and an index of name-value columns to associate with the primary key. It is an UPSERT that defines schema as you go, adding new and updating existing data

Once you have loaded data into your database, you access the data using a simplified SQL SELECT statement, msdb_4db_query().

4db SQL querying

  1. SELECT columns can be "value" to get the primary key, or the name of another column you've defined with msdb_4db_define()
  2. FROM can specify one table
  3. WHERE can include simple comparisons against "value" or a column name, and the values for the comparisons must be parameters. You can use the MATCH operator for full-text keyword search. You can join multiple comparisons with an AND or an OR. Parentheses are not supported at this time
  4. ORDER BY can only include columns from the SELECT columns
  5. LIMIT is supported, no parameter needed
This would be a valid msdb_4db_query() call:
$ query_results = \
msdb_4db_query \
( \
    "SELECT value, column1, column2 \
    FROM my_table \
    WHERE column1 MATCHES @match AND column2 <= @max_val \
    ORDER BY column2 DESC \
    LIMIT 10", \
    index("@match", "some thing not other", @max_val, 10) \

Extra built-in columns you can include in your query:

  1. id - Row ID in the table in 4db's own schema, useful for sorting oldest to newest
  2. created - when the row was added to the database
  3. lastmodified - when the row was last modified
  4. count - the number of rows you would get if you SELECT'd out the rows
  5. rank - for full-text searches, automatically added to ORDER BY

When you want to remove data from your database, you use msdb_4db_delete(). Just pass in the table name and primary key value, and it's gone

4db API Reference

msdb_4db_init(ctxt_name, db_file_path)
 - associate a ctxt_name with a new 4db database, creating the database file if it does not exist

 - free the 4db context associated with the given ctxt_name

msdb_4db_define(ctxt_name, table_name, primary_key_value, metadata_index)
 - create a row in the schema in table_name, with primary_key_value, and the columns and values in metadata_index
 - 4db only supports strings and numbers, so only pass those types of data in   if you expect to get those types of data back out

msdb_4db_undefine(ctxt_name, table_name, primary_key_value, metadata_name)
 - erase from table_name with primary_key_value the metadata value for the given metadata_name

msdb_4db_query(ctxt_name, sql_query, parameters_index)
 - issue a simple subset of SQL using sql_query using parameters from parameters_index
 - hands back a list of lists, with the first list having the column names

msdb_4db_delete(ctxt_name, table_name, primary_key_value)
 - erase a row from table_name with primary_key_value

msdb_4db_drop(ctxt_name, table_name)
 - drop table_name from the schema

 - get an index mapping the name of each table in the schema to a list of the table's column names


a block

/ a single-line comment, on its own line, can't be at the end of a line

// another single-line comment, can appear anywhere in a line

NOTE: You used to use ! for single-line comments; ! is now used for error handling

> "print the value of an expression, like this string, including pi: " + round(pi, 4)

>> print exaclty what is on this line, allowing for any "! '= " 0!')* anything you'd like

/ Declare a variable with an initial value
$ new_variable = "initial value"

/ A variable assignment
& new_variable = "some other value"
/ Once a variable has be assigned a non-null value
/ the variable cannot be assigned to a value of another type, including null
/ So mscript has some type safety.  Some...

/ The O signifies an unbounded loop, a while(true) or for (;;)
/ All loops end in a closing curly brace, but do not start with an opening one
    / the V statement is break
    > "gotta get out!"

/ If, else if, else
/ Curly braces are required at the ends of each if or else if clause, 
/ and at the end of the overall if-else statement
? some_number = 12
    some_number = 13
? some_number = 15
    some_number = 16
    some_number = -1

/ A foreach loop
/ list(1, 2, 3) creates a new list with the given items
/ This statements processes each item in the list, printing them out
/ Note the string promotion in the print line
@ item : list(1, 2, 3)
    > "Item: " + item

/ Indexing loops
/ Notice the pseudo-OOP of the my_list.length() and my_list.get() calls
$ my_list = list(1, 2, 3)

/ Use a ++ loop to go up from a starting index to an ending index
++ idx : 0 -> my_list.length() - 1
    > "Item: " + my_list.get(idx)

/ Use a -- loop to go down from a starting index to an ending index
-- idx : my_list.length() - 1 -> 0
    > "Item: " + my_list.get(idx)

/ Use a # loop to go in either direction between start and end
# i : 1 -> 10
    > "i: " + i
/ NOTE: If you use a # loop to iterate an index number over the size of a list
/       and the list is empty:
# i : 0 -> length(list()) - 1
    / i will be -1 in here
/ Hence the ++ and -- statements.  
/ # is kept for simplicity and backwards compatibility

    / Just a little block statement for keeping variable scopes separate
    / Useful for freeing resources like large strings as soon as they've been used
    / Variables declared in here...
/ ...are not visible out here

/ Functions are declared like other statements
~ my_function (param1, param2)
    / do something with param1 and param2

    / Function return values...

    /...with a value
    <- 15

    / ...or without a value

/ A little loop example
~ counter(low_value, high_value)
    $ cur_value = low_value
    $ counted = list()
        / Use the * statement to evaluate an expression and ignore its return value
        / Used for adding to lists or indexes or executing functions with no return values
        cur_value = cur_value + 1
        ? cur_value > high_value
            / Use the V statement to leave the loop, a break statement
            / Use the ^ statement to go back up to the start of the loop, a continue statement
    <- counted

/ Load and run another script here, an import statement of sorts
+ ""

/ The script path is an expression, so you can dynamically load different things
/ Scripts are loaded relative to the script they are imported from
/ Imported scripts are processed just like top-level scripts;
/ they can declare global variables, define functions, and...execute script!


A value in mscript can be one of six types of things:

  1. null
  2. number
  3. string
  4. bool
  5. list
  6. index - with order of insertion preserved

Binary operators, from least to highest precedence:
or || and && <> != <= >= < > == = % - + / * ^

Unary operators: - ! not

An expression can be:
    variable as defined by a $ statement

Strings can be double- or single-quoted, both 'foo ("bar")' and "foo ('bar')" are valid 
This is handy for building command lines that involve double-quotes
Just use single quotes around them

String promotion:
    If either side of binary expression evaluates to a string, 
    the expression promotes both sides to string

Bool short-circuiting:
    The left expression is evaluated first
        If && and left is false, expression is false
        If || and left is true, expression is true

error handling

Error handling revolves around the error() function, and ! error handling statements
Note that ! was used for single line comments in mscript 1.0
This is a breaking change to the meaning of !
Use / for single line comments going forward; you can use // if that makes you happy

Say we have...

~ verifyLow(value)
    ? value >= 10
        * error("Value is not low: " + value)

Then we call it...

    * verifyLow(11)

As written, the error() function call will cause mscript to output the error message and exit
Not very useful

Now we have error handling!  Simply add a ! statement after code that you want to handle the errors of:

* verifyLow(23)
! err
    > "verifyLow fails: " + err

In this case the call to verifyLow causes an error, then mscript looks for the first ! statement
in the verifyLow function.  Not found there, the error bubbles up to the top-level, where mscript
finds the ! statement following the call to verifyLow

You can pass any kind of object to the error() function, not just strings
You can handle any type of object in your ! statements
Using indexes for error objects could provide just the sophistication you need

With the ! statement, you can name the error handler's variable whatever you like
It doesn't have to be err

You can have any number of statements before a ! statement, it's not just one ! per prior statement

This is sort of like error handling in other languages, 
just with a lot less code organization and finger wear,
which is mscript's mantra

dll integration

mscript is a great little language with a fun little runtime

At the end of the day, I think I left you wanting more with version 1.0 

So to give you more, mscript now supports dll integration

You can write DLLs that export functions that you call from mscripts,
just like built-in functions or your own mscript functions

To develop an mscript DLL...

Use Visual Studio 2022

Clone the mscript solution from GitHub

Clone the nlohmann JSON library on GitHub
and put it alongside the mscript solution's directory, not inside it, next to it

Get the mscript solution to build and get the unit tests to pass

To understand dll integration, it's best to look at the mscript-dll-sample project

#pragma once
#include "../mscript-core/module.h"
#pragma comment(lib, "mscript-core")

That alone brings in everything you need for doing mscript DLL work

Then you write a dllinterface.cpp file to implement your DLL

Here is mscript-dll-sample's dllinterface.cpp:

#include "pch.h"

using namespace mscript;

// You implement mscript_GetExports to specify which functions you will be exporting
// Your function names have to be globally unique, and can't have dots, so use underscores and make it unique
wchar_t* __cdecl mscript_GetExports()
    std::vector<std::wstring> exports
    return module_utils::getExports(exports);

// You need to provide a memory freeing function for strings that your DLL allocates
void mscript_FreeString(wchar_t* str)
    delete[] str;

// Here's the big one.  You get a function name, and JSON for a list of parameters, 
// and you return JSON of an object
wchar_t* mscript_ExecuteFunction(const wchar_t* functionName, const wchar_t* parametersJson)
        std::wstring funcName = functionName;
        if (funcName == L"ms_sample_sum")
            double retVal = 0.0;
            for (double numVal : module_utils::getNumberParams(parametersJson))
                retVal += numVal;
            return module_utils::jsonStr(retVal);
        else if (funcName == L"ms_sample_cat")
            std::wstring retVal;
            for (double numVal : module_utils::getNumberParams(parametersJson))
                retVal += num2wstr(numVal);
            return module_utils::jsonStr(retVal);
            raiseWError(L"Unknown mscript-dll-sample function: " + funcName);
    catch (const user_exception& exp)
        return module_utils::errorStr(functionName, exp);
    catch (const std::exception& exp)
        return module_utils::errorStr(functionName, exp);
    catch (...)
        return nullptr;
// module_utils implements useful routines for command object / JSON operations
// module_utils::getNumberParams keeps this code pristine, returning vector<double>
// Use module_utils::getParams instead for more general parameter handling
// module_utils::jsonStr(retVal) turns any object into a JSON wchar_t* to return to mscript
// module_utils::errorStr(functionName, exp) gives you consolidated error handling,
// returning an error message JSON wchar_t* that mscript expects

You can tread far off this beaten path

Process the parameter list JSON and return JSON that maps to an mscript value
That's all that's assumed

Once you've created your own DLL, in mscript code you import it with the same + statement as 
importing mscripts

DLLs are searched in the folder the mscript EXE resides in,
and for security, not from anywhere else

Also DLLs must have the same code signing certificate 
as the mscript EXE


dll integrations need to happen inside the mscript solution,
where I can review the code, sign the DLL like the mscript EXE,
and include the DLL in the mscript installer hosted on this website
Write to [email protected] with questions and comments