Back

TechnologyMay 18, 2016

Perfect Progress Bars for PowerShell

Bobby Crotty

As you begin to write more complex PowerShell scripts, the progress bar is a time-saving and slick feature you need to add to your toolkit. Progress bars are incredibly useful while debugging to figure out which part of the script is executing, and they’re satisfying for the people running your scripts to track what’s happening.

the basics

Write-Progress is the PowerShell cmdlet that creates a progress bar. PowerShell supports showing three levels of detail on the current status as well as a percent complete. The ActivityStatus, and CurrentOperation parameters are intended to show increasing levels of detail into what is going on behind the scenes. You can actually show more levels of detail in a single progress bar, as discussed below, but you can also nest several progress bars to show nested loops.

Below is a basic example of a progress bar. Typically, the Activity in the first progress bar should describe the main function of the script, and you should keep it consistent throughout the script. The Id is not required, but it becomes useful later. The Status is the first place you should show what’s happening immediately

$Activity = "Creating Administrator Report"$Id       = 1$Task     = "Setting Initial Variables"Write-Progress -Id $Id -Activity $Activity -Status $Task

a full progress bar and runtime strings

The Write-Progress command below contains all four major components of a progress bar, and it uses a script block for the Status parameter. A script block allows PowerShell to evaluate a string with embedded variables at runtime each time it’s used. If you set the $Status variable to just the inner double-quoted string, it would attempt to calculate the values of all the embedded variables immediately and only once. By surrounding that string with single quotes and then creating a script block from it, you can execute that script block whenever you want to recalculate the string

$TotalSteps = 4$Step       = 1$StepText   = "Setting Initial Variables"$StatusText = '"Step $($Step.ToString().PadLeft($TotalSteps.Count.ToString().Length)) of $TotalSteps | $StepText"'$StatusBlock = [ScriptBlock]::Create($StatusText)$Task        = "Creating Progress Bar Script Block for Groups"Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -CurrentOperation $Task -PercentComplete ($Step / $TotalSteps * 100)

multiple progress bars

Create a child progress bar by calling Write-Progress with a different Id. It doesn’t matter what number you use for Id, but incrementing your original Id by one allows you to easily see which level of progress bar you’re creating. You don’t need to specify the ParentId parameter, but doing so offsets the progress bar to the right a little

$CurGroupText  = '"Group $($CurGroup.ToString().PadLeft($Groups.Count.ToString().Length)) of $($Groups.Count) | $($Group.Name)"'$CurGroupBlock = [ScriptBlock]::Create($CurGroupText)Write-Progress -Id ($Id+1) -Activity $Activity -Status (& $CurGroupBlock) -CurrentOperation $Task -PercentComplete $CurGroupPercent -ParentId $Id

getting fancy with string padding

The extra code around $CurGroup in the $CurGroupText variable pads the number with extra space so the line does not change its length. Note above that the 1 in “1 of 31” has one extra space before it because the maximum value it will be takes up one more character. While this isn’t a big issue here, it is much more important when we use the same technique below with usernames.

If you want to include more levels of detail, you can supplement Status or CurrentOperation with additional detail at the end of the line. Because usernames can vary in length from one to the next, it is important to use the padding technique so you can easily read the text that comes after the username. If you don’t use the padding, the “Getting User Details” below will flicker left and right making it difficult to read

$CurUserText  = '"User $($CurUser.ToString().PadLeft($Users.Count.ToString().Length)) of $($Users.Count) | $($_.SamAccountName.PadRight($UsersNameLengthMax+3))"'$CurUserBlock = [ScriptBlock]::Create($CurUserText)Write-Progress -Id ($Id+2) -Activity $UserActivity -Status ((& $CurUserBlock) + $Task) -PercentComplete $UserPercentProcessed -ParentId ($Id+1)

keeping it clean

If you want to keep some extra space between progress bars, you can draw a progress bar with a space character for the CurrentOperation before you start the loop that contains the child progress bar. In the image above, the extra space was added between the main progress bar and the group progress bar, but not between the group and user progress bars.

Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -CurrentOperation " " -PercentComplete ($Step / $TotalSteps * 100)

If you want no space between the bars, make sure to redraw the parent progress bar without the CurrentOperation parameter, otherwise whatever was previously the CurrentOperation will remain there.

Progress bars will remain visible until you redraw or hide them. When a progress bar finishes, it is important to complete it using the Completed parameter, but make sure to do so outside of the loop generating it, otherwise that bar will flicker every time you go through another loop. To complete the user bar above, use the command:

Write-Progress -Id ($Id+2) -Activity $UserActivity -Completed

putting it all together

Below is a script that reports on users in various administrator groups. It should work as is on any system with the Active Directory PowerShell module, and it will output a CSV in your Downloads folder.

################################################################################ NAME: ProgressBarTest.ps1# AUTHOR: Bobby Crotty, Credera# DATE: 3/31/2016## This script demonstrates the functionality and options of progress bars.## VERSION HISTORY:# 1.0 3/31/2016 Initial Version###############################################################################   ############################################################# SETUP #############################################################   # Progress Bar Variables$Activity = "Creating Administrator Report"$UserActivity = "Processing Users"$Id = 1   # Progress Bar Pause Variables$ProgressBarWait = 1500 # Set the pause length for operations in the main script$ProgressBarWaitGroup = 250 # Set the pause length for operations while processing groups$ProgressBarWaitUser = 50 # Set the pause length for operations while processing users$AddPauses = $true # Set to $true to add pauses that help highlight progress bar functionality   # Simple Progress Bar$Task = "Setting Initial Variables"Write-Progress -Id $Id -Activity $Activity -Status $Taskif ($AddPauses) { Start-Sleep -Milliseconds $ProgressBarWait }   # Complex Progress Bar$TotalSteps = 4 # Manually count the total number of steps in the script$Step = 1 # Set this at the beginning of each step$StepText = "Setting Initial Variables" # Set this at the beginning of each step$StatusText = '"Step $($Step.ToString().PadLeft($TotalSteps.Count.ToString().Length)) of $TotalSteps | $StepText"' # Single quotes need to be on the outside$StatusBlock = [ScriptBlock]::Create($StatusText) # This script block allows the string above to use the current values of embedded values each time it's run   # Groups Script Block$Task = "Creating Progress Bar Script Block for Groups"Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -CurrentOperation $Task -PercentComplete ($Step / $TotalSteps * 100)if ($AddPauses) { Start-Sleep -Milliseconds $ProgressBarWait }   $CurGroupText = '"Group $($CurGroup.ToString().PadLeft($Groups.Count.ToString().Length)) of $($Groups.Count) | $($Group.Name)"'$CurGroupBlock = [ScriptBlock]::Create($CurGroupText)   # Users Script Block$Task = "Creating Progress Bar Script Block for Users"Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -CurrentOperation $Task -PercentComplete ($Step / $TotalSteps * 100)if ($AddPauses) { Start-Sleep -Milliseconds $ProgressBarWait }   $CurUserText = '"User $($CurUser.ToString().PadLeft($Users.Count.ToString().Length)) of $($Users.Count) | $($_.SamAccountName.PadRight($UsersNameLengthMax+3))"'$CurUserBlock = [ScriptBlock]::Create($CurUserText)   # Filter Variables$GroupFilter = "*admin*" # Report on groups that match this filter     ############################################################# SCRIPT ############################################################   $Step = 2$StepText = "Getting Groups"$Task = "Running Get-ADGroup"Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -CurrentOperation $Task -PercentComplete ($Step / $TotalSteps * 100) Import-Module ActiveDirectory $Groups = Get-ADGroup -Filter {Name -like $GroupFilter}if ($AddPauses) { Start-Sleep -Milliseconds $ProgressBarWait }$Task = "Pausing After Running Get-ADGroup"Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -CurrentOperation $Task -PercentComplete ($Step / $TotalSteps * 100)if ($AddPauses) { Start-Sleep -Milliseconds $ProgressBarWait }     $Step = 3$StepText = "Processing Groups"Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -CurrentOperation " " -PercentComplete ($Step / $TotalSteps * 100) # CurrentOperation needs to have a space to keep vertical spacing   $CurGroup = 0foreach ($Group in $Groups) { $CurGroup++ $CurGroupPercent = $CurGroup / $Groups.Count * 100 $Task = "Getting Group Members" Write-Progress -Id ($Id+1) -Activity $Activity -Status (& $CurGroupBlock) -CurrentOperation $Task -PercentComplete $CurGroupPercent -ParentId $Id $Users = @($Group | Get-ADGroupMember -Recursive) if ($AddPauses) { Start-Sleep -Milliseconds $ProgressBarWaitGroup}   $Task = "Calculating Username Max Length" Write-Progress -Id ($Id+1) -Activity $Activity -Status (& $CurGroupBlock) -CurrentOperation $Task -PercentComplete $CurGroupPercent -ParentId $Id $UsersNameLengthMax = $Users | Select -ExpandProperty SamAccountName | Select -ExpandProperty Length | Measure -Maximum | Select -ExpandProperty Maximum if ($AddPauses) { Start-Sleep -Milliseconds $ProgressBarWaitGroup}   Write-Progress -Id ($Id+1) -Activity $Activity -Status (& $CurGroupBlock) -PercentComplete $CurGroupPercent -ParentId $Id   $CurUser = 0 $Users | %{ $CurUser++ $UserPercentProcessed = $CurUser / $Users.Count * 100 $Task = "Getting User Details" Write-Progress -Id ($Id+2) -Activity $UserActivity -Status ((& $CurUserBlock) + $Task) -PercentComplete $UserPercentProcessed -ParentId ($Id+1) if ($AddPauses) { Start-Sleep -Milliseconds $ProgressBarWaitUser} $User = $_ | Get-ADUser -Properties PasswordLastSet   $Task = "Collating User Details" Write-Progress -Id ($Id+2) -Activity $UserActivity -Status ((& $CurUserBlock) + $Task) -PercentComplete $UserPercentProcessed -ParentId ($Id+1) if ($AddPauses) { Start-Sleep -Milliseconds $ProgressBarWaitUser} $_ | Select Name,SamAccountName,@{Name="PasswordAge"; Expr={((Get-Date) - $_.PasswordLastSet).Days}} } | Add-Member -MemberType NoteProperty -Name "Group" -Value $Group.Name -PassThru | Export-Csv "$env:USERPROFILE\Downloads\Admins.csv" -NoTypeInformation -Append Write-Progress -Id ($Id+2) -Activity $Activity -Completed}   $Step = 4$StepText = "Finishing Script"$Task = "Completing Progress Bars"Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -CurrentOperation $Task -PercentComplete ($Step / $TotalSteps * 100)if ($AddPauses) { Start-Sleep -Milliseconds $ProgressBarWait }Write-Progress -Id ($Id+1) -Activity $Activity -Completedif ($AddPauses) { Start-Sleep -Milliseconds $ProgressBarWait }

Hopefully this has helped you think of some new ways to use progress bars to enhance your scripts. For help with scripting or automation, contact the infrastructure experts at Credera.