Rube Goldberg Memorial Scripting Page

Alex Angelopoulos (aka at mvps dot org)

Note: all object strings are activated using the progid4: URL protocol.

Some things can be done in "unusual" ways in WSH (as well as the other hosts for VBScript/Jscript). In fact, some things have to be done in unusual ways to do them at all!  This page documents a few of the more baroque workarounds using WSH-hosted VBScript.

Why Workarounds Are Necessary

The original concept of a scripting host and scripting languages was that the scripting engines would simply supply a language and interfaces - providing a way to do things using the operating system and applications is the responsibility of the people who work on operating systems and applications.  This is a fundamentally sound idea because it completely modularizes scripting at its very foundation.  Microsoft provided some hosts which can work in various environments: WSH, IE, MSHTA, and the Microsoft Script Control are examples.  They also provided two scripting languages: Jscript and VBScript. Finally, they provided objects which could be manipulated by hosts natively OR by the scripting languages via run-time binding.

The true brilliance of the idea was the modular architecture with well-defined interfaces.  Do you want to take advantage of Windows scripting with Perl, Python, Ruby, Haskell, REXX?  You can write plug-in engines that can use everything; no need to define capabilities (and people have done this).  Would you like to make your application a host so it can use Windows scripting languages? Write it, and you can host anything - including the Perl interpreter that you didn't know someone was going to write; several of the X.10 developers can do this.  Do you want to make your application externally controllable? Give it COM interfaces and you're done!  In fact, someone running that X.10 application you have never seen or heard of can control it in HaskellScript  using the interpreter that neither you nor the X.10 developer ever heard of.

This brings us to the fundamental problem: if an application has refused to learn COM, COM-based scripting languages can't natively converse with it.

When I finally understood this, it turned several of my questions about scripting upside-down.  I went from asking "Why can't the Wscript.Network object do much?" to "Why did the core OS people never make netapi32 accessible from script?".  I also began to see why some of the crude appendages to WSH were actually clever ways of getting you to the point where you can at least partially automate a process that is utterly inaccessible otherwise. The Run method is a no-brainer since it was available in VB and VBA.  Exec was a very nice enhancement enabling shell access and capture in a way which is not natively available anywhere else in the OS.  Finally, the native COM support is what really makes life easy.  Wrapping API calls has become an advanced science in VB circles, and VB classes which can be quickly compiled into instantly scriptable solutions are available all over the Internet.  To see some local examples, check out my own COM wrappers for WSH use, as well as some ports of other people's classes to ActiveX.

As for Scripting.FileSystemObject, Wscript.Network, and the other shell objects... The scripting designers had no reason to do any of them other than end user demand.  They are completely above and beyond the call of duty if you look at functional areas of development.  So the next time you find yourself frustrated by Windows interfaces being difficult to access from script, just remember that the WSH developers provided all the tools and all the interfaces needed to make the OS scriptable.

Now on to the goldbergisms!

Why Workarounds Are OK

Checking NumLock state is a classic example of "special workaround required". If you do a Google search on "NumLock" in the scripting hierarchy, you will run across some very bizarre methods for getting and setting the Numlock state of the keyboard. They are methods which are used are totally unacceptable in terms of good application design, but in my opinion are quite fine for scripting.

One of my theories about scripting is that for ad-hoc tasks, you don't necessarily need to have "optimized" solutions. I know, you don't want to drag a machine to its knees - but the point about scripting is that it emphasizes speed of design first and foremost.  If you want "optimal" you can go write an app from scratch in a programming language (if you're really serious, prototype in C and write the final product in assembly language yourself).

On the other hand, if you think that computing resources are cheaper than trained technical staff, you will admit that there is a role for visual concision in scripting, and will have no problem with using:

Microsoft Word as a NumLock State Checker

WScript.Echo IsNumlocked

Function IsNumLocked()
Dim oWrd
Set oWrd = CreateObject("Word.Application")
IsNumLocked = oWrd.Numlock
oWrd.Application.Quit True
End Function

Since you never make it visible or add a document, runtime is more rapid than an actual launch - but not that much more rapid.  It can take between 0.7 and 5.5 seconds to run that routine on a P3-550 with 320 MB of RAM and Office XP.  

Determine Your OS with Microsoft Excel

WScript.Echo OS_From_XL

Function OS_From_XL()
Dim oXL
Set oXL = CreateObject("Excel.Application")
OS_From_XL = oXL.OperatingSystem
End Function

This one is better - it usually only takes about half a second on the above platform.

Getting Screen Resolution with Internet Explorer

Nothing native will do this without WMI.  You can use IE to do this even faster than a WMI call, though:

screen = ScreenResolution
WScript.Echo screen(0),screen(1)

Function ScreenResolution()
Set oIE = CreateObject("InternetExplorer.Application")
With oIE
.Navigate("about:blank")
Do Until .readyState = 4: wscript.sleep 100: Loop
width = .document.ParentWindow.screen.width
height = .document.ParentWindow.screen.height
End With
oIE.Quit
ScreenResolution = array(width,height)
End Function

Automating Outlook Express Email (Outbound)

A common query with a short, negative answer: there is no automation model for Microsoft Outlook Express.

There _is_, however, a method for doing this with a couple of ugly hacks.  The process involves allowing the shell to handle and parse a "mailto" URL, then uses SendKeys.  Two problems with this approach are somewhat resolved, as detailed below.  Tom Hingston posted a partial fix using the "mailto" protocol in one of the scripting newsgroups, with some questions about why he couldn't get white space and other delimiter characters to work; I extended it to do the complete process. 

(1) The shell stops cold when it hits a space or other ambiguous character in the string it is processing.  This can be worked around by use of the "escape" function, which escapes special characters (NOTE: You won't find escape in the VBScript documentation - it's been overlooked since it was introduced for VBScript in about version 5.1).  This allows spaces, line feeds, and any other ASCII character you need.  If you send email in UTF-8, it can even handle those characters.

(2) The best way to deal with enhancing the reliability of SendKeys is to not use it. SendKeys was designed for "last-ditch-efforts" in automation, and just wraps around the raw shell SendKeys API.  Barring that, the best you can do is to ensure you allow time for a window to activate and get focus, then write your SendKeys to use the minimum number of actions possible - which is what this does.

Dim sTo, sSubj, sMsg, sleepdelay
set oShell = CreateObject("WScript.Shell")
sleepdelay = 2000 ' adjust this delay as necessary
sTo = "someone@somwehere.org"
sSubj = "Test in body"
sMsg = "This is a test." & vbcrlf & "This is ONLY a test."
sSubj= escape(sSubj)
sMsg= escape(vbNull & sMsg)
oShell.Run "mailto:" & sTo & "?subject=" & sSubj & "&body=" & sMsg
wscript.sleep sleepdelay
oShell.SendKeys "%F"
oShell.SendKeys "L" ' puts the email in your outbox waiting to be sent

Colored Cscript-Hosted Console (Batch file, WSH, WinNT/2000/XP)

This topic of producing a colored console window when running WSH scripts with cscript.exe was apparently the source of a raging controversy a long time ago in microsoft.public.scripting.wsh.  The conclusion was that it couldn't be done within WSH - which is correct.

This is a batch file for someone who simply insists that they need it, though.  you run it with 3 parameters:

  1. The standard NT foreground-background colors as two-digit hex (like "17" for white-on-blue)
  2. A desired window title enclosed in quotes if it includes spaces
  3. The full path to the actual WSH script you will run if it is not in the current directory.

So, for example,

cslauncher 17 "running the script..." c:\bin\myscript.vbs

Would launch myscript in a console with white letters and blue background; and the title "running the script...".  Since I dropped it into a boilerplate CMD script I use, it also has a simple help menu if run with no parameters or with a help switch such as "/h" and will pass up to 5 command line parameters on over to the WSH script.

It does appear that a simple DLL which gets a handle to the parent console window might be able to do this on the fly by exposing portions of the WriteConsole API call - but then again, I'm not exactly a console API guru, so I'm just waiting for the "you can't do that!" emails. 

Using Internet Explorer to Read the Clipboard

[2002.10.15] Christoph Basedau posted this little gem of a function to the Microsoft WSH newsgroup. It reads the clipboard for WSH via Internet Explorer.  Modified very slightly from Christoph's post:

msgbox ReadClipboard

function ReadClipboard
 ' Reads Clipboard into a variable
 ' by Christoph Basedau
 with createobject("internetexplorer.application")
  .navigate "about:blank" 
  ReadClipboard = _
   .document.parentwindow.clipboardData.getData ("text")
  .quit
 end with
end function

Using Internet Explorer to Parse Text From HTML

This is a fairly simple trick once you've seen it, and to me seems to be a classic example of leveraging scripting.  Sure, you could write your own tiny HTML parser, but why bother when you have the mother of all HTML parsers on your system?

x = TextFromHTML("http://www.yahoo.com")
WScript.Echo x

Function TextFromHTML(URL)
  'Michael Harris
  set ie = createobject("internetexplorer.application")
  ie.navigate URL
  do until ie.readystate = 4 : wscript.sleep 10: loop
  TextFromHTML = ie.document.body.innerText
  ie.quit
End Function

A Synthetic Sleep Function

Clay Calvert posted an interesting hack for producing an artificial "sleep" command on systems which don't have a sleep installed for command prompt use.  It's low-utilization and is also very nice for use in an HTA (which cannot access WScript's sleep).  You just use ping the loopback IP and tell it you want 1 + the number of seconds to sleep as the number of echoes to send.  For example, from a command prompt, if you want a sleep of 1 seconds, you just issue the command:

ping -n 16 127.0.0.1 > nul

and there you are.  This is of particular interest within an HTA; one of the many things you *cannot* do reliably, across all platforms, without use of a 3rd-party component is sleep.  The following scriptlet wraps up Clay's method for use in WSH or an HTA (or IE, if security settings allow) and demos how precise it is on your system.  On mine I noted about a .2 second variation over the run time, due to the final round of ping responses.

t0 = timer
ccSleep 15
MsgBox timer - t0

Sub ccSleep(seconds)
 set oShell = CreateObject("Wscript.Shell")
 cmd = "%COMSPEC% /c ping -n " & 1 + seconds & " 127.0.0.1>nul"
 oShell.Run cmd,0,1
End Sub

Netscape and ActiveX - Sure...Sort of

You cannot use VBScript or ActiveX within Netscape.  In a rather ironic twist, you can automate recent versions of either externally  as long as you host them within Internet Explorer. The following snippet of code will work if you embed it in an Internet Explorer page and have the correct Mozilla DLL registered.

<script language="VBScript">
sub goMoz 
 oMoz.width = 800
 oMoz.height = 600
 oMoz.Visible = True
 oMoz.Navigate "http://www.yahoo.com"
end sub
</script>
<BODY onload='vbscript:goMoz'>
<object
classid="clsid:1339B54C-3453-11D2-93B9-000000000000"
id="oMoz">
</BODY>
</HTML>

 


Miscellaneous Quirks and Workarounds

Direct API Calls

To reiterate the problem of scripting the unscriptable: if there is no external interface designed for something, how do you get to it?

As a matter of fact, there are interfaces to almost everything on Windows: the APIs. Unfortunately, you can't call an API from script.

One workaround is to write your own API wrappers as ActiveX components - a speedy way of doing it if you have the requisite knowledge of the APIs, and have a VB or C compiler and can use them.  I've done that with my XAPI and SOX DLLs.[In transition, link removed... sorry folks!]

Another way to do this -a more "generic' approach - is an ActiveX DLL that will act as a wrapper for any API.  This is much more work, and has been implemented by Jim Warrington in his WSHATO object.

A third way, which is fraught with security perils, is to do API calls by proxy - something scripting is quite capable of doing.  Thus we have:

API Calls From Script Via Microsoft Office

The following example is in the form of a WSF file - save it as, say, APICallViaXL.wsf and run it.  Note that this may require temporarily lowering Microsoft Office's macro security settings.  After you do it, this should also motivate you to push them right back up, since if you can poke around with a system API from WSH this way, so can any other script that runs on the system.

<?xml version="1.0" encoding="ISO-8859-1" ?>
<package>
<job>
<object id="xl" progid="Excel.Application"/>
<script language="VBScript">
<![CDATA[
' Started Excel above
xl.Visible = True
' Add a new workbook
Set xlBk = xl.Workbooks.Add
' Add a module
Set xlMod = xlBk.VBProject.VBComponents.Add(1)
' Add a macro to the module...
strCode = Getresource("macro")
xlMod.CodeModule.AddFromString strCode
' Run the new macro!
iData = xl.Run("Uptime")
Set xlMod = Nothing
xlBk.Saved = True
xl.Quit
wscript.echo f
]]>
</script>
<resource id="macro">
<![CDATA[
Declare Function GetTickCount Lib "kernel32" () As Long
Function Uptime()
Uptime = GetTickcount
End Function
]]>
</resource>
</job>
</package>

The Oddities of ByVal and ByRef

This is not strictly a Rube Goldberg solution; I'm just documenting the quirks in how the ByVal and ByRef calls in VBScript work.  Basically, they are an absolute mess in terms of order due to requirements for backwards compatibility.  Eric Lippert of the Windows Scripting team covered this in excruciating detail in a post to a scripting newsgroup in 1999, and I am simply linking to a local copy of his post.

Accessing Jscript/ActivePerl/ActivePython from VBScript

If you switch to a WSF file format - or are hosted by IE for example - you can simply include a *reference* to any other locally supported scripting language and you instantly have access to its feature set.  Do this:

<script language="Jscript">
<![CDATA[

]]>
</script>

And you can immediately reference functions such as Jscript's encodeURI directly from your VBScript routines.  The price you pay is that you have additional overhead from also loading the Jscript language engine.

Synthetic Includes in Traditional VBScript Files

This should in no way be considered a "hack".  It's a very simple, elegant method of doing an include which requires a minimal amount of extra coding effort using Scripting.FileSystemObject and the GlobalExecute statement.  In fact, I have it written here as a simple function which just returns the number of the error if there is a failure accessing or using the file. Note that you don't need to have a traditional extension for the filename.  VBScript processes the text, not the file, so you simply need to have valid VBScript within the file.

x = Include("c:\temp\text.txt")
WScript.Echo x

Function Include (Scriptname)
'example usage
'Include "c:\scriptsegment.vbs"
' COMMON ERRORS
' 13 - type mismatch - usually a sign of garbage in the file
' 62 - read past end of file, due to a bug in WSH this 
' occurs if executing an empty statement. (So even if you intend
' to write an empty file, give it a ' and carriage return).
' 424 - could not find file
'
On Error Resume Next
Err.Clear
Set oFSO = CreateObject("Scripting.FileSystemObject")
Set oFile = oFSO.OpenTextFile(Scriptname)
ExecuteGlobal oFile.ReadAll()
oFile.Close
Include = Err.Number
End Function

Getting Pi

This is not strictly a workaround, and is not really a goldbergism. I've seen this question pop up occasionally, though - "Why doesn't VBScript have Pi defined?"

My answer is: because pi is a calculated value - we really don't "know" the value of pi. If you want it in your script, calculate it using the trig functions provided!

pi = 4*Atn(1)

And there you are.

In reality, there is some justification to my claim that you don't want it in the core language - after all, they did include all of the trig functions, so it can be calculated accurately.  Furthermore, if you toss in an arbitrary non-terminating constant such as pi, you introduce a host of potential problems if the core language is ever changed to increase precision.  Not a likely possibility with a scripting language never intended to do heavy math work, but still possible.

Bookmarklets

In case you haven't heard of them (at least by name) "bookmarklets" are shortcuts which actually contain a JavaScript/Jscript and can be used to do some useful function.  Here is a mini-calculator bookmarklet; drag it to your desktop to keep a local copy. OK, you have "calc" anyway, but this allows you to type in complex functions and relationships.

Calculator

For a look at a wide variety of bookmarklets, check out the Bookmarklet site.