internal/Use-Progress.ps1

<#
.SYNOPSIS
    Display progress bar for processing array of objects.
.EXAMPLE
    PS C:\>Use-Progress -InputObjects @(1..10) -Activity "Processing Parent Objects" -ScriptBlock {
        $Parent = $args[0]
        Use-Progress -InputObjects @(1..200) -Activity "Processing Child Objects" -ScriptBlock {
            $Child = $args[0]
            Write-Host "Child $Child of Parent $Parent."
            Start-Sleep -Milliseconds 50
        }
    }
    Display progress bar for processing array of objects.
.INPUTS
    System.Object[]
.LINK
    https://github.com/jasoth/Utility.PS
#>

function Use-Progress {
    [CmdletBinding()]
    param
    (
        # Array of objects to loop through.
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object[]] $InputObjects,
        # Specifies the first line of text in the heading above the status bar. This text describes the activity whose progress is being reported.
        [Parameter(Mandatory = $true)]
        [string] $Activity,
        # Script block to execute for each object in array.
        [Parameter(Mandatory = $true)]
        [scriptblock] $ScriptBlock,
        # Property name to use for current operation
        [Parameter(Mandatory = $false)]
        [string] $Property,
        # Minimum timespan between each progress update.
        [Parameter(Mandatory = $false)]
        [timespan] $MinimumUpdateFrequency = (New-TimeSpan -Seconds 1)
    )

    begin {
        [System.Collections.Generic.List[object]] $listObjects = New-Object System.Collections.Generic.List[object]
    }

    process {
        $listObjects.AddRange($InputObjects)
    }

    end {
        if ($listObjects.Count -gt 0) { [object[]] $InputObjects = $listObjects.ToArray() }
        [int] $Id = 0
        if (!(Get-Variable stackProgressId -ErrorAction SilentlyContinue)) { New-Variable -Name stackProgressId -Scope Script -Value (New-Object System.Collections.Generic.Stack[int]) }
        while ($stackProgressId.Contains($Id)) { $Id += 1 }
        [hashtable] $paramWriteProgress = @{
            Id       = $Id
            Activity = $Activity
        }
        if ($stackProgressId.Count -gt 0) { $paramWriteProgress['ParentId'] = $stackProgressId.Peek() }
        [int] $SecondsRemaining = -1
        [int] $total = $InputObjects.Count
        $stackProgressId.Push($Id)

        try {
            [System.Diagnostics.Stopwatch] $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
            for ($iObject = 0; $iObject -lt $total; $iObject++) {
                if ($iObject -eq 0 -or ($stopwatch.Elapsed - $TimeElapsed) -gt $MinimumUpdateFrequency) {
                    [timespan] $TimeElapsed = $stopwatch.Elapsed
                    $PercentComplete = [System.Math]::Truncate([decimal]($iObject / $total * 100))
                    if ($PercentComplete -gt 0) { $SecondsRemaining = $TimeElapsed.TotalSeconds / $PercentComplete - $TimeElapsed.TotalSeconds }
                    if ($Property) { $CurrentOperation = $InputObjects[$iObject].$Property }
                    else { $CurrentOperation = $InputObjects[$iObject] }
                    Write-Progress -CurrentOperation $CurrentOperation -Status ("{0:P0} Completed ({1} of {2}) in {3:c}" -f ($PercentComplete / 100), $iObject, $total, $TimeElapsed.Subtract($TimeElapsed.Ticks % [TimeSpan]::TicksPerSecond)) -PercentComplete $PercentComplete -SecondsRemaining $SecondsRemaining @paramWriteProgress
                }
                Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $InputObjects[$iObject]
            }
            $stopwatch.Stop()
            Write-Progress -Status ("{0:P0} Completed ({1} of {2}) in {3:c}" -f 1, $total, $total, $TimeElapsed.Subtract($TimeElapsed.Ticks % [TimeSpan]::TicksPerSecond)) -PercentComplete 100 -SecondsRemaining 0 @paramWriteProgress
        }
        finally {
            [void] $stackProgressId.Pop()
            #Start-Sleep -Seconds 1
            Write-Progress -Id $Id -Activity $Activity -Completed
        }
    }
}

function Start-Progress {
    [CmdletBinding()]
    param (
        # Specifies the first line of text in the heading above the status bar. This text describes the activity whose progress is being reported.
        [Parameter(Mandatory = $true)]
        [string] $Activity,
        # Total Number of Items
        [Parameter(Mandatory = $true)]
        [int] $Total,
        # Minimum timespan between each progress update.
        [Parameter(Mandatory = $false)]
        [timespan] $MinimumUpdateFrequency = (New-TimeSpan -Seconds 1)
    )

    [int] $Id = 0
    if (!(Get-Variable stackProgressId -ErrorAction SilentlyContinue)) { New-Variable -Name stackProgressId -Scope Script -Value (New-Object System.Collections.Generic.Stack[int]) }
    while ($stackProgressId.Contains($Id)) { $Id += 1 }

    [hashtable] $paramWriteProgress = @{
        Id = $Id
        Activity = $Activity
    }
    if ($stackProgressId.Count -gt 0) { $paramWriteProgress['ParentId'] = $stackProgressId.Peek() }
    $stackProgressId.Push($Id)

    ## Progress Bar
    [timespan] $TimeElapsed = New-TimeSpan
    Write-Progress -Status ("{0:P0} Completed ({1} of {2}) in {3:c}" -f 0, 0, $Total, $TimeElapsed) -PercentComplete 0 @paramWriteProgress

    [PSCustomObject]@{
        WriteProgressParameters = $paramWriteProgress
        CurrentIteration        = 0
        Total                   = $Total
        MinimumUpdateFrequency  = $MinimumUpdateFrequency
        TimeElapsed             = $TimeElapsed
        Stopwatch               = [System.Diagnostics.Stopwatch]::StartNew()
    }
}

function Update-Progress {
    [CmdletBinding()]
    param (
        # Progress State Object
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object] $InputObject,
        # Number of items being completed
        [Parameter(Mandatory = $true)]
        [int] $IncrementBy,
        # Specifies the line of text below the progress bar. This text describes the operation that is currently taking place.
        [Parameter(Mandatory = $false)]
        [string] $CurrentOperation
    )

    if ($InputObject.CurrentIteration -gt $InputObject.Total) { $InputObject.CurrentIteration = $InputObject.Total }

    [hashtable] $paramWriteProgress = $InputObject.WriteProgressParameters
    if ($CurrentOperation) { $paramWriteProgress['CurrentOperation'] = $CurrentOperation }
    [int] $SecondsRemaining = -1

    ## Progress Bar
    if ($InputObject.CurrentIteration -eq 0 -or ($InputObject.Stopwatch.Elapsed - $InputObject.TimeElapsed) -gt $InputObject.MinimumUpdateFrequency) {
        $InputObject.TimeElapsed = $InputObject.Stopwatch.Elapsed
        $PercentComplete = $InputObject.CurrentIteration / $InputObject.Total
        $PercentCompleteRoundDown = [System.Math]::Truncate([decimal]($PercentComplete * 100))
        if ($PercentComplete -gt 0) { $SecondsRemaining = $InputObject.TimeElapsed.TotalSeconds / $PercentComplete - $InputObject.TimeElapsed.TotalSeconds }
        Write-Progress -Status ("{0:P0} Completed ({1} of {2}) in {3:c}" -f ($PercentCompleteRoundDown / 100), $InputObject.CurrentIteration, $InputObject.Total, $InputObject.TimeElapsed.Subtract($InputObject.TimeElapsed.Ticks % [TimeSpan]::TicksPerSecond)) -PercentComplete $PercentCompleteRoundDown -SecondsRemaining $SecondsRemaining @paramWriteProgress
    }

    $InputObject.CurrentIteration += $IncrementBy
}

function Stop-Progress {
    [CmdletBinding()]
    param (
        # Progress State Object
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [object] $InputObject
    )

    [void] $script:stackProgressId.Pop()
    $InputObject.Stopwatch.Stop()
    [hashtable] $paramWriteProgress = $InputObject.WriteProgressParameters
    Write-Progress -Completed @paramWriteProgress
}