Skip to content

Instantly share code, notes, and snippets.

@scriptingstudio
Created February 9, 2023 15:52
Show Gist options
  • Save scriptingstudio/29d36563f9175e40958f81b4c9f0823c to your computer and use it in GitHub Desktop.
Save scriptingstudio/29d36563f9175e40958f81b4c9f0823c to your computer and use it in GitHub Desktop.
Simple one-level asynchronous wrapper over the builtin PowerShell progress bar
<#
.SYNOPSIS
Displays an asynchronous progress bar within a PowerShell command window.
.DESCRIPTION
A simple one-level asynchronous wrapper over the builtin PowerShell progress bar.
Features:
- Non-blocking screen output
- Separate run phases: start,next,stop
- Automatic percentage calculation
- Automatic state control
- Automatic overflow control
- Input object counter step
- Internal variables
.PARAMETER Start
Indicates to initialize the progress bar.
.PARAMETER Next
Indicates to increase the counter of the progress bar.
.PARAMETER Stop
Indicates to stop the progress bar.
.PARAMETER Range
Specifies the data range.
.PARAMETER Frequency
Specifies input object counter step - progress bar update frequency - once a step. This parameter provides eventing deceleration that can be useful on big data series.
.PARAMETER Activity
Specifies the first line of text in the heading above the status bar. This text describes the activity whose progress is being reported.
.PARAMETER Status
Specifies the second line of text in the heading above the status line. This text describes current state of the activity.
.PARAMETER ProgressBar
Specifies the progress bar control data structure.
.PARAMETER PcDigit
Specifies the percent precision to display in the status line.
.PARAMETER InputObject
Experimental. Specifies the current input object from the data pipeline.
.PARAMETER Keep
Experimental. Enables to leave the progress bar on the screen after stopping it.
.PARAMETER AutoFinish
Experimental. Enables autofinish upon progress bar complete.
.INPUTS
None
You can't pipe objects to this cmdlet.
.OUTPUTS
Object/None
Upon initialization this cmdlet returns Progress Bar control data structure. Otherwise the cmdlet returns no output.
.NOTES
Version: 2.1
Internal variables you can use in Status line:
- %PercentComplete
- %Progress
- %Inputobject
On crash or pressing Ctrl-C event handler will keep running in background.
.EXAMPLE
$range = 100
$jobtimeMS = 50
$frequency = 1
$param = @{
range = $range
activity = 'Progress Bar [range - {0}; step - {1}; pct - {2}]' -f $range,$frequency,($range/100)
status = "'Complete: {0:P1} [{1}]' -f (%PercentComplete/100), %progress"
pcdigit = $Pcdigit
}
$pb = Write-ProgressBar -start @param
1..$range | % {
Write-ProgressBar -next -progressbar $pb
Start-Sleep -Milliseconds $jobtimeMS
}
Write-ProgressBar -stop -progressbar $pb
#>
function Write-ProgressBar {
#[CmdletBinding()]
param (
# PB controls/commands
$progressbar,
[switch] $start, # starter
[switch] $next, # iterator
[alias('close')][switch] $stop, # cleaner
# runtime options
[int] $range,
[alias('step')][int] $frequency, # input object counter step
[string] $activity,
[string] $status,
[alias('precision')][int] $pcdigit, # percent precision
$inputobject, # experimental; how to use?
[switch] $autofinish, # experimental;
[switch] $keep # experimental; leave progress bar on screen
)
# parameterset autoresolver
if (($start,$next,$stop).Where{$_}.count -gt 1) {
Write-Warning "Cannot resolve a specified parameter set."
return
}
if ($next) { # iterator
if (-not $progressbar -or -not $progressbar['event']) {return}
if ($progressbar.overflow) {return} # set by event handler
$progressbar.Progress++
# eventing decelerator
if (($progressbar.progress - $progressbar.frequency) -lt $progressbar.prev) {
return
}
$progressbar.Inputobject = if ($inputobject) {$inputobject} else {$null}
# remove old data to prevent high memory consumption
if ($progressbar.range -gt 100 -and $progressbar.trigger.count) {$progressbar.trigger.clear()} # {$progressbar.trigger.removeAt(0)}
# trigger the next progress event
$progressbar.trigger.add($progressbar)
} # next
elseif ($stop) { # cleaner
if (-not $progressbar -or -not $progressbar['event']) {return}
if (-not $keep) {
Write-Progress -PercentComplete 100 -Activity $progressbar['Activity'] -Completed
}
Get-EventSubscriber -SourceIdentifier $progressbar['eventId'] | Unregister-Event
$progressbar['event'] | Remove-Job -Force
$progressbar['event'] = $null
$progressbar['trigger'].clear()
$progressbar.clear()
} # stop
elseif ($start) { # initializer
if ($range -lt 1) {
Write-Host 'No data range specified.' -ForegroundColor Red
return
}
# input defaults
if (-not $activity) {$activity = 'Progress Bar'}
if ($status) {
$status = $status -replace '%PercentComplete','$param["PercentComplete"]' -replace '%progress','$progress.progress' -replace '%inputobject','$progress.inputobject'
# %inputobject can be object!!!
} elseif ($psversiontable.PSVersion.major -gt 5) {
$status = '$param["PercentComplete"]'
}
# progress bar control descriptor
$pbstate = [hashtable]::Synchronized(@{
Inputobject = $null
Progress = 0
Prev = 0 # previous progress value
Overflow = $false
Range = $range
Percent = $range/100 # items per percent; how to use?
Frequency = if ($frequency -gt 1) {$frequency} else {1}
Activity = $activity
Status = if ($status) {[scriptblock]::create($status)} else {$null}
Autofinish = $autofinish
Pcdigit = if ($range -gt 5000) {2} elseif ($range -gt 500) {1} else {0}
Trigger = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
})
if ($pcdigit -lt 0) {$pcdigit = 0}
if ($pcdigit -gt 0) {$pbstate['pcdigit'] = $pcdigit}
$pbstate['eventId'] = [guid]::newguid().guid
$evparam = @{
InputObject = $pbstate['trigger']
SourceIdentifier = $pbstate['eventId']
EventName = 'CollectionChanged'
MessageData = $pbstate
}
$pbstate['event'] = Register-ObjectEvent @evparam -Action {
if ($Event.SourceEventArgs.Action.ToString() -ne 'Add') {return} # filter
$progress = $Event.MessageData
# display progress bar
$pcdigit = $progress.pcdigit
$w = if ($pcdigit) {5} else {4}
$pcwidth = $w + $pcdigit # 4-0 6-1 7-2 8-3...
$param = @{
#$progress.inputobject
PercentComplete = [int](($Event.SourceEventArgs.NewItems[-1].progress*100)/$progress.Range)
Activity = $progress.Activity
}
# stop eventing if scale is overflow
if ($param['PercentComplete'] -gt 100) {
$progress.Overflow = $true
$progress.Trigger.clear()
return
}
$param['Status'] = if ($progress.Status) {. $progress.Status}
else { # default status line
"Complete: {0,${pcwidth}:P${pcdigit}} [{1}]" -f ($param['PercentComplete']/100),$progress.progress
}
Write-Progress @param
$progress.prev = $progress.progress
<#if ($param['PercentComplete'] -eq 100 -and $progress.autofinish) {
Write-Progress -PercentComplete 100 -Activity $progress['Activity'] -Completed
Get-EventSubscriber -SourceIdentifier $progress['eventId'] | Unregister-Event
$progress['event'] | Remove-Job -Force
$progress['event'] = $null
$progress['trigger'].clear()
$progress.clear()
}#>
} # event handler
$pbstate
} # start
} # END Write-ProgressBar
# EXAMPLE
$range = if ($range) {$range} else {100}
$frequency = if ($frequency -gt 1) {$frequency} else {1}
if (-not $jobtimeMS) {$jobtimeMS = 100}
$param = @{
range = $range
activity = 'Progress Bar [range - {0}; step - {1}; pct - {2}]' -f $range,$frequency,($range/100)
status = "'Complete: {0,6:P1} [{1}]' -f (%PercentComplete/100), %progress"
pcdigit = $Pcdigit
frequency = $frequency
}
$pb = Write-ProgressBar -start @param
1..$range | % {
Write-ProgressBar -next -progressbar $pb -inputobject $_
Start-Sleep -Milliseconds $jobtimeMS
}
Write-ProgressBar -stop -progressbar $pb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment