From d22701e52df62d40bdbf4921568da23dd73c4ff4 Mon Sep 17 00:00:00 2001 From: Darian Miller Date: Mon, 30 Mar 2026 21:24:27 -0500 Subject: [PATCH] Add a new `-Check` mode to return 0 for clean and 1 for dirty Add a new `OutputLevel` option with `detailed, summary, quiet` modes to control level of output For issue #17 --- README.md | 266 +++++++++++++++++++++++++----------- source/delphi-clean.ps1 | 294 +++++++++++++++++++++++++++++----------- 2 files changed, 406 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index 28a837f..fa2566d 100644 --- a/README.md +++ b/README.md @@ -11,112 +11,107 @@ ## Overview -`delphi-clean` is a PowerShell utility designed for Delphi developers to help remove -build artifacts, intermediate files, and IDE-generated clutter. +`delphi-clean` is a PowerShell utility for Delphi developers to remove build +artifacts, intermediate files, and IDE-generated clutter, with support for +preview, validation, and CI workflows. --- -## Example Workflow +## Running the Tool + +If `delphi-clean` is on your PATH: ```powershell -# Preview cleanup -pwsh -File .\delphi-clean.ps1 -WhatIf +delphi-clean -Level standard +``` -# Perform cleanup -pwsh -File .\delphi-clean.ps1 +Otherwise, run it directly: -# CI usage -pwsh -File .\delphi-clean.ps1 -Level deep -Json +```powershell +pwsh -File .\delphi-clean.ps1 -Level standard +# or +powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\delphi-clean.ps1 -Level standard ``` +## PowerShell Compatibility + +Runs on the widely available Windows PowerShell 5.1 (`powershell.exe`) +and the newer PowerShell 7+ (`pwsh`). + +--- ## Features -- Three cleanup levels: `basic`, `standard`, `deep` -- CI-friendly output with optional JSON mode +- Specify cleanup levels: `basic`, `standard`, `deep` via `-Level` +- CI-friendly output with optional `-Json` mode - Optional structured output via `-PassThru` - Supports the `-WhatIf` dry-run mode - Add extra file patterns with `-IncludeFilePattern` - Exclude directories by wildcard pattern with `-ExcludeDirectoryPattern` - Send items to the recycle bin / trash instead of permanent deletion with `-RecycleBin` - ---- - -## PowerShell Compatibility - -Runs on the widely available Windows PowerShell 5.1 (powershell.exe) -and the newer PowerShell 7+ (pwsh). +- Use `-OutputLevel` to adjust how much detail is shown. +- Check for cleanup artifacts without modifying files using `-Check`. --- ## Usage -Run the script from its directory, or provide a path explicitly. +Run `delphi-clean` from the current working directory, +or specify a different root with `-RootPath` ### Basic Usage ```powershell -pwsh -File .\delphi-clean.ps1 +delphi-clean ``` -Defaults to: - -- Level: `basic` -- RootPath: current directory of the script - --- -### Specify Level +## -RootPath -```powershell -pwsh -File .\delphi-clean.ps1 -Level basic -pwsh -File .\delphi-clean.ps1 -Level standard -pwsh -File .\delphi-clean.ps1 -Level deep -``` - ---- +Root path defaults to the current working directory (via `Get-Location`), +or you can provide it explicitly. -### Specify Root Path +Example: ```powershell -pwsh -File .\delphi-clean.ps1 -RootPath C:\code\my-project +delphi-clean -RootPath C:\code\my-project ``` --- -### Dry Run (Recommended First) +## -WhatIf +Preview cleanup without making changes. (Dry Run Mode) ```powershell -pwsh -File .\delphi-clean.ps1 -WhatIf +delphi-clean -WhatIf ``` Shows what would be deleted without making changes. --- -### Verbose Output +## -Verbose + +Extra output for debugging purposes ```powershell -pwsh -File .\delphi-clean.ps1 -Verbose +delphi-clean -Verbose ``` --- -### PassThru (Structured Output) +## -PassThru + +Structured Output: Returns objects describing each item processed. ```powershell -pwsh -File .\delphi-clean.ps1 -PassThru +delphi-clean -PassThru ``` -Returns objects describing each item processed. - --- -### JSON Output (CI-friendly) - -```powershell -pwsh -File .\delphi-clean.ps1 -Json -``` +## -Json Outputs a JSON summary including: @@ -127,41 +122,40 @@ Outputs a JSON summary including: - Item-level details - `Disposition` (`Permanent` or `Recycle Bin`) and `RecycleBin` flag ---- - -### Include Extra File Patterns - ```powershell -pwsh -File .\delphi-clean.ps1 -Level basic -IncludeFilePattern '*.res' -pwsh -File .\delphi-clean.ps1 -Level basic -IncludeFilePattern '*.res','*.mab' +delphi-clean -Json ``` -Appends additional glob patterns to the level's built-in file list. Useful for -project-specific artifacts not covered by the standard levels. - --- -### Exclude Directory Patterns +## -IncludeFilePattern + +Appends additional glob patterns to the level's built-in file list. Useful for +project-specific artifacts not covered by the standard levels. ```powershell -pwsh -File .\delphi-clean.ps1 -ExcludeDirectoryPattern 'vendor*' -pwsh -File .\delphi-clean.ps1 -ExcludeDirectoryPattern 'vendor*','assets' +delphi-clean -Level basic -IncludeFilePattern '*.res' +delphi-clean -Level basic -IncludeFilePattern '*.res','*.mab' ``` +--- + +## -ExcludeDirectoryPattern + Skips any directory whose name matches one of the given wildcard patterns. Patterns are matched with `-like` so wildcards (`*`, `?`) are supported. Specific directories are excluded by default: `.git`, `.vs`, and `.claude`. ---- - -### Recycle Bin - ```powershell -pwsh -File .\delphi-clean.ps1 -RecycleBin -pwsh -File .\delphi-clean.ps1 -Level standard -RecycleBin +delphi-clean -ExcludeDirectoryPattern 'vendor*' +delphi-clean -ExcludeDirectoryPattern 'vendor*','assets' ``` +--- + +## -RecycleBin + Sends items to the platform trash instead of permanently deleting them. | Platform | Destination | @@ -173,13 +167,18 @@ Sends items to the platform trash instead of permanently deleting them. Combine with `-WhatIf` to preview which items would be recycled without making any changes. +```powershell +delphi-clean -RecycleBin +delphi-clean -Level standard -RecycleBin +``` + --- -## Clean Levels +## -Level -- see: [/docs/cleanup-levels.md](/docs/cleanup-levels.md) for breakdown of each level +### Clean Levels -### `basic` (default) +#### `basic` (default) Safe cleanup of common transient files. @@ -190,7 +189,7 @@ Includes: --- -### `standard` +#### `standard` Removes build outputs and generated artifacts. @@ -202,7 +201,7 @@ Includes everything in `basic`, plus: --- -### `deep` +#### `deep` More aggressive cleanup @@ -212,16 +211,129 @@ Includes everything in `standard`, plus: - FinalBuilder related files (logs, breakpoint, lock) - TestInsight custom settings +Examples: + +```powershell +delphi-clean -Level basic +delphi-clean -Level standard +delphi-clean -Level deep +``` + +See [/docs/cleanup-levels.md](/docs/cleanup-levels.md) for breakdown +of what is included by default in each level + + +--- + +## -OutputLevel + +Use `-OutputLevel` to control how much information `delphi-clean` writes +during execution or check mode. + +### Options + +- `detailed` (default) + + Shows full output including headers, per-item actions (or matches in -Check), and summary totals. + +- `summary` + + Suppresses per-item output and shows only high-level information and totals. + +- `quiet` + + Suppresses all normal output. Only warnings and errors are displayed. + Intended for automation scenarios where the exit code is the primary signal. + +### Behavior + +- Applies to both normal cleanup runs and `-Check` mode +- Does not affect JSON output (`-Json` always returns structured data) +- Warnings and errors are never suppressed, even with quiet + +### Examples + +Show full detail (default): + +```powershell +delphi-clean +``` + +Show only totals: + +```powershell +delphi-clean -OutputLevel summary +``` + +Silent run for CI: + +```powershell +delphi-clean -Level standard -OutputLevel quiet +``` + +### Notes + +- `detailed` is useful for interactive use and troubleshooting +- `summary` is ideal for large repositories to reduce noise +- `quiet` is best for scripts where only the exit code matters + + +## -Check + +Use `-Check` to audit a project folder without making any changes. + +In check mode, `delphi-clean` performs a full scan using the selected cleanup level +and reports any files or directories that would be removed during a normal run. + +No files are deleted, moved, or modified. + +```powershell +delphi-clean -Check +``` + +Output controlled by `-OutputLevel`: + +- detailed (default): lists matching items and summary +- summary: totals only +- quiet: no output (use exit code only) + +Example: use in CI to validate a workspace + +```powershell +delphi-clean -Check -OutputLevel quiet +if ($LASTEXITCODE -ne 0) { + throw "Repository contains build artifacts" +} +``` + +Example: view detailed list of matching files: + +```powershell +delphi-clean -Level deep -Check -OutputLevel detailed +``` + +Notes: + +- `-Check` cannot be combined with `-WhatIf` +- `-RecycleBin` has no effect in check mode +- For a simulated run that shows what would be deleted during execution, use `-WhatIf` + +Check vs WhatIf: +- Use `-WhatIf` to preview what a cleanup run would do +- Use `-Check` to determine whether cleanup is needed (with exit code support) + --- ## Exit Codes ```text - 0 = success: every matched item was removed (or WhatIf run, or nothing to clean) - 1 = fatal: unhandled exception before or during the scan phase - bad root path, - unsupported platform for -RecycleBin, scan error, etc. + 0 = success: cleanup completed, WhatIf completed, or Check found no artifacts + 1 = dirty: check mode found artifacts (validation failure) 2 = partial: the script reached the removal phase but at least one item could not - be deleted or recycled; successfully removed items are not rolled back + be deleted or recycled; + Note: successfully removed items are not rolled back + 3 = fatal: unhandled exception before or during the scan phase - bad root path, + unsupported platform for -RecycleBin, scan error, etc. ``` --- @@ -235,9 +347,11 @@ It will graduate to `stable` once: Until graduation, breaking changes may occur +--- + ## Continuous-Delphi This tool is part of the [Continuous-Delphi](https://github.com/continuous-delphi) -ecosystem, focused on improving engineering discipline for long-lived Delphi systems. +ecosystem, focused on strengthening Delphi’s continued success ![continuous-delphi logo](https://continuous-delphi.github.io/assets/logos/continuous-delphi-480x270.png) diff --git a/source/delphi-clean.ps1 b/source/delphi-clean.ps1 index 37b1f5c..44682c9 100644 --- a/source/delphi-clean.ps1 +++ b/source/delphi-clean.ps1 @@ -8,9 +8,11 @@ Cleans Delphi build artifacts from a repository tree using three cleanup levels. Targets the current working directory by default. Supports three cleanup levels: - basic - safe, low-risk cleanup of common transient files + basic - safe, low-risk cleanup of common transient files standard - removes build outputs and common generated files - deep - aggressive cleanup including user-local IDE state files + deep - aggressive cleanup including user-local IDE state files + +Use -Check to audit without deleting. Use -OutputLevel to control verbosity. .EXAMPLE powershell.exe -File .\delphi-clean.ps1 @@ -44,6 +46,15 @@ powershell.exe -File .\delphi-clean.ps1 -Version -Format json .EXAMPLE powershell.exe -File .\delphi-clean.ps1 -Level standard -RecycleBin + +.EXAMPLE +powershell.exe -File .\delphi-clean.ps1 -Level standard -Check + +.EXAMPLE +powershell.exe -File .\delphi-clean.ps1 -Level standard -Check -OutputLevel quiet + +.EXAMPLE +powershell.exe -File .\delphi-clean.ps1 -Level standard -OutputLevel summary #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Clean')] @@ -79,13 +90,28 @@ param( [switch]$Json, [Parameter(ParameterSetName = 'Clean')] - [switch]$RecycleBin + [switch]$RecycleBin, + + # Audit mode: scan only, never deletes. Exit 0 = nothing found, 2 = artifacts found. + # Cannot be combined with -WhatIf. + [Parameter(ParameterSetName = 'Clean')] + [switch]$Check, + + # Controls how much output is produced during a clean or check run. + # detailed - header, per-item lines, and summary (default) + # summary - header and summary only; per-item lines are suppressed + # quiet - no output at all; use the exit code as the signal + [Parameter(ParameterSetName = 'Clean')] + [ValidateSet('detailed', 'summary', 'quiet')] + [string]$OutputLevel = 'detailed' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -$script:ToolVersion = '0.8.0' +$script:ToolVersion = '0.9.0' + +$script:OutputLevel = $OutputLevel if ($Version) { if ($Format -eq 'json') { @@ -104,22 +130,54 @@ if ($Version) { exit 0 } -function Write-Section { - param( - [Parameter(Mandatory)] - [string]$Message - ) +# --------------------------------------------------------------------------- +# Output helpers +# --------------------------------------------------------------------------- - if ($Json) { - return +# Write-Detail : visible at 'detailed' only +# Write-Summary : visible at 'detailed' and 'summary' +# Write-Section : section banner, visible at 'detailed' only +# Write-SummarySection: section banner, visible at 'detailed' and 'summary' +# Warnings and errors are never suppressed by -OutputLevel. + +function Write-Detail { + param([AllowEmptyString()][Parameter(Mandatory)][string]$Message) + if ($script:OutputLevel -eq 'detailed' -and -not $Json) { + Write-Information $Message -InformationAction Continue } +} - Write-Information '' -InformationAction Continue - Write-Information ('=' * 70) -InformationAction Continue - Write-Information $Message -InformationAction Continue - Write-Information ('=' * 70) -InformationAction Continue +function Write-Summary { + param([AllowEmptyString()][Parameter(Mandatory)][string]$Message) + if ($script:OutputLevel -ne 'quiet' -and -not $Json) { + Write-Information $Message -InformationAction Continue + } } +function Write-Section { + param([AllowEmptyString()][Parameter(Mandatory)][string]$Message) + if ($script:OutputLevel -eq 'detailed' -and -not $Json) { + Write-Information '' -InformationAction Continue + Write-Information ('=' * 70) -InformationAction Continue + Write-Information $Message -InformationAction Continue + Write-Information ('=' * 70) -InformationAction Continue + } +} + +function Write-SummarySection { + param([AllowEmptyString()][Parameter(Mandatory)][string]$Message) + if ($script:OutputLevel -ne 'quiet' -and -not $Json) { + Write-Information '' -InformationAction Continue + Write-Information ('=' * 70) -InformationAction Continue + Write-Information $Message -InformationAction Continue + Write-Information ('=' * 70) -InformationAction Continue + } +} + +# --------------------------------------------------------------------------- +# Trash / recycle helpers +# --------------------------------------------------------------------------- + function Get-TrashDestination { param( [Parameter(Mandatory)] @@ -235,6 +293,10 @@ function Send-ToRecycleBin { } } +# --------------------------------------------------------------------------- +# Path helpers +# --------------------------------------------------------------------------- + function Get-RelativePathCompat { param( [Parameter(Mandatory)] @@ -291,7 +353,7 @@ function Test-SafeCleanRoot { $resolved = Resolve-Path -LiteralPath $fullRoot if (-not $resolved) { - throw "Invalid root path: $fullRoot" + throw "Invalid root path: $fullRoot" } } @@ -325,6 +387,10 @@ function Test-PathUnderExcludedDirectory { return $false } +# --------------------------------------------------------------------------- +# Level definitions +# --------------------------------------------------------------------------- + function Get-LevelDefinition { param( [Parameter(Mandatory)] @@ -426,11 +492,15 @@ function Get-LevelDefinition { $dirs = $dirs | Sort-Object -Unique return @{ - FilePatterns = $files + FilePatterns = $files DirectoryNames = $dirs } } +# --------------------------------------------------------------------------- +# Scan functions +# --------------------------------------------------------------------------- + function Get-FilesToDelete { param( [Parameter(Mandatory)] @@ -491,6 +561,10 @@ function Get-DirectoriesToDelete { Sort-Object -Property { $_.FullName.Length } -Descending } +# --------------------------------------------------------------------------- +# Deletion record +# --------------------------------------------------------------------------- + function ConvertTo-DeletionRecord { param( [Parameter(Mandatory)] @@ -511,6 +585,10 @@ function ConvertTo-DeletionRecord { } } +# --------------------------------------------------------------------------- +# Removal functions +# --------------------------------------------------------------------------- + function Remove-FileList { [CmdletBinding(SupportsShouldProcess = $true)] param( @@ -544,7 +622,7 @@ function Remove-FileList { Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Stop } $result.DeletedCount++ - Write-Information "$verb file: $($file.FullName)" -InformationAction Continue + Write-Detail "$verb file: $($file.FullName)" if ($ReturnRecords) { $result.Records.Add((ConvertTo-DeletionRecord -Type File -Path $file.FullName -Deleted $true)) @@ -560,7 +638,7 @@ function Remove-FileList { } } elseif ($WhatIfPreference) { - Write-Information "Would $($action.ToLower()): $($file.FullName)" -InformationAction Continue + Write-Detail "Would $($action.ToLower()): $($file.FullName)" if ($ReturnRecords) { $result.Records.Add((ConvertTo-DeletionRecord -Type File -Path $file.FullName -Deleted $false)) } @@ -616,7 +694,7 @@ function Remove-DirectoryList { } $result.DeletedCount++ - Write-Information "$verb directory: $($dir.FullName)" -InformationAction Continue + Write-Detail "$verb directory: $($dir.FullName)" if ($ReturnRecords) { $result.Records.Add((ConvertTo-DeletionRecord -Type Directory -Path $dir.FullName -Deleted $true)) @@ -632,7 +710,7 @@ function Remove-DirectoryList { } } elseif ($WhatIfPreference) { - Write-Information "Would $($action.ToLower()): $($dir.FullName)" -InformationAction Continue + Write-Detail "Would $($action.ToLower()): $($dir.FullName)" if ($ReturnRecords) { $result.Records.Add((ConvertTo-DeletionRecord -Type Directory -Path $dir.FullName -Deleted $false)) } @@ -642,71 +720,127 @@ function Remove-DirectoryList { return $result } +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + try { + # -Check cannot be combined with -WhatIf: they are both no-op scan modes + # but have different exit code semantics that cannot be meaningfully reconciled. + if ($Check -and $WhatIfPreference) { + Write-Error '-Check cannot be combined with -WhatIf. Use -Check with -OutputLevel instead.' + Write-Verbose 'Exit code = 3' + exit 3 + } + $cleanRoot = Resolve-CleanRoot -InputRoot $RootPath Test-SafeCleanRoot -Root $cleanRoot - $definition = Get-LevelDefinition -Name $Level - $mode = if ($WhatIfPreference) { 'WhatIf (no changes)' } else { 'Execute' } - $disposition = if ($RecycleBin) { 'Recycle Bin' } else { 'Permanent' } + $definition = Get-LevelDefinition -Name $Level + $mode = if ($Check) { 'Check (no changes)' } elseif ($WhatIfPreference) { 'WhatIf (no changes)' } else { 'Execute' } + $disposition = if ($RecycleBin) { 'Recycle Bin' } else { 'Permanent' } $returnRecords = ($PassThru -or $Json) $allFilePatterns = @($definition.FilePatterns) + @($IncludeFilePattern) | Sort-Object -Unique Write-Section 'Delphi Clean' - - if (-not $Json) { - Write-Information ('Level : {0}' -f $Level) -InformationAction Continue - Write-Information ('Root : {0}' -f $cleanRoot) -InformationAction Continue - Write-Information ('Excluded dirs : {0}' -f ($ExcludeDirectoryPattern -join ', ')) -InformationAction Continue - if ($IncludeFilePattern.Count -gt 0) { - Write-Information ('Extra patterns : {0}' -f ($IncludeFilePattern -join ', ')) -InformationAction Continue - } - Write-Information ('Mode : {0}' -f $mode) -InformationAction Continue - Write-Information ('Disposition : {0}' -f $disposition) -InformationAction Continue + Write-Detail ('Level : {0}' -f $Level) + Write-Detail ('Root : {0}' -f $cleanRoot) + Write-Detail ('Excluded dirs : {0}' -f ($ExcludeDirectoryPattern -join ', ')) + if ($IncludeFilePattern.Count -gt 0) { + Write-Detail ('Extra patterns : {0}' -f ($IncludeFilePattern -join ', ')) + } + Write-Detail ('Mode : {0}' -f $mode) + if (-not $Check) { + Write-Detail ('Disposition : {0}' -f $disposition) } - $filesToDelete = @(Get-FilesToDelete -Root $cleanRoot -Patterns $allFilePatterns -ExcludedDirPatterns $ExcludeDirectoryPattern) + $filesToDelete = @(Get-FilesToDelete -Root $cleanRoot -Patterns $allFilePatterns -ExcludedDirPatterns $ExcludeDirectoryPattern) $dirsToDelete = @(Get-DirectoriesToDelete -Root $cleanRoot -DirectoryNames $definition.DirectoryNames -ExcludedDirPatterns $ExcludeDirectoryPattern) - if (-not $Json) { - Write-Information '' -InformationAction Continue - Write-Information ('Files found : {0}' -f $filesToDelete.Count) -InformationAction Continue - Write-Information ('Directories found: {0}' -f $dirsToDelete.Count) -InformationAction Continue + if (-not $Check) { + Write-Detail '' + Write-Detail ('Files found : {0}' -f $filesToDelete.Count) + Write-Detail ('Directories found: {0}' -f $dirsToDelete.Count) } - if (($filesToDelete.Count -eq 0) -and ($dirsToDelete.Count -eq 0)) { + $nothingFound = ($filesToDelete.Count -eq 0) -and ($dirsToDelete.Count -eq 0) + if ($nothingFound) { if ($Json) { [PSCustomObject]@{ - Level = $Level - Root = $cleanRoot + Level = $Level + Root = $cleanRoot ExcludeDirectoryPattern = @($ExcludeDirectoryPattern) IncludeFilePattern = @($IncludeFilePattern) Mode = $mode Disposition = $disposition RecycleBin = $RecycleBin.IsPresent + Check = $Check.IsPresent FilesFound = 0 - DirectoriesFound = 0 - FilesDeleted = 0 - DirectoriesDeleted = 0 - FilesFailed = 0 - DirectoriesFailed = 0 - Items = @() + DirectoriesFound = 0 + FilesDeleted = 0 + DirectoriesDeleted = 0 + FilesFailed = 0 + DirectoriesFailed = 0 + Items = @() } | ConvertTo-Json -Depth 5 } else { - Write-Information '' -InformationAction Continue - Write-Information 'Nothing to clean.' -InformationAction Continue + Write-Summary '' + Write-Summary 'Nothing to clean.' } Write-Verbose 'Exit code = 0' exit 0 } + # -Check: report what was found and exit without deleting. + if ($Check) { + if ($Json) { + [PSCustomObject]@{ + Level = $Level + Root = $cleanRoot + ExcludeDirectoryPattern = @($ExcludeDirectoryPattern) + IncludeFilePattern = @($IncludeFilePattern) + Mode = $mode + Disposition = $disposition + RecycleBin = $RecycleBin.IsPresent + Check = $true + FilesFound = $filesToDelete.Count + DirectoriesFound = $dirsToDelete.Count + FilesDeleted = 0 + DirectoriesDeleted = 0 + FilesFailed = 0 + DirectoriesFailed = 0 + Items = @( + @($filesToDelete | ForEach-Object { ConvertTo-DeletionRecord -Type File -Path $_.FullName -Deleted $false }) + + @($dirsToDelete | ForEach-Object { ConvertTo-DeletionRecord -Type Directory -Path $_.FullName -Deleted $false }) + ) + } | ConvertTo-Json -Depth 5 + } + else { + Write-Section 'Artifacts found' + foreach ($file in $filesToDelete) { + Write-Detail " File : $($file.FullName)" + } + foreach ($dir in $dirsToDelete) { + Write-Detail " Directory : $($dir.FullName)" + } + + Write-SummarySection 'Check summary' + Write-Summary ('Files found : {0}' -f $filesToDelete.Count) + Write-Summary ('Directories found: {0}' -f $dirsToDelete.Count) + } + + Write-Verbose 'Exit code = 1' + exit 1 + } + + # Normal clean path Write-Section 'Cleaning' - $fileRemovalResult = Remove-FileList -Files $filesToDelete -ReturnRecords:$returnRecords -RecycleBin:$RecycleBin - $dirRemovalResult = Remove-DirectoryList -Directories $dirsToDelete -ReturnRecords:$returnRecords -RecycleBin:$RecycleBin + $fileRemovalResult = Remove-FileList -Files $filesToDelete -ReturnRecords:$returnRecords -RecycleBin:$RecycleBin + $dirRemovalResult = Remove-DirectoryList -Directories $dirsToDelete -ReturnRecords:$returnRecords -RecycleBin:$RecycleBin $allRecords = New-Object System.Collections.Generic.List[object] $allRecords.AddRange([object[]]$fileRemovalResult.Records) @@ -716,33 +850,34 @@ try { if ($Json) { [PSCustomObject]@{ - Level = $Level - Root = $cleanRoot + Level = $Level + Root = $cleanRoot ExcludeDirectoryPattern = @($ExcludeDirectoryPattern) IncludeFilePattern = @($IncludeFilePattern) Mode = $mode - Disposition = $disposition - RecycleBin = $RecycleBin.IsPresent - FilesFound = $filesToDelete.Count - DirectoriesFound = $dirsToDelete.Count - FilesDeleted = $fileRemovalResult.DeletedCount - DirectoriesDeleted = $dirRemovalResult.DeletedCount - FilesFailed = $fileRemovalResult.FailedCount - DirectoriesFailed = $dirRemovalResult.FailedCount - Items = $allRecords + Disposition = $disposition + RecycleBin = $RecycleBin.IsPresent + Check = $false + FilesFound = $filesToDelete.Count + DirectoriesFound = $dirsToDelete.Count + FilesDeleted = $fileRemovalResult.DeletedCount + DirectoriesDeleted = $dirRemovalResult.DeletedCount + FilesFailed = $fileRemovalResult.FailedCount + DirectoriesFailed = $dirRemovalResult.FailedCount + Items = $allRecords } | ConvertTo-Json -Depth 5 } else { $removedLabel = if ($RecycleBin) { 'recycled' } else { 'deleted' } - Write-Section 'Summary' + Write-SummarySection 'Summary' if ($WhatIfPreference) { - Write-Information ('Files would be {0} : {1}' -f $removedLabel, $filesToDelete.Count) -InformationAction Continue - Write-Information ('Directories would be {0}: {1}' -f $removedLabel, $dirsToDelete.Count) -InformationAction Continue + Write-Summary ('Files would be {0} : {1}' -f $removedLabel, $filesToDelete.Count) + Write-Summary ('Directories would be {0}: {1}' -f $removedLabel, $dirsToDelete.Count) } else { - Write-Information ('Files {0} : {1}' -f $removedLabel, $fileRemovalResult.DeletedCount) -InformationAction Continue - Write-Information ('Directories {0} : {1}' -f $removedLabel, $dirRemovalResult.DeletedCount) -InformationAction Continue + Write-Summary ('Files {0} : {1}' -f $removedLabel, $fileRemovalResult.DeletedCount) + Write-Summary ('Directories {0} : {1}' -f $removedLabel, $dirRemovalResult.DeletedCount) if ($totalFailed -gt 0) { Write-Warning ('Items failed to {0}: {1}' -f $removedLabel, $totalFailed) @@ -754,15 +889,18 @@ try { $allRecords } - # Exit codes: - # 0 = success: every matched item was removed (or WhatIf run, or nothing to clean) - # 1 = fatal: unhandled exception before or during the scan phase - bad root path, - # unsupported platform for -RecycleBin, scan error, etc. (catch block below) - # 2 = partial: the script reached the removal phase but at least one item could not - # be deleted or recycled; successfully removed items are not rolled back + # Exit code contract: + # 0 = success: + # normal mode - every matched item was removed + # -WhatIf - dry run completed (nothing removed by design) + # -Check - scan completed, no artifacts found + # any mode - nothing to clean found during scan + # 1 = dirty (check mode.) [Validation failures] + # 2 = Error deleting a file or directory. [Cleanup failures] + # 3 = Fatal. Invalid usage / exceptions (root path, invalid platform) if ($totalFailed -gt 0) { - Write-Verbose 'Exit code = 2' - exit 2 + Write-Verbose 'Exit code = 2' + exit 2 } Write-Verbose 'Exit code = 0' @@ -770,6 +908,6 @@ try { } catch { Write-Error -ErrorRecord $_ - Write-Verbose 'Exit code = 1' - exit 1 + Write-Verbose 'Exit code = 3' + exit 3 }