Add a Progress Bar to Your PowerShell Script


 

Summary: Microsoft Scripting Guy Ed Wilson shows you how to add a progress bar to your script.

 

Microsoft Scripting Guy Ed Wilson here. I was sitting in the kitchen reflecting on how much nicer things are nowadays than they used to be even a few years ago. For example, the Scripting Wife and I have a computer in our kitchen. The computer itself is about the size of a small paperback book, and it is hooked up to a nice flat panel monitor. With a wireless keyboard, mouse, and network connection, it is a rather compact installation. Our first kitchen computer was a mini-tower, hooked up to a 15 inch monitor, and it took us several days to cut holes in dry wall, drill holes through wall studs to run category five wire from the upstairs rack to the downstairs kitchen. It was worth the effort because having a computer in the kitchen is super handy around our household. Comparing the amount of work involved, the old installation took several days, and the new installation took only a few minutes.

That same economy of effort also works for scripting progress bars. It is common to display some kind of progress when a script takes a long time to complete. When a user launches the script and nothing happens, one begins to wonder if the script launched correctly. We probably have all had the occasion to start three or four copies of a program that is slow in launching because we clicked and nothing happened. The same principle applies to scripts: if the script will take a long time to run, provide feedback to the user (or even to yourself if you run the script six months from now and you forget that it takes a long time to run.)

Five years ago, I wrote a VBScript script such as the one that follows. It collects a list of all running services, and back then it took a long time to run.

CollectRunningServicesDisplayProgress.vbs

strComputer = “.”

wmiNS = “\root\cimv2”

wmiQuery = “Select name from win32_service where state = ‘running'”

Set stdout = WScript.StdOut

stdout.write “Please wait”

strout = wmiQuery & VbCrLf

Set objWMIService = GetObject(“winmgmts:\\” & strComputer & wmiNS)

Set colItems = objWMIService.ExecQuery(wmiQuery)

For Each objItem in colItems

stdout.Write “.”

strout = strout & objItem.name & VbCrLf

Next

stdout.WriteBlankLines(1)

stdout.write(strOut)

Using the Write-Progress Windows PowerShell cmdlet, I can trim the previous VBScript from 14 lines to six lines of code. One thing that is cool about WMI queries is that the result from Get-WmiObject is a collection. That collection has a count property. Armed with the count property, I can display a progress bar that indicates a percentage of completion of the command. The ProgressDemoWMI.ps1 script is shown here.

ProgressDemoWMI.ps1

$wmiQuery = “Select name from win32_service where state = ‘running'”

$colItems = Get-WmiObject -Query $wmiQuery

For($i = 1; $i -le $colItems.count; $i++)

{ Write-Progress -Activity “Gathering Services” -status “Found Service $i” `

-percentComplete ($i / $colItems.count*100)}

$colItems | Select name

When using the Write-Progress cmdlet, two parameters are required. The first is the activity parameter. The string supplied for this parameter appears on the first line in the progress dialog. The second required parameter is the status parameter. It appears under the Activity line. The dialog that appears in the following image is shown when the ProgressDemoWMI.ps1 script runs from within the Windows PowerShell ISE.

Image of dialog box that appears when PowerShell script is run in the PowerShell ISE

When the ProgressDemoWMI.ps1 script runs inside the Windows PowerShell console, a green bar appears at the top of the console, and yellow o’s track their way across the console window. This is shown in the following image.

Image of progress bar in PowerShell console

Displaying a percentage of completion is a great progress indicator if you know how many work items exist. Unfortunately, many times you have no idea how many items are in the collection. This is especially true when commands are piped, because there is no way to determine how many items need to be streamed as items are coming over the pipeline.

One solution is to store items in a variable, and then iterate over the items collected in the variable. This is illustrated in the FilesProgressDemo.ps1 script.

FilesProgressDemo.ps1

$files = Get-ChildItem -Path c:\fso

For($i = 1; $i -le $files.count; $i++)

{ Write-Progress -Activity “Collecting files” -status “Finding file $i” `

-percentComplete ($i / $files.count*100)}

$files | Select name

As you can tell, the FilesProgressDemo.ps1 script is nearly identical to the earlier script—the difference is collecting the files instead of services.

That is all there is to displaying a progress indicator.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Comments (15)

  1. Rich Kusak says:

    The problem I sometimes run into with WMI is it can take a while to finish its collection. For instance, the Win32_Product class can take some time to process. Progress isn't displayed in real time because the count property isn't populated until the collection is complete. Progress is actually counted after it's already been done and the delay kinda defeats the purpose of the progress indicator. It's just a thing with me, wish it could be while it's happening instead of after the fact.

  2. Marc Queshon says:

    Hello Scripting Guy,

    would you please be so kind to tell me how I can put up a progress bar for the creation of a sharepoint database, which I create through powershell cmdlets? Or for similar tasks like creating a site in SharePoint 2010? The creation of a SharePoint DB in PowerShell can take some time and I want to indicate a progress so someone would not think of a system freeze.

    Thank you very much

    Kind regards

  3. JV says:

    @Marc

    Run the creation as a job.  Write a loop that polls the job every second or so and updates the progress bar.

    Post your question here for more assistance: social.technet.microsoft.com/…/threads

  4. ed wilson says:

    @Rich @marc one option is to use a time based progress indicator instead of a percent complete. You can then "estimate" about how long the task will take, and move your indicator from there. i talk about this in other articles.

  5. JB says:

    You could also do something like this:

    $files = Get-ChildItem -Path c:fso

    foreach ($file in $files)

    { Write-Progress -Activity "Collecting files" -status "Finding file $i" `

    -percentComplete ($i++ / $files.count*100)}

    Of course, that throws off the -Staus value a little bit

  6. ehsan says:

    Hi everybody

    Nice article.But my question is,how can I show the progress for tasks that there is no items for counting??? .Here,the calculation is based on the NO of for example files or processes,but how about the jobs that there is no object to count…somthing like moving one virtual machine from one computer to another with powershell commands????

  7. Michael Brockman says:

    If you want to display progress when there are no items to count, try something like the following:

    while (CheckIfJobIsDone($name)) {

           $status += '.'

           Write-Progress -Activity "Waiting for some job to finish" -Status $status -PercentComplete -1

           Start-Sleep 1

    }

  8. orlando says:

    great post, thanks. I was looking for the foreach variant which I’ve found below in one of the comments 🙂

  9. sam says:

    This is so awesome! Thanks

  10. Nirmal says:

    Not useful if you process large data.
    Output is stored in a variable and then processed through another loop….what..??? Does it make any sense? You’re adding double work to your code, which, in turn, eats up system resources.

    /Nirmal

  11. Dennis Stevens says:

    Although I generally love the solutions offered by you, this one sure isn’t one of them. To test this out I generated a txt file with 200 random numbers:
    1..199 | % {get-random -Maximum 200 >> c:random.txt}

    When i load the txt file with your progress bar it takes 13.20 seconds.
    # Get Start Time
    $startDTM = (Get-Date)

    # Progress Bar
    $files = Get-Content C:random.txt
    For($i = 1; $i -le $files.count; $i++)

    { Write-Progress -Activity "Collecting files" -status "Finding file $i" `

    -percentComplete ($i / $files.count*100)}

    $files

    # Get End Time
    $endDTM = (Get-Date)

    # Echo Time elapsed
    "Elapsed Time: $(($endDTM-$startDTM).totalseconds) seconds"

    When i run it without the progress bar it takes 1/4th of a second!

    # Get Start Time
    $startDTM = (Get-Date)

    # No progressbar
    $files = Get-Content C:random.txt
    $files

    # Get End Time
    $endDTM = (Get-Date)

    # Echo Time elapsed
    "Elapsed Time: $(($endDTM-$startDTM).totalseconds) seconds"

  12. Chase Roth says:

    @Dennis, not sure why your machine is giving you 13 seconds. Using your examples, it took my machine in PowerShell console an average of about 1.65 seconds with the progress bar and about .3 seconds without it. Same code in ISE with progress bar 0.096
    seconds and 0.059 seconds without progress bar. Surprisingly ISE is twice as fast…may be a "subsequent run thing" as described below, because I didn’t close ISE each time.

    Switching your code a bit to use the "Measure-Command" shows even faster speeds. Code:

    Measure-Command {
    # Progress Bar
    $files = Get-Content D:PowerShellrandom.txt
    For ($i = 1; $i -le $files.count; $i++)
    {
    Write-Progress -Activity "Collecting files" -status "Finding file $i" -percentComplete ($i / $files.count * 100)
    }
    $files
    } | fl TotalSeconds

    Measure-Command {
    # No progressbar
    $files = Get-Content D:PowerShellrandom.txt
    $files
    } | fl TotalSeconds

    PowerShell Console:
    TotalSeconds : 1.4400027 (with)
    TotalSeconds : 0.0012088 (without)

    PowerShell ISE:
    TotalSeconds : 0.0574694 (with)
    TotalSeconds : 0.0017804 (without)

    I am straying from my main point through…The part that all of the examples on this page are missing is that nothing is being done inside the "for" loop. The progress bar should be used to indicate progress of some process or calculation your doing with each
    element of your loop. Just throwing the loop in there without doing anything will of course add time compared to not.

    …continued in next comment.

  13. Chase Roth says:

    …continued

    *NOTE: Must close PowerShell Console between each test
    *NOTE: I removed the Get-Content code from the Measure-Command

    # 5 Server names used for each below
    $servers = Get-Content D:PowerShellservers.txt

    ### FOR LOOP WITH PROGRESS BAR
    Measure-Command {
    # Progress Bar
    For ($i = 0; $i -lt $servers.count; $i++)
    {
    Write-Progress -Activity "Get services for computer" -status "Computer $($servers[$i])" -percentComplete ($i / $servers.count * 100)
    Get-Service -ComputerName $servers[$i] | Out-Null
    }
    } | fl TotalSeconds

    Initial Run (closing window between):
    TotalSeconds : 105.2290161
    TotalSeconds : 105.3718451

    Subsequent runs in same console window:
    TotalSeconds : 0.239106
    TotalSeconds : 0.1401543
    TotalSeconds : 0.1991284

    ### FOREACH LOOP WITH PROGRESS BAR
    $i = 0
    Measure-Command {
    foreach ($server in $servers)
    {
    Write-Progress -Activity "Get services for computer" -status "Computer $server" -percentComplete ($i / $servers.count * 100)
    Get-Service -ComputerName $server | Out-Null
    $i++
    }

    } | fl TotalSeconds

    Initial Run (closing window between):
    TotalSeconds : 105.2799883
    TotalSeconds : 105.2820863

    Subsequent runs in same console window:
    TotalSeconds : 0.1943805
    TotalSeconds : 0.1284316
    TotalSeconds : 0.1697569

    ### FOR LOOP WITHOUT PROGRESS BAR
    Measure-Command {
    # No progressbar
    For ($i = 0; $i -lt $servers.count; $i++)
    {
    Get-Service -ComputerName $servers[$i] | Out-Null
    }
    } | fl TotalSeconds

    Initial Run (closing window between):
    TotalSeconds : 105.204725
    TotalSeconds : 105.1879435

    Subsequent runs in same console window:
    TotalSeconds : 0.0926272
    TotalSeconds : 0.1074859
    TotalSeconds : 0.0705483

    ### FOREACH LOOP WITHOUT PROGRESS BAR
    Measure-Command {
    foreach ($server in $servers)
    {
    Get-Service -ComputerName $server | Out-Null
    }

    } | fl TotalSeconds

    Initial Run (closing window between):
    TotalSeconds : 105.1809158
    TotalSeconds : 105.2194897

    Subsequent runs in same console window:
    TotalSeconds : 0.074843
    TotalSeconds : 0.0915758
    TotalSeconds : 0.0704207

    # SUMMARY OF RESULTS
    #======================================
    TotalSeconds : 105.2290161 # FOR LOOP WITH PROGRESS
    TotalSeconds : 105.3718451 # FOR LOOP WITH PROGRESS
    TotalSeconds : 105.2047250 # FOR LOOP WITHOUT PROGRESS
    TotalSeconds : 105.1879435 # FOR LOOP WITHOUT PROGRESS

    TotalSeconds : 105.2799883 # FOREACH WITH PROGRESS
    TotalSeconds : 105.2820863 # FOREACH WITH PROGRESS
    TotalSeconds : 105.1809158 # FOREACH WITHOUT PROGRESS
    TotalSeconds : 105.2194897 # FOREACH WITHOUT PROGRESS

    # CONCLUSION
    #======================================

    The way I look at it, adding the progress bar does not add any significant time to the overall process, when your code is actually performing a task. The numbers support my conclusion. The additional tenth of a second (over my 5 servers) is well worth knowing
    where I am at in the process and not just twiddling my thumbs wondering when it will complete.

    Hope this helps someone else as it did me to verify my intuition.

    @CRoth2

Skip to main content