Perl provides a good shell scripting language

Sometimes you must be creative to make do with the tools you have. With a little bit of effort, you can mold, shape, and bend NT's command-line utilities in ways you might not have thought possible. This month, I'll demonstrate some of Perl's powerful text-processing capabilities and how they put you in control of NT's command line.

The ingredients I'll use include three Perl operators--backticks, grep, and map--and regular expressions. When you combine the Perl operators, you can slice and dice the output of any NT command-line utility into any format. You can format the output of one command so that another command can use it. Or you can reformat and combine the output of several commands to generate a report. First I'll explain how the three Perl operators work, and then I'll walk you through an example script.

Enclosing a command in backtick, or backquote, characters (`) lets you execute the command from within a Perl script. The syntax is straightforward. For example, in the expression

@results = `net user`;

I've enclosed the command I want to invoke (NET USER) in backticks.

Backtick strings, like double-quoted strings, support variable interpolation. That is, when a script encounters a command enclosed in backticks, the script replaces the variable names with their current value before execution.

Following any variable substitution, the script passes the command to NT's command processor for execution; the script waits for the command to complete before continuing. At completion, the script captures the standard output of the command in string form. You can capture the command's output in a scalar variable or an array. My example statement (@results = `net user`;) executes NT's NET USER command and captures its standard output in an array named @results. Each element of @results corresponds to one line of NET USER's output. Figure 1 shows the @results values for my examples.

Perl provides quoted execution notation as another way to express the backtick command. This method lets you choose an alternative command delimiter for the backtick symbol. Each of the following statements is functionally equivalent to @results = `net user`;:

@results = qx/net user/;
@results = qx(net user);
@results = qx\[net user\];

Perl's grep function finds or counts matching elements in an array. For example, you can use grep to find all users whose first names begin with T; grep is similar to NT's findstr command. The syntax for grep is

grep(/expression/ or \{code block\}, @array);

Grep evaluates the expression or code block for each element in the array. If the expression is true, grep returns the corresponding element in the array. If you use grep in a scalar context, grep returns the number of times the expression was true.

For example, in the two statements

@newresults = grep(/sql/i, @results);   # array or list context
$numresults = grep(/sql/i, @results);   # scalar context

I use a regular expression (/sql/i) as the first argument and the @results array as the second argument. Grep successively evaluates the regular expression for each element of @results. Any line containing the string sql (i tells the function to ignore case) results in a match, or true expression.

For the contents of the @results array shown in Figure 1, Table 1 lists the values of the @newresults and $numresults variables. Notice that grep returns a value only when the expression is true. Also note that if your expression or code block modifies the contents of a list element, the corresponding element in the original list also changes.

The final operator is Perl's map function. Map's syntax is similar to grep's syntax. However, the purpose and behavior of the two functions are different. You use grep to find matching elements in a list; you use map to transform a list. That is, map creates a new list based on the contents of another list.

In the example

@users = map(/(\S+)\s+/g, @newresults);

I use a regular expression (/(\S+)\s+/g) as the first argument and the @newresults array from Table 1 as the second argument. Like grep, map successively applies the expression or code block to each element in the list. The regular expression looks for one or more non-whitespace characters, the (\S+) part, followed by one or more whitespace characters, the \s+ part. The g, or global modifier, tells the script to find all occurrences of this pattern in the array element. The parentheses around the \S+ group the pattern substring I want returned. For the contents of the @newresults list in Table 1, the @users array contains the values shown in Table 2.

The Script
Let's apply this basic understanding of backticks, grep, and map in a script. Listing 1,, uses NT's built-in NET USER command to obtain information about the last logon date and time for all users on the server or domain, and generates a report listing that information.

The script first invokes NET USER to create the list of users on the system. Then the script loops through this list a second time invoking NET USER username to obtain the desired data for each user. The result will be a neatly formatted report of all users and their associated last logon date and time stamp.

The code at callout A creates the user list. At A, you can see where the @users array is set using the results of the map command. The map command in turn is using the output of the grep command as input. Likewise, the grep command is using the output of the NET USER command as input. To help you understand how this works, let's look at the different stages of this process.

The NET USER command at the end of callout A produces the output in Stage 1 of Figure 2. Grep treats each line of NET USER's output as one list element. I use an if/else code block as grep's first argument. The code block tells grep to return a section of the output rather than elements based on a pattern.

The code block tells grep to return any elements located between a line that contains one or more hyphens and a line containing the string The command. For this task, I control the return value of grep's code block with the control variable $i. Grep returns an element only when the expression or code block is true.

As grep evaluates each line of NET USER's output, the if part of the code block tests whether the line contains one or more hyphens followed by a whitespace character ( /-+\s/ ). If it does, the code sets the control variable to 1 (true) but forces the if part of the block to return false with the 0; statement. You don't want the line with hyphens returned.

When grep evaluates the next element, the else part of the block will execute. With $i set to true, grep will return the lines containing usernames until it reaches a line that matches the /The command/ pattern. At this point, the code block sets the control variable back to 0 (false). Stage 2 of Figure 2 shows the elements grep returns.

Grep's return values then pass to the map command, which initializes the @users array as Stage 3 of Figure 2 shows. The code at B, page 208, opens the output report named lstlgn.txt and writes the title and column headings to it.

The foreach loop comes next. Through each iteration, the script assigns an element of the @users array to the scalar variable $user. The script then executes NET USER a second time, passing to `net user $user` the current username. This procedure produces the output in Stage 4 of Figure 2, which becomes the source list to the map function. This time I use map to transform the list of elements that NET USER username provided into a one-element list containing only the data I'm interested in: the last logon date and time stamp.

The regular expression /Last logon\s+(\S+\s\S+\s\S+|Never)\s+/ is the pattern that tells map what you're looking for. The regular expression tells the script to match the line that begins with the string Last logon, followed by one or more whitespace characters \s+, followed by three groups of non-whitespace characters separated with single spaces \S+\s\S+\s\S+ or ( | ) the string Never, and ending with one or more whitespace characters \s+.

I use the parentheses around the pattern substring I want returned. The result is the single-element @lastlogon list in Stage 5 of Figure 2.

The script then writes this data to the report file, undefines the @lastlogon array, and moves on to the next user in the @users list. The write function makes use of the REPORT format defined at C in Listing 1, page 208, which produces the neatly arranged lstlgn.txt output file in Stage 6 of Figure 2. Formats are a Perl feature that give you precise control over the format of reports. The REPORT format defines two left-justified (by way of the less than symbol), fixed-width fields to which the contents of the $user and $lastlogon\[0\] variables are written. Formats behave in a way similar to functions. When the script encounters the write(REPORT); statement, execution jumps to the REPORT format definition which dictates the output of write. Format definitions must end with a period on a line by itself. The script ends by closing the output file with a call to the close function, close(REPORT).

At Your Command
With the strength of Perl's text-processing facilities, you can easily extend the power of NT's command-line utilities. Sometimes the tool you're looking for is staring right at you; all you need is a bit of creativity.