Skip to content

Instantly share code, notes, and snippets.

@josheinstein
Last active December 22, 2015 18:09
Show Gist options
  • Save josheinstein/6510682 to your computer and use it in GitHub Desktop.
Save josheinstein/6510682 to your computer and use it in GitHub Desktop.
A simple, drop-in replacement for ForEach-Object that automatically gathers up objects from the input pipeline and presents a progress bar as each item is processed by subsequent pipeline commands. Flexible options for presenting progress messages along with the progress bar.
Import-Module ActiveDirectory
# Get all of the computer names in active directory.
# Displays a progress bar as each computer is queried for
# the state of its hard drives using WMI.
# The Status parameter selects the computer name to display
# in the progress message.
Get-ADComputer -Filter * |
Measure-Progress -Status { $_.Name } {
Get-WmiObject Win32_LogicalDisk -Filter 'DriveType=3' -Computer $_.Name
}
##############################################################################
#.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. Measure-Progress, 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.
#
#.EXAMPLE
# Dir C:\largefiles\* | Measure-Progress | Copy-Item -dest c:\archive
#
#.EXAMPLE
# # Assumes the default alias %% is associated
# 1..10 | %% { Sleep $_ } -Activity 'Sleeping' -Status {"for $_ sec"}
##############################################################################
function Measure-Progress {
[CmdletBinding()]
param(
# The current pipeline 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.
[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.
[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.
[Parameter()]
[ValidateRange(0, 0x7FFFFFFF)]
[Int32] $Id = 0,
# Identifies the parent activity of the current activity. Use the
# value -1 if the current activity has no parent activity.
[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.
[Parameter()]
[ValidateRange(0, 0x7FFFFFFF)]
[Int32] $ProgressDelay = 500
)
begin {
if ($Activity -is [ScriptBlock]) {
function GetActivity($UnderBar) {
$Result = "$(Invoke-ScriptBlock $Activity -InputObject $UnderBar)"
if ([String]::IsNullOrEmpty($Result)) { $Result = 'Processing' }
Return $Result
}
}
else {
function GetActivity($UnderBar) {
$Result = $Activity
if ([String]::IsNullOrEmpty($Result)) { $Result = 'Processing' }
Return $Result
}
}
if ($Status -is [ScriptBlock]) {
function GetStatus($UnderBar) {
$Result = "$(Invoke-ScriptBlock $Status -InputObject $UnderBar)"
if ([String]::IsNullOrEmpty($Result)) { $Result = '(empty)' }
Return $Result
}
}
else {
function GetStatus($UnderBar) {
$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.Start()
}
process {
# Write a progress record but not more than once every ###ms
# (otherwise this slows down the processing anyway)
if ($StopWatch.ElapsedMilliseconds -gt $ProgressDelay) {
Write-Progress -Id $Id -ParentId $ParentId `
-Activity $(GetActivity) `
-Status "Gathering input... ($($Items.Count))"
$StopWatch.Restart()
}
$Items.Add($InputObject)
}
end {
$StopWatch.Restart()
# Writes each item in $ItemsSource to the output pipeline,
# updating the progress bar for each processed item.
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 ###ms
# (otherwise this slows down the processing anyway)
if ($i -eq 0 -or $StopWatch.ElapsedMilliseconds -gt $ProgressDelay) {
Write-Progress -Id $Id -ParentId $ParentId `
-Activity $(GetActivity $Item) `
-Status $(GetStatus $Item) `
-PercentComplete (($i / $Count) * 100)
$StopWatch.Restart()
}
Write-Output $Item
}
}
if ($Process) { FeedItemsWithProgress $Items | Invoke-ScriptBlock $Process }
else { FeedItemsWithProgress $Items }
# write a completion message
Write-Progress -Id $Id -ParentId $ParentId `
-Activity $(GetActivity) `
-Status 'Done' `
-PercentComplete 100 -Completed
}
}
##############################################################################
#.SYNOPSIS
# Invokes a ScriptBlock with a pre-defined set of variables including the
# special variables $this and $_.
#
#.DESCRIPTION
# Under normal circumstances, invoking a ScriptBlock in a standard PowerShell
# scope will allow it to inherit the variables of the caller's scope. With
# modules, however, the scope inheritence is broken and any state modified
# by the invoker (the module that takes a ScriptBlock parameter) will not be
# seen by the ScriptBlock at runtime. This command allows a map of variable
# names and values to be injected into the ScriptBlock at execution time.
# This also allows functions and cmdlets the ability to execute ScriptBlocks
# that use special variables $_ and $this.
#
#.RETURNVALUE
# Any output returned by the ScriptBlock will be returned by this command.
##############################################################################
function Invoke-ScriptBlock {
[CmdletBinding(DefaultParameterSetName='Simple')]
param (
# The ScriptBlock to execute.
[Parameter(Position=1, Mandatory=$true)]
[ScriptBlock]$ScriptBlock,
# The object that will be accessed with the $_ variable in the ScriptBlock.
[Parameter(ParameterSetName='Simple', ValueFromPipeline=$true)]
[Object]$InputObject,
# The object that will be accessed with the $this variable in the ScriptBlock.
[Parameter(ParameterSetName='Simple')]
[Object]$ThisObject,
# Allows complete control over the variables that will be injected into the
# scope of the ScriptBlock. If these variables overwrite existing variables
# in another scope, the old values will be set back upon returning.
[Parameter(ParameterSetName='Complex', Mandatory=$true)]
[Hashtable]$Variables
)
begin {
# Use reflection to get access to the session state
# that the scriptblock is attached to. this will allow us
# to reach "back" into a scope that is unavailable to
# this module in order to push a $_ variable into the
# session state in which the script block will execute
$SessionStateProperty = [ScriptBlock].GetProperty('SessionState',([System.Reflection.BindingFlags]'NonPublic,Instance'))
$SessionState = $SessionStateProperty.GetValue($ScriptBlock, $null)
}
process {
# If InputObject/ThisObject were specified,
# we'll create a variables hashtable with these
# two special variables already present.
if ($PSCmdlet.ParameterSetName -eq 'Simple') {
$Variables = @{}
if ($InputObject -ne $null) { $Variables['_'] = $InputObject }
if ($ThisObject -ne $null) { $Variables['this'] = $ThisObject }
}
# set the underbar value before calling the scriptblock
# note that the context in which the scriptblock was defined
# may already have a current underbar value so we need to
# store the old one and set it back when we're done
$OldVariables = @{}
foreach ($VariableName in $Variables.Keys) {
$OldVariables[$VariableName] = $SessionState.PSVariable.GetValue($VariableName)
}
try {
# Set the variables in the session state of the scriptblock
foreach ($VarName in $Variables.Keys) {
Write-Verbose "Setting $VarName to $($Variables[$VarName])"
$SessionState.PSVariable.Set($VarName, $Variables[$VarName])
}
$SessionState.InvokeCommand.InvokeScript($SessionState, $ScriptBlock, @())
}
finally {
# Restore the old variables
foreach ($VarName in $OldVariables.Keys) {
$SessionState.PSVariable.Set($VarName, $OldVariables[$VarName])
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment