Blog about anything related to my learnings
  • About
  • posts
bluesky
Blog about anything related to my learnings
POSTS

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"
    © Blog about anything related to my learnings 2026
    bluesky