Duplicate list using PnP Powershell
Tags: spo, pnp, powershell
Published: 2021-11-29 07:12:28

Invoke-PnPSiteTemplate allows, between others, copying lists and libraries between sites. 

Duplicating a list requires, however, a small modification of the exported template. 
First, export the list to a variable:
 `$template = Get-PnPSiteTemplate -OutputInstance -ListsToExtract $listName -Handlers Lists`

Since the duplicated list must have new url, the template must be updated to change the list, forms and views urls:
```
for ($i = 0; $i -lt $data.Value.Count; $i++) {
            $data.Value[$i].Title = $newName
            $data.Value[$i].Url = "Lists/$newName"
            $data.Value[$i].DefaultDisplayFormUrl = ($template.Lists[0].DefaultDisplayFormUrl -replace "/$listName/", "/$newName/")
            $data.Value[$i].DefaultEditFormUrl = ($template.Lists[0].DefaultEditFormUrl -replace "/$listName/", "/$newName/")
            $data.Value[$i].DefaultNewFormUrl = ($template.Lists[0].DefaultNewFormUrl -replace "/$listName/", "/$newName/")
            for ($j = 0; $j -lt $data.Value[$i].Views.Count; $j++) {
                $data.Value[$i].Views[$j].SchemaXml = ($data.Value[$i].Views[$j].SchemaXml -replace "/$listName/", "/$newName/")
            }
        }
```

If the list contains calculated fields, these must be updated as well to avoid errors during provisioning. It's a [knowns issue](https://github.com/pnp/PnP-Sites-Core/issues/2081) and the code below is inspired by the [code sample](https://github.com/pnp/PnP-Sites-Core/issues/2081#issuecomment-756379285), using regex for simplicity 
```
for ($i = 0; $i -lt $data.Value.Count; $i++) {
            $list = $data.Value[$i]
            $lookupField = @{}
            $list.Fields | ForEach-Object { $schema = [xml]$_.SchemaXml; $lookupField[$schema.Field.Name] = $schema.Field.DisplayName}
            $list.FieldRefs | ForEach-Object { $lookupField[$_.Name] = $_.DisplayName}

            # Find all calculated fields. Because of a PnP bug, a template uses [{fieldtitle:<internalFieldName>}] in calculated field formulas and not the field's Display Name
            # The code below finds all instances of {fieldtitle:xxxx} in the field's formula, figures out all of the internal names used (the xxxx after the "fieldtitle:"),
            # looks up the field's Display Name, and replaces {fieldtitle:xxxx} in the calculation with the field's Display Name.
            for ($j = 0; $j -lt $list.Fields.Count; $j++) {
                $results = ([Regex]::Matches($list.Fields[$j].SchemaXml, '(?<={fieldtitle:)(.*?)(?=})') | Select-Object Value).Value | Get-Unique


                foreach ($field in $results) {
                    $displayName = $lookupField[$field]
                    "$results - $displayName"
                    $data.Value[$i].Fields[$j].SchemaXml = $data.Value[$i].Fields[$j].SchemaXml.Replace("{fieldtitle:$($field)}", $displayName)
                }
            }
        }
```

Full code:

```
function Copy-List{
    param (
        [string]$listName, 
        [string]$newName
    )

    function Set-Formulas{
        param(
            [ref]$data
        )
        Write-Host "Set-Formulas"

        # We're using a for loop instead of a foreach so that we can have an index that we can use to update the original $template object
        for ($i = 0; $i -lt $data.Value.Count; $i++) {
            $list = $data.Value[$i]
            $lookupField = @{}
            $list.Fields | ForEach-Object { $schema = [xml]$_.SchemaXml; $lookupField[$schema.Field.Name] = $schema.Field.DisplayName}
            $list.FieldRefs | ForEach-Object { $lookupField[$_.Name] = $_.DisplayName}

            # Find all calculated fields. Because of a PnP bug, a template uses [{fieldtitle:<internalFieldName>}] in calculated field formulas and not the field's Display Name
            # The code below finds all instances of {fieldtitle:xxxx} in the field's formula, figures out all of the internal names used (the xxxx after the "fieldtitle:"),
            # looks up the field's Display Name, and replaces {fieldtitle:xxxx} in the calculation with the field's Display Name.
            for ($j = 0; $j -lt $list.Fields.Count; $j++) {
                $results = ([Regex]::Matches($list.Fields[$j].SchemaXml, '(?<={fieldtitle:)(.*?)(?=})') | Select-Object Value).Value | Get-Unique


                foreach ($field in $results) {
                    $displayName = $lookupField[$field]
                    "$results - $displayName"
                    $data.Value[$i].Fields[$j].SchemaXml = $data.Value[$i].Fields[$j].SchemaXml.Replace("{fieldtitle:$($field)}", $displayName)
                }
            }
        }
    }

    function Set-ListViews{
        param(
            [ref]$data,
            [string]$listName,
            [string]$newName
        )
        Write-Host "Set-ListViews"
        for ($i = 0; $i -lt $data.Value.Count; $i++) {
            $data.Value[$i].Title = $newName
            $data.Value[$i].Url = "Lists/$newName"
            $data.Value[$i].DefaultDisplayFormUrl = ($template.Lists[0].DefaultDisplayFormUrl -replace "/$listName/", "/$newName/")
            $data.Value[$i].DefaultEditFormUrl = ($template.Lists[0].DefaultEditFormUrl -replace "/$listName/", "/$newName/")
            $data.Value[$i].DefaultNewFormUrl = ($template.Lists[0].DefaultNewFormUrl -replace "/$listName/", "/$newName/")
            for ($j = 0; $j -lt $data.Value[$i].Views.Count; $j++) {
                $data.Value[$i].Views[$j].SchemaXml = ($data.Value[$i].Views[$j].SchemaXml -replace "/$listName/", "/$newName/")
            }
        }
    }

    Write-Host "Duplicating list $listName"
    $currentFolder = Split-Path -Parent $PSCommandPath

    $template = Get-PnPSiteTemplate -OutputInstance -ListsToExtract $listName -Handlers Lists 
    Set-ListViews -data ([ref]$template.Lists) -listName $listName -newName $newName
    Set-Formulas -data ([ref]$template.Lists) 
    Save-PnPSiteTemplate -Template $template -Out "$currentFolder/temp/$newName.xml" -Force
    Invoke-PnPSiteTemplate -Path "$currentFolder/temp/$newName.xml" 
}
```

Using the function is as easy as:
 `Copy-List -listName "List" -newName "ListDuplicate" `