Hey, Scripting Guy! QuestionHey Scripting Guy! At our company, we have a lot of Windows PowerShell scripts we use to gather information from various client computers distributed throughout the network. All of these scripts are in a single folder. Recently, several users have been reporting that these scripts are causing them problems, and they have been complaining to my boss. I suspect the reason they are complaining is that one of the things the scripts do is look for games and other pieces of software they are not supposed to have installed on their computers, but my boss is a political animal who hopes to get promoted to director soon and therefore he does not want to make any waves.

He has dictated that I test every one of the scripts and create a list that indicates if the script causes errors. He also wants to know how long each of the scripts takes to run. I told him that the scripts were all tested in the lab before we deployed them, but he is asking for documentation that he can show in a meeting of the User Advisory Board next week. I told him there are more than 200 scripts and that a minimum of 15 minutes per script it will take at least 50 hours to do. I have a job to do already that causes me to work nearly 60 hours a week. Do you know what his answer was? He said I had better get started because it seems I am going to be busy this week. This is crazy. Can you help me?

-- DJ

Hey, Scripting Guy! AnswerHello DJ,

There is nothing I can do about a crazy and insensitive boss. I really wish I could but there is no delete method for the Win32_CrazyBoss WMI class. In fact, there is no Win32_CrazyBoss WMI class. Bummer! However, what I can do is write a test harness script for you. There are two things you state that the crazy boss wants in his report: the length of time it takes the script to run and the number of errors generated by the script. The Test-ScriptHarness.ps1 searches a folder for all the .ps1 scripts, executes each script while checking for errors, and records the length of time it takes for each script to run. It writes the results to a text file and displays the report. The complete Test-ScriptHarness.ps1 script is seen here.

Test-ScriptHarness.ps1

if((Get-WmiObject win32_computersystem).model -ne "virtual machine")
  {
    $response = Read-Host -prompt "This script is best run in a VM.
    Do you wish to continue? <y / n>"
    if ($response -eq "n") { exit }
  }
$path = "C:\bp"
$report = [io.path]::GetTempFileName()
Get-ChildItem -Path $path -Include *.ps1 -Recurse |
ForEach-Object -Begin `
  {
   $stime = Get-Date
   $ErrorActionPreference = "SilentlyContinue"
   "Testing ps1 scripts in $path $stime" |
     Out-File -append -FilePath $report
  } -Process `
  {
   $error.Clear()
   $startTime = Get-Date
   "  Begin Testing $_ at $startTime" |
     Out-File -append -FilePath $report
   Invoke-Expression -Command $_
   $endTime = Get-Date
   "  End testing $_ at $endTime." |
     Out-File -append -FilePath $report
   "    Script generated $($error.Count) errors" |
     Out-File -append -FilePath $report
   "    Elasped time: $($endTime - $startTime)" |
     Out-File -append -FilePath $report
  } -end `
  {
   $etime = Get-Date
   $ErrorActionPreference = "Continue"
   "Completed testing all scripts in $path $etime" |
     Out-File -append -FilePath $report
   "Testing took $($etime - $stime)" |
     Out-File -append -FilePath $report
  }
 
  Notepad $report

The first thing the Test-ScriptHarness.ps1 script does is determine if the script is running inside a virtual machine. A script that is going to execute a large number of Windows PowerShell scripts could potentially cause significant damage to your workstation, depending on what the scripts are actually doing. For example, if one of the scripts was one that kicked off an automated installation of Windows Vista, you could potentially wipe out all of your data and end up with a fresh installation of Windows Vista. If you run the script inside a virtual machine on Microsoft Virtual PC with undo disks enabled, you are minimizing the potential disruption the scripts could cause.

Because the Win32_ComputerSystem WMI class returns a single instance, you can directly access the properties of the class. This means you do not need to work through a collection of instances of the class to retrieve the model property value. On Microsoft Virtual PC, the model is reported as "virtual machine."  If the model is not reported as "virtual machine," the script will display a prompt that asks if you wish to run the script. This prompt is created by using the Read-Host cmdlet. If you press n in reply to the prompt, the script will exit. Any other response to the Read-Host prompt permits the script to run. This is seen here:

if((Get-WmiObject Win32_ComputerSystem).model -ne "virtual machine")
  {
    $response = Read-Host -prompt "This script is best run in a VM.
    Do you wish to continue? <y / n>"
    if ($response -eq "n") { exit }
  }

The path to search for Windows PowerShell scripts is stored in the $path variable. Depending on how you plan to run this script, you might want to change this to a command-line parameter:

$path = "C:\bp"

The GetTempFileName static method from the System.Io.Path .NET Framework class is used to create a temporary file name in the users temporary directory. The path to this temporary file name is stored in the $report directory. An example of a temporary file name is seen here:

C:\Users\administrator.NWTRADERS.000\AppData\Local\Temp\tmpC484.tmp

Because the file name is randomly generated each time the GetTempFileName method is called, it is stored in the $report variable for use later in the script:

$report = [io.path]::GetTempFileName()

The Get-ChildItem cmdlet is used to produce a listing of all the .ps1 files in the folder referenced by the $path variable. The recurse parameter is required to permit the Get-ChildItem cmdlet to retrieve all the .ps1 files in the folder. The results from the Get-ChildItem cmdlet are pipelined to the ForEach-Object cmdlet. This command is seen here:

Get-ChildItem -Path $path -Include *.ps1 -Recurse |

The ForEach-Object cmdlet uses the Begin parameter to perform an action once for all the items that enter the pipeline. In this example the starting time of the script processing is stored in the $stime variable (the $stime variable is used instead of $startTime because $startTime will be used later). The value of the $ErrorActionPreference automatic variable is set to SilentlyContinue, which will cause errors to not be displayed. It also permits the script to attempt to continue processing when an error is encountered. A status message is written to the $report file that indicates the beginning of script testing and the time it commenced. This is seen here:

ForEach-Object -Begin `

  {

   $stime = Get-Date

   $ErrorActionPreference = "SilentlyContinue"

   "Testing ps1 scripts in $path $stime" |

     Out-File -append -FilePath $report

The process parameter will occur once for each object that comes through the pipeline. The first thing that is done is to clear all errors from the error stack. This will ensure that any errors that occur will be specific to the particular script that is being tested. A new time is written to the $startTime variable. This time stamp will be used to calculate how long it takes the specific script to run. An entry is written to the report that indicates the name of the script and the starting time from the $startTime variable. The name of the script is obtained from the $_ automatic variable. The $_ automatic variable refers to the current object on the pipeline. All of the output from this section is then pipelined to the Out-File cmdlet with the –append parameter to tell it to add to the $report file instead of overwriting the file. This is seen here:

  } -Process `

  {

   $error.Clear()

   $startTime = Get-Date

   "  Begin Testing $_ at $startTime" |

     Out-File -append -FilePath $report

It is now time to run the script that is on the pipeline. To execute the script, you use the Invoke-Expression cmdlet with the command parameter and provide it with the $_ automatic variable:

   Invoke-Expression -Command $_

When the script has completed running, it is time to retrieve the time the script completed. The end time of the script is then pipelined to the Out-File cmdlet with the append parameter. This is shown here:

   $endTime = Get-Date

   "  End testing $_ at $endTime." |

     Out-File -append -FilePath $report

To continue with the report, the numbers of errors on the error stack is obtained and written to the $report file. Because the $error automatic variable contains an object, a subexpression is used (a dollar sign and a set of parentheses surround the $error variable) to force the evaluation of the count property from the $error object. This value is then sent down the pipeline to the Out-File cmdlet. This is seen here:

   "    Script generated $($error.Count) errors" |

     Out-File -append -FilePath $report

The DateTime object that is stored in the $startTime variable is subtracted from the DateTime object that is stored in the $endTime variable. Once again, to force the evaluation of this operation, a subexpression is used. If you did not use a subexpression inside the expanding string double quotation marks, you would have to use concatenation to combine the string and the DateTime objects. The time that is created by subtracting the starting time from the ending time is pipelined to the Out-File cmdlet for inclusion in the report. After this is done, the process block of the Foreach-Object cmdlet is completed. This is seen here:

   "    Elasped time: $($endTime - $startTime)" |

     Out-File -append -FilePath $report

  } -end `

After the last script has run, the ending time is stored in the $etime variable. The value of the $ErrorActionPreference variable is set back to the default value of Continue, and the ending time is written to the report. This is seen here:

  {

   $etime = Get-Date

   $ErrorActionPreference = "Continue"

   "Completed testing all scripts in $path $etime" |

     Out-File -append -FilePath $report

The last thing that needs to be done is to record the total running time for all the script testing. To do this, the start time that is recorded in the $stime variable is subtracted from the time stored in the $etime variable. A subexpression is used to force the evaluation of the total elapsed time. The total time pipelined to the Out-File cmdlet and the entire report is displayed in Notepad. This is seen here:

   "Testing took $($etime - $stime)" |

     Out-File -append -FilePath $report

  }

  Notepad $report

The report that is produced is seen here:

Image of produced report


DJ, hopefully the Test-ScriptHarness.ps1 script will fit the bill for enabling you to test several hundred scripts and producing a report. We Scripting Guys really hate to think of someone working 110 hours in a single week, especially when a script can be written in a few hours that saves you 50 hours of work. Join us tomorrow as we continue talking about testing scripts. Follow us on Twitter to stay up to date will all the latest news on the Script Center. You can also join our Facebook group for similar purposes. Don't forget about the Official Scripting Guys Forum where thousands of dedicated scripters from all over the world meet to discuss scripts. Drop us e-mail at scripter@microsoft.com to let us know how things are going. Until tomorrow, have a great day!

Ed Wilson and Craig Liebendorfer, Scripting Guys