Einstein.Progress.psm1
############################################################################## #.SYNOPSIS # Performs an operation against each of a set of input objects with the aid # of a progress indicator. # #.DESCRIPTION # This function is very similar to the ForEach-Object command in that it takes # a ScriptBlock as a parameter and executes that ScriptBlock once for each # item on the pipeline. Progress-Object however, presents an automatic # progress bar which is often useful but can be frustrating to implement for # every function. # # Due to the way Measure-Progress operates, it is not ideal for all scenarios. # Particularly, this function must buffer all input objects first before any # of them can be processed. Until all the input objects are gathered, it is # impossible to know how far along you are in processing them. # # Do not use Measure-Progress when the size of the input pipeline cannot be # reasonably estimated. If the input pipeline is very large, you may run out # of memory. If the input pipeline takes a long time to produce or runs # indefinitely, Measure-Progress is not ideal because gathering objects will # take longer than actually processing them. # # Measure-Progress is very useful for churning a bunch of files. Since you can # usually list the files pretty quickly, you would put Get-ChildItem to the # left of Measure-Progress and the processing commands to the right. # #.EXAMPLE # Dir C:\largefiles\* | Measure-Progress | Copy-Item -dest c:\archive # #.EXAMPLE # # assumes alias %% # 1..10 | %% { Sleep $_ } -Activity 'Sleeping' -Status {"for $_ seconds"} # #.LINK # ForEach-Object ############################################################################## function Measure-Progress { [Alias('%%')] [CmdletBinding()] param( # Specifies the input objects. # Measure-Progress runs the script block on each input object. Enter a variable that contains the objects, or # type a command or expression that gets the objects. When you use the InputObject parameter with Measure-Progress, # instead of piping command results to Measure-Progress, the InputObject value even if the value is a collection # that is the result of a command, such as -InputObject (Get-Process) is treated as a single object. [Parameter(ValueFromPipeline=$true)] [Object] $InputObject, # A ScriptBlock that is called once for each item on the pipeline, after all # of the input objects have been gathered and counted. Use the automatic # variable $_ to refer to the pipeline object. [Parameter(Mandatory=$false, Position=1)] [ScriptBlock[]] $Process, # A ScriptBlock or static value that will be used as the Activity message # for Write-Progress calls. When using a ScriptBlock, $_ can be used to # refer to the current input object. [Alias('ProgressActivity', 'pa')] [Parameter()] [Object] $Activity = 'Processing', # A ScriptBlock or static value that will be used as the Status message # for Write-Progress calls. When using a ScriptBlock, $_ can be used to # refer to the current input object. If Status is not specified, the current # pipeline object is used as the status message. [Alias('ProgressStatus', 'ps')] [Parameter()] [Object] $Status, # Specifies an ID that distinguishes each progress bar from the others. Use this # parameter when you are creating more than one progress bar in a single command. # If the progress bars do not have different IDs, they are superimposed instead # of being displayed in series. [Alias('ProgressID', 'pn')] [Parameter()] [ValidateRange(0, 0x7FFFFFFF )] [Int32] $Id = $(Get-Random -Min 100000 -Max 1000000), # Identifies the parent activity of the current activity. Use the value -1 if # the current activity has no parent activity. [Alias('ProgressParentID', 'pp')] [Parameter()] [ValidateRange(-1, 0x7FFFFFFF)] [Int32] $ParentId = -1, # Minimum delay, in milliseconds, between progress updates. Higher values can # drastically speed up performance when dealing with a large number of inputs. [Alias('ProgressInterval', 'pi')] [Parameter()] [ValidateRange(0, 0x7FFFFFFF)] [Int32] $ProgressDelay = 500, # By default, Measure-Progress estimates the time remaining based on the average # time it took to reach the current progress point. If this simple calculation is # known to be inaccurate (i.e. because each item is known to take longer than the # previous), -NoEstimate will suppress the estimated time remaining from progress # messages. [Parameter()] [Switch] $NoEstimate ) begin { function InvokeScriptBlock { [CmdletBinding()] param( [Parameter(Position=1)] [ScriptBlock[]]$ScriptBlock, [Parameter(ValueFromPipeline=$true)] [Object]$InputObject ) process { if ($InputObject -ne $Null) { $Variables = [PSVariable[]]@( New-Object PSVariable @('_', $InputObject) ) foreach ($SB in $ScriptBlock) { $SB.InvokeWithContext($Null, $Variables, $Null) } } else { foreach ($SB in $ScriptBlock) { $SB.Invoke() } } } } function GetActivity($UnderBar) { if ($Activity -is [ScriptBlock]) { $Variables = [PSVariable[]]@(New-Object PSVariable @('_', $UnderBar)) $Result = "$($Activity.InvokeWithContext($Null, $Variables, $Null))" Return $Result } else { $Result = "$Activity" if ( [String]::IsNullOrEmpty($Result) ) { $Result = 'Processing' } Return $Result } } function GetStatus($UnderBar) { if ($Status -is [ScriptBlock]) { $Variables = [PSVariable[]]@(New-Object PSVariable @('_', $UnderBar)) $Result = "$($Status.InvokeWithContext($Null, $Variables, $Null))" Return $Result } else { $Result = "$UnderBar" if ( [String]::IsNullOrEmpty($Result) ) { $Result = '(empty)' } Return $Result } } $StopWatch = New-Object System.Diagnostics.Stopwatch # throttles progress $Items = New-Object System.Collections.Generic.List[Object] # holds inputobjects # we may be gathering for a while so write a message to that effect Write-Progress -Id $Id -ParentId $ParentId ` -Activity $(GetActivity) ` -Status 'Gathering input...' $StopWatch.Reset() $StopWatch.Start() } process { $Items.Add($InputObject) # Write a progress record but not more than once every 200ms # (otherwise this slows down the processing anyway) if ( (-not $StopWatch.IsRunning) -or ($StopWatch.ElapsedMilliseconds -gt $ProgressDelay) ) { Write-Progress -Id $Id -ParentId $ParentId ` -Activity $(GetActivity) ` -Status "Gathering input... ($($Items.Count))" $StopWatch.Reset() $StopWatch.Start() } } end { $StartTime = [DateTime]::UtcNow $StopWatch.Reset() function FeedItemsWithProgress($ItemsSource) { [Double]$Count = $ItemsSource.Count for ($i = 0; $i -lt $Items.Count; $i++) { $Item = $ItemsSource[$i] # Write a progress record but not more than once every 500ms # (otherwise this slows down the processing anyway) if ( (-not $StopWatch.IsRunning) -or ($StopWatch.ElapsedMilliseconds -gt $ProgressDelay) ) { $Percent = (($i / $Count) * 100) # Calculate seconds remaining based on the average time it # took to process the items we've processed so far. $SecondsRemaining = -1 if (!$NoEstimate -and $i -gt 0) { $Elapsed = [DateTime]::UtcNow.Subtract($StartTime).TotalSeconds $SecondsRemaining = ($Count - $i) * ($Elapsed / $i) } Write-Progress -Id $Id -ParentId $ParentId ` -Activity $(GetActivity $Item) ` -Status $(GetStatus $Item) ` -PercentComplete $Percent ` -SecondsRemaining $SecondsRemaining $StopWatch.Reset() $StopWatch.Start() } Write-Output $Item } } if ($Process) { FeedItemsWithProgress $Items | ForEach-Object { $Variables = [PSVariable[]]@(New-Object PSVariable @('_', $_)) foreach ($SB in $Process) { $SB.InvokeWithContext($Null,$Variables,$Null) } } } else { FeedItemsWithProgress $Items } # write a completion message Write-Progress -Id $Id -ParentId $ParentId ` -Activity $(GetActivity) ` -Status 'Done' ` -PercentComplete 100 -Completed } } |