Progressive Perl for Windows
All administrators keep an arsenal of tools at their disposal, copying the tools from machine to machine so that the tools are always available. Through the years, I've created indispensable tools that perform specific but necessary tasks. I once wrote these tools in C and C++, but I eventually migrated to Perl because I found that I didn't need the sheer speed of C for such tools. In addition, modifying a tool is simpler when it's written in a scripting language.
I want to share a couple of my tools with you. These particular tools let you search the directories specified in the Path environment variable for a file, calculate a directory tree's disk usage and number of files, delete files based on criteria such as age and file size, and query or change system properties. These tools aren't terribly dramatic or special, but my clients and I have found them repeatedly useful. Most administrators will need their functionality at one time or another.
Searching the Path
When you type an application name on the command line, then press Enter, the OS first looks for the application in the current directory — unless you specify the full path to the file. If the OS fails to find the file in the current directory, it systematically checks each directory in the Path environment variable, in the order in which the directories occur in the variable.
Occasionally, an undesired file (e.g., an old version of a .dll or .exe file) resides somewhere in the Path variable. For example, this situation might occur if you install an application that modifies the Path or installs an alternative version of a .dll or .exe file. A potential result of this scenario is undesired collisions, in which the OS finds and uses an old file version instead of the intended file. You might witness such a collision if a program complains about bad versions of the Microsoft Visual Basic (VB) runtime file (vbrun.dll) or the C language runtime file (msvcrt.dll).
Locating all copies of offending files can be difficult because they might be scattered among many directories. FindInPath.pl, which Web Listing 1 (http://www.winscriptingsolutions.com, InstantDoc ID 27332) shows, searches every directory in the Path for a specified file and shows you where to find it. (You can find FindInPath.pl as well as the other Perl scripts I discuss in the Code Library on the Windows Scripting Solutions Web site at http://www.winscriptingsolutions.com.) Simply pass in the appropriate filenames, as follows:
Running this script with these parameters displays the full path to all versions of the vbrun300.dll and msvcrt.dll files that reside somewhere in the Path. The script accepts the DOS wildcards * and ?. So, to quickly find all Visual C++ (VC++) runtime libraries, you can run
perl findinpath.pl msvc*.dll
FindInPath.pl relies on Perl's glob function. Globbing essentially lets you use wildcards to group files and directories. For example, if you want a list of all .dll files in the \windows\system32 directory, you can use the following Perl code:
@Files = glob( 'C:\windows\system32\*.dll' );
The glob function works similarly to the DOS Dir command. However, glob can't handle embedded spaces. So, the following Perl code would result in an empty array:
@Files = glob( 'C:\program files\*.*' );
The File::DosGlob module (included in ActiveState's Perl) addresses the embedded-spaces problem. However, the module isn't without faults. The first problem is that it recognizes only UNIX's path delimiter (i.e., the slash, unlike the DOS and Windows backslash). Therefore, you can't pass in C:\windows\system32\*.dll. Instead, you must use C:/windows/system32/*.dll. This flaw is fairly easy to work around, but it's a nuisance, particularly because Perl's built-in glob function supports the backslash.
The second problem is that the File::DosGlob module doesn't handle embedded spaces elegantly. The module requires that you either surround the directory or file mask with quotation marks (C:/"program files"/*.*) or escape all spaces with a backslash (C:/program\ files/*.*).
Pruning a Directory Tree
Over time, temporary files can fill up a hard disk. As disk space decreases to an unacceptably small amount, application functionality and general OS performance can suffer. Also, letting the number of files grow without regularly weeding out old files can complicate searches for the files you need.
Administrators commonly add to users' logon scripts logic that deletes all files from specific directories (e.g., a user's temporary directory). However, this approach is limited to filtering files based on filename or file type. For example, a logon batch file might include the command
This command deletes all files in the temporary directory. Unfortunately, such an approach can wipe out files (e.g., temporary versions of documents that a user is working on) that are important to keep. Ideally, you'd like to specify age and size parameters for file deletion. Then, you could use a logon script to delete all files older than a specified age (e.g., 2 weeks), larger than a particular file size, or both.
CleanOldFiles.pl lets you use such criteria when you delete files. You can specify a date by supplying the number of months, weeks, days, hours, minutes, and seconds or by supplying a particular time and date. The script will identify any file older than the specified date and time. For example, if you launch the script with the command
CleanOldFiles.pl displays all .dll files in the \windows\system32 directory that are older than July 1, 2001.
As another example, the command
C:\temp\*.txt -mon 3 -d 22
prompts the script to delete from the C:\temp directory all .tmp and .txt files older than 3 months 22 days. By specifying the -r option, the command also deletes files in subdirectories. To prevent someone from accidentally deleting files, the script requires the -delete option to delete files. Otherwise, the script merely displays files.
The script also lets you use the -minsize and -maxsize options to specify a minimum and maximum size, respectively. For the script to consider a file for deletion (or display), the file must be larger than or equal to the minimum size and smaller than or equal to the maximum size. You can specify these sizes in bytes (e.g., 10240), or you can use a suffix (e.g., 1K, 27.5M). The script determines whether the specified size is in kilobytes (K), megabytes (M), gigabytes (G), or terabytes (T). If you don't specify a suffix, the script assumes bytes.
Listing 1 shows an excerpt from CleanOldFiles.pl. At callout A in Listing 1, the script parses any passed-in date string into a @Date array. The format of the date string can be either YYYY-MM-DD or YYYY-MM-DD H:M:S. The date string must have a four-digit date (e.g., 2003), a two-digit month (e.g., 06), and a two-digit day (e.g., 23). If you provide both the date and time, the time string is more flexible because it permits a one- or two-digit hour, minute, and second value. For example, you might specify
-date 2001-07-01 1:02:03
on the command line. After parsing the string, the script calls the timelocal() function (exported from the Time::Local module). Doing so results in a timestamp value similar to that returned by the time() function (i.e., the number of seconds since midnight on January 1, 1970).
If you didn't specify a date string, the script runs the code that callout B in Listing 1 shows. Unlike the code at callout A, this code computes a time relative to when the script runs based on any time parameters (e.g., minutes, hours, days) that you specified. If you didn't specify any time parameters, this code block results in an age difference of 0 seconds, in which case the script will consider all files older than the current time.
The code at callout C in Listing 1 processes each path that you pass in when running the script. The code breaks each specified path into a directory and a file mask. The code then passes both components to the ProcessMask() function, in which the script globs the list of files based on the directory and file mask, as you see at callout D in Listing 1. For example, if the mask was *.dll, the script would evaluate all files with a .dll extension. When the beginning of the script loads the File::DosGlob module, it exports the module's glob function. So, although the code at callout D appears to call glob(), it's actually calling File::DosGlob::glob().
The real work occurs at callout E in Listing 1. This code collects statistics from each file. If the file's size is greater than or equal to the minimum allowed size but smaller than or equal to the maximum allowed size and the file hasn't been accessed (not created) within the specified age limit, the script displays the file. If you specified the -delete parameter, the script deletes the file.
Directory Tree Size
Administrators often need to determine how large a directory tree has grown. Doing so is important for file pruning and determining available space in a particular directory. DirTreeSize.pl is a simple but useful script that scans all files and subdirectories and displays the total number of files and the total space that the directory tree consumes. The script is easy to use. For example, to scan all files and subdirectories in the C:\temp directory, just pass in the directory, as follows:
The script passes the directory path into the ProcessDir() function. The function enumerates each file and subdirectory, calls the ProcessFile() function for each file, and recursively calls the ProcessDir() function for each subdirectory. Listing 2 contains an excerpt from DirTreeSize.pl. At callout A in Listing 2, the code is about to call ProcessDir() for a subdirectory, so it increases the directory counter and updates the display before processing the subdirectory.
The script uses Perl's stat() function to discover the size of a file and update the total file counter, as the code at callout B in Listing 2 shows. The script then calls the UpdateDisplay() function to display updated total directory and file-counter values, as well as the amount of disk space used for all files, as the code at callout C in Listing 2 shows. The end of the print statement displays the \r character, which introduces the equivalent of a carriage return without moving to the next line, thereby letting you overwrite text on the current line. So, the display is updated but never takes more than one line.
Querying and Setting System Properties
The Windows OS provides so much functionality that programmatically managing even the simplest features might seem difficult. Take, for example, setting a background image as your desktop's wallpaper. Manually setting the background is reasonably simple. You right-click the desktop and select Properties, then select the Desktop tab (or the Background tab on Windows 2000 and Windows NT machines) and choose a graphic file. (You can also use the Control Panel Display applet.) But how do you perform the same function from a script? If you know which function to use, the process is pretty straightforward.
SystemInfo.pl lets you modify or query the status of specific system parameters. The script uses the SystemParametersInfo() Win32 API function. With this function, you can manipulate more than 150 system parameters, ranging from the obscure to the indispensable. SystemInfo.pl's limited implementation of SystemParametersInfo() lets you set or query the desktop wallpaper file, the screen saver's enabled/disabled status and timeout value, the keyboard's key-repeat rate, and the enabled/disabled status of full-window dragging (as opposed to window-outline dragging). For more information about the SystemParametersInfo() function, including the parameters that it can query and set, see the Microsoft Developer Network (MSDN) article "SystemParametersInfo" at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/sysinfo/base/systemparametersinfo.asp.
SystemInfo.pl requires the use of the Win32::API extension and the Win32::API::Prototype module. Aldo Calpini's useful Win32::API extension lets Perl directly access DLLs so that your script can call any DLL's functions. You can download the latest version of this extension from http://dada.perl.it. The Win32::API::Prototype module, which simplifies the use of the Win32::API extension, is available from http://www.roth.net/perl.
SystemInfo.pl uses several constant values. The supported constant values are available in the MSDN article I mentioned previously, and the constant values are available in the Win32 software development kit's (SDK's) WinUser.h header file. The script's BEGIN block programmatically defines a series of constants. The block creates constants from a hash. I chose this method instead of defining global scalar variables because I didn't want to define each of the scalar variables. Therefore, managing the list of constants is easier. Perl automatically executes all BEGIN blocks before executing the body of the script. When you use a BEGIN block to define the constants, the constant values are immediately available.
To create the constants, the script assigns anonymous subroutines (or CODE blocks) directly into Perl's symbol table. Doing so lets the script dynamically create the subroutines at runtime. In the script's BEGIN block, notice that the name of the subroutine the script assigns a value to is the same as the name of the constant. When the script refers to a particular constant, the script is really just calling the subroutine, which returns the constant's value. Although this functionality might seem like a horrendous hack, it's how constants work in Perl.
The rest of the script is reasonably straightforward. Based on the options that you can pass into the script, the script takes one of many code paths. If you specify an optional parameter, the code assumes that you're changing a setting. The line
tests for such a setting operation. If you specifically passed in a parameter intending to change the specified setting, the script's Configure () function defines a key called parameter in the % Config hash. Otherwise, the code assumes that you're simply querying the current setting. The script permits the following options:
- The -w option lets you query or change the path to the desktop's wallpaper graphic. Note that this option works only on Windows .NET Server (Win.NET Server) 2003, Windows XP, and Win2K.
- The -s option lets you query or change the screen saver's enabled/disabled status.
- The -st option lets you query or change the length of time (in seconds) the system must be idle before the screen saver starts.
- The -k option lets you query or change the keyboard's key-repeat rate.
- The -d option lets you query or change the enabled/disabled status of full-window dragging (as opposed to window-outline dragging).
If you want to see the file that the system currently uses as the desktop wallpaper, you type
at the command line. If you want to change the wallpaper, you pass in the .bmp file's path, as follows:
Note that wallpaper graphics must be .bmp files. Interestingly, if you use the Windows Desktop Properties dialog box, you can select other file formats (such as .jpg). However, the system first converts and stores the picture as a .bmp file, then Windows sets the wallpaper path to the .bmp file.
Tools for Every Administrator
These tools are a great way to indoctrinate someone into the world of Perl. Even on Win32 platforms on which scripting is relatively new, Perl outshines other languages and proves its power, utility, and functionality. These scripts are just a small part of my personal tool chest. I hope they prove worthy of your tool collection.