Click here to Skip to main content
16,004,574 members
Articles / Programming Languages / C++

mscript Version 4: A Scripting Language Reinvented

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
14 Jun 2024Apache6 min read 7K   93   2   2
This article documents foundational changes to mscript that position it to take on PowerShell and Python in bang for the buck for tasks currently implemented using nasty batch files.

Image 1

Marty - photo by Rodney Ludwig with FotoSketcher

Introduction

mscript is a simple scripting language with a basic runtime, suitable for the sorts of things you use batch files for. mscript is simpler and easier to use than PowerShell, and unlike Python it is delivered as an easily transported tiny self-contained EXE or small MSI.

This article marks the fourth major iteration of mscript. Even if you had exposure to it from years ago, dig in; you will hopefully be surprised and not disappointed.

Big credit goes to my co-conspirator, Rodney Ludwig. A long-time mscript user and supporter, Version 4 would not be possible without Rodney's feedback and creativity.

Downloads & Documentation

Head over to mscript.io for the latest software and documentation.

An Example

I learn by example, so here is mscript's build script.

Hints:

  • Lines that do not start with a symbol are command lines that are executed just like in a batch file, and if the command fails then an error is raised
  • Lines that start with > are like echo
  • A command line run after a >! line raises no errors if the command fails
mscript
? 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."

Selling Points

mscript is simple, powerful, and small.

How Small?

mscript4.exe is self-contained and 934 KB.

mscript4.msi, shipping with extensions, is 3.2 MB.

Executing Commands

mscript offers six ways to run commands:

1. Plain command lines

Any line of text in the script that doesn't start with a symbol is treated as a command line, just like in a .bat file.

After running the command, local variables are set for seeing how it went:

  • ms_LastCommand is set before the command is even attempted
  • ms_ErrorLevel is the exit code from the last command, just like %errorlevel% in batch files.

The output from the command is written to the console.

2. >> lines

The text after the >> is treated as a string expression - use a variable or surround it with matching " or ', build it up however you want - and it is processed the same as a plain command line.

3. The system() function

This runs the specified command using the C function system() and returns its exit code. You do not get the command's output in your script, but the output goes to the console. If the command has a non-zero exit code an error is raised, unless you pass true as a second argument to suppress errors.

4. The popen() function

This runs the specified command using the C function popen() and returns the program output. The command's output does not go to the console. If the command has a non-zero exit code an error is raised, unless you pass true as a second argument to suppress errors.

5. The exec() function

This gives you control over how your command is run (popen() or system()), and whether errors are ignored. The return value contains the text of the command's output (if popen() was chosen) and the exit code.

6. >! lines

If a line begins with >! and has a command after it on that same line then error raising is suppressed for running just that command. The command is treated like a 2. >> statement above.

Command Line Error Handling

For plain command lines and >> lines, if the command fails (non-zero exit code), by default an error is raised, triggering script error handling described below.

If the statement prior to the command that failed is simply >!, then an error is not raised. This little wudgie (technical term) restores, for one command, the default error-ignoring behavior of batch files.

So mscript gives you great default command line error handling and program output capture. If you want to go back to ignoring command line errors or handling exit codes directly, just add >! on the line before it or at the beginning of the command to run, and you can examine ms_ErrorLevel as you would with %errorlevel% in a batch file.

Script Error Handling

mscript's script error handling mechanism is novel. Instead of requiring that you wrap code that you want to catch errors from in a try block of code...

try {
	some code
} catch (something) {
	handle error from some code
}

...mscript lets you organize your code how you'd like, then you put the error handling anywhere after or up from there you'd like...

some code
! error
	handle error from some code
}

It's the same concept of errors being raised and errors handlers being sought, it's just simpler in mscript with more concise packaging.

Looping Over Lists To Run Commands

If you have ever had to apply the same command to multiple files, you've had to duplicate the commands. With mscript you put the filenames in a list, then loop over the elements in the list running the command:

mscript
$ filenames = list()
* filenames.add("mscript4.exe")
* filenames.add("mscript-db.dll")
* filenames.add("mscript-http.dll")
* filenames.add("mscript-log.dll")
* filenames.add("mscript-registry.dll")
* filenames.add("mscript-sample.dll")
* filenames.add("mscript-timestamp.dll")
@ filename : filenames
	> "..." + filename
	* setenv("ms_filepath", "binaries\" + filename)
	ResourceHacker.exe -open %ms_filepath% -save %ms_filepath% -action addoverwrite -resource resources.res
	signtool sign /f mscript.pfx /p password %ms_filepath%
}

In that case there are four lines of script per filename, that would have been a pain for the seven files. Imagine if there were twenty lines of script / commands per file and you got the list of files from dir /B. That's the power of mscript: basic data structures and script programming combined with command-line savvy.

Powerful Extensions

The language's built-in function library is sufficient for most scripting. Extensions to the language by way of DLL's provide a rich runtime for advanced scripting, including SQL and NoSQL database programming, HTTP request processing (new), registry manipulation, and logging.

What's New in Version 4?

Plain command lines

The plain command lines "feature" fundamentally changes the surface of mscripts. Lines that need to assign a new value to a variable or run some sort of side effect like adding to a list, all those lines need to be prefixed with *'s. This requirement was lifted in v3, but it's back in a big way with v4.

There is a new function for executing commands, system(command[, suppressErrors]). It executes the command and returns the exit code.

There is another new function for executing commands, popen(command[, suppressErrors]). It executes the command and returns the output of the command.

Mostly case-insensitive

Variable names, built-in and user function names, and comparison operators like AND or OR or not, all ignore case. Index lookups and string comparisons are still case-sensitive.

New switch statement

Symbolic in true mscript style:

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

No more chained if / else-if; new switch statements takes the place

This is no more if / else-if / else with chained ?'s, there's just if-else with ? and <>. If you want to do more complicated else-if type things, follow this "[] true" pattern:

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

DOS comparison operators are now supported

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

HTTP / HTML / URL processing

HTTP request processing is provided by HTML and URL encoding and decoding functions and a new mshttp.dll extension that wraps WinHTTP with rich input and output interfaces.

Extension DLLs no longer need to be signed with the same certificate authority as the mscript EXE. This allows for an extension ecosystem outside the author's four walls, a good thing.

>>> tracing statement and setTracing() function

You can use a new >>> statement to add trace logging to your scripts.

The statement looks like this: >>> section1 : TRACE_LEVEL_DEBUG : "My trace message"

You then call the function setTracing(list("section1"), TRACE_LEVEL_DEBUG) and when execution reaches that line of code "My trace message" will be output.

Dynamic programming

For dynamic programming, use a ** statement in which a variable's value can be used as a function name:

mscript
/ Create a function that prints
~ do_print(val)
	> val
}

/ Assign the name of the function to a variable
$ my_func = "do_print"

/ Using the special ** statement type, call the function through the variable
/ This will print foo
** my_func("foo")

/ The ** statement is nothing special, it simply unlocks this otherwise unexpected behavior

For even more dynamic programming, there is now a built-in expression evaluating function:

mscript
> eval("2 * 5")
/ This prints 10

Conclusion

As you venture out into the Windows world wanting to run commands in myriad new and varied ways, consider putting mscript on your toolbelt; it has the functionality you need in the tidy package you want.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
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 since then, 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

 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA14-Jun-24 19:23
professionalȘtefan-Mihai MOGA14-Jun-24 19:23 
GeneralRe: My vote of 5 Pin
Michael Sydney Balloni21-Jun-24 18:02
professionalMichael Sydney Balloni21-Jun-24 18:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.