The Evolution of a Script: This is How You Learn PowerShell

I just got back from teaching a PowerShell class in Stockton, California to@esacteksab and some of his coworkers. We got to a bit of a late start on the first day, so the two of us sat down and looked at some of the scripts he'd written for the recent Scripting Games. In particular, he shared his series of scripts for Event 10. Here's how he started out:
$Test1 = Measure-Command {Start-Sleep 5}
$Test2 = Measure-Command {Start-Sleep 5}
$Test3 = Measure-Command {Start-Sleep 5}
$Test4 = Measure-Command {Start-Sleep 5}
$Test5 = Measure-Command {Start-Sleep 5}

$A = [int]$Test1.TotalMilliseconds
$B = [int]$Test2.TotalMilliseconds
$C = [int]$Test3.TotalMilliseconds
$D = [int]$Test4.TotalMilliseconds
$E = [int]$Test5.TotalMilliseconds

($A + $B + $C + $D + $E)/5

The goal of the event was to measure five executions of a given command, and then display the average execution time. As you can see, Barry took a very literal approach in this first attempt. Many folks would have dropped the matter right here, satisfied that this result was correct. This was, however, the Scripting Games, and Barry knew he'd need to do something a bit more clever.
$Result = @(1..5 | %{ Measure-Command {Start-Sleep 5}} |
Select-Object -ExpandProperty TotalMilliseconds )

$Average = ($Result[0]+$Result[1]+$Result[2]+$Result[3]+$Result[4])/5

Write-Host "Average time of 5 runs of 5 seconds is $Average milliseconds"

This was his second attempt. Here, he's used the range (..) operator to generate integers 1 through 5. Those are fed into a ForEach-Object loop (% is an unfortunate alias for ForEach-Object), which executes the command five times, measuring each execution. Each time, he extracts the value of the measurement's TotalMilliseconds. All five results wind up in an array, $Result, and he then averages up the five values individually.

It's still a bit clunky because it's hardcoded to 5 values. What if you later wanted to do just 4? Or 6? Or 100? 
$Average = (
 @(1..5 | %{ Measure-Command {Start-Sleep 5}} |
 Select-Object -ExpandProperty TotalMilliseconds ) |
 Measure-Object -average).Average

Write-Host "Average time of 5 runs of 5 seconds is $Average milliseconds"

Brilliant. Just let Measure-Object average out all the results, however many there are. The only thing that makes me a little uncomfortable is the ().Average syntax. It's not wrong, it's just a little code-y, and it's a bit hard to parse. You really have to follow the punctuation.

Here's the fourth attempt:
param(
    [string]$Count = 5,
    $Script = {Start-Sleep 5}
    )

$Average = (
 @(1..$Count | %{ Measure-Command $Script} |
 Select-Object -ExpandProperty TotalMilliseconds) | Measure-Object -average).Average

Write-Host "Average time of $Count runs of $Script is $Average milliseconds"

Ah, adding some parameters there, so that the final script can be given the command to run, as well as the execution count, making it even more reusable. This is brilliant: Start small, get it working the way you want, and then parameterize it to make it more reusable in the future.

His final submission for the event:
<#
   .Synopsis
        Get's the Average Run Time of a Command
   .Description
        Get's the Average Run Time of a Command
   .Parameter Count
        Define number of times to run command, will default to 5 if no option is given
   .Parameter Script
        Define Script to run, will run 'Start-Sleep 5' if not command is entered
   .Example 1
        Get-AverageRunTime.ps1
       
        Average time of 5 runs of Start-Sleep 5 is 4999.8988 milliseconds
        --Get's the Average run time of 'Start-Sleep 5' ran 5 times
   .Example 2
        Get-AverageRunTime.ps1 -Count 7
       
        Average time of 7 runs of Start-Sleep 5 is 4999.84081428572 milliseconds
        --Get's the Average run time of 'Start-Sleep 5' ran 7 times
   .Example 3
        Get-AverageRunTime.ps1 -Script .\event10-test.ps1
       
        Average time of 5 runs of .\event10-test.ps1 is 5002.18004 milliseconds
        --Get's the Average run time of '.\Event10-Test.ps1' ran 5 times
   .Example 4
        Get-AverageRunTime.ps1 -count 7 -Script .\event10-test.ps1
       
        Average time of 7 runs of .\event10-test.ps1 is 5001.71274285714 milliseconds
        --Get's the Average run time of '.\Event10-Test.ps1' ran 7 times
   
    NAME: Get-AverageRunTime.ps1
    AUTHOR: Barry Morrison
    LASTEDIT: 04/17/2011 19:44:33
     
#>

param(
    [string]$Count = 5,
    $Script = "Start-Sleep 5"
    )
   
$ScriptBlock = $ExecutionContext.InvokeCommand.NewScriptBlock($Script)

$Average = (@(1..$Count | %{ Measure-Command $ScriptBlock } | Select-Object -ExpandProperty TotalMilliseconds ) | Measure-Object -average).Average

Write-Host "Average time of $Count runs of $Script is $Average milliseconds"

Ah, I'm getting a bit teary-eyed... comment-based help. A self-documenting script.

So what would I do? Well... probably lose the Write-Host. I'd probably name this script something like Measure-CommandAverage, and I'd write it as follows (skipping the comment-based help for brevity):
param(
    [string]$executionCount = 5,
    $scriptBlock = {Start-Sleep 5}
    )

1..$Count |
ForEach-Object { Measure-Command $Script | Select-Object -ExpandProperty TotalMilliseconds } |
Measure-Object -average |
Select-Object -property Average

Not so different. Mainly, I'm just allowing the Measure-Object's Average property - which is part of an object - to be the script's final output, rather than outputting plain-text. PowerShell scripts and functions should always output text objects (Barry's clear on that after this week's class). 

Actually, aside from this specific Scripting Games event, I'd do this:
param(
    [string]$executionCount = 5,
    $scriptBlock = {Start-Sleep 5}
    )

1..$Count |
ForEach-Object { Measure-Command $Script | Select-Object -ExpandProperty TotalMilliseconds } |
Measure-Object -average -sum -minimum -maximum

This way my script outputs more information that is strictly required today, but later on if I decide I need the maximum execution time, or the fastest execution time, the data is being output. 

Anyway, this is exactly how you should approach PowerShell as a beginner. Start with the obvious... then learn how to trim down each bit. Improve in iterations, testing after each new attempt. Never be afraid to revisit after you've learned new things, to see if you can apply them to making older scripts even better.

Please or Register to post comments.

What's PowerShell with a Purpose Blog?

Don Jones demystifies Windows PowerShell.

Blog Archive

Sponsored Introduction Continue on to (or wait seconds) ×