Martyphoto by Rodney Ludwig with FotoSketcher

mscript

replace nasty batch files with simple mscripts

mscript is line-based and uses symbols instead of keywords.
In this document a 'statement prefix' is the same as a 'symbol':

if (verbose_flag) {
    printf("verbose_flag is turned on\n");
}

in mscript becomes

? verbose_flag
    > "verbose_flag is turned on"
}

mscript is developed and maintained by Michael Balloni and Rodney Ludwig

CodeProject articles document mscript versions: 1.0 - 2.0 - 2.0.2 - 3.0 - 4.0

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? contact@mscript.io

downloads

installer: mscript4.msi - standalone EXE (< 1 MB): mscript4.exe (TERMS OF USE)

example script

mscript's own build script is a good example:

? arguments.length() != 1
	> "Usage: mscript-builder.exe build.ms version"
	> 'version should be like "4.0.0"'
	* exit(0)
}

$ version = arguments.get(0)
> "Version " + version
$ version_parts = version.split('.')
? version_parts.length() != 3
	> "ERROR: Invalid version, not three parts!"
	* exit(1)
}
* setenv("ms_version", version)

> "Binaries..."
>!
rmdir /S /Q binaries
mkdir binaries
xcopy /Y ..\mscript\Win32\Release\*.dll binaries\.
xcopy /Y ..\mscript\Win32\Release\*.exe binaries\.

> "Samples..."
>!
rmdir /S /Q samples
mkdir samples
xcopy /Y ..\mscript\mscript-examples\*.ms samples\.

> "Licenses..."
>!
rmdir /S /Q licenses
mkdir licenses
xcopy /Y ..\mscript\mscript-licenses\*.* licenses\.

/ Collect our list of filenames we care about
$ filenames = list("mscript4.exe", "mscript-db.dll", "mscript-http.dll", "mscript-log.dll", "mscript-registry.dll", "mscript-sample.dll", "mscript-timestamp.dll")

/ Update our filenames to be in the relative binaries folder
++ f : 0 -> filenames.length() - 1
	* filenames.set(f, "binaries\" + filenames.get(f))
}

> "Resource hacking..."
$ resource_contents = readFile("resources.template", "utf8")
* resource_contents = \
	resource_contents.replaced \
	( \
		"%MAJOR%", version_parts.get(0), \
		"%MINOR%", version_parts.get(1), \
		"%BUILD%", version_parts.get(2) \
	)
* writeFile("resources.rc", resource_contents, "utf8")

>!
del resources.res
ResourceHacker.exe -open resources.rc -save resources.res -action compile
@ filename : filenames
	> "..." + filename
	* setenv("ms_filepath", filename)
	ResourceHacker.exe -open %ms_filepath% -save %ms_filepath% -action addoverwrite -resource resources.res
}

> "Signing...all at once..."
>!
* setenv("filenames_combined", filenames.join(" "))
signtool sign /n "Michael Balloni" %filenames_combined%

> "Building installer..."
AdvancedInstaller.com /edit mscript.aip /SetProperty ProductVersion="%ms_version%"
AdvancedInstaller.com /rebuild mscript.aip

> "Building release site..."
>!
rmdir /S /Q site\releases\%ms_version%
mkdir site\releases\%ms_version%

> "Collecting samples..."
>!
rmdir /S /Q site\samples
mkdir site\samples
xcopy /Y ..\mscript\mscript-examples\*.ms site\samples\.

> "Assembling release..."
@ filename : filenames
	* setenv("ms_filepath", filename)
	xcopy /Y %ms_filepath% site\releases\%ms_version%\.
}

> "Finalizing installer..."
* setenv("installer_filename", "mscript4.msi")
xcopy /Y /-I mscript-SetupFiles\mscript.msi site\releases\%ms_version%\%installer_filename%
signtool sign /n "Michael Balloni" site\releases\%ms_version%\%installer_filename%

> "Clean up..."
rmdir /S /Q binaries
rmdir /S /Q samples
rmdir /S /Q licenses
rmdir /S /Q mscript-cache
rmdir /S /Q mscript-SetupFiles

> "All done."

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 executable file requiring no setup, no ugly setup required

licenses

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:

what's new?

Lines without a symbol as first token are now treated and look the same as command lines in a batch file

The language is now mostly case-insensitive

This includes...

new switch statement

[] some_variable_or_expression
	= some_value_expression
		code
	}
	= some_other_value_expression
		code
	}
	<>
		fall through code
	}
}

Break statements (V) work as in other languages, exiting the switch statement.

no more if / else-if / else-if2 / ...

In previous versions, you could chain if's and else-if's by putting by having ? statements immediately follow each other. This is no longer allowed, just if / else now with ? / <>. If you want to do multiple if / if-else's, you can use a switch statement like so:

$ x = 12
[] true
	= x < 0
		> "negative"
	}
	= x > 0
		> "positive"
	}
	<>
		> "zero"
	}
}

mscript evaluates the switch expression (true), then walks the case statements evaluating them, in this case for true or false, and executes the code for the first case that matches (in this case true), or the default case (<>). It's kind of a while (true) for if / else-if type coding.

HTTP request processing

new built-in functions htmlEncoded(), htmlDecoded(), urlEncoded(), and urlDecoded(),
useful for working with new mscript-http extension
this extension has one function: mshttp_process_request()
that takes an index with a full HTTP request interface,
and returns an index with the response status and headers

DOS comparison operators

EQU | equal to
NEQ | not equal to
LSS | less than
LEQ | less than or equal to
GTR | greater than
GEQ | greater than or equal to

remember that mscript is now case-insensitive, so you don't have to use Caps Lock

Section-Level tracing

To support debugging larger scripts, you can now add trace statements.
The statements conditionally output messages that help you debug.
A global function is used to control which trace statements are active.
More details

Extension signing restriction lifted

In previous versions, extension DLLs had to be signed by the same authority that the mscript EXE was.
This restriction is lifted, which should encourage growth of an mscript extension ecosystem.
To use an extension DLL, the requirement remains that it be in the same directory as the mscript EXE.

Interested in info about the previous major version of mscript?

Check out mscript3

Reference

statements

NOTE: The language is mostly case-insensitive.  Only index lookups and string comparisons are case-sensitive.

/*
a block
comment
*/

/ a single-line comment, on its own line, can't be at the end of a line
NOTE: You used to use ! for single-line comments; ! is now used for error handling

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

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

Any statement without a prefix is considered a command line to execute,
making an mscript an adorned batch file of sorts, like Classic ASP for batch files

Unadorned lines (normal commands) are executed and the local variables 
ms_LastCommand and ms_ErrorLevel are set

>> evaluates the rest of the line as a string expression and runs it like a normal command

The statement >! causes the next command line that runs to not raise errors if it fails;
without this line preceding a command, a command failure raises an error

If you put a command after >! on the same line then that command runs with errors suppressed, it's more concise

Errors raised by command failures are indexes with entries "Error", "Command", and "ErrorLevel"
This way you can have error handling defined far from the source of the error and still have the context of the error

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

/ A general statement type, the * symbol, is usable for assignments or for side-effects like adding elements to a list
$ my_list = list()
* my_list.add("foo")
* my_other_list = my_list.clone()

/ 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
O
    ...
    / the V statement is break
    > "gotta get out!"
    V
}

/ If / Else
/ Curly braces are required at the ends of if and else clauses
? some_number = 12
    some_number = 13
}
<>
    some_number = -1
}

/ Switch
[] some_expression_to_look_for
	= some_value_expression
		code
	}
	= some_other_value_expression
		code
	}
	<>
		fall through code
	}
}

IMPORTANT:
Subsequent = clauses cannot have anything between them.
So if you have...
$ str = ""
[] str.length()
	= 0
		...handle blank string
	}
	...something
	= 5 / this command fails because it does not immediately follow the = 0 clause
		...handle 5
	}
}

With ?'s and ='s and <>'s, there can be no code or even comments or blank lines in the no man's land between the clauses.

/ 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()
    O
        / 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
        * counted.add(cur_value)
        * cur_value = cur_value + 1
        ? cur_value > high_value
            / Use the V statement to leave the loop, a break statement
            V
        <>
            / 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
+ "some_other_script.ms"

/ 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

/ If you want to decide what function to call with a string variable, use the ** statement which unlocks this possibility
$ my_counter = "counter"
$ ret_val
** ret_val = my_counter(1, 3)
/ ret_val is now list(1, 2, 3)
/ The ** statement allows for this dynamic programming which would otherwise result in unexpected behavior

Section-Level tracing

To support debugging larger scripts, you can now add trace statements.
The statements conditionally output messages that help you.
A global function is used to control which trace statements are active.

Tracing is often directed towards one particular section of code, with changing levels of importance over time.
The levels attached to sections are: trace_debug, trace_info, trace_warning, trace_error, and trace_critical.

1. setTracing defines a list of section names with a tracing level, often created by command line paramenters
new function: setTracing(listOfSectionLabelsToTrace, traceLevel)

2. >>> statements are embedded in the code
>>> section-label : level#-expression : output-msg-expression

Example setup:
* setTracing(list("function1","function2"), trace_debug) / Turn tracing on for sections function1 and function2, / and output all statements including debug tracing

Example tracing statment:
>>> "function1" : trace_debug : "Function1 entered"

Logic for executing >>> "section1" : trace_info : "Trace message"
"Is section1 in the list of sections to output set by setTracing()?"
"If so, is the configured trace level <= trace_info? (this is consistent with cx_Logger / mslog)"
"If so, output Trace message"

expressions

A value in mscript can be one of six types:

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

<> 
!= 
neq 

<= 
leq 

>= 
geq 

< 
lss 

> 
gtr 

== 
= 
equ 

% 
- 
+ 
/ 
* 
^

Unary operators: - ! not

An expression can be:
    null
    true
    false
    number
    string
    dquote
    squote
    tab
    lf
    cr
    crlf
    pi
    e
	TRACE_NONE
	TRACE_CRITICAL
	TRACE_ERROR
	TRACE_WARNING
	TRACE_INFO
	TRACE_DEBUG
    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

functions

exec(cmd_line, setttings)
 - Prior to v4, this was the function that gave 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
 - You can now do much of what exec() does using normal commands and >> statements
 - exec() gives you control over what method is used, and you can manage the results of the command
 - beyond the lifetimes of the local variables that normal commands and >> statements leave behind

system(cmd_line[, suppress_errors)
 - execute the command line and return the exit code
 - raises errors on non-zero exist codes by default
 - pass true to the optional second parameter to suppress these errors

popen(cmd_line[, suppress_errors)
 - execute the command line and return the command's output
 - raises errors on non-zero exist codes by default
 - pass true to the optional second parameter to suppress these errors

input()
 - Read one line from the standard input and return it as a string
 
getEnv(name)
 - 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()

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

curDir(optional_drive_letter)
 - get the current working directory, with an optional drive letter

obj.toJson()
 - convert any value into JSON

fromJson(json)
 - take JSON and return any kind of object

error(error_msg)
 - raise an error, see the previous discussion on error processing

exit(exit_code)
 - 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
See example usage
	
obj.getType()
 - the type of an object obj as a string

number(val)
 - convert a string or bool into a number
string(val)
 - 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

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

obj.length()
 - 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

obj.get(key)
 - return character of string, element in list, or value for key in index

obj.has(value)
 - returns whether a string has a substring, a list has an item, or an index has key

obj.keys()
obj.values()
 - index collection access

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

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

list.join(separator)
 - join list items together into a string
 - the separator can be a string of any length

str.split(separator)
 - split a string into a list of substrings
 - the separator can be a string of any length

str.splitLines()
 - split a string into a list of line strings

str.trimmed()
 - 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
 - multiple from / to pairs can be provided for easy template-filling functionality

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

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

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

str.isMatch(regex)
 - return true 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

str.getMatchLength(regex)
 - returns the length of the first match for a regular expression, or -1 if no match
 - by default matches are looked for anywhere in the string
 - pass true as a second parameter to require a match against the full string

str.getMatches(regex)
 - 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

setEnv(name, value)
 - set a process environment variable, usable in executed commands
 
getEnv(name)
 - get the value of an environment variable

num.round(places)
 - return the number rounded to the given number of decimal places

str.expandedEnvVars()
 - return a copy of a string with embedded environment variables expanded

getExeFilePath()
 - return the path to the mscript EXE

getBinaryVersion(binary_file_path)
 - return the four-part version number of any EXE or DLL

sleep(seconds)
 - pause script execution for a number of seconds
 
cd(directory)
 - change the current directory

curDir(driveLetter)
 - get the current directory for an option drive letter

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

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

getLastError()
 - get the last Win32 error number

getLastErrorMsg()
 - 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

htmlEncoded(), htmlDecoded() / urlEncoded(), urlDecoded()
 - encode and decode strings for use in HTML / URLs
 - useful for working with the mscript-http extension
 
setTracing(sectionsList, traceLevel)
 - this function controls tracing done by the >>> statement
 - the section names provided in the first parameter are enabled, 
 - but the statements only execute if they are at or more serious 
 - than the trace level parameter
 
eval(expression)
 - this function takes a string, and then evaluates it as an expression
 - it works with expressions like "2 * 5", not statements like "> 'foobar'"
 - the expression could be a function call, and it can refer to local and global variables
 - it returns the result of evaluating the expression, so it can be the right hand side of a $ or &, otherwise call it with * for a side effect
 - it’s not as robust as JavaScript's eval(), but it’s enough to get you in plenty of trouble, so use with caution, not abandon

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


/ How to add a command line argument

/ 1) Each command line argument is defined with an index in the following manner
$ verbose_index = \
	index( \
	"flag", "-v", \
	"long-flag", "--verbose", \
	"description", "verbose output", \
	"takes", false, \
	"default", false \
	)

/ 2) A list of all of the arguments is created to be used by parseArgs()
$ arg_info = list(verbose_index)

/ 3) the arguments variable is created from the command line arguments. Note: default is false for -v flag 
$ args = parseArgs(arguments, arg_info)

/ 4) now define a variable that is to be used in the program
$ verbose_flag = args.get("-v")

/ here is an example of using the global variable we have turned on with the command line arguments
? verbose_flag
    > "verbose_flag has been turned on"
}

/ If you want to know the arguments for a program, use the -? argument to get a printout

dlls

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
  5. mscript-http

mscript-timestamp

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

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

    msts_to_utc(timestamp)
     - convert a local date-time to UTC
    msts_to_local(timestamp)
     - convert a UTC date-time to local

    msts_last_modified(file_path)
     - when was a file last modified?
    msts_created(file_path)
     - when was a file created?
    msts_last_accessed(file_path)
    - 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()


mscript-registry

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

    msreg_create_key(key)
     - ensure that a registry key exists

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

    msreg_get_sub_keys(key)
     - 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

    msreg_get_settings(key)
     - 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


mscript-log

This DLL is a simplified logging library based on cx_Logging:
http://cx-logging.readthedocs.io/en/latest/

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

mslog_stop()
 - turn off logging to the file entirely
		
mslog_setlevel(log_level)
 - set the logging level as a string, "NONE", "INFO", "ERROR", "DEBUG"

mslog_getlevel()
 - get the log level back out as a string

mslog_error(message)
mslog_info(message)
mslog_debug(message)
 - write a string message to the log


mscript-db

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

SQLite

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

msdb_sql_close(db_name)
 - 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

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

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

4db

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

msdb_4db_close(ctxt_name)
 - 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

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


mscript-http

This simple DLL makes working with HTTP a breeze...

Example basic GET request:

+ "mscript-http.dll"

$ in_idx = \
index \
(
    "server", "mscript.io", \
    "path", "/", \
    "outputfile", "mscript.html" \
)
$ out_idx = mshttp_process_request(in_idx)
$ html = readFile("mscript.html", "utf-8")
> out_idx.get("statuscode")

The input index can have the following fields:
server: string
usetls: bool = true, set to true if port is set to 443
port: number = usetls ? 443 : 80
verb: string = GET
path: string
headers: index, optional
inputfile: string, optional
outputfile: string, optional

The output index has the following fields:
statuscode: number
headers: index

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

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

Do the same for 4db library on GitHub

Get the contents of the SQLite ZIP file
and place the contents of the folder inside the ZIP, some two .c and two .h files, 
and put those four files in a folder called sqlite alongside the mscript solution's directory, 
just like 4db and the JSON library

So your directory structure should look like:

4db
JSON
mscript
sqlite

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

// pch.h
#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

// 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...and lowercase!
wchar_t* __cdecl mscript_GetExports()
{
    std::vector<std::wstring> exports
    {
        L"ms_sample_sum",
        L"ms_sample_cat"
    };
    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)
{
    try
    {
        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);
        }
        else
            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 for in the folder the mscript EXE resides in and not from anywhere else

version history

3.0

installer: mscript3.msi - standalone EXE: mscript3.exe (TERMS OF USE)

2.0.4

2.0.3

2.0.2

Write to contact@mscript.io with questions and comments - copyright © 2022-2024 - Michael Balloni & Rodney Ludwig