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:

Nested Write-Progress bars
Nested Write-Progress bars

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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.