Although Windows Scripting is much simpler and more flexible than programming, I discovered early on that this did not mean it was always free of drudgework.
. It is a collection of wrapper functions I have written over time to allow me to do rapid administrative scripting, and as such may be useful to admin scripters. Experienced scripters will notice that they are patently NOT optimized for anything but ease of use (they're intended as "plug and play" procedures), but they work quite adequately. If any are used thousands of times in a time-critical task, it is probably a good idea to move object bindings out to global declarations.
That's a good question, and the answer to why - and what they should be like - is wrapped in the key concept of the role of scripting. This is all from my perspective, of course. One of the key points about scripting is that it is highly flexible and winds up working for different people in different ways.
For me, scripting is about instant solutions to problems in administration and automation of tasks on a PC. That means that I am focused on local (usually WSH-based) solutions, and that I want to be able to implement them with little or no work. I also want to focus on the big picture and not get bogged down in coding details.
The concept of using foundation code has given me quite a few benefits; it makes coding fun, reliable, easier to do, and just about everything else. Before we go into the details, let's discuss what I mean when I talk about foundation code.
My SFCs are procedures - functions or subroutines - for common tasks which I write once and then use repeatedly ever-after.
Let's take reading a file for example. There are many cases where you want to read in text, and the Scripting.FilesystemObject is the usual tool for the job.
Well, every time you want to do this, you have to:
This adds up to roughly 300+ characters of coding just to read a file whose name you already may know!
How do we work around this?
Easy. Write a procedure to do it - and write it ONCE. After that, it is permanent clip-text you use for code.
I think the reasons are obvious when you consider the potential savings in time alone. Here's an in-depth explanation for why, though, including some things I had not initially considered in scripting.
Need to read a file? No problem; after your 15 minutes spent on the original function, it is a matter of 5-15 seconds to get the function into a script that uses it!
As an example, grepping my script junkbin I find that over the last 4 months I have implemented a minimum of 64 file reads. That would conservatively be 2-5 hours of work over time. In reality, I've probably used the function I wrote more like 100 times, and since a couple of the minor steps (ForReading use) I would regularly forget and need to debug, the total is closer to 10 hours. Since I click-insert the function, I probably have spent less than 10 minutes worrying about it instead. This doesn't even account for "spillover" savings from other issues noted below.
A given, no doubt, but worth emphasizing. If you use a function 30 times, you will implement it 30 times in the same way.
One significant time loss in coding is understanding a routine and fixing problems with it. By using the same foundation code repeatedly, when you find a potential flaw, you can fix it in your foundation and have it for future use.
Key conceptual functions are broken out of any main routines you have into a procedure. This makes the main body of code much simpler to read.
Using foundation material gives you a big picture focus.
One of the more annoying things with coding is that when you are immersed in finding a solution, you often spend many hours drudging through details. Even a disciplined scripter may do that with a complex project.
Take a simple case where you have a file with some ASCII character garbage which you need to clean out. This breaks down into three tasks: reading in the data, massaging it to get out the bad characters, then writing out the data. With a set of foundations available, you tend to break things down into major tasks which can be delegated within your code.
No, this does not contradict the prior point. Simply put, using the prior example, if you have a foundation element for reading a file and another for writing a file, you have one task to focus on: massaging the text. Within seconds of starting your project, you have accomplished 2 of the 3 top-level tasks and are free to concentrate on the core "new" area.
Note that when you are done, you may have a new foundation element, one which takes a blob of text and removes all unusable characters.
Let's use our example of text cleanup once more. Data input and output have been "blobbed" into black boxes for writing and reading.
Now suppose we want to make it interactive: read the source from an inputbox or a web page, then write it to standard output.
No massive internal surgery on your code is required. You have 1 line in the main routine that handles input, and 1 line in the main routine that handles output. Remove those preparatory to your code conversion - and the referenced functions - and you are done.
Yes, it makes coding more fun. Repetitive work goes from minutes per script to seconds; you can look at the "new" things in each project; you get a reputation as a fast, reliable coder; reusability increases exponentially; everybody thinks you're a genius when in reality you're just being incredibly lazy and getting better at it each day...
Yes. Laziness is the hallmark of a good scripter. If you want to do things once and then never do them again, read on.
There are some things you should do in writing foundation code. They take a little more time, but they are immensely helpful in reuse.
I can't lay these down as absolute rules, but for me they have been singularly helpful. First, remember that we are dealing with blobs of script code here: they will typically be sets of declarations or single procedures.
When you use a function a few times, start making it easy to get to and reuse. This means putting the key items in something you can quickly pull them from. It may start out as a raw text file, but you will start finding it is hard to browse if you go very far with the idea.
Another option is a database. You can write your own; FMS makes a very nice one for VB/VBA which can easily be adapted to use for VBScripting/WSH support. Microsoft also has a "Code Librarian" tool available as part of the Office Developer's Kit.
My most frequently used code snippets are part of a TextPad clip library. With 3 clicks I have a function or code segment inserted and am on my way.
Comments Are Always A Good Thing.
Remember, code is communication.
There is roughly zero performance penalty for including comments; they get dumped during the interpreter's pre-processing. OK, it's not exactly zero. In fact, if you have a script which consists of the following statement:
x = 2
Then your pre-processing speed could be cut in half if you add 1000 lines (!) of commenting to it. In other words, to get even a 1% loss in pre-processing time, you would need to have 10 lines of comments for each simple statement.
For execution, it simply does not matter whether you have 1, 1000, or 1 million lines of commenting per line of interpretable code. I would see 1 million to 1 as a bit excessive, though.
I prefer that my procedures be self-documenting. They should normally contain the following wherever applicable
Purpose/Output: What the function does, and what it returns if anything.
Arguments: What arguments the procedure takes. Document any shortcut or "special" arguments, such as if supplying an empty value or zero-length string does something special. I have several "generic" WMI functions I use which take a host name as an argument; if "." is provided as the hostname, the functions will work against the local host.
Dependencies: You will quickly discover that you are building larger blocks out of smaller ones. In those cases, you should mention what a procedure depends upon.
For example, I have a generic Cmd function I use regularly; supply a string which is a console command of some kind, and it returns the standard output from that command. There are in turn several console commands which I like to use frequently and which require some specialty massing to produce usable output. Instead of rewriting the entire Cmd function as a portion of the new command, I simply call it and then have a DEPENDENCIES: line which mentions this.
Normally you will want to ensure that your code is a "black box": it takes something as input, gives you something back, and has no uncontrollable side-effects or bugs. To do that, there are some important points to consider (which will become instinctive as you develop complex code).
If you dimension your variables within a procedure, they will definitely be local and will not affect global code. This is critical for library functions; although there are interesting tricks you can play by not dimensioning variables, they typically require excruciating knowledge of every detail of your global code.
A corollary to the above point is that you should declare any objects you use within the local code. This makes clean drop-in code that won't break. You certainly will take a little extra time binding the object, but if you aren't doing event sinking, one simple cleanup factor will reduce your time significantly anyway: don't use WScript.CreateObject, use CreateObject within local code. This makes for dramatically faster code with a smaller memory footprint.
An exception to this is if you are using a WSF file and must do many instantiations. If you aren't sinking events, setting an object reference globally and then not dimensioning or creating a local reference will speed you up and be fairly safe.
Again, there are certain exceptions, but in general you do NOT want to do this. The reason is that you have no local control to ensure that an argument is passed ByVal or ByRef. If you don't know what this means, you definitely don't want to manipulate the original arguments. If you need to play with the argument sValue supplied to a procedure, use something like this:
sTmp = sValue
and then you can modify sTmp to your heart's content without producing side effects.
Good code, like any growing self-consistent and enduring structure, has a lot of self-similarity. Patterns and ideas are repeated. One of the key ones for me is naming.
By dimensioning everything, we have ensured that any locally used names are not going to collide with global ones. Having done that, I tend to use the exact same names everywhere within local code. Scripting.FilesystemObject is always FSO; WScript.Network is always Net; WScript.Shell is always Sh. A temporary string variable is always sTmp; a temporary array is always aTmp.
What does this do for me?
First, when I glance at the procedure ObscureFunctionIHaventSeenForSixMonths, I immediately know that FSO a Scripting.FileSystemObject reference; and I always know that if I want to find a Scripting.FileSystemObject reference in it, I just have to look for FSO.
Second, if I DO need to recurse or globalize references, I can yank the "Set FSO = " and the "Dim FSO" lines throughout the code and be done with it.
Let's start taking a look at some foundation code I have written in the past. Most of it is trivial - which is exactly why it is critical to have available. I tend to classify my key foundation code into critical operations I do a lot. Most of those center around the following areas: File operations, shell operations, string manipulation, and then advanced interface operations.
There are 3 things I do regularly with files. I read them, I write to them, and I append to them.
The following routines are the quick-and-dirty functions I always use for this.
Function fRead(FilePath)
'Given the path to a file, will return entire contents
' works with either ANSI or Unicode
Const ForReading = 1, TristateUseDefault = -2, _
DoNotCreateFile = False
With CreateObject("Scripting.FileSystemObject")._
OpenTextFile(FilePath, ForReading, _
False, TristateUseDefault)
fRead = .ReadAll: .Close
End With
End Function
Sub fWrite(FilePath, sData)
'writes sData to FilePath
With CreateObject("Scripting.FileSystemObject")._
OpenTextFile(FilePath, 2, True)
.Write sData: .Close
End With
End Sub
Sub fAppend(FilePath, sData)
'Given the path to a file, will append sData to it
With CreateObject("Scripting.FileSystemObject")._
OpenTextFile(FilePath, 8)
.Write sData: .Close
End With
End Sub
With WScript.Shell, I regularly do 3 things: get a console tool's output; read a registry value; and write a registry value.
Getting console output back - especially if you may be using wscript as the host, have a pre-5.6 version of WSH, or don't want a console window to show - can be annoying, but it relatively simple to wrap.
Getting and setting registry data is not too hard, but I still like to be able to quickly check or set a value without writing several lines of code; the GetRegVal and SetRegVal procedures make that simple.
Function Cmd(cmdline)
' Wrapper for getting StdOut from a console command
Dim Sh, FSO, fOut, OutF, sCmd
Set Sh = CreateObject("WScript.Shell")
Set FSO = CreateObject("Scripting.FileSystemObject")
fOut = FSO.GetTempName
sCmd = "%COMSPEC% /c " & cmdline & " >" & fOut
Sh.Run sCmd, 0, True
If FSO.FileExists(fOut) Then
If FSO.GetFile(fOut).Size>0 Then
Set OutF = FSO.OpenTextFile(fOut)
Cmd = OutF.Readall
OutF.Close
End If
FSO.DeleteFile(fOut)
End If
End Function
Function GetRegVal(RegKey, RegValueName)
' ARGUMENTS: RegKey, RegValueName
' RETURNS: Value if there; empty if not
' If wanting default value, use "" for RegValueName
' ROOT KEY ABBREVS: HKCU, HKLM, HKCR
Dim sTmpPath, Sh
sTmpPath = RegKey & "\" & RegValueName
Set Sh = CreateObject("WScript.Shell")
On Error Resume Next
GetRegVal = Sh.RegRead(sTmpPath)
On Error Goto 0
End Function
Function SetRegVal(RegKey, RegValueName, sValue, sType)
' ARGUMENTS: Registry Path, Valuename, sValue, type
' RETURNS: True if successful, False if not
' TYPES: REG_SZ, REG_EXPAND_SZ, REG_DWORD, REG_BINARY
' ROOT KEY ABBREVS: HKCU, HKLM, HKCR
Dim sTmpPath, Sh
sTmpPath = RegKey & "\" & RegValueName
Set Sh = CreateObject("WScript.Shell")
SetRegVal = FALSE
On Error Resume Next
Sh.RegWrite sTmpPath, sValue, sType
If Err.Number = 0 Then SetRegVal = True
On Error Goto 0
End Function