param( [Parameter(Position=0)][string]$InputPath, [Parameter(Position=1)][string]$OutputPath, [switch]$Execute ) # kb_sanitize_hashtable_v1.0.ps1 # Minimal, PS5.1-safe sanitizer for text files (eg. hashtable-like). # - Normalizes common Unicode punctuation to ASCII # - Trims trailing spaces # - Ensures CRLF line endings # If OutputPath is empty, will write back to InputPath only when -Execute is provided. $ErrorActionPreference = "Stop" function Normalize-Ascii([string]$s){ if ([string]::IsNullOrEmpty($s)) { return "" } $t = $s # Replace curly quotes and dashes $t = $t -replace "[\u2018\u2019\u201A\u201B]", "'" # single quotes $t = $t -replace "[\u201C\u201D\u201E\u201F]", '"' # double quotes $t = $t -replace "[\u2013\u2014\u2212]", "-" # en/em/minus $t = $t -replace "\u00A0", " " # nbsp -> space $t = $t -replace "\u2026", "..." # ellipsis $t = $t -replace "[\u2000-\u200F\u202A-\u202E]", "" # bidi/zwsp remove # Strip non-ASCII control except CR/LF/TAB $chars = New-Object System.Text.StringBuilder foreach($ch in $t.ToCharArray()){ $code = [int][char]$ch if ( ($code -eq 9) -or ($code -eq 10) -or ($code -eq 13) -or ($code -ge 32 -and $code -le 126) ){ [void]$chars.Append($ch) } else { # drop or replace with '?' [void]$chars.Append('?') } } return $chars.ToString() } if ([string]::IsNullOrWhiteSpace($InputPath)){ Write-Host "[INFO] No input path provided. Nothing to do." exit 0 } if (-not (Test-Path -LiteralPath $InputPath)){ Write-Error ("Input not found: {0}" -f $InputPath) exit 1 } $raw = Get-Content -LiteralPath $InputPath -Raw # Normalize line endings to CRLF and trimmed lines $norm = Normalize-Ascii $raw $norm = ($norm -split "`r?`n") | ForEach-Object { $_.TrimEnd() } | ForEach-Object { $_ + "`r" } | Out-String # Out-String adds a trailing newline; enforce CRLF $norm = $norm -replace "`r`n`r`n$", "`r`n" $target = $OutputPath if ([string]::IsNullOrWhiteSpace($target)) { $target = $InputPath } if (-not $Execute){ Write-Host ("[PREVIEW] Sanitized length = {0} bytes (no write)" -f ([Text.Encoding]::UTF8.GetByteCount($norm))) exit 0 } # Atomic write: write to .tmp then move $dir = Split-Path -LiteralPath $target -Parent if (-not (Test-Path -LiteralPath $dir)){ New-Item -ItemType Directory -Path $dir | Out-Null } $tmp = "$target.tmp" # Write UTF-8 with BOM $bytes = [Text.Encoding]::UTF8.GetPreamble() + [Text.Encoding]::UTF8.GetBytes($norm) [IO.File]::WriteAllBytes($tmp, $bytes) if (Test-Path -LiteralPath $target){ Remove-Item -LiteralPath $target -Force } Move-Item -LiteralPath $tmp -Destination $target -Force Write-Host ("[OK] Written: {0}" -f $target) exit 0