LocationHistory.psm1
<# LocationHistory.psm1 - Written by Bill Stewart (bstewart AT iname.com) Special thanks to Keith Hill for Pscx.CD.psm1 - this module uses the same basic technique (i.e., two list objects to maintain the location history). See README.md for module details. #> #requires -version 5.1 # Imports <lang>\Messages.psd1 as $Messages hashtable Import-LocalizedData Messages -FileName Messages # Remember no more than this many locations. $MAX_HISTORY_SIZE = 100 # Class definition for LocationHistoryHistoryEntry object. class LocationHistoryHistoryEntry { [String] $Current [Int] $Id [String] $Location LocationHistoryHistoryEntry([Boolean] $Current,[Int] $Id,[String] $Location) { $this.Current = ($null,'=>')[$Current] $this.Id = $Id $this.Location = $Location } } # Module-level global variables store the location stacks $BackwardStack = New-Object Collections.Generic.List[String] $ForwardStack = New-Object Collections.Generic.List[String] # Clear location history. function Clear-LocationHistory { [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] param() if ( $PSCmdlet.ShouldProcess($Messages.ClearLocationHistoryTarget,$Messages.ClearLocationHistoryAction) ) { $ForwardStack.Clear() $BackwardStack.Clear() } } # Output location history. function Get-LocationHistory { if ( $BackwardStack.Count -ge 0 ) { for ( $i = 0; $i -lt $BackwardStack.Count; $i++ ) { New-Object -TypeName LocationHistoryHistoryEntry $false,$i,$BackwardStack[$i] } } $ndx = $BackwardStack.Count New-Object -TypeName LocationHistoryHistoryEntry $true,$ndx,$ExecutionContext.SessionState.Path.CurrentLocation.Path if ( $ForwardStack.Count -ge 0 ) { $ndx++ for ( $i = 0; $i -lt $ForwardStack.Count; $i++ ) { New-Object -TypeName LocationHistoryHistoryEntry $false,($ndx + $i),$ForwardStack[$i] } } } # Remove location from location history. function Remove-LocationHistory { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory,ValueFromPipeline)] [Int] $Id ) if ( $PSCmdlet.ShouldProcess(($Messages.RemoveLocationHistoryTarget -f $Id),$Messages.RemoveLocationHistoryAction) ) { if ( ($Id -lt 0) -or ($Id -gt $MAX_HISTORY_SIZE - 1) ) { Write-Warning ($Messages.IdOutOfRange -f ($MAX_HISTORY_SIZE - 1)) return } if ( $Id -eq $BackwardStack.Count ) { Write-Warning $Messages.CannotRemoveCurrentLocation return } if ( $Id -lt $BackwardStack.Count ) { $BackwardStack.RemoveAt($Id) } elseif ( ($Id -gt $BackwardStack.Count) -and ($Id -lt ($BackwardStack.Count + 1 + $ForwardStack.Count)) ) { $ndx = $Id - ($BackwardStack.Count + 1) $ForwardStack.RemoveAt($ndx) } else { Write-Warning ($Messages.IdDoesNotExist -f $Id) } } } # Set location and update location history. function Set-LocationEx { [CmdletBinding(DefaultParameterSetName = "Path")] param( [Parameter(ParameterSetName = "Path",Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)] [String] $Path, [Parameter(ParameterSetName = "LiteralPath",Position = 0,Mandatory)] [String] $LiteralPath, [Parameter(ParameterSetName = "Id",Position = 0,Mandatory)] [Int] $Id, [Switch] [Alias("Clipboard")] $CopyToClipboard, [Switch] $PassThru ) process { $currentPathInfo = $ExecutionContext.SessionState.Path.CurrentLocation if ( "Path","LiteralPath" -contains $PSCmdlet.ParameterSetName ) { $newPath = ($LiteralPath,$Path)[$PSCmdlet.ParameterSetName -eq "Path"] if ( $PSCmdlet.ParameterSetName -eq "Path" ) { if ( -not $newPath ) { if ( -not $CopyToClipboard ) { Get-LocationHistory } else { $ExecutionContext.SessionState.Path.CurrentLocation.Path | Set-Clipboard } return } if ( $newPath -eq '-' ) { if ( $BackwardStack.Count -eq 0 ) { Write-Warning $Messages.NoPreviousLocation } else { $lastNdx = $BackwardStack.Count - 1 Set-Location -LiteralPath $BackwardStack[$lastNdx] -PassThru:$PassThru if ( $currentPathInfo.Path -ne $ExecutionContext.SessionState.Path.CurrentLocation.Path ) { $ForwardStack.Insert(0,$currentPathInfo.Path) $BackwardStack.RemoveAt($lastNdx) if ( $CopyToClipboard ) { $ExecutionContext.SessionState.Path.CurrentLocation.Path | Set-Clipboard } } } return } if ( $newPath -eq '+' ) { if ( $ForwardStack.Count -eq 0 ) { Write-Warning $Messages.NoNextLocation } else { Set-Location -LiteralPath $ForwardStack[0] -PassThru:$PassThru if ( $currentPathInfo.Path -ne $ExecutionContext.SessionState.Path.CurrentLocation.Path ) { $BackwardStack.Add($currentPathInfo.Path) $ForwardStack.RemoveAt(0) if ( $CopyToClipboard ) { $ExecutionContext.SessionState.Path.CurrentLocation.Path | Set-Clipboard } } } return } } # Expand ..[.]+ to ..\..[\..]+ if ( $newPath -like "*...*" ) { $regex = [Regex] '\.\.\.' while ( $regex.IsMatch($newPath) ) { $newPath = $regex.Replace($newPath,'..\..') } } $params = @{ ("LiteralPath","Path")[$PSCmdlet.ParameterSetName -eq "Path"] = $newPath PassThru = $PassThru } Set-Location @params if ( $currentPathInfo.Path -ne $ExecutionContext.SessionState.Path.CurrentLocation.Path ) { # Remove oldest entry if size exceeded if ( ($BackwardStack.Count + $ForwardStack.Count + 1) -eq $MAX_HISTORY_SIZE ) { $BackwardStack.RemoveAt(0) } $BackwardStack.Add($currentPathInfo.Path) # Append new locations to end of stack if ( $ForwardStack.Count -gt 0 ) { $BackwardStack.InsertRange($BackwardStack.Count,$ForwardStack) $ForwardStack.Clear() } if ( $CopyToClipboard ) { $ExecutionContext.SessionState.Path.CurrentLocation.Path | Set-Clipboard } } return } if ( $PSCmdlet.ParameterSetName -eq "Id" ) { if ( ($Id -lt 0) -or ($id -gt ($MAX_HISTORY_SIZE - 1)) ) { Write-Warning ($Messages.IdOutOfRange -f ($MAX_HISTORY_SIZE - 1)) return } if ( $Id -eq $BackwardStack.Count ) { if ( $CopyToClipboard ) { $currentPathInfo.Path | Set-Clipboard } if ( $PassThru ) { $currentPathInfo } return # Going nowhere } if ( $Id -lt $BackwardStack.Count ) { Set-Location -LiteralPath $BackwardStack[$Id] -PassThru:$PassThru if ( $currentPathInfo.Path -ne $ExecutionContext.SessionState.Path.CurrentLocation.Path ) { $ForwardStack.Insert(0,$currentPathInfo.Path) $BackwardStack.RemoveAt($Id) $ndx = $Id $count = $BackwardStack.Count - $ndx if ( $count -gt 0 ) { $itemsToMove = $BackwardStack.GetRange($ndx,$count) $ForwardStack.InsertRange(0,$itemsToMove) $BackwardStack.RemoveRange($ndx,$count) } if ( $CopyToClipboard ) { $ExecutionContext.SessionState.Path.CurrentLocation.Path | Set-Clipboard } } } elseif ( ($Id -gt $BackwardStack.Count) -and ($Id -lt ($BackwardStack.Count + 1 + $ForwardStack.Count)) ) { $ndx = $Id - ($BackwardStack.Count + 1) Set-Location -LiteralPath $ForwardStack[$ndx] -PassThru:$PassThru if ( $currentPathInfo.Path -ne $ExecutionContext.SessionState.Path.CurrentLocation.Path ) { $BackwardStack.Add($currentPathInfo.Path) $ForwardStack.RemoveAt($ndx) $count = $ndx if ( $count -gt 0 ) { $itemsToMove = $ForwardStack.GetRange(0,$count) $BackwardStack.InsertRange(($BackwardStack.Count),$itemsToMove) $ForwardStack.RemoveRange(0,$count) } if ( $CopyToClipboard ) { $ExecutionContext.SessionState.Path.CurrentLocation.Path | Set-Clipboard } } } else { Write-Warning ($Messages.IdDoesNotExist -f $Id) } return } } } $PreviousAlias = Get-Alias cd -ErrorAction SilentlyContinue Set-Alias ` -Name cd ` -Value Set-LocationEx ` -Description $Messages.AliasDescription ` -Force ` -Option AllScope ` -Scope Global $ExecutionContext.SessionState.Module.OnRemove = { if ( $PreviousAlias ) { Set-Alias ` -Name cd ` -Value $PreviousAlias.Definition ` -Force ` -Option $PreviousAlias.Options ` -Scope Global } }.GetNewClosure() |