Public/Set-CleanPath.ps1
|
function Set-CleanPath { <# .SYNOPSIS Applies a cleaned PATH by removing duplicates and obsolete entries. .DESCRIPTION Removes duplicate and non-existent paths from the specified PATH environment variable. Optionally reorders entries by priority. Creates a backup before making changes. When targeting User PATH with -RemoveCrossScopeDuplicates, entries that already exist in Machine PATH will be removed since they are redundant (Windows loads Machine PATH before User PATH). .PARAMETER Target Which PATH to clean: User or Machine. Default is User. .PARAMETER OptimizeOrder If specified, also reorders PATH entries by priority category after cleaning. Priority order: System Critical > PowerShell > Dev Tools > Languages > Apps .PARAMETER RemoveDuplicates If specified, removes duplicate entries. Default is true. .PARAMETER RemoveObsolete If specified, removes paths that don't exist. Default is true. .PARAMETER RemoveCrossScopeDuplicates If specified and Target is User, removes User PATH entries that already exist in Machine PATH (since they are redundant). Has no effect when Target is Machine. .PARAMETER WhatIf Shows what changes would be made without applying them. .PARAMETER Force Skips confirmation prompt. .EXAMPLE Set-CleanPath -Target User -WhatIf .EXAMPLE Set-CleanPath -Target User -OptimizeOrder -RemoveCrossScopeDuplicates .EXAMPLE Set-CleanPath -Target Machine -Force #> [CmdletBinding(SupportsShouldProcess)] param( [ValidateSet('User', 'Machine')] [string]$Target = 'User', [switch]$OptimizeOrder, [switch]$RemoveDuplicates = $true, [switch]$RemoveObsolete = $true, [switch]$RemoveCrossScopeDuplicates, [switch]$Force ) # Check for admin rights if targeting Machine if ($Target -eq 'Machine') { $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { Write-Error "Administrator privileges required to modify the Machine PATH. Please run PowerShell as Administrator." return } } # Get current analysis $analysis = Get-PathAnalysis -Target $Target $currentPath = [Environment]::GetEnvironmentVariable('PATH', $Target) # Get cross-scope duplicates if needed (only for User target) $crossScopePaths = @{} if ($RemoveCrossScopeDuplicates -and $Target -eq 'User') { $allEntries = Get-PathEntries -Target Both $crossScopeDups = Find-CrossScopeDuplicates -UserPaths $allEntries.User -MachinePaths $allEntries.Machine foreach ($dup in $crossScopeDups) { $crossScopePaths[$dup.ExpandedPath] = $dup.MachinePath } } # Filter paths to keep $pathsToKeep = @() $pathsToRemove = @() $seenPaths = @{} foreach ($entry in $analysis) { $normalizedPath = $entry.ExpandedPath.TrimEnd('\').ToLowerInvariant() $shouldKeep = $true $reason = '' # Check for duplicates if ($RemoveDuplicates -and $seenPaths.ContainsKey($normalizedPath)) { $shouldKeep = $false $reason = 'Duplicate' } else { $seenPaths[$normalizedPath] = $true } # Check for obsolete if ($RemoveObsolete -and -not $entry.Exists) { $shouldKeep = $false $reason = 'Does not exist' } # Check for cross-scope duplicates (User paths that exist in Machine) if ($RemoveCrossScopeDuplicates -and $Target -eq 'User' -and $crossScopePaths.ContainsKey($normalizedPath)) { $shouldKeep = $false $reason = 'Redundant (exists in Machine PATH)' } if ($shouldKeep) { $pathsToKeep += $entry.Path } else { $pathsToRemove += @{ Path = $entry.Path Reason = $reason } } } # Apply order optimization if requested if ($OptimizeOrder -and $pathsToKeep.Count -gt 0) { Write-Host " 🔄 Optimizing path order..." -ForegroundColor DarkCyan $pathsToKeep = Get-OptimizedPathOrder -Paths $pathsToKeep } # Build new PATH $newPath = $pathsToKeep -join ';' $savedChars = $currentPath.Length - $newPath.Length # Display summary Write-Host Write-Host " PATH Cleanup Summary for [$Target]" -ForegroundColor Cyan Write-Host " ═══════════════════════════════════════" -ForegroundColor DarkGray Write-Host Write-Host " Current entries : $($analysis.Count)" -ForegroundColor White Write-Host " Entries to keep : $($pathsToKeep.Count)" -ForegroundColor Green Write-Host " Entries to remove: $($pathsToRemove.Count)" -ForegroundColor Yellow Write-Host Write-Host " Current length : $($currentPath.Length) characters" -ForegroundColor White Write-Host " New length : $($newPath.Length) characters" -ForegroundColor Green Write-Host " Characters saved: $savedChars characters" -ForegroundColor Cyan Write-Host if ($pathsToRemove.Count -gt 0) { Write-Host " Entries to be removed:" -ForegroundColor Yellow foreach ($item in $pathsToRemove) { Write-Host " ✗ $($item.Path) [$($item.Reason)]" -ForegroundColor DarkYellow } Write-Host } # Apply changes if ($WhatIfPreference) { Write-Host " [WhatIf] Would apply the cleaned PATH to $Target scope." -ForegroundColor Magenta return } if (-not $Force) { $confirm = Read-Host " Apply changes? (Y/N)" if ($confirm -ne 'Y' -and $confirm -ne 'y') { Write-Host " Operation cancelled." -ForegroundColor Gray return } } # Create backup first $backupResult = Backup-Path -Target $Target Write-Host " Backup created: $($backupResult.BackupFile)" -ForegroundColor Green # Apply the new PATH try { [Environment]::SetEnvironmentVariable('PATH', $newPath, $Target) Write-Host Write-Host " ✓ PATH successfully updated!" -ForegroundColor Green Write-Host " Note: You may need to restart your terminal for changes to take effect." -ForegroundColor DarkGray return [PSCustomObject]@{ Success = $true Target = $Target EntriesRemoved = $pathsToRemove.Count CharactersSaved = $savedChars BackupFile = $backupResult.BackupFile NewPath = $newPath } } catch { Write-Error "Failed to update PATH: $_" return [PSCustomObject]@{ Success = $false Error = $_.Exception.Message } } } |