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
? 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:
$ 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:
[] 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:
$ 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:
/ 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:
> 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.