Introduction


Ever written a really cool PowerShell script that performs lots of actions, or iterates over hundreds or thousands of items? Wouldn't it be nice to see the status of the script as it's running to get an indication of the progress? You could use Write-Host to output lots of messages, like "File X just got updated...". But then your status messages are mixed through your scripts output, and you still have no clear indication of the scripts progress, or how many actions remain. In steps Write-Progress...

Write-Progress is a PowerShell Utility Cmdlet, and allows you to write a status and progress message to the top of the PowerShell console window. Write-Progress is fairly well documented on the TechNet site, and you can read about it here: Write-Progress

This article covers a simple scenario of using Write-Progress with a long running script that performs iterations of all the item versions in a SharePoint list, within a web and all of its sub-webs. It highlights how to nest progress bars and dynamically set the percent (%) complete. It also demonstrates using informative progress status messages.

The full script that this example is based on has additional functions (that allows you to delete old versions) as well as help files. The script is available for download from the TechNet Gallery: List or Delete excess ListItem Versions in a SharePoint List or Document Library

Example PowerShell Function


The following (minimized) section of the PowerShell script is commented, and contains 2 functions, and some statements (at the bottom) that call the Get-ExcessListItemVersions function. The comments should give you an understanding of how to call the Write-Progress cmdlet using different ID's and Parent ID's (for nesting the progress bars) and displaying dynamic messages and "percent complete" information.

You can copy this code into a PowerShell window on a SharePoint server and run the command (just change the SiteUrl parameter and ListTitle parameter).

function Get-ExcessListItemVersions{                   
    [CmdletBinding()]            
        Param(              
                [parameter(Mandatory=$true)][string]$SiteUrl,            
                [parameter(Mandatory=$true)][string]$ListTitle,                   
                [parameter(Mandatory=$true)][int]$MaxVersions,            
                [parameter(Mandatory=$false)][int]$ParentProgressBarId,            
                [parameter(Mandatory=$false)][switch]$Recurse            
            )            
                 
    if($Recurse){            
        #Display the first progress message.            
        #Set the Percentage Complete to 1% using the "-PercentComplete (1)" parameter            
        #Set the Id of the progress bar to 1            
        #For the activity, display a message to the user, inserting the URL of the root site being checked, by using the $SiteUrl variable in the "-Activity" parameter.            
        Write-Progress -Id 1 -Activity "Recursively checking the web, $SiteUrl, and all sub webs." -PercentComplete (1) -Status "Recursively enumerating the webs to process.";            
        #Because the -Recurse switch has been set, get a list of Urls, containing the current webs Url, and any of its Sub-Webs            
        $websToSearch = @();            
        $websToSearch = Get-Webs $SiteUrl;            
        #To update the progress bar, we need to pass the -PercentComplete parameter a number between 1 - 100, representing the progress of the script. Because the -Recurse switch has been set, we need to dynamically set the maximum number of progress actions (or steps) this script will take as it enumerates through the list of webs.            
        #Set the default number of actions and webs to 1.            
        $progressActions = 1;            
        $totalWebCount = 1;            
        #If the Get-Webs function returns an array, the Web (specified by -SiteUrl) contained sub-webs. So we need to set the number of progress actions and the number of webs to the number of sub-webs.                 
        if($websToSearch.GetType().ToString() -ne "System.String")  {            
            $progressActions = $websToSearch.Count;            
            $totalWebCount = $websToSearch.Count;            
        }            
        $currentProgress=1;            
        $currentweb =1;            
        #Loop through each web in $websToSearch, and call Get-ExcessListItemVersions using the URL of the current item.            
        #When making the call to Get-ExcessListItemVersions, pass in the ID of the current Progress Bar, as the -ParentProgressBarId            
        #By setting the -ParentProgressBarId parameter, you ensure that the current progress bar is not updated by the recursive call to the Get-ExcessListItemVersions function. The -ParentProgressBarId parameter tells the Get-ExcessListItemVersions to increment the ID's of the progress bars it uses, in effect, nesting the progress bars.            
        foreach($weburl in $websToSearch)            
        {              
            Write-Host "Searching web: $weburl $Current Item: $currentProgress Number of Actions: $progressActions";            
            #Update the parent progress bars progress indicator. You can work out the current progress by dividing the current item in the foreach loop ($currentProgress) by the total number of items to be checked ($progressActions), then multiplying it by 100 to get a percentage. This is represented by "($currentProgress/$progressActions * 100)", which is passed to the -PercentComplete parameter            
            Write-Progress -Id 1 -Activity "Recursively checking the web, $SiteUrl, and all sub webs." -PercentComplete ($currentProgress/$progressActions * 100) -Status "Checking web $currentweb of $totalWebCount ($weburl) ";            
            Get-ExcessListItemVersions -SiteUrl $weburl -ListTitle $ListTitle -MaxVersions $MaxVersions -ParentProgressBarId 1;            
            #Increment the $currentProgress variable            
            $currentProgress++;            
            $currentweb++;            
        }            
        #Once we have finished looping through the collection of webs to search, update the progress bar one final time, setting the percent complete to 100%.            
        #The Sleep cmdlet is used here to keep the progress bar on the screen for a few seconds as a visual indicator that the script has finished.            
        Write-Progress -Id 1 -Activity "Recursively checking the web, $SiteUrl, and all sub webs." -PercentComplete (100) -Status "Finished!";            
        Sleep 3;            
        return;            
    }              
                 
    $itemversionobj = New-Object psobject            
    $itemversionobj | Add-Member -MemberType NoteProperty -Name "WebUrl" -value ""            
    $itemversionobj | Add-Member -MemberType NoteProperty -Name "ListTitle" -value ""            
    $itemversionobj | Add-Member -MemberType NoteProperty -Name "ItemTitle" -value ""            
    $itemversionobj | Add-Member -MemberType NoteProperty -Name "ItemId" -value ""            
    $itemversionobj | Add-Member -MemberType NoteProperty -Name "Author" -value ""            
    $itemversionobj | Add-Member -MemberType NoteProperty -Name "VersionAuthor" -value ""            
    $itemversionobj | Add-Member -MemberType NoteProperty -Name "VersionLabel" -value ""            
    $itemversionobj | Add-Member -MemberType NoteProperty -Name "VersionComment" -value ""            
    $versionList = $null;            
    $versionList = @();            
                 
    #Set the initial ID's used for the two progress bars that will be displayed as the script checks items.            
    $outerProgressBarId = 1;            
    $innerProgressBarId = 2;            
    #If the -ParentProgressBarId parameter has been set, update the $outerProgressBarId and $innerProgressBarId variables so that they don't clash with the progressbar ID of the calling function. If we don't do this and the progress bars we display in this function have the same ID as the calling functions progress bar, the calling functions progress will be overwritten when we make new calls to Write-Progress            
    if($ParentProgressBarId -ne $null)            
    {            
        $outerProgressBarId = $ParentProgressBarId + 1;            
        $innerProgressBarId = $outerProgressBarId + 1;            
    }            
    else            
    {            
        $ParentProgressBarId = 0;            
    }            
    #In this part of the function, we are going to use two progress bars.            
    #The first progress bar, the Outer (or parent) progress bar, is going to display the overall status of the function, which has four stages (steps), which are: 1. "Getting the web", 2. "Getting the list", 3. "Checking list items", and finally, 4. Successfully finished parsing the list            
    #The second progress bar, the Inner progress bar, is going to display progress about each item being iterated. This progress bar will show a message that contains the "current item of totals items", as a visual indicator as to where the script is up (how many items of the total number of items has been checked). The second progress bar has its parent progress bar  (-ParentId) set to the ID of the Outer progress bar, so that it is nested underneath it.            
    $numberOfActions = 4;            
    #Set the percentage of the first progress bar to 25% (calculated from (1/$numberOfActions *100), which is effectively 1/4*100)            
    Write-Progress -Id $outerProgressBarId -ParentId $ParentProgressBarId -Activity "Processing items in $SiteUrl" -PercentComplete (1/$numberOfActions *100) -Status "Getting the $SiteUrl web.";            
    $w = get-spweb $siteUrl            
    try            
    {            
        #Set the percentage of the first progress bar to 50% (calculated from (2/$numberOfActions *100), which is effectily 2/4*100)            
        Write-Progress -Id $outerProgressBarId -ParentId $ParentProgressBarId -Activity "Processing items in $SiteUrl" -PercentComplete (2/$numberOfActions *100) -Status "Getting the list.";            
        $l = $w.Lists.TryGetList($ListTitle);            
        if($l -eq $null)            
        {              
            #If the list was not found in the current web, set the progress to 100%, complete, and return.                     
            Write-Progress -Id $outerProgressBarId -ParentId $ParentProgressBarId -Activity "Processing items in $SiteUrl" -PercentComplete (100) -Status "List, $ListTitle, not found in the  current web, $SiteUrl";            
            return;            
        }            
        #Get the count of items. This will be used by the second progress bar (the inner progress bar) to calculate the percent complete value as the items are iterated.            
        $items = $l.Items;            
        $count = $items.Count;            
        $currentItem =1;            
        #Set the percentage of the first progress bar to 75% (calculated from (3/$numberOfActions *100), which is effectively 3/4*100). Show the user how many items are being checked by setting the status (-Status) of the progress bar using the count of items.            
        Write-Progress -Id $outerProgressBarId -ParentId $ParentProgressBarId -Activity "Processing items in $SiteUrl" -PercentComplete (3/$numberOfActions *100) -Status "Found the '$ListTitle' List. Checking $count items.";            
        $mltf = $l.Fields["Check In Comment"];            
        foreach($item in $items)            
        {            
            $itemTitle = $item.Title;            
            $itemAuthor = ($item.Fields["Created By"]).GetFieldValueAsText($item["Created By"]);            
            $itemId = $item.ID;            
            #As each item in the collection of items is iterated through, update the progress bars percent complete value by dividing the current item by the total number of items and multiplying it by 100. This is represented by ($currentItem/$count*100), which is passed to the -PercentComplete parameter. Also update the status message, using the -Status parameter, with a message about the position of the current item being checked within the total number of items. This is represented by  -Status "Checking item $currentItem of $count ($itemTitle)", which uses the $currentItem, $count and $itemTitle variables to display a dynamic message.            
            Write-Progress -Id $innerProgressBarId -ParentId $outerProgressBarId -Activity "Enumerating List Items" -PercentComplete ($currentItem/$count*100) -Status "Checking item $currentItem of $count ($itemTitle)";            
            $excessVersions = $false;            
            $versionsDeleted = 0;            
            if($item.Versions.Count -gt $MaxVersions){                     
                $vtr = $item.Versions.Count;            
                $versionsDeleted = $vtr - $MaxVersions;            
                $excessVersions = $true;            
                Write-Host "[$SiteUrl] $itemTitle has $vtr versions.";            
                while($vtr -gt $MaxVersions){              
                    $vtr--;            
                    $comment = "";            
                    [Microsoft.SharePoint.SPListItemVersion]$iv = $item.Versions[$vtr];            
                    $versionLabel = $iv.VersionLabel;            
                    $versionAuthor = $iv.CreatedBy.User.DisplayName;            
                    $comment = ($mltf.GetFieldValueAsText($item.File.Versions[($versionLabel)-1].CheckInComment)).Replace("`r`n"," ").Replace("`n"," ");            
                    Write-Host "$itemTitle (version $versionLabel) [Comment: $comment]";            
                    $nvi = $itemversionobj | Select-Object *; $nvi.WebUrl=$SiteUrl;$nvi.ListTitle=$ListTitle;$nvi.ItemTitle=$itemTitle;$nvi.VersionLabel=$versionLabel;$nvi.VersionComment=$comment;$nvi.Author=$itemAuthor;$nvi.VersionAuthor=$versionAuthor;$nvi.ItemId = $itemId;            
                    $versionList += $nvi;            
                }            
            }            
            if($excessVersions)            
            {            
                #If excess versions were found, update the progress bars current status message, without changing the percent complete value.            
                Write-Progress -Id $innerProgressBarId -ParentId $outerProgressBarId -Activity "Enumerating List Items" -PercentComplete ($currentItem/$count*100) -Status "Found $versionsDeleted excess versions from the list item '$itemTitle'";            
            }            
            #Increment the $currentItem value before the next loop.            
            $currentItem++;            
        }            
    }            
    finally            
    {            
        $w.Dispose();            
    }            
    #Finally, update the outer (parent) progress bar with a success message, and set the percent complete to 100%            
    Write-Progress -Id $outerProgressBarId -ParentId $ParentProgressBarId -Activity "Processing items in $SiteUrl" -PercentComplete (100) -Status "Successfully finished enumerating items in the $ListTitle list.";            
    return $versionList;            
}            
             
function Get-Webs{            
    [CmdletBinding()]            
    Param(              
            [parameter(Mandatory=$true)][string]$WebUrl            
        )            
    $w = Get-SPWeb $WebUrl;            
    try            
    {            
        $webCollection = @();            
        $webCollection += $w.Url;            
        if($w.Webs.Count -gt 0)            
        {            
            foreach($web in $w.Webs)            
            {            
                $webCollection += Get-Webs -WebUrl $web.Url;            
            }            
        }            
    }            
    finally            
    {            
        $w.Dispose();            
    }            
    return $webCollection;            
}

Example: Enumerate all of the publishing pages in the Pages library of the input web, and all sub-webs


Enumerate all of the publishing pages in the Pages library (if found) of the sneakpreview web, and all sub-webs of sneakpreview. Store all versions of a page in excess of 5 versions in the $excessVersions variable.

$excessVersions = Get-ExcessListItemVersions -SiteUrl "http://sneakpreview" -ListTitle "Pages" -MaxVersions 5 -Recurse



Example: Enumerate all of the publishing pages in the Pages library


Enumerate all of the publishing pages in the Pages library (if found) of the sneakpreview/informationtechnology/teamsite web. 

$excessVersions = Get-ExcessListItemVersions -SiteUrl "http://sneakpreview/informationtechnology/teamsite" -ListTitle "Pages" -MaxVersions 5



Example: The three progress bars used (with -Recurse)




Example: Two progress bars used (without -Recurse)




See Also

References