Jul 22, 2017

PowerShell QuickTip: 3 characters to improve a script

Hello SharePoint fellows,

Today, I want to share with you something that will probably remain as my quickest bug fix ever: Add 3 characters to fix a bug which was not a typo.

A few months ago, I wrote a post about SharePoint maintenance tasks using PowerShell script and SharePoint PnP Powershell where I presented a script I wrote to migrate the content type applied to a bunch of documents to another content type.   In that same post I wrote:

Whenever I code something, I tend to (over-)split it into reusable and generic parts, sometines I never re-use them… but you never know, one day, maybe…

I knew I would reuse that script someday! In a totally different context (change of existing SharePoint structure and used Content Types), I had to reuse it. In this case however, I have dozens of Content Types to migrate. Still developer and still lazy as I am, I do not want to reenter the command dozens of times. Then, I wrote another script where I crafted, using Excel, the commands with the proper arguments

screenshot-bulk-migrate-ct.png

Run the script, proud of the accomplished job before is is actually done ! But BOOM ! Some ugly red messages appear!

error-script-migrate

What's wrong with the script ?

The error message states than I am trying to access an indexed property of $item which is null. The script hasn't changed even a bit since the last time I used it and it worked ! (Does is sound like one of these developer lame excuses ? I admit... but still). What's funky with the $item variable ?

erroneous-line

Something I knew, but kind of forgot when I wrote this script is that PowerShell automatically unboxes a single item array to that item. If only a single document has the specified source content type, the $itemsToUpdate variable will actually contain the item itself instead of an array. In this case, when I write: $item = $itemsToUpdate[$i] It makes no sense since a SharePoint ListItem has no indexed properties, so $item is null.

Wait a minute, if it is not an array, why didn't it crash ?

$total Count

Because, just like in my sample above, a common way to check the result array is not empty is to check its Count property. Before PowerShell 3.0, the Count property was not working if the result was the unboxed single item, which made the scripts crash on that simple check. Since PowerShell 3.0, it has been solved by providing this Count property even for single item results (Check this for more info).

ps-count ps-count2

You can see, in the screenshots, the intellisense of PowerShell ISE doesn't suggest the Count property, however, I can read it and it contains a value.

It is not a bug, it's a feature !

No, seriously, while the red messages could make think there is a bug in the script, it was originally designed to migrate the content type of a collection of documents to another one, and that's the only way I used it and tested it (It is a script to help me doing stuff quicker, I did not spend much time on testing,... Please don't blame me !). I never tested that script to work on a single document. In this new context, I run multiple commands, one for each Content Type and without a clue of the Content Types that are (or are not) in use and how many times.

Enough with the excuses, time to fix it !

Yeah, I meant "improve it", PowerShell and its magic allow us to improve this script with a very small change. Use the array constructor on the result @ ( ). We have to surround the call of the Get-PnPListItem cmdlet with the @ ( ) operator, this will make sure the result is an array, even if it is the automatically boxed single item. So here is the adapted code

fix

And that's how, with only 3 added characters, I improved (or bug-fixed) my script! Here is the complete improved script

[CmdletBinding()]
Param (
    [Parameter(Mandatory=$True)]
    [string]$List,

    [Parameter(Mandatory=$True)]
    [string]$SourceContentType,

    [Parameter(Mandatory=$True)]
    [string]$TargetContentType
)


$ctx = Get-PnPContext
if (!$ctx) {
    Connect-PnPOnline
}

$ListObj = $null
$SourceContentTypeObj = $null
$TargetContentTypeObj = $null

# Instantiate the list object
$ListObj = Get-PnPList -Identity $List
If (!$ListObj) {
    Throw "The target list cannot be found"
}
Get-PnPProperty -ClientObject $ListObj -Property Title,ContentTypes

# Instantiate the source content type object
$SourceContentTypeObj = Get-PnPContentType -List $ListObj | ? {$_.Name -eq $SourceContentType}
If (!$SourceContentTypeObj) {
    Throw "The specified content type cannot be found in the list"
}

# Ensure the source content type name and id
Get-PnPProperty -ClientObject $SourceContentTypeObj -Property Name,Id

# Try to retrieve the target content type from the target list
$TargetContentTypeObj = Get-PnPContentType -List $ListObj | ?{$_.Name -eq $TargetContentType}

 # If the target content type does not already exist in the target list, add it
If (!$TargetContentTypeObj) {
    # Retrieve it from the available content types
    $TargetContentTypeObj = Get-PnPContentType -InSiteHierarchy | ?{$_.Name -eq $TargetContentType}
    If (!$TargetContentTypeObj) {
        Throw "The specified content type does not exists neither in list nor in site collection"
    }

    # Add the content type to the target list
    Add-PnPContentTypeToList -List $ListObj -ContentType $TargetContentTypeObj
}

# Get all items of the target list having the source content type
$camlQuery = $("<View>
                    <Query>
                        <Where>
                            <Eq>
                                <FieldRef Name='ContentType'/>
                                <Value Type='Computed'>$SourceContentType</Value>
                            </Eq>
                        </Where>
                    </Query>
                    <ViewFields>
                        <FieldRef Name='Id'/>
                        <FieldRef Name='Title'/>
                        <FieldRef Name='FileLeafRef'/>
                    </ViewFields>
                </View>")

$itemsToUpdate = @(Get-PnPListItem -List $ListObj -Query $camlQuery)

$total = $itemsToUpdate.Count
if ($total -eq 0) {
    Write-Host "No items to migrate"
    Return
}


$progressStep = 100/$total
For ($i = 0; $i -lt $total; $i++) {
    $item = $itemsToUpdate[$i]
    $title = if ($item.Title) {$item.Title} else {$item["FileLeafRef"]}
    $itemInfo = "$title [$($item.Id)]"
    $currentProgress = ($i+1)*$progressStep
    Try
    {    
        
        $dummy = Set-PnPListItem -List $ListObj -Identity $item -ContentType $TargetContentTypeObj
        Write-Progress -Activity "Migrating Content Types" -Status "Content type of item $itemInfo has been migrated" -PercentComplete $currentProgress
    }
    Catch
    {
        Write-Progress -Activity "Migrating Content Types" -Status "Content type of item $itemInfo has not been migrated" -PercentComplete $currentProgress
        Write-Warning "Item $itemInfo cannot be fully updated."
        Write-Error $_.Error.Message
        Write-Error $_.Error.StackTrace
    }
}

Write-Progress -Activity "Migrating Content Types" -Status "Operation complete" -PercentComplete 1

I hope this tip will help you as well someday!

Regards,

Yannick

Other posts