@@ -102,72 +102,190 @@ if (!$reportOnly) {
102102
103103# [ CLI for Microsoft 365] ( #tab/cli-m365-ps )
104104``` powershell
105+ function Invoke-SiteColumnCleanup {
106+ [CmdletBinding(SupportsShouldProcess)]
107+ param(
108+ [Parameter(Mandatory, HelpMessage = "SharePoint site URL hosting the content types and lists")]
109+ [ValidateNotNullOrEmpty()][string] $SiteUrl,
110+
111+ [Parameter(Mandatory, HelpMessage = "Names of content types that should be scanned for the site column")]
112+ [ValidateNotNullOrEmpty()][string[]] $ContentTypeNames,
113+
114+ [Parameter(Mandatory, HelpMessage = "Display name of the site column to remove")]
115+ [ValidateNotNullOrEmpty()][string] $SiteColumnName,
116+
117+ [Parameter(HelpMessage = "When set, only report usage without removing the column")]
118+ [switch] $ReportOnly
119+ )
120+
121+ begin {
122+ Write-Verbose "Ensuring CLI session is authenticated."
123+ $loginOutput = m365 login --ensure 2>&1
124+ if ($LASTEXITCODE -ne 0) {
125+ throw "Failed to ensure CLI login. CLI output: $loginOutput"
126+ }
105127
106- # Base variables
107- $siteURL = "https://tenant.sharepoint.com/sites/sitename"
108- $contentTypeArray = @('testCT1','CustomContentType1')
109- $siteColumn = "EffectiveDate"
110- $reportOnly = $true # If $true, just report. If $false, take action.
111-
112- $m365Status = m365 status
113- if ($m365Status -match "Logged Out") {
114- m365 login
115- }
116-
117- # Remove the Site Column from all Content Types which have it
118- Write-Host -BackgroundColor Blue "Checking Content Types"
119-
120- foreach ($contentTypeName in $contentTypeArray) {
121-
122- Write-Host "Checking Content Type $contentTypeName"
123-
124- $contentType = m365 spo contenttype get --webUrl $siteURL --name $contentTypeName
125- $contentType = $contentType | ConvertFrom-Json
126- $schemaXml = $contentType.SchemaXml
127- $schemaXml = [xml]"<xml>$schemaXml</xml>"
128- $field = $schemaXml.xml.ContentType.Fields.Field | ? { $_.Name -eq $siteColumn }
129-
130- if ($field) {
131- Write-Host -ForegroundColor Green "Found column $($siteColumn) in $($contentTypeName)"
132- if (!$reportOnly) {
133- Write-Host -ForegroundColor Yellow "Removing column $($siteColumn) in $($contentTypeName)"
134- $contentTypeId = $contentType.Id.StringValue
135- $fieldLinkId = $field.ID.Replace("{", "").Replace("}", "")
136- m365 spo contenttype field remove --contentTypeId $contentTypeId --fieldLinkId $fieldLinkId --webUrl $siteURL --confirm
128+ $script:Summary = [ordered]@{
129+ ContentTypesChecked = 0
130+ ContentTypesUpdated = 0
131+ ListsChecked = 0
132+ ListsUpdated = 0
133+ SiteColumnRemoved = 0
134+ Failures = 0
137135 }
138136 }
139- }
140-
141137
142- # Remove the orphaned Site Column from all lists/libraries which have it
143- Write-Host -BackgroundColor Blue "Checking Lists"
144-
145- $lists = m365 spo list list --webUrl $siteURL
146- $lists = $lists | ConvertFrom-Json
147-
148- foreach ($list in $lists) {
138+ process {
139+ Write-Host "Checking content types for column '$SiteColumnName'."
140+
141+ foreach ($ctName in $ContentTypeNames) {
142+ $script:Summary.ContentTypesChecked++
143+ Write-Host "Examining content type '$ctName'."
144+
145+ $ctOutput = m365 spo contenttype get --webUrl $SiteUrl --name $ctName --output json 2>&1
146+ if ($LASTEXITCODE -ne 0) {
147+ $script:Summary.Failures++
148+ Write-Warning "Failed to retrieve content type '$ctName'. CLI output: $ctOutput"
149+ continue
150+ }
151+
152+ try {
153+ $ct = $ctOutput | ConvertFrom-Json -ErrorAction Stop
154+ }
155+ catch {
156+ $script:Summary.Failures++
157+ Write-Warning "Unable to parse content type '$ctName'. $($_.Exception.Message)"
158+ continue
159+ }
160+
161+ $query = "[?Title=='$SiteColumnName' || InternalName=='$SiteColumnName']"
162+ $fieldsOutput = m365 spo contenttype field list --webUrl $SiteUrl --contentTypeName $ctName --properties "Title,Id,InternalName" --query $query --output json 2>&1
163+ if ($LASTEXITCODE -ne 0) {
164+ $script:Summary.Failures++
165+ Write-Warning "Failed to list fields for content type '$ctName'. CLI output: $fieldsOutput"
166+ continue
167+ }
168+
169+ try {
170+ $ctFields = $fieldsOutput | ConvertFrom-Json -ErrorAction Stop
171+ }
172+ catch {
173+ $script:Summary.Failures++
174+ Write-Warning "Unable to parse field list for '$ctName'. $($_.Exception.Message)"
175+ continue
176+ }
177+
178+ $fieldLink = $ctFields | Select-Object -First 1
179+ if ($fieldLink) {
180+ Write-Host -ForegroundColor Green "Found field '$SiteColumnName' in content type '$ctName'."
181+ if (-not $ReportOnly -and $PSCmdlet.ShouldProcess("Content type '$ctName'", "Remove field link")) {
182+ $removeOutput = m365 spo contenttype field remove --webUrl $SiteUrl --contentTypeId $ct.Id.StringValue --id $fieldLink.Id --force 2>&1
183+ if ($LASTEXITCODE -ne 0) {
184+ $script:Summary.Failures++
185+ Write-Warning "Failed to remove field '$SiteColumnName' from '$ctName'. CLI output: $removeOutput"
186+ }
187+ else {
188+ $script:Summary.ContentTypesUpdated++
189+ }
190+ }
191+ }
192+ }
149193
150- $listTitle = $list.Title
151- Write-Host "Checking list $($listTitle)"
194+ Write-Host "Checking lists for orphaned column '$SiteColumnName'."
195+ $listOutput = m365 spo list list --webUrl $SiteUrl --output json 2>&1
196+ if ($LASTEXITCODE -ne 0) {
197+ $script:Summary.Failures++
198+ throw "Failed to retrieve lists. CLI output: $listOutput"
199+ }
152200
153- $field = m365 spo field get --webUrl $siteURL --listTitle $listTitle --fieldTitle $siteColumn
201+ try {
202+ $lists = $listOutput | ConvertFrom-Json -ErrorAction Stop
203+ }
204+ catch {
205+ throw "Unable to parse lists response. $($_.Exception.Message)"
206+ }
154207
155- if ($field) {
156- Write-Host -ForegroundColor Green "Found column $($siteColumn) in $($listTitle)"
208+ foreach ($list in $lists) {
209+ $script:Summary.ListsChecked++
210+ $listTitle = $list.Title
211+ Write-Host "Examining list '$listTitle'."
212+
213+ $listQuery = "[?Title=='$SiteColumnName' || InternalName=='$SiteColumnName']"
214+ $listFieldsOutput = m365 spo field list --webUrl $SiteUrl --listTitle $listTitle --query $listQuery --output json 2>&1
215+ if ($LASTEXITCODE -ne 0) {
216+ $script:Summary.Failures++
217+ Write-Warning "Failed to list fields for list '$listTitle'. CLI output: $listFieldsOutput"
218+ continue
219+ }
220+
221+ try {
222+ $listFields = $listFieldsOutput | ConvertFrom-Json -ErrorAction Stop
223+ }
224+ catch {
225+ $script:Summary.Failures++
226+ Write-Warning "Unable to parse field list for '$listTitle'. $($_.Exception.Message)"
227+ continue
228+ }
229+
230+ $listField = $listFields | Select-Object -First 1
231+ if (-not $listField) {
232+ continue
233+ }
234+
235+ Write-Host -ForegroundColor Green "Found field '$SiteColumnName' in list '$listTitle'."
236+ if (-not $ReportOnly -and $PSCmdlet.ShouldProcess("List '$listTitle'", "Remove field")) {
237+ $removeFieldOutput = m365 spo field remove --webUrl $SiteUrl --listTitle $listTitle --id $listField.Id --force 2>&1
238+ if ($LASTEXITCODE -ne 0) {
239+ $script:Summary.Failures++
240+ Write-Warning "Failed to remove field from list '$listTitle'. CLI output: $removeFieldOutput"
241+ }
242+ else {
243+ $script:Summary.ListsUpdated++
244+ }
245+ }
246+ }
157247
158- if (!$reportOnly) {
159- Write-Host -ForegroundColor Yellow "Removing column $($siteColumn) in $($listTitle)"
160- m365 spo field remove --webUrl $siteURL --listTitle $listTitle --fieldTitle $siteColumn --confirm
248+ if (-not $ReportOnly -and $PSCmdlet.ShouldProcess("Site '$SiteUrl'", "Remove site column '$SiteColumnName'")) {
249+ $siteFieldOutput = m365 spo field get --webUrl $SiteUrl --title $SiteColumnName --output json 2>&1
250+ if ($LASTEXITCODE -eq 0) {
251+ try {
252+ $siteField = $siteFieldOutput | ConvertFrom-Json -ErrorAction Stop
253+ }
254+ catch {
255+ $script:Summary.Failures++
256+ throw "Unable to parse site column details for '$SiteColumnName'. $($_.Exception.Message)"
257+ }
258+
259+ $removeSiteField = m365 spo field remove --webUrl $SiteUrl --id $siteField.Id --force 2>&1
260+ if ($LASTEXITCODE -ne 0) {
261+ $script:Summary.Failures++
262+ Write-Warning "Failed to remove site column '$SiteColumnName'. CLI output: $removeSiteField"
263+ }
264+ else {
265+ $script:Summary.SiteColumnRemoved++
266+ }
267+ }
268+ else {
269+ Write-Verbose "Site column '$SiteColumnName' was not found at the site level."
270+ }
161271 }
162272 }
163- }
164273
165- # Remove the Site Column itself
166- if (!$reportOnly) {
167- m365 spo field remove --webUrl $siteURL --fieldTitle $siteColumn --confirm
274+ end {
275+ Write-Host "`nCleanup summary:" -ForegroundColor Cyan
276+ Write-Host " Content types checked : $($script:Summary.ContentTypesChecked)"
277+ Write-Host " Content types updated : $($script:Summary.ContentTypesUpdated)"
278+ Write-Host " Lists checked : $($script:Summary.ListsChecked)"
279+ Write-Host " Lists updated : $($script:Summary.ListsUpdated)"
280+ Write-Host " Site columns removed : $($script:Summary.SiteColumnRemoved)"
281+ Write-Host " Failures : $($script:Summary.Failures)"
282+ }
168283}
169284
285+ # Example usage:
286+ # Invoke-SiteColumnCleanup -SiteUrl "https://tenant.sharepoint.com/sites/sitename" -ContentTypeNames 'testCT1','CustomContentType1' -SiteColumnName 'EffectiveDate' -ReportOnly
170287
288+ Invoke-SiteColumnCleanup -SiteUrl "https://tenanttocheck.sharepoint.com/sites/PnPDemo2" -ContentTypeNames 'testContentTypeA','testContentTypeB','testContentTypeC' -SiteColumnName 'testColumn1' -ReportOnly
171289```
172290[ !INCLUDE [ More about CLI for Microsoft 365] ( ../../docfx/includes/MORE-CLIM365.md )]
173291***
0 commit comments