Last active
April 10, 2025 00:01
-
-
Save rkeithhill/4099bfd8420eed0e6dbc to your computer and use it in GitHub Desktop.
Removes duplicate and optionally short commands from your PSReadline history file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
Optimizes your PSReadline history save file. | |
.DESCRIPTION | |
Optimizes your PSReadline history save file by removing duplicate | |
entries and optionally removing commands that are not longer than | |
a minimum length | |
.EXAMPLE | |
C:\PS> Optimize-PSReadlineHistory | |
Removes all the duplicate commands. | |
.EXAMPLE | |
C:\PS> Optimize-PSReadlineHistory -MinimumCommandLength 3 | |
Removes all the duplicate commands and any commands less than 3 characters in length. | |
.NOTES | |
May 15, 2017 - fix bug in handling of multiline commands. | |
#> | |
[CmdletBinding(SupportsShouldProcess = $true)] | |
param( | |
# Path to the PSReadline history file to optimize. | |
[Parameter()] | |
[string] | |
$HistoryPath, | |
# If specified, any commands less than $MinimumCommandLength will be removed from the history file. | |
[Parameter()] | |
[int] | |
$MinimumCommandLength = 1, | |
# If specified, removes leading whitespace from the beginning of the command or the beginning of | |
# the first line of multiline commands. | |
[Parameter()] | |
[switch] | |
$TrimLeadingWhitespace, | |
# If specified, the check for other PowerShell processes is skipped. You can do this when you are operating on a | |
# copy of PSReadline history file. | |
[Parameter()] | |
[switch] | |
$SkipRunningPowerShellCheck | |
) | |
if (!$SkipRunningPowerShellCheck -and ((Get-PSHostProcessInfo | Where-Object ProcessId -ne $pid).Count -gt 0)) { | |
throw "This command can only be run when other PowerShell hosts are not running. Other hosts may have PSReadline loaded." | |
} | |
if (!$HistoryPath) { | |
if (Get-Module PSReadline -ErrorAction SilentlyContinue) { | |
$HistoryPath = (Get-PSReadlineOption).HistorySavePath | |
} | |
else { | |
throw "You must provide a value for the HistoryPath parameter." | |
} | |
Remove-Module PSReadline | |
if (Get-Module PSReadline -ErrorAction SilentlyContinue) { | |
throw "Failed to remove the PSReadline module. This command can only be run when PSReadline is not loaded." | |
} | |
} | |
if (![System.IO.Path]::IsPathRooted($HistoryPath)) { | |
$HistoryPath = Convert-Path $HistoryPath | |
} | |
$history = Get-Content -LiteralPath $HistoryPath -Encoding UTF8 | |
$origFileSize = (Get-Item -LiteralPath $HistoryPath).Length | |
$strBld = New-Object System.Text.StringBuilder | |
$commands = New-Object System.Collections.Generic.List[string] -ArgumentList $history.Length | |
$uniqCommands = New-Object System.Collections.Generic.List[string] -ArgumentList $history.Length | |
$comparer = if ($IsLinux) { [System.StringComparer]::Ordinal } else { [System.StringComparer]::OrdinalIgnoreCase } | |
$uniqCommandSet = New-Object System.Collections.Generic.HashSet[string] -ArgumentList $comparer | |
$numCommands = 0 | |
$numMinLengthCommandsRemoved = 0 | |
$numMultilineCommands = 0 | |
$whatIfMsg = if ($PSBoundParameters['WhatIf']) { 'WHAT IF: ' } else { '' } | |
$activityMsg = "${whatIfMsg}Optimizing $HistoryPath" | |
# Process multiline commands in the history file contents | |
$Ten | |
for ($i = 0; $i -lt $history.Count; $i++) { | |
$percentComplete = [int](33 * (($i + 1) / $history.Count)) | |
if ($percentComplete % 10 -eq 0) { | |
Write-Progress -Activity $activityMsg -Status "Processing multiline commands" -PercentComplete $percentComplete | |
} | |
$line = $history[$i].TrimEnd() | |
if ($line[-1] -eq '`') { | |
$null = $strBld.Append($line + [System.Environment]::NewLine) | |
} | |
else { | |
$numCommands++ | |
if ($strBld.Length -gt 0) { | |
$null = $strBld.Append($line) | |
$commandStr = $strBld.ToString() | |
$null = $strBld.Clear() | |
$numMultilineCommands++ | |
} | |
else { | |
$commandStr = $line | |
} | |
# Trim leading whitesapce if requested | |
if ($TrimLeadingWhitespace) { | |
$commandStr = $commandStr.TrimStart() | |
} | |
# This is where we filter out commands that are less than the specified minimum length | |
if ($commandStr.Length -ge $MinimumCommandLength) { | |
$null = $commands.Add($commandStr) | |
} | |
else { | |
$numMinLengthCommandsRemoved++ | |
} | |
} | |
} | |
# Walk the history file backwards so we preserve the most recent duplicate command | |
for ($i = $commands.Count - 1; $i -ge 0 ; $i--) { | |
$percentComplete = [int](33 + (33 * ($history.Count - 1 - $i) / $history.Count)) | |
if ($percentComplete % 10 -eq 0) { | |
Write-Progress -Activity $activityMsg -Status "Removing duplicate commands" -PercentComplete $percentComplete | |
} | |
# This is where we check for a duplicate command | |
$commandStr = $commands[$i] | |
if (!$uniqCommandSet.Contains($commandStr)) { | |
$null = $uniqCommandSet.Add($commandStr) | |
$null = $uniqCommands.Add($commandStr) | |
} | |
} | |
$uniqCommandSet = $null | |
$numUniqCommands = $uniqCommands.Count | |
if ($PSCmdlet.ShouldProcess($HistoryPath, "Optimize")) { | |
Copy-Item -LiteralPath $HistoryPath "${HistoryPath}.bak" | |
Remove-Item -LiteralPath $HistoryPath | |
$utf8NoBom = [System.Text.UTF8Encoding]::new($false, $true) | |
$writer = [System.IO.StreamWriter]::new($HistoryPath, $false, $utf8NoBom) | |
try { | |
for ($i = $uniqCommands.Count - 1; $i -ge 0 ; $i--) { | |
$percentComplete = [int](66 + (34 * ($uniqCommands.Count - 1 - $i) / $uniqCommands.Count)) | |
if ($percentComplete % 25 -eq 0) { | |
Write-Progress -Activity $activityMsg -Status "Saving optimized history" -PercentComplete $percentComplete | |
} | |
$line = $uniqCommands[$i] | |
$writer.WriteLine($line) | |
} | |
} | |
finally { | |
if ($writer) { $writer.Dispose() } | |
} | |
$newFileSize = (Get-Item -LiteralPath $HistoryPath).Length | |
} | |
else { | |
# Estimate the resulting file size for -WhatIf | |
$newFileSize = 0 | |
foreach ($command in $uniqCommands) { | |
$newFileSize += $command.Length + [System.Environment]::NewLine.Length | |
} | |
} | |
$strBld = $commands = $uniqCommands = $null | |
Write-Host "Removed $($numCommands - $numUniqCommands) duplicate commands." | |
if ($MinimumCommandLength -gt 0) { | |
Write-Host "Removed $numMinLengthCommandsRemoved commands with less than $MinimumCommandLength characters." | |
} | |
Write-Host "Number of commands reduced from $numCommands to $numUniqCommands." | |
Write-Host "Number of multiline commands $numMultilineCommands." | |
Write-Host ("History file size reduced from {0:F1} KB to {1:F1} KB." -f ($origFileSize / 1KB), ($newFileSize / 1KB)) | |
Write-Progress -Activity $activityMsg -Completed |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment