Duplicate tags/term sets were noticed while typing the term to select and the term set had 10k of terms making it harder to spot the issue. There are exisiting script to export term set but woul export only the first level and not the other levels and synonymns, hence the script
# ===== Logging ===============================================================
$logFile = ".\term_export_Log-$(Get-Date -Format "yyyyMMdd-HHmm").csv"
function Log-Message {
param (
[string]$Message,
[string]$Status = "Info"
)
switch ($Status) {
"Success" { $color = "Green" }
"Info" { $color = "Blue" }
"Error" { $color = "Red" }
default { $color = "White" }
}
Write-Host -ForegroundColor $color "$Status => $Message"
$logEntry = [PSCustomObject]@{
Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Message = $Message
Status = $Status
}
# Ensure log file exists and append
if (-not (Test-Path $logFile)) {
$logEntry | Export-Csv -Path $logFile -NoTypeInformation
} else {
$logEntry | Export-Csv -Path $logFile -Append -NoTypeInformation
}
}
# ===== Config ================================================================
$clientId = "xxxxxxx"
# Term store groups to export
$groups = @("Document Management Tags - PROD")
# Output path
$invocation = (Get-Variable MyInvocation).Value
$directoryPath = Split-Path $invocation.MyCommand.Path
$currentTime = (Get-Date).ToString("yyyyMMddHHmmss")
# Create output folder if missing
$outputFolder = Join-Path $directoryPath "output_files"
if (-not (Test-Path $outputFolder)) { New-Item -Path $outputFolder -ItemType Directory | Out-Null }
# Final CSV file path
$FilePath = Join-Path $outputFolder ("TermStoreReport-" + $currentTime + ".csv")
# ===== Helpers ===============================================================
# Use Get-PnPTermLabel to retrieve synonyms (non-default labels)
function Get-Synonyms {
param(
[Guid]$TermId,
[string]$TermName
)
try {
$labels = Get-PnPTermLabel -Lcid 1033 -Term $TermId -ErrorAction Stop
$labels = $labels | where-object{$_.Value -ne $TermName}
# Return a comma-separated unique string
return $labels.Value -join ","
} catch {
Log-Message -Message "Get-PnPTermLabel failed for TermId $TermId : $($_.Exception.Message)" -Status "Error"
return ""
}
}
# ===== Export ================================================================
# Split a semicolon-delimited path and return cleaned segments
function Split-PathSegments {
param(
[Parameter(Mandatory)]
[string]$PathString
)
# Split on ';', trim whitespace, remove empty segments
$segments = $PathString -split ';' |
ForEach-Object { $_.Trim() } |
Where-Object { $_ -ne "" }
return ,$segments # return as array
}
# Return the numeric level (count of segments after cleaning)
function Get-PathLevel {
param(
[Parameter(Mandatory)]
[string]$PathString
)
$segments = Split-PathSegments -PathString $PathString
return $segments.Count
}
# Pad/clip segments to exactly 7 levels for CSV columns
function Get-Levels1to7 {
param(
[Parameter(Mandatory)]
[string]$PathString,
[int]$MaxLevels = 7
)
$segments = Split-PathSegments -PathString $PathString
# Pad with blanks and clip to MaxLevels
$padded = @($segments + @("", "", "", "", "", "", ""))[0..($MaxLevels-1)]
return ,$padded
}
# Example row builder when you have a semicolon path string
$rows = New-Object System.Collections.Generic.List[object]
function Add-RowFromPath {
param(
[string]$TermSetName,
[string]$TermSetDescription,
[string]$PathString,
[string]$AvailableForTagging,
[string]$TermDescription,
[string]$Synonyms
)
$levels = Get-Levels1to7 -PathString $PathString
$Rows.Add([PSCustomObject]@{
"Term Set Name" = $TermSetName
"Term Set Description" = $TermSetDescription
"Available for Tagging" = $AvailableForTagging
"Term Description" = $TermDescription
"Level 1 Term" = $levels[0]
"Level 2 Term" = $levels[1]
"Level 3 Term" = $levels[2]
"Level 4 Term" = $levels[3]
"Level 5 Term" = $levels[4]
"Level 6 Term" = $levels[5]
"Level 7 Term" = $levels[6]
"Synonyms" = $Synonyms
})
}
function Process-Term {
param(
[object]$Term,
[string]$TermSetName,
[string]$TermSetDescription
)
$available = [string]$Term.IsAvailableForTagging -replace 'True','TRUE' -replace 'False','FALSE'
$termDesc = $Term.Description
$synonyms = Get-Synonyms -TermId $Term.Id -TermName $Term.Name
$PathOfTerm = $Term.PathOfTerm
Add-RowFromPath -TermSetName $TermSetName -TermSetDescription $TermSetDescription -PathString $PathOfTerm -AvailableForTagging $available -TermDescription $termDesc -Synonyms $synonyms -Rows $rows
}
function ExportTerms {
try {
# If groups were not specified, enumerate all groups
if ($groups.Length -eq 0) {
$groups = @(Get-PnPTermGroup | ForEach-Object { $_.Name })
}
foreach ($termGroupName in $groups) {
try {
Log-Message -Message "Exporting terms from group: $termGroupName" -Status "Info"
# Load term sets in the group
$termGroupObj = Get-PnPTermGroup -Identity $termGroupName -Includes TermSets
foreach ($termSet in $termGroupObj.TermSets) {
# Ensure Terms collection loaded
$termSetTerms = Get-PnPTerm -TermSet $termSet.Id -TermGroup $termGroupName -IncludeChildTerms -Recursive
$tsName = $termSet.Name
$tsDesc = $termSet.Description
# Level 1 terms (root terms under the term set)
foreach ($lvl1 in $termSetTerms) {
# If the level 1 term has children, output each level bases on path of term
if ($lvl1.Terms -and $lvl1.Terms.Count -gt 0) {
foreach ($lvl2 in $lvl1.Terms) {
Process-Term -Term $lvl2 -TermSetName $tsName -TermSetDescription $tsDesc
if ($lvl2.Terms -and $lvl2.Terms.Count -gt 0) {
foreach($lvl3 in $lvl2.Terms)
{
Process-Term -Term $lvl3 -TermSetName $tsName -TermSetDescription $tsDesc
if ($lvl3.Terms -and $lvl3.Terms.Count -gt 0) {
foreach($lvl4 in $lvl3.Terms)
{
Process-Term -Term $lvl4 -TermSetName $tsName -TermSetDescription $tsDesc
if ($lvl4.Terms -and $lvl4.Terms.Count -gt 0) {
foreach($lvl5 in $lvl4.Terms)
{
Process-Term -Term $lvl5 -TermSetName $tsName -TermSetDescription $tsDesc
if ($lvl5.Terms -and $lvl5.Terms.Count -gt 0) {
foreach($lvl6 in $lvl5.Terms)
{
Process-Term -Term $lvl6 -TermSetName $tsName -TermSetDescription $tsDesc
}
}
}
}
}
}
}
}
}
}
else {
Process-Term -Term $lvl1 -TermSetName $tsName -TermSetDescription $tsDesc
}
}
}
}
catch {
Log-Message -Message "Error in group '$termGroupName': $($_.Exception.Message)" -Status "Error"
}
}
# Write the CSV once (safer than repeating Add-Content)
$rows | Export-Csv -Path $FilePath -NoTypeInformation
Log-Message -Message "Export complete. File: $FilePath" -Status "Success"
}
catch {
Log-Message -Message "Unhandled error: $($_.Exception.Message)" -Status "Error"
}
}
# ===== Connect, Run, Disconnect =============================================
Connect-PnPOnline -Url "https://contoso.sharepoint.com/" -ClientId $clientId
Log-Message -Message "Connected to SharePoint" -Status "Info"
ExportTerms
Disconnect-PnPOnline
Log-Message -Message "Disconnected" -Status "Info"