Public/Add-SafePath.ps1
|
function Add-SafePath { <# .SYNOPSIS Adds a new path entry safely, with validation and optimal positioning. .DESCRIPTION Adds a path to the PATH environment variable after verifying: - The path exists on the filesystem - The path is not already in PATH - Inserts at the optimal position based on priority category .PARAMETER Path The path to add to the PATH environment variable. .PARAMETER Target Which PATH to modify: User or Machine. Default is User. .PARAMETER Force Skips confirmation prompt. .PARAMETER WhatIf Shows what would happen without making changes. .EXAMPLE Add-SafePath -Path "C:\MyApp\bin" .EXAMPLE Add-SafePath -Path "C:\Tools" -Target Machine .EXAMPLE Add-SafePath -Path "C:\NewTool\bin" -WhatIf #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, Position = 0)] [string]$Path, [ValidateSet('User', 'Machine')] [string]$Target = 'User', [switch]$Force ) # Normalize the path $Path = $Path.TrimEnd('\') # 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 } } Write-Host Write-Host " Add Path to [$Target] PATH" -ForegroundColor Cyan Write-Host " ═══════════════════════════════════════" -ForegroundColor DarkGray Write-Host Write-Host " Path to add: " -NoNewline -ForegroundColor White Write-Host $Path -ForegroundColor Yellow Write-Host # Security Check: Validate path for security issues $securityCheck = Test-PathSecurity -Path $Path if (-not $securityCheck.IsValid) { Write-Host " ✗ SECURITY: Path failed validation" -ForegroundColor Red foreach ($issue in $securityCheck.Issues) { Write-Host " ⚠ $issue" -ForegroundColor Red } Write-Host return [PSCustomObject]@{ Success = $false Reason = 'Security validation failed' SecurityIssues = $securityCheck.Issues Severity = $securityCheck.Severity Path = $Path } } elseif ($securityCheck.Issues.Count -gt 0) { # Has warnings but not critical Write-Host " ⚠ Security warnings:" -ForegroundColor Yellow foreach ($issue in $securityCheck.Issues) { Write-Host " • $issue" -ForegroundColor Yellow } Write-Host } else { Write-Host " ✓ Security validation passed" -ForegroundColor Green } # Check 1: Does the path exist? $expandedPath = [Environment]::ExpandEnvironmentVariables($Path) if (-not (Test-Path -LiteralPath $expandedPath -PathType Container)) { Write-Host " ✗ Path does not exist: $expandedPath" -ForegroundColor Red Write-Host return [PSCustomObject]@{ Success = $false Reason = 'Path does not exist' Path = $Path } } Write-Host " ✓ Path exists" -ForegroundColor Green # Check 2: Is it already in PATH? $currentPath = [Environment]::GetEnvironmentVariable('PATH', $Target) $currentPaths = $currentPath -split ';' | Where-Object { $_ -ne '' } $normalizedNew = $expandedPath.TrimEnd('\').ToLowerInvariant() $alreadyExists = $false foreach ($existing in $currentPaths) { $normalizedExisting = [Environment]::ExpandEnvironmentVariables($existing).TrimEnd('\').ToLowerInvariant() if ($normalizedExisting -eq $normalizedNew) { $alreadyExists = $true Write-Host " ✗ Path already exists in $Target PATH" -ForegroundColor Red Write-Host " Existing entry: $existing" -ForegroundColor DarkGray Write-Host return [PSCustomObject]@{ Success = $false Reason = 'Path already exists' Path = $Path ExistingEntry = $existing } } } Write-Host " ✓ Path is not a duplicate" -ForegroundColor Green # Check 3: If targeting User, check if path already exists in Machine PATH (redundant) if ($Target -eq 'User') { $machinePath = [Environment]::GetEnvironmentVariable('PATH', 'Machine') $machinePaths = $machinePath -split ';' | Where-Object { $_ -ne '' } foreach ($existing in $machinePaths) { $normalizedExisting = [Environment]::ExpandEnvironmentVariables($existing).TrimEnd('\').ToLowerInvariant() if ($normalizedExisting -eq $normalizedNew) { Write-Host " ✗ Path already exists in Machine PATH (would be redundant)" -ForegroundColor Red Write-Host " Machine PATH entry: $existing" -ForegroundColor DarkGray Write-Host " Adding it to User PATH is unnecessary since Machine PATH is always loaded." -ForegroundColor Yellow Write-Host return [PSCustomObject]@{ Success = $false Reason = 'Path exists in Machine PATH (redundant)' Path = $Path MachinePathEntry = $existing } } } Write-Host " ✓ Path is not redundant (not in Machine PATH)" -ForegroundColor Green } # Determine priority category $priority = Get-PathPriority -Path $Path Write-Host " ✓ Category detected: $($priority.Category) (Priority: $($priority.Priority))" -ForegroundColor Green Write-Host # Find optimal insertion position $insertPosition = 0 for ($i = 0; $i -lt $currentPaths.Count; $i++) { $existingPriority = Get-PathPriority -Path $currentPaths[$i] if ($existingPriority.Priority -gt $priority.Priority) { $insertPosition = $i break } $insertPosition = $i + 1 } # Build new PATH $newPaths = [System.Collections.ArrayList]::new() $newPaths.AddRange($currentPaths) $newPaths.Insert($insertPosition, $Path) $newPath = $newPaths -join ';' # Show what will happen Write-Host " Insertion position: $($insertPosition + 1) of $($currentPaths.Count + 1)" -ForegroundColor White if ($insertPosition -gt 0) { Write-Host " After: $($currentPaths[$insertPosition - 1])" -ForegroundColor DarkGray } if ($insertPosition -lt $currentPaths.Count) { Write-Host " Before: $($currentPaths[$insertPosition])" -ForegroundColor DarkGray } Write-Host # WhatIf handling if ($WhatIfPreference) { Write-Host " [WhatIf] Would add path to $Target PATH at position $($insertPosition + 1)" -ForegroundColor Magenta return [PSCustomObject]@{ Success = $true WhatIf = $true Path = $Path Position = $insertPosition + 1 Category = $priority.Category } } # Confirmation if (-not $Force) { $confirm = Read-Host " Add this path? (Y/N)" if ($confirm -ne 'Y' -and $confirm -ne 'y') { Write-Host " Operation cancelled." -ForegroundColor Gray return [PSCustomObject]@{ Success = $false Cancelled = $true } } } # Create backup $backup = Backup-Path -Target $Target Write-Host " ✓ Backup created: $($backup.BackupFile)" -ForegroundColor Green # Apply the change try { [Environment]::SetEnvironmentVariable('PATH', $newPath, $Target) Write-Host Write-Host " ✓ Path added successfully!" -ForegroundColor Green Write-Host " Note: Restart your terminal for changes to take effect." -ForegroundColor DarkGray Write-Host return [PSCustomObject]@{ Success = $true Path = $Path Target = $Target Position = $insertPosition + 1 Category = $priority.Category BackupFile = $backup.BackupFile } } catch { Write-Error "Failed to update PATH: $_" return [PSCustomObject]@{ Success = $false Error = $_.Exception.Message } } } |