// you’re reading...

Techno-Babble

Easy Progress Reporting in Powershell

I decided to write a function to make it easier to report progress in PowerShell.

The Problem

You may be familiar with Write-Progress, but if you’re like me, you probably don’t use it that often, partly because  you don’t use it that often, so whenever you want to it means you have to go look up the help documentation or search the web for examples.

Wouldn’t it be much more fun and easy to use if all you had to do was pipe in a collection of stuff, just like with ForEach-Object? 

Loop-WithProgress

I give you Loop-WithProgress.  It works just like ForEach-Object.  Pipe something into it and provide a code block to execute while looping.  In addition, it will write-progress as it goes.

For example, lets say we want to count lines in all the files in C:\.

So we write a function:

filter Get-LineCount { $_ | ? { !$_.PSIsContainer} | % { (gc ($_|gi)).Count } }

Then we pump our C: files through it:

ls c:\ | Get-LineCount

Now maybe that’s fine for you, but if there’s a really big file or you have a lot of files, you might want some more info.  So, we just pipe it through Loop-WithProgress

ls c:\ | Loop-WithProgress | Get-LineCount

This time we get a nice little progress bar showing what item we’re on.  It shows a message saying “Looping - Item 12″.   This alone can be helpful when you’re processing a large number of files, for example if you were recursing through the entire C:\ drive, which could take forever.  But in this case we are dealing with a small number of items whose quantity we can easily get.  Which brings us to…

Item Counts, Percent Complete, and Progress Bars

ls c:\ | measure-object

Tells us how many items there are (in my case, 41).  With this knowledge we can have a progress bar that actually shows percent complete.  We do this by supplying the Count argument:

ls c:\ | Loop-WithProgress -Count | Get-LineCount

Now, that’s kind of tedious to use measure-object, and not very pipeline friendly, so I wrote another function called Count-Items that sets a variable with the item count in it.  When the pipeline is done, it will clear the variable back out of scope.

ls c:\ | Count-Items MyCount | Loop-WithProgress -Count $MyCount | Get-LineCount

If you give Count-Items a variable name, it will use TempCount, and Loop-WithProgress is smart enough to use it.  So we can shorten this to:

ls c:\ | Count-Items | Loop-WithProgress  | Get-LineCount

Reporting Status

This is all fine and good, but what if I want to know which file is currently being processed?  I can report this information back to the UI using the StatusScriptBlock argument:

ls c:\ | Count-Items | Loop-WithProgress -status {$_} | Get-LineCount

In this case I’m just returning the actual item as the status.  When I do this, my progress window will also show the name of the file whose lines are being counted.

Aliases

I’ve been using full-names for all the examples, but I usually alias “~” to Loop-WithProgress and “ci” to Count-Items.  So my one-liner to count lines of code with progress looks like this:

ls c:\ | ci | ~ -s {$_} | Get-LineCount

The code

Here is the code:


function Loop-WithProgress {
	param (
		[ScriptBlock] $ProcessBlock,
		$Count,
		[int] $Id = 0,
		[string] $ActionLabel = "Looping",
		[ScriptBlock] $StatusReportBlock,
		[switch] $BreakAfterCountReached
	)

	begin {
		$i = 1;

		function Done {
			Write-Progress -Activity $ActionLabel -Completed -Status "All done." -id $id
		}

	}	

	process {

		if ($StatusReportBlock) {
			$status = . $StatusReportBlock $_;
		}
		if (!$status) {
			$status = "Processing Item";
		}		

		if (!$Count -and $TempCount){
			$Count = $TempCount
		}

		if ($Count) {
			$completeAmount = [System.Math]::Min($i,$Count);
			Write-Progress "$ActionLabel - $i of $Count" $status -id $id -percentComplete (($completeAmount/$Count)*100)
		} else {
			Write-Progress "$ActionLabel - Item $i" $Status -id $id
		}

		if ($ProcessBlock) {
			$result = . $ProcessBlock $_
		} else {
			$result = $_
		}
		$i++

		if ($BreakAfterCountReached -and $i -eq $Count) {
			Done;
			break;
		}		

		return $result

	}

	end {
		Done;
	}
}
set-alias lwp Loop-WithProgress
set-alias ~ Loop-WithProgress

function Count-Items {
	param ( $Name = "TempCount" )
	$list =@($input)
	Set-Variable -Name $Name -Value $list.Count -Scope script
	$list
	Remove-Variable $Name -scope script
}
set-alias ci Count-Items

Discussion

No comments for “Easy Progress Reporting in Powershell”

Post a comment

Recent Comments