[NoBrainer] PowerShell Write-Progress wrapper
Sometimes I have to write scripts that are executed interactively and actually watched by operators as they progress – I know this sounds strange enough. In these rare cases I […]
Audit and Consulting of Information Systems and Business Processes
Sometimes I have to write scripts that are executed interactively and actually watched by operators as they progress – I know this sounds strange enough. In these rare cases I […]
Sometimes I have to write scripts that are executed interactively and actually watched by operators as they progress – I know this sounds strange enough. In these rare cases I like to use something like Write-Progress as the poor operator might not want to scroll through endless lines of logs but just wants to literally see some progress. However, how good-meaning this Cmdlet’s intentions might be, I find it quite impractical to use. All this counting for percentage, updating status lines and keeping track of the time remaining requires more code as it should be.
Therefore I created a small wrapper around it to make it easier to use. It essentially starts like this:
1. You start by calling ‘New-ActivityProgress’ passing it along any parameters the original ‘Write-Progress’ would also understand and expect. In addition, you can specify switches as ‘ShowtimeRemaining’ and to the upper limit the of items being processed.
2. Whenever you want to display some update to the user you call ‘Set-ActivityProgress’ and specify any (or all) parameters you like (again the same as the original Cmdlet would expect). All parameters that remain ‘untouched’ are just repeated from the previous call.
3. When you are finished you can call ‘Remove-ActivityProgress’ and the progress bar disappears.
All timing calcuations and state information is saved in a hashtable on a stack. That means you can create nested progress bars without worrying about ‘Id’ for current and parent progress bars.
Here is an example on how one could use it:
$MaxItems = Get-Random -Minimum 1 -Maximum 500; $null = New-ActivityProgress -Activity "Parent activity [$MaxItems]" -ShowTimeRemaining -Show -MaxItems $MaxItems -AutoRemove:$false; 1..$MaxItems |% { Start-Sleep -Milliseconds 50; if(!($_ % 10)) { Set-ActivityProgress -Status "Status $_" -CurrentOperation "CurrentOperation $_" -CurrentItem $_; } else { Set-ActivityProgress -CurrentOperation "CurrentOperation $_" -CurrentItem $_; } # if $MaxItemsInner1 = Get-Random -Minimum 1 -Maximum 15 $null = New-ActivityProgress -Activity "InnerLoop1 [$_] [$MaxItemsInner1]" -MaxItems $MaxItemsInner1 -AutoRemove:$false; 1..$MaxItemsInner1 |% { Start-Sleep -Milliseconds 10; $null = Set-ActivityProgress -Status "Status $_" -CurrentOperation "CurrentOperation $_" -CurrentItem $_; } # For-Each $null = Remove-ActivityProgress; } # For-Each Remove-ActivityProgress -ReturnDetails
Ahd here is how it looks:
As a side note: parameter passing to the original Write-Progress Cmdlet is performed via the Splatting operator that I recently used to describe a method for easy Cmdlet development.
The Cmdlets are part of the ‘biz.dfch.PS.System.Utilities’ module but with a little modification you can use them standalone as well (just replace the references to the module variable ‘$biz_dfch_…’ to a varaible of your liking and remove any references to the logging module).
function New-ActivityProgress { [CmdletBinding( HelpURI='http://dfch.biz/PS/System/Utilities/New-ActivityProgress/' )] [OutputType([Int])] Param ( [Parameter(Mandatory = $false, Position = 0)] [string] $Activity = $MyInvocation.MyCommand.Name , [Parameter(Mandatory = $false, Position = 1)] [string] $Status , [Parameter(Mandatory = $false, Position = 2)] [string] $CurrentOperation , [ValidateRange(0,[long]::MaxValue)] [Parameter(Mandatory = $false, Position = 3)] [long] $CurrentItem = 0 , [Parameter(Mandatory = $false, Position = 4)] [Alias('Items')] [long] $MaxItems = 100 , [ValidateRange(-1,[int]::MaxValue)] [Parameter(Mandatory = $false)] [int] $ParentID = -1 , [Parameter(Mandatory = $false)] [datetime] $Begin = [datetime]::Now , [Parameter(Mandatory = $false)] [switch] $ShowTimeRemaining = $false , [Parameter(Mandatory = $false)] [switch] $Show = $true , [Parameter(Mandatory = $false)] [switch] $AutoRemove = $true ) # Param BEGIN { $datBegin = [datetime]::Now; [string] $fn = $MyInvocation.MyCommand.Name; } # BEGIN PROCESS { [Boolean] $fReturn = $false; $OutputParameter = $null; try { $act = @{}; $act.Activity = $Activity; $act.Status = $Status; $act.CurrentOperation = $CurrentOperation; $act.CurrentItem = $CurrentItem; $act.MaxItems = $MaxItems; $act.ParentID = $ParentID; $act.ShowTimeRemaining = $ShowTimeRemaining; $act.Begin = $Begin; $act.AutoRemove = $AutoRemove; $biz_dfch_PS_System_Utilities.ActivityStack.Push($act); $fReturn = $true; if($Show) { $null = Set-ActivityProgress; } $OutputParameter = $biz_dfch_PS_System_Utilities.ActivityStack.Count; } # try catch { # ... } # catch finally { # Cleanup } # finally } # PROCESS END { $datEnd = [datetime]::Now; return $OutputParameter; } # END } # function Export-ModuleMember -Function New-ActivityProgress;
function Set-ActivityProgress { [CmdletBinding( HelpURI='http://dfch.biz/PS/System/Utilities/Set-ActivityProgress/' )] [OutputType([Boolean])] Param ( [Parameter(Mandatory = $false, Position = 0)] [string] $Activity , [Parameter(Mandatory = $false, Position = 1)] [string] $Status , [Parameter(Mandatory = $false, Position = 2)] [string] $CurrentOperation , [ValidateRange(0,[long]::MaxValue)] [Parameter(Mandatory = $false, Position = 3)] [long] $CurrentItem , [Parameter(Mandatory = $false, Position = 4)] [Alias('Items')] [long] $MaxItems , [Parameter(Mandatory = $false)] [switch] $ShowTimeRemaining = $false ) # Param BEGIN { $datBegin = [datetime]::Now; [string] $fn = $MyInvocation.MyCommand.Name; } # BEGIN PROCESS { [Boolean] $fReturn = $false; $OutputParameter = $null; try { $act = $biz_dfch_PS_System_Utilities.ActivityStack.Peek(); if($PSBoundParameters.ContainsKey('Activity')) { $act.Activity = $Activity; } if($PSBoundParameters.ContainsKey('Status')) { $act.Status = $Status; } if($PSBoundParameters.ContainsKey('CurrentOperation')) { $act.CurrentOperation = $CurrentOperation; } if($PSBoundParameters.ContainsKey('CurrentItem')) { $act.CurrentItem = $CurrentItem; } if($PSBoundParameters.ContainsKey('MaxItems')) { $act.MaxItems = $MaxItems; } if($PSBoundParameters.ContainsKey('ShowTimeRemaining')) { $act.ShowTimeRemaining = $ShowTimeRemaining; } $null = $biz_dfch_PS_System_Utilities.ActivityStack.Pop(); $biz_dfch_PS_System_Utilities.ActivityStack.Push($act); $WriteProgress = @{}; $WriteProgress.Id = $biz_dfch_PS_System_Utilities.ActivityStack.Count; if($act.ParentID -eq -1) { $WriteProgress.ParentID = $WriteProgress.Id -1; } if($act.Activity) { $WriteProgress.Activity = $act.Activity; } if($act.Status) { $WriteProgress.Status = $act.Status; } if($act.CurrentOperation) { $WriteProgress.CurrentOperation = $act.CurrentOperation; } $WriteProgress.PercentComplete = ((($act.CurrentItem / [Math]::Max($act.MaxItems,1))*100)%100); if( ($act.ShowTimeRemaining) -And ($act.CurrentItem -gt 1) ) { $ts = New-Object TimeSpan(($datBegin - $act.Begin).Ticks); $WriteProgress.SecondsRemaining = [Math]::Min((($act.MaxItems - $act.CurrentItem) * $ts.TotalSeconds) / $act.CurrentItem, [int]::MaxValue); } # if Write-Progress @WriteProgress; if($act.AutoRemove -And ($act.MaxItems -eq $act.CurrentItem)) { $null = Remove-ActivityProgress -Confirm:$false; } # if $fReturn = $true; $OutputParameter = $fReturn; } # try catch { # ... } # catch finally { # Cleanup } # finally } # PROCESS END { $datEnd = [datetime]::Now; } # END } # function Export-ModuleMember -Function Set-ActivityProgress;
function Remove-ActivityProgress { [CmdletBinding( SupportsShouldProcess=$true, ConfirmImpact="Low", HelpURI='http://dfch.biz/PS/System/Utilities/Remove-ActivityProgress/' )] Param ( [Parameter(Mandatory = $false)] [switch] $ReturnDetails = $false ) # Param BEGIN { $datBegin = [datetime]::Now; [string] $fn = $MyInvocation.MyCommand.Name; } # BEGIN PROCESS { [Boolean] $fReturn = $false; $OutputParameter = $null; try { # Parameter validation if($biz_dfch_PS_System_Utilities.ActivityStack.Count -le 0) { $msg = ('ActivityStack contains no activity that could be removed. Aborting ...'); $e = New-CustomErrorRecord -m $msg -cat InvalidOperation -o $biz_dfch_PS_System_Utilities.ActivityStack; Log-Critical $fn $msg; throw($gotoError); } # if $act = $biz_dfch_PS_System_Utilities.ActivityStack.Peek(); if(!$PSCmdlet.ShouldProcess($act.Activity)) { $fReturn = $false; } else { Write-Progress -Activity $act.Activity -Completed; $null = $biz_dfch_PS_System_Utilities.ActivityStack.Pop(); $fReturn = $true; if($ReturnDetails) { $OutputParameter = $act; } else { $OutputParameter = $fReturn; } # if } # if } # try catch { # ... } # catch finally { # Cleanup } # finally } # PROCESS END { $datEnd = [datetime]::Now; return $OutputParameter; } # END } # function Export-ModuleMember -Function Remove-ActivityProgress;
Note: exception handling has been removed to ease readability.