Probably many people know, that Windows 98 and later includes Windows script host (WSH) by default, which allows to run VBScript and JScript code, but few ones used it at least once. In this article I am going to show you useful WSH snippets and script examples and convince you that this feature is really worthwhile. I will also tell you about very useful and cool WSH features, which are almost unknown, and therefore it is not easy to find information about them on the Internet.
First, a little about languages supported by WSH. JScript is actually JavaScript with slightly modified object model (for example, JScript does not include window object, like browsers, but it has WScript object, which allows to interact with script environment). VBScript is based on Visual Basic 6 (and possibly earlier versions) syntax and features. Both languages have similar capabilities. Besides that, you can install other languages for WSH, for example, PerlScript, which, as you have figured out already, is based on Perl. To perform this you should use, for example, ActiveState Perl installer:
JS file extension is associated with JScript scripts, VBS - with VBScript in Windows by default. If PerlScript is installed, PLS extension becomes associated with PerlScript. You can encode JS and VBS scripts using screnc.exe Microsoft utility and get a file with JSC or VBE extension respectively. Unfortunately, such encoding can protect only from inexperienced users - many decoders can be found over the Internet. Besides that, encoding text in languages other than English may have issues.
Another great WSH feature is to combine code in all languages installed on system in one WSF file. For example, VBScript has a function to display a text input window (InputBox), and you need it, but you write your script in JScript, which does not have such function. This problem is very easy to solve - you have to create WSF file with such content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="windows-1251"?> <job id="MyTestJob"> <script language="VBScript"> <![CDATA[ Function WSHInputBox(Message, Title, Value) WSHInputBox = InputBox(Message, Title, Value) End Function ]]> </script> <script language="JScript"> <![CDATA[ //Your JS code var name = WSHInputBox("Enter your name:", "Query", "Obama"); WScript.Echo("Name: " + name); ]]> </script> </job> |
For example, you can combine JScript and PerlScript in the same way, if you have PerlScript installed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?xml version="1.0" encoding="windows-1251"?> <job id="MyTestJob"> <script language="PerlScript"> <![CDATA[ use strict; our $WScript; use LWP::UserAgent; sub do_request { my $lwp = new LWP::UserAgent; my $response = $lwp->get($_[0]); if($response->is_success) { return $response->headers->as_string; } else { return $response->error_as_HTML; } } ]]> </script> <script language="JScript"> <![CDATA[ WScript.Echo(do_request("http://ya.ru")); ]]> </script> </job> |
Let's take a brief disgression: I'm going to tell you about how to run WSH scripts. They are launched on double-click with wscript.exe. In this case all WScript.Echo calls are translated to common messagebox'es. If you use a script to automate some process, for example, a project build, and you want to display multiple messages, you should run this script with cscript.exe, which translates WScript.Echo calls to console outputs. You can run a script via console by creating BAT file with code like this:
1 2 3 |
@echo off cscript my_script.wsf [parameters] pause |
You can also right-click a script file and use menu option "Open with command prompt".
Probably this will surprise someone, but .NET classes can be used easily with WSH! I'll show you JScript example (JS file):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
function vote_form() { //Here we create forms and different conrols this.form = WScript.CreateObject("System.Windows.Forms.Form"); this.radioButton1 = WScript.CreateObject("System.Windows.Forms.RadioButton"); this.radioButton2 = WScript.CreateObject("System.Windows.Forms.RadioButton"); this.radioButton3 = WScript.CreateObject("System.Windows.Forms.RadioButton"); this.radioButton4 = WScript.CreateObject("System.Windows.Forms.RadioButton"); this.button1 = WScript.CreateObject("System.Windows.Forms.Button"); this.button2 = WScript.CreateObject("System.Windows.Forms.Button"); this.linkLabel1 = WScript.CreateObject("System.Windows.Forms.LinkLabel"); //Setting up controls with(this.radioButton1) { Parent = this.form; Checked = true; Left = 12; Top = 12; Width = 110; Height = 17; TabStop = true; Text = "VBScript"; } with(this.radioButton2) { Parent = this.form; Left = 12; Top = 35; Width = 110; Height = 17; TabStop = true; Text = "JScript"; } with(this.radioButton3) { Parent = this.form; Left = 12; Top = 58; Width = 110; Height = 17; TabStop = true; Text = "PerlScript"; } with(this.radioButton4) { Parent = this.form; Left = 12; Top = 81; Width = 110; Height = 17; TabStop = true; Text = "Manbearpig"; } with(this.button1) { Parent = this.form; Left = 12; Top = 112; Width = 85; Height = 23; Text = "Yes!"; DialogResult = 1; } with(this.button2) { Parent = this.form; Left = 125; Top = 112; Width = 85; Height = 23; Text = "Go ... yourself!"; DialogResult = 0; } with(this.linkLabel1) { Parent = this.form; Left = 155; Top = 9; Width = 60; Height = 15; Text = "kaimi.io"; } //Setting up form with(this.form) { Width = 222; Height = 125; Text = "Which language do you like?"; AutoSize = true; FormBorderStyle = 5; //FixedToolWindow CancelButton = this.button1; CancelButton = this.button2; } //Show form and return true if user presses first button this.show = function() { this.form.ShowDialog(); return this.form.DialogResult == 1; }; //Get selected result (see above, there are 4 radio buttons on form) this.result = function() { if(this.radioButton1.Checked) return this.radioButton1.Text; else if(this.radioButton2.Checked) return this.radioButton2.Text; else if(this.radioButton3.Checked) return this.radioButton3.Text; else if(this.radioButton4.Checked) return this.radioButton4.Text; }; }; //Create form var my_form = new vote_form; //Let user make a choice while(true) { if(my_form.show()) { WScript.Echo("You chose: " + my_form.result()); break; } else { WScript.Echo("No way, you have to choose!"); } } |
We will see this form after executing this script:
And all this is created with .NET-classes! In fact, there are some complications. Firstly, I haven't found any way to use delegates, which process .NET events. Secondly, there is no way to call COM object constructor, which can take parameters, via CreateObject (by the way, this is related not only to .NET). Thirdly, only a limited number of .NET classes and assemblies is available by default:
System.Collections.Queue
System.Collections.Stack
System.Collections.ArrayList
System.Collections.SortedList
System.Collections.Hashtable
System.IO.StringWriter
System.IO.MemoryStream
System.Text.StringBuilder
System.Random
On the other side, it is really simple to publish .NET assembly to make it available via COM interfaces. If Windows Forms script presented above did't work (which is most possibly true), type this command in console:
1 |
%WINDIR%\Microsoft.NET\Framework\v2.0.50727\RegAsm.exe System.Windows.Forms.dll /codebase |
or, under 64-bit operating system:
1 |
%WINDIR%\Microsoft.NET\Framework64\v2.0.50727\RegAsm.exe System.Windows.Forms.dll /codebase |
This command publishes System.Windows.Forms assembly, which becomes available via COM interfaces, then you can use its classes in your scripts. Unfortunately, you can register only assemblies which have ComVisible=true attribute (if assembly has such attribute, this is stated on respective MSDN assembly description page).
To cancel assembly registration run mentioned above command with /unregister key.
What else do you have to know when using .NET assemblies? Frequently .NET classes have overloaded functions with same names and different parameter types and count. How can we call them? After all, script variables are almost not typified! This is very simple. For example, let's take System.Random class. It is registered by default and available from WSH. It has three methods named Next. How can we call the right one? .NET maps identical methods names in such way: first one from the end of method table is named Next (in this example), the next one - Next_2, further is Next_3. How can we see this table with proper function order? For example, using ildasm:
Here I opened mscorlib.dll assembly from %WINDIR%\Microsoft.NET\Framework64\v2.0.50727, after that I navigated to System namespace and located Random class in it. Now it is clear - if we want to use Next method, which allows to set minimal and maximal values for random generation, we should use Next_2 (because this method is the second from the end in Next method list, see screenshot above):
1 2 |
var random = WScript.CreateObject("System.Random"); WScript.Echo(random.Next_2(10, 20)); //prints random number from 10 to 20 |
Another interesting possibility of WSH scripts is drag&drop support. You can drag different files on JS, VBS, JSE, VBE, WSF files and their names will become available through WScript.Arguments.
So, let's sum it up. What is remarkable in writing scripts in JScript, VBScript or PerlScript? Why it is better and easier than using BAT files or PowerShell?
[+] You can choose your familiar favorite syntax. You prefer JavaScript - use it! You adore Visual Basic - then VBScript is for you!
[+] You can combine code written in all these languages in one script, thus expanding one language possibilities with features of another one.
[+] All standard language options are available for you. External programs execution support, binary and text files operations, regular expressions and even more is included. Masked operations on files, operations with network paths (samba, for example), too.
[+] You can use lots of registered COM classes.
[+] You can use a number of COM-Visible .NET assemblies.
[+] You can work with WMI, because it is available through COM.
[+] There is full Unicode support, you just have to save a file as Unicode Little Endian.
Lots of evident benefits. WSH can be used to write powerful applications and scripts, which will make your life easier and perform some useful tasks automatically.
Now I just have to show you several useful snippets, which will be handy if you decide to use WSH for writing scripts to perform automated project building/parsing/file operations etc. In this example I will use my favorite JScript, because it has most common syntax and it will be clear to most of you. Besides that, JScript provides convenient way to catch exceptions, which can be thrown by COM classes and WScript object functions, using try-catch. Also, you can throw exceptions by yourself (this is standard JavaScript feature, and of course it is included in JScript).
1. Working with file system.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var fso = WScript.CreateObject("Scripting.FileSystemObject"); //Creates object, allowing to work with file system fso.CopyFile("xx.txt", "yy.txt"); // copies xx.txt to yy.txt fso.CopyFile("C:\\Test\\*.exe", "C:\\test2\\"); //copies all exe files from C:\Test to C:\test2 fso.DeleteFile("xx.txt"); //Deletes xx.txt fso.DeleteFile("*.*"); //Deletes all files from current folder if(!fso.FolderExists("C:\\Temp123")) fso.CreateFolder("C:\\Temp123"); //Creates folder, if it does not exist //Lists all folder names on C: var folder = fso.GetFolder("C:\\"); if(folder) { var it = new Enumerator(folder.SubFolders); for(; !it.atEnd(); it.moveNext()) WScript.Echo(it.item().path); } |
2. Working with environment variables.
1 2 3 4 5 6 7 8 9 |
var shell = WScript.CreateObject("WScript.Shell"); WScript.Echo(shell.ExpandEnvironmentStrings("Windows folder: %WINDIR%")); //Gets environment variable value //Check if environment variable is set var check = "%VS100COMNTOOLS%"; if(shell.ExpandEnvironmentStrings(check) != check) WScript.Echo("You have Visual Studio 2010 installed!"); else WScript.Echo("You don't have Visual Studio 2010 installed :("); |
3. External program execution
Run a program, wait until it finishes and get its return code:
1 2 3 4 5 6 7 8 9 10 |
try { var shell = WScript.CreateObject("WScript.Shell"); var ret = shell.Run("calc.exe", 1, true); //Executes calc.exe WScript.Echo("Return code: " + ret); } catch(error) { WScript.Echo("Error code: " + error.number + "\n" + error.description); } |
Execute external program and capture its output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
try { var shell = WScript.CreateObject("WScript.Shell"); var exec = shell.Exec("find.exe /?"); //Execute find.exe with parameter while(exec.Status == 0) //Wait until command execution is finished WScript.Sleep(100); if(exec.ExitCode != 0) //If any error occurred (usually all console programs return non-zero value if error occurred, but not always) throw new Error(exec.ExitCode, "Cannot execute command!"); var output = ""; if(!exec.StdOut.AtEndOfStream) output = exec.StdOut.ReadAll(); //Capture executed program output WScript.Echo("FIND.EXE help:\n\n" + output); } catch(error) { WScript.Echo("Error code: " + error.number + "\n" + error.description); } |
You can also input any data to program via Exec.StdIn.
4. Working with Windows registry.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var shell = WScript.CreateObject("WScript.Shell"); //Creates Test123\WScriptTest branch in HKCU\Software //Please pay attention that I put backslash at the end of path name //Last parameter - value data type - is optional shell.RegWrite("HKCU\\Software\\Test123\\WScriptTest\\", 1, "REG_BINARY"); //Creates string value in this branch shell.RegWrite("HKCU\\Software\\Test123\\WScriptTest\\myvalue", "xyz", "REG_SZ"); //Change value with the same call shell.RegWrite("HKCU\\Software\\Test123\\WScriptTest\\myvalue", "12345", "REG_SZ"); //Get written value WScript.Echo("Saved value: " + shell.RegRead("HKCU\\Software\\Test123\\WScriptTest\\myvalue")); //As you can not delete branch in registry which contains nested branches, //delete nested one first shell.RegDelete("HKCU\\Software\\Test123\\WScriptTest\\"); //And then parent branch shell.RegDelete("HKCU\\Software\\Test123\\"); |
5. Working with WMI.
List all nested registry keys in specified branch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//Constants required for registry access var HKEY_CLASSES_ROOT = 0x80000000; var HKEY_CURRENT_USER = 0x80000001; var HKEY_LOCAL_MACHINE = 0x80000002; var HKEY_USERS = 0x80000003; var HKEY_CURRENT_CONFIG = 0x80000005; var locator = WScript.CreateObject("WbemScripting.SWbemLocator"); //Let's connect to local computer WMI var server_conn = locator.ConnectServer(null, "root\\default"); //Get access to registry var registry = server_conn.Get("StdRegProv"); //Get key listing method var method = registry.Methods_.Item("EnumKey"); //Set up method call parameters var input_params = method.InParameters.SpawnInstance_(); input_params.hDefKey = HKEY_CURRENT_USER; //List all keys in this registry branch input_params.sSubKeyName = "Software\\Microsoft\\Windows\\CurrentVersion\\"; //Run key listing method var output = registry.ExecMethod_(method.Name, input_params); var subkeys = output.sNames.toArray(); //Print resulting values for(var key in subkeys) WScript.Echo(subkeys[key]); |
Curiously, but this thing is much easier to do in VBScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const HKEY_CLASSES_ROOT = &H80000000 const HKEY_CURRENT_USER = &H80000001 const HKEY_LOCAL_MACHINE = &H80000002 const HKEY_USERS = &H80000003 const HKEY_CURRENT_CONFIG = &H80000005 'Get access to local registry via WMI Set registry = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv") 'List key names registry.EnumKey HKEY_CURRENT_USER, "Software\Microsoft\Windows\CurrentVersion", key_names 'Print them For i = 0 To UBound(key_names) WScript.Echo key_names(i) Next |
Execute WMI query (in this example - print all accounts of a computer):
1 2 3 4 5 6 7 8 |
//Connect to local computer WMI (".") var root = GetObject("winmgmts:\\\\.\\root\\cimv2"); //Run query to obtain all local accounts on a computer var items = root.ExecQuery("SELECT * FROM Win32_Account where LocalAccount = true and SIDType = 1"); //Print names and descriptions for(var it = new Enumerator(items); !it.atEnd(); it.moveNext()) WScript.Echo(it.item().Name + " - " + it.item().Description); |
At this point I will complete my narration. I think, if you coped with whole article to its end, you realize all the power and convenience provided by WSH scripts and will certainly use this awesome Windows functionality!
For further study I can recommend MSDN (there are lots of WSH documentation, and, of course, .NET and WMI), and Google (there are many WSH script examples). Good luck in studying!