Last active
December 22, 2015 18:09
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
############################################################################## | |
#.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