Making Progress
Making Progress
In a recent article, I showed a sample function to illustrate a point. Part of the function used the Write-Progress
cmdlet. This wasn't the point of the example so I didn't make much of it. However, I think this is an under-utilized command and one you should be using more often, especially in long-running scripts or functions. In the past, many of use, myself included, have used simple Write-Host
commands to show progress.
Write-Host "Processing file 1 of 10" -ForegroundColor yellow
If you use
Write-Host
this way, I encourage you to use a foreground color to distinguish it from command output.
It doesn't take much to implement Write-Progress
and I think it makes your code look more professional. Let's take a look at the Write-Progress
cmdlet and how you can use it in your scripts and functions.
Progress Parameters
First, let's take a look at the parameters for Write-Progress
. The cmdlet has a number of parameters, but only a few are required. Yes, you should read the cmdlet help, but I'm going to use the Get-ParameterInfo
command from the PSScriptTools module.

You can see that Activity
is a mandatory parameter. In PowerShell 7, you would think it was removed.

However, this isn't entirely true. Even though Get-Command
shows the parameter is not mandatory.
PS C:\> (Get-Command -Name Write-Progress).parameters["Activity"].attributes
ExperimentName :
ExperimentAction : None
Position : 0
ParameterSetName : __AllParameterSets
Mandatory : False
ValueFromPipeline : False
ValueFromPipelineByPropertyName : False
ValueFromRemainingArguments : False
HelpMessage :
HelpMessageBaseName : WriteProgressResourceStrings
HelpMessageResourceId : ActivityParameterHelpMessage
DontShow : False
TypeId : System.Management.Automation.ParameterAttribute
The help documentation shows that it is required.

This is why you need to read the help, although the help I think needs a little updating. In PowerShell 7.4, Microsoft incorporated a pull request that made the Activity
parameter optional when combining it with the -Completed
parameter. You would have discovered this if tried running the command in PowerShell 7 without the Activity
parameter. The short answer is that you need to specify an Activity
parameter. Think of this like a title.
You should show something that indicates the current state. In other words, what is the code doing now? There are two options you can use. I think of he Status
property as a high-level overview, like Step 1
or Processing file 1 of 10
. The CurrentOperation
property is more detailed and might be used to show a file name or a specific task.
The last option is to indicate actual progress. You can show a completion percentage (-PercentComplete
) or the number of seconds remaining (-SecondsRemaining
). You can use both parameters, but I think you will use one or the other.
Here's a simple example.
1..10 | ForEach-Object {
Switch ($_) {
{$_ -le 3} {$status = "Step 1";break}
{$_ -lt 7} {$status = "Step 2";break}
{$_ -gt 7} {$status = "Step 3";break}
}
Write-Progress -Activity "Demo" -Status $status -CurrentOperation "Processing $_" -PercentComplete ($_ * 10)
Start-Sleep -Seconds 2
}

You aren't required to show progress like I'm doing here with a PercentComplete
value. But often this is the case. Calculating the percentage can be tricky, especially if you are piping objects and you don't necessarily know how many there are.
I find it easier to use Write-Progress
in a loop where I can calculate the percentage.
$Path = 'C:\Scripts'
#define hash table of parameter values for Write-Progress
$progParam = @{
Activity = 'Directory Usage Analysis'
CurrentOperation = $path
Status = 'Querying top level folders'
PercentComplete = 0
}
Write-Progress @progParam
#get top level folders
$top = Get-ChildItem -Path $Path -Directory
Start-Sleep -Milliseconds 300
#initialize a counter
$i = 0
foreach ($folder in $top) {
#calculate percentage complete
$i++
[int]$pct = ($i / $top.count) * 100
#Update the parameter splatting hashtable
$progParam.CurrentOperation = "Measuring size: $($folder.FullName)"
$progParam.Status = 'Analyzing folder'
$progParam.PercentComplete = $pct
Write-Progress @progParam
Start-Sleep -Milliseconds 100
$stats = Get-ChildItem -Path $folder.FullName -Recurse -File |
Measure-Object -Property Length -Sum -Average
[PSCustomObject]@{
Parent = (Convert-Path $Path)
Path = $folder.Name
Files = $stats.count
SizeKB = [math]::Round($stats.sum / 1KB, 2)
Avg = [math]::Round($stats.average, 2)
}
} #foreach

In this example I've inserted some arbitrary sleep statements to emphasize the progress. Notice how I update the Write-Progress
parameters in the Foreach
loop.
It isn't always required, and I'm not using it in my example, but you can end the progress display with a Complete
status.
Write-Progress -Activity Demo -Completed
Even though you won't see anything, you might want to include this code for the sake of completeness, or to make it clear you are no longer showing progress.
This example runs very quickly so you could argue that the progress bar is unnecessary. An advantage over using Write-Host
is that all of the progress disappears when completed, without filling the screen with hundreds of Write-Host
messages. And even though processing C:\scripts might be quick, a user might select a path that does take a long time to process.
I rarely use the -SecondsRemaining
parameter as I think it is more difficult to calculate. But I can see where it might be useful in some scenarios.
$i = 20
$i..0 | ForEach-Object {
Write-Progress -Activity "Server Build" -Status "Waiting for configurations to merge" -secondsRemaining $_
Start-Sleep -Seconds 1
}

The type of progress you show is up to you and much will depend on your scenario and code.
Progress Options
PowerShell has a few options you can use to fine-tune your use of Write-Progress
. These options will vary depending on the version of PowerShell you are using.
Preference
There is some overhead in displaying the progress bar, or you may not want to see it at all. You can use the $ProgressPreference
variable to control this.
PS C:\> Get-Variable ProgressPreference
Name Value
---- -----
ProgressPreference Continue
If you set the value to SilentlyContinue
, this will hide the progress bar display.
$ProgressPreference = 'SilentlyContinue'
In PowerShell 7.4, Microsoft added a new common parameter, -ProgressAction
that is supposed to allow you to control progress display on a per command basis.
PS C:\> Test-NetConnection DOM1 -ProgressAction SilentlyContinue
But as of now, this fails to work as expected. Looks like a bug has crept back in to the code. But you can always manage this on your own.
I took my usage script and turned it into a function.
Function Get-FolderUsage {
[cmdletbinding()]
Param(
[Parameter(Position = 0)]
[ValidateScript({ Test-Path $_ })]
[string]$Path = 'C:\Scripts',
[switch]$NoProgress
)
if ($NoProgress) {
$ProgressPreference = 'SilentlyContinue'
}
#define hash table of parameter values for Write-Progress
$progParam = @{
Activity = 'Directory Usage Analysis'
CurrentOperation = $path
Status = 'Querying top level folders'
PercentComplete = 0
}
Write-Progress @progParam
#get top level folders
$top = Get-ChildItem -Path $Path -Directory
Start-Sleep -Milliseconds 300
#initialize a counter
$i = 0
foreach ($folder in $top) {
#calculate percentage
$i++
[int]$pct = ($i / $top.count) * 100
$progParam.CurrentOperation = "Measuring size: $($folder.FullName)"
$progParam.Status = 'Analyzing folder'
$progParam.PercentComplete = $pct
Write-Progress @progParam
Start-Sleep -Milliseconds 100
$stats = Get-ChildItem -Path $folder.FullName -Recurse -File |
Measure-Object -Property Length -Sum -Average
[PSCustomObject]@{
PSTypeName = "psFolderUsage"
Parent = (Convert-Path $Path)
Path = $folder.Name
Files = $stats.count
Size = $stats.sum
Average = $stats.average
}
} #foreach
} #close function
I added a -NoProgress
switch parameter. If the switch is used, the function will set $ProgressPreference
to SilentlyContinue
. This way, I can control progress display on a per function basis. One advantage to this approach is that works the same on Windows PowerShell and PowerShell 7.
Color
In Windows PowerShell you can control the color of the progress bar by changing values in the $Host.PrivateData
object.
PS C:\> $host.PrivateData
ErrorForegroundColor : Green
ErrorBackgroundColor : Black
WarningForegroundColor : Yellow
WarningBackgroundColor : Black
DebugForegroundColor : Yellow
DebugBackgroundColor : Black
VerboseForegroundColor : Yellow
VerboseBackgroundColor : Black
ProgressForegroundColor : Yellow
ProgressBackgroundColor : DarkCyan
Let's try changing the progress bar color. Use any value you would use with Write-Host
PS C:\> $host.PrivateData.ProgressBackgroundColor = "darkgreen"
PS C:\> $host.PrivateData.ProgressForegroundColor = "White"
Now I'll run my function.

Or, you could make these changes in your code. You could use this to display dynamically colored progress bars to indicate different states or conditions.
PowerShell 7 Progress
In PowerShell 7, you can use $PSStyle
to control the progress bar appearance.
PS C:\> $PSStyle.Progress
Style : `e[33;1m
MaxWidth : 120
View : Minimal
UseOSCIndicator : False
You can change the style to any ANSI escape sequence.
$PSStyle.Progress.Style = "`e[38;5;216;3m"
Now I've changed the progress bar color to a salmon color

One thing you'll notice in PowerShell 7 is that the progress bar is much more compact. This is because the default View
is Minimal
. You can change this to Classic
if you want to use the legacy format you see in Windows PowerShell.
$pSStyle.Progress.View = "Classic"
You might want to do this if you are using the -CurrentOperation
parameter to display detailed information. That information is lost in the Minimal
view. If you want to change the color and use the classic view, you will need to set colors in $host.PrivateData
as I showed you earlier.
Summary
This should be enough to get you started. I hoped I've piqued your interest in at least experimenting with Write-Progress
. It is a terrific alternative to Write-Host
and can make your code look more professional. There are a few advanced things you can do with Write-Progress
but I'll save that for another time. If you have any questions or comments, please feel free to leave a comment.