#Requires -Version 7.0 [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Directory, # Treat the first line as a header and keep it in place while sorting the # remaining rows. Set to $false for headerless files. [bool] $HasHeader = $true ) if (-not (Test-Path -LiteralPath $Directory -PathType Container)) { throw "Directory not found: $Directory" } # Matches a trailing "_<14 digits>" immediately before a mandatory file extension. $timestampPattern = '^(?.*)_\d{14}(?\.[^.\\/]+)$' $utf8NoBom = [System.Text.UTF8Encoding]::new($false) # $false => no BOM $csvFiles = Get-ChildItem -LiteralPath $Directory -Filter "*.csv" -File if ($csvFiles.Count -eq 0) { Write-Warning "No .csv files found in: $Directory" return } foreach ($file in $csvFiles) { $fileName = $file.Name $match = [regex]::Match($fileName, $timestampPattern) if (-not $match.Success) { Write-Warning "No timestamp pattern found, skipping: $fileName" continue } $newName = $match.Groups['stem'].Value + $match.Groups['ext'].Value $newPath = Join-Path $file.DirectoryName $newName if ($newPath -ieq $file.FullName) { Write-Warning "Target name equals source, skipping: $fileName" continue } # Read as an array of lines. Get-Content splits on the file's line breaks; # this normalises the output to the platform newline on write-out. $lines = @(Get-Content -LiteralPath $file.FullName) if ($lines.Count -eq 0) { Write-Warning "File is empty, writing as-is: $fileName" $sorted = $lines } elseif ($HasHeader) { # Keep line 0 (header) fixed, sort the rest ordinally. $header = $lines[0] $dataLines = if ($lines.Count -gt 1) { $lines[1..($lines.Count - 1)] } else { @() } $sorted = @($header) + @($dataLines | Sort-Object -CaseSensitive) } else { $sorted = @($lines | Sort-Object -CaseSensitive) } # Join with LF and terminate with a trailing newline. $content = ($sorted -join "`n") + "`n" [System.IO.File]::WriteAllText($newPath, $content, $utf8NoBom) Write-Output "Wrote: $newPath ($($sorted.Count) lines)" }