cdim.psm1
# all we need to know about a bookmark Add-Type -Language CSharp @" public class Bookmark { public string Name; public string Path; public System.DateTime TimeStamp; } "@; # the location of our settings $cdim_path = Join-Path (Split-Path $PROFILE -Parent) .cdim # the last version, we've read $cdim_last_update = Get-Date 1970-01-01 # the stack of directories [Collections.Generic.List[String]] $cdim_history = @() # the dictionary of bookmarks [System.Collections.Generic.SortedDictionary[String, Bookmark]]$cdim_bookmarks = @{} # maximum size of history $cdim_max_history = 15 <# .SYNOPSIS Improved cd command with history and bookmarks. .DESCRIPTION Add to cd command: - history (only for the current session) - bookmarks (persisted) .PARAMETER Path If any of the other options have been provided, this the path to set the location to. If it starts with a '%', this is: - the ith item in the history, if a number follows the '%' - the name of a bookmark otherwise If the bookmark option has been set, this is the location to use for setting the bookmark if empty, the location will be set to the current directory .PARAMETER List This list the location history and available history .PARAMETER Bookmark This is the name of the bookmark to create or change .PARAMETER Delete This is the name of the bookmark to delete .PARAMETER Fuzzy Will cd to the most recent directory in history match the regex (case-insensitive) .NOTES It can be aliased to 'CD': Set-Alias cd cdim -Option AllScope .EXAMPLE cdim C:\windows CD to a directory .EXAMPLE cdim %2 CD to the 2nd most recent directory .EXAMPLE cdim %d1 CD to the bookmark 'd1' .EXAMPLE cdim first-dir cdim second-dir cdim first-dir2 cdim -f econd will cd to the directory second-dir cdim -f irst.*d will cd to the directory first-dir .EXAMPLE cdim -b d1 Bookmark the current directory to 'd1' .EXAMPLE cdim c:\windows -b d1 Bookmark the c:\windows directory to 'd1' .EXAMPLE cdim -d d1 Delete the bookmark 'd1' #> function cdim { param( [Parameter(Position=0, ValueFromPipeline=$true)] [string]${Path}, [string]${Bookmark}, [string]${Delete}, [switch]${List}, [string]${Fuzzy} ) # get any "external" change get-settings # the path # add a new bookmark ? if (-not [System.String]::IsNullOrWhiteSpace($Bookmark)) { if ([System.String]::IsNullOrWhiteSpace($Path)) { $Path = $PWD } $b = New-Object Bookmark -Property @{ Name=$Bookmark.Trim(); Path=$Path; TimeStamp = Get-Date } $cdim_bookmarks[$b.Name] = $b Write-Output "Added bookmark '$($b.Name)' for path '$($b.Path)'" save-settings } elseif (-not [System.String]::IsNullOrWhiteSpace($Delete)) { $cdim_bookmarks.Remove($Delete.Trim()) save-settings } elseif ($List) { Write-Output "----------" Write-Output "Bookmarks:" Write-Output "----------" # NB: foreach iterates with one null value if the collection is empty if ($cdim_bookmarks.Count -ne 0) { $cdim_bookmarks.Values | ForEach-Object { Write-Output "$($_.Name): $($_.Path)" } } Write-Output "----------" Write-Output "History:" Write-Output "----------" $i = 1 if ($cdim_history.Count -ne 0) { $cdim_history | ForEach-Object { Write-Output "${i}: $_"; $i += 1 } } } else { # fuzzy search if (-not [System.String]::IsNullOrWhiteSpace($Fuzzy)) { if ($cdim_history.Count -eq 0) { Throw "no directory in history" } foreach($h_path in $cdim_history) { # matches only on the directory name $dir_name = (Split-Path $h_path -Leaf) if (${dir_name} -imatch $Fuzzy) { $Path = $h_path break } } if ([System.String]::IsNullOrWhiteSpace($Path)) { Throw "no match for '$Fuzzy'" } } # cd home elseif ([System.String]::IsNullOrWhiteSpace($Path)) { $Path = [Environment]::GetFolderPath("UserProfile") } # support for "cd -"" elseif ($Path.Trim().equals("-")) { if ($cdim_history.Count -ge 2) { $Path = $cdim_history[1] } else { $Path = (Get-Location).Path } } if ($Path.StartsWith('%')) { $Path = $Path.Substring(1) $i = $Path -as [int] if ($null -eq $i) { if (-not $cdim_bookmarks.ContainsKey($Path)) { Throw "unknown bookmark %$Path" } $Path = $cdim_bookmarks[$Path].Path } else { if ($cdim_history.Count -lt $i) { Throw "Only $($cdim_history.Count) item(s) in history" } $Path = $cdim_history[$i - 1] } } Set-Location $Path $np = $PWD.Path $cdim_history.Insert(0, $np) # remove any previous instance for ($i=$cdim_history.Count - 1; $i -gt 0; $i--) { if ($cdim_history[$i] -ieq $np) { $cdim_history.RemoveAt($i) } } # avoid too large history while ($cdim_max_history -lt $cdim_history.Count) { $cdim_history.RemoveAt($cdim_history.Count - 1) } } } function save-settings() { # first merge with any new definitions get-settings # then save them Export-Clixml -InputObject $cdim_bookmarks -Path $cdim_path $script:cdim_last_update = (Get-Item $cdim_path -Force).LastWriteTime } function get-settings() { # only load if newer if (Test-Path $cdim_path) { $nts = (Get-Item $cdim_path -Force).LastWriteTime #NB: we must specify the scope for a DateTime as they are used by value if ($nts -gt $script:cdim_last_update) { #Write-Output "reloading settings" $import = Import-Clixml -Path $cdim_path $script:cdim_last_update = $nts # merge $import.Values | ForEach-Object { if ((-not $cdim_bookmarks.ContainsKey($_.Name)) -or ($_.TimeStamp -gt $cdim_bookmarks[$_.Name].TimeStamp)) { # NB: Import-Clixml does not recreate the original object but objects with the same public properties # so we need to re-create the object $cdim_bookmarks[$_.Name] = New-Object Bookmark -Property @{ Name=$_.Name; Path=$_.Path; TimeStamp = $_.TimeStamp } }} } } } function cdim_complete() { param($wordToComplete) # get any "external" change get-settings if ($wordToComplete.StartsWith('%')) { $key,$sep,$rest = $wordToComplete.SubString(1) -split "([/\\])",2 # %key/ + a/b -> dir_key/a/b if ($sep -and $cdim_bookmarks.ContainsKey($key)) { $res = (Join-Path $cdim_bookmarks[$key].Path "$rest") [System.Management.Automation.CompletionResult]::new($res, $res, 'ParameterValue', 'Directory') } else { # bookmark name completion $cdim_bookmarks.Keys | where-object { $_.StartsWith($key) } | ForEach-Object { [System.Management.Automation.CompletionResult]::new('%' + $_, '%' + $_, 'ParameterValue', 'Bookmark') } } } else { # directory name completion $key=$wordToComplete Get-ChildItem -Directory -Path . | where-object { $_.Name.StartsWith($key) } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', 'Directory') } } } Set-Alias cd cdim -Option AllScope -scope Global # PsReadline keeps a cache of the cd alias, we need to reset it # see https://jamesone111.wordpress.com/2019/11/24/redefining-cd-in-powershell/ # warning, all PsReadline personalisation will be lost. So it's better to load this module BEFORE chaning any PSReadLine settings! if (Get-Module PSReadLine) { Remove-Module -Force PsReadline Import-Module -Force PSReadLine } Register-ArgumentCompleter -CommandName 'cdim' -ParameterName 'Path' -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) cdim_complete $wordToComplete } |