LocationStack.psm1
function Add-LocationToStack { <# .SYNOPSIS Adds path to the location stack .DESCRIPTION Adds a directory path specified by <-location> parameter to $LocationStack hashtable, with key defined by <-id> parameter. If path is not specified, current directory path is used. If path or id is already present in $LocationStack, use <-force> to overwrite .LINK https://github.com/PavelStsefanovich/LocationStack #> param ( [string]$id = $(throw 'Mandatory parameter not provided: <id>.'), [string]$location = $PWD.Path, [switch]$force ) $ErrorActionPreference = 'Stop' $location = (Resolve-Path $location).Path.TrimEnd('\/') if (!$Global:LocationStack) { $Global:LocationStack = @{ } } if ($id -match '\W') { throw "Invalid ID: '$id' (allowed characters: [a-zA-Z0-9_])" } if ($id -in $Global:LocationStack.Keys) { if (!$force) { Write-Warning "Found LocationStack ID '$id' with following path: '$($Global:LocationStack.$id)'." Write-Warning "Use <-force> to overwrite." break } } if ($location -in $Global:LocationStack.Values) { $existing_location_id = ($Global:LocationStack.GetEnumerator() | ? { $_.value -eq $location }).key if (!$force) { Write-Warning "Found '$location' with following ID: '$existing_location_id'." Write-Warning "Use <-force> to overwrite." break } } if ($existing_location_id) { $Global:LocationStack.Remove($existing_location_id) } $Global:LocationStack.$id = $location } function Remove-LocationFromStack { <# .SYNOPSIS Removes path from the location stack .DESCRIPTION Removes an entry from $LocationStack hashtable, with key defined by <-id> parameter. Use <-force> to confirm your intent to delete. .LINK https://github.com/PavelStsefanovich/LocationStack #> param ( [string]$id = $(throw 'Mandatory parameter not provided: <id>.'), [switch]$force ) $ErrorActionPreference = 'Stop' if (!$Global:LocationStack -or ($Global:LocationStack.Count -eq 0)) { Write-Warning "Location Stack is currently empty." break } if ($id -match '\W') { throw "Invalid ID: '$id' (allowed characters: [a-zA-Z0-9_])" } if ($id -notin $Global:LocationStack.Keys) { Write-Warning "Location ID '$id' not found in the Location Stack." break } if (!$force) { Write-Warning "Found '$id' : '$($Global:LocationStack.$id)'." Write-Warning "To delete, re-run with <-force>." break } $location = $Global:LocationStack.$id $Global:LocationStack.Remove($id) Write-Host "Removed '$id' : '$location'" } function Show-LocationStack { <# .SYNOPSIS Lists paths in the location stack. .DESCRIPTION Returns $LocationStack hashtable. Use <-ids>(array) and <-locations>(array) to filter returned results (both accept wildcard '*'). .LINK https://github.com/PavelStsefanovich/LocationStack #> param ( [string[]]$ids, [string[]]$locations ) $ErrorActionPreference = 'Stop' if (!$Global:LocationStack -or ($Global:LocationStack.Count -eq 0)) { Write-Warning "Location Stack is currently empty." break } $show_hash = @{} if ($ids) { foreach ($id in $ids) { if ($id -match '[^a-zA-Z0-9_\*]') { Write-Warning "Invalid ID: '$id' (allowed characters: [a-zA-Z0-9_] + wildcard '*')" } $keys = @($Global:LocationStack.Keys.GetEnumerator() | ?{$_ -like $id}) $keys | %{$show_hash.$_ = $Global:LocationStack.$_} } } if ($locations) { $values = @() foreach ($location in $locations) { $Global:LocationStack.Values.GetEnumerator() | ? {$_ -like $location} | % {$values += $_} } $Global:LocationStack.Keys.GetEnumerator() | %{ if ($Global:LocationStack.$_ -in $values) { $show_hash.$_ = $Global:LocationStack.$_ } } } if (!$id -and !$location) { $show_hash = $Global:LocationStack.Clone() } return $show_hash } function Switch-Location { <# .SYNOPSIS Changes current directory to the path in the location stack .DESCRIPTION If <-id> is specified, uses path with that ID in $LocationStack hashtable. If <-location> is specified, changes to that location if it can be resolved. Before changing location, current directory path is recorded in $LocationStack hashtable under 'last' key. If no parameters specified, changes location to the path defined by 'last' key in $LocationStack hashtable. .LINK https://github.com/PavelStsefanovich/LocationStack #> param ( [string]$id, [string]$location ) $ErrorActionPreference = 'Stop' if ($id -and $location) { throw "Both <-id> and <-location> cannot be used simultaneously." } if (!$Global:LocationStack -or ($Global:LocationStack.Count -eq 0)) { Write-Warning "Location Stack is currently empty." break } if ($id) { if ($id -match '\W') { throw "Invalid ID: '$id' (allowed characters: [a-zA-Z0-9_])" } if ($id -in $Global:LocationStack.Keys) { $location = $Global:LocationStack.$id } else { Write-Warning "Location ID '$id' not found in the Location Stack." break } } if (!$location) { $location = $Global:LocationStack.last } if ($location) { $location = (Resolve-Path $location).Path $Global:LocationStack.last = $PWD.Path cd $location } } function Open-LocationInExplorer { <# .SYNOPSIS Opens path(s) from the location stack in Windows Explorer. .DESCRIPTION Use <-ids>(array) and <-locations>(array) to filter paths to open (both accept wildcard '*'). .LINK https://github.com/PavelStsefanovich/LocationStack #> param ( [string[]]$ids, [string[]]$locations ) $ErrorActionPreference = 'Stop' if (!$Global:LocationStack -or ($Global:LocationStack.Count -eq 0)) { Write-Warning "Location Stack is currently empty." break } $locations_to_open = @() if ($ids) { foreach ($id in $ids) { if ($id -match '[^a-zA-Z0-9_\*]') { Write-Warning "Invalid ID: '$id' (allowed characters: [a-zA-Z0-9_] + wildcard '*')" } $keys = @($Global:LocationStack.Keys.GetEnumerator() | ?{$_ -like $id}) $keys | %{$locations_to_open += $Global:LocationStack.$_} } } if (!$ids -and !$locations) { $locations_to_open += $PWD.Path } foreach ($location in $locations) { if (Test-Path $location) { $locations_to_open += $location } else { Write-Warning "Location not found on disk: '$location'." } } $opened_locations = @() foreach ($path in $locations_to_open) { if ($path -notin $opened_locations) { explorer "$path" $opened_locations += $path } } } function Clear-LocationStack { <# .SYNOPSIS Clears location stack. .DESCRIPTION Removes all entries from $LocationStack hashtable except the one with 'last' key. Use <-force> to delete $LocationStack hashtable completely. .LINK https://github.com/PavelStsefanovich/LocationStack #> param ( [switch]$force ) $ErrorActionPreference = 'Stop' if ($force) { Get-Variable LocationStack -Scope Global -ErrorAction SilentlyContinue | rm } else { if (!$Global:LocationStack) { break } $keys_to_delete = @() foreach ($key in $Global:LocationStack.Keys.GetEnumerator()) { if ($key -ne 'last') { $keys_to_delete += $key } } $keys_to_delete | % {$Global:LocationStack.Remove($_)} } } function Export-LocationStack { <# .SYNOPSIS Saves location stack to a file. .DESCRIPTION If <-name> is not specified, location stack is saved as 'default'. If location stack with provided name already exists, use <-force> to overwrite. .LINK https://github.com/PavelStsefanovich/LocationStack #> param ( [string]$name, [switch]$force ) $ErrorActionPreference = 'Stop' if (!$Global:LocationStack -or ($Global:LocationStack.Count -eq 0)) { Write-Warning "Location Stack is currently empty." break } if ($name -match '[^a-zA-Z0-9]') { throw "Invalid name: '$name' (allowed characters: [a-zA-Z0-9])" } $save_directory = Join-Path (Split-Path $PROFILE) 'data' mkdir $save_directory -ErrorAction SilentlyContinue | Out-Null if (!$name) {$name = 'default'} $filepath = Join-Path $save_directory "locstack_$name`.json" if (Test-Path $filepath) { if (!$force) { Write-Warning "Found location stack '$name' on disk." Write-Warning "Use <-force> to overwrite." break } } $Global:LocationStack | ConvertTo-Json | Out-File $filepath -Force -ErrorAction Stop } function Import-LocationStack { <# .SYNOPSIS Loads location stack from a file. .DESCRIPTION If <-name> is not specified, 'default' stack is loaded. Parameter <-force> is required to avoid accidental replacement of current stack. .LINK https://github.com/PavelStsefanovich/LocationStack #> param ( [string]$name, [switch]$force ) $ErrorActionPreference = 'Stop' if ($name -match '[^a-zA-Z0-9]') { throw "Invalid name: '$name' (allowed characters: [a-zA-Z0-9])" } if (!$force) { Write-Warning "(!) Re-run with '-Force' parameter to import location stack from file." break } $save_directory = Join-Path (Split-Path $PROFILE) 'data' if (!$name) {$name = 'default'} $filepath = Join-Path $save_directory "locstack_$name`.json" if (!(Test-Path $filepath)) { Write-Warning "LocationStack '$name' not found on disk" Write-Warning "Use Get-LocationStack to list available stacks." break } $load_hash = @{} (cat $filepath -ErrorAction Stop | ConvertFrom-Json).psobject.properties | %{ $load_hash[$_.Name] = $_.Value } $Global:LocationStack = $load_hash.Clone() Remove-Variable load_hash -force } function Get-LocationStack { <# .SYNOPSIS Lists exported location stacks. .DESCRIPTION Use <-name>(array) to filter listed stacks (accepts wildcard '*') .LINK https://github.com/PavelStsefanovich/LocationStack #> param ( [string[]]$names = @('*') ) $save_directory = Join-Path (Split-Path $PROFILE) 'data' $stacks_list = @() foreach ($name in $names) { $filter = "locstack_$name`.json" if ($name -match '[^a-zA-Z0-9*]') { Write-Warning "Invalid name: '$name' (allowed characters: [a-zA-Z0-9*])" } $stacks_on_disk = (ls $save_directory -Filter $filter -ErrorAction SilentlyContinue).BaseName $stacks_on_disk | %{ if ($_ -notin $stacks_list) { $stacks_list += $_ } } } if ($stacks_list) { $stacks_list | % { if ($_) { Write-Host $_.replace('locstack_','') } } } } function Remove-LocationStack { <# .SYNOPSIS Removes exported location stack from disk. .DESCRIPTION Use mandatory <-name> parameter to specify which stack file to delete (use 'default' to remove default stack). Parameter <-force> is required to avoid accidental removal of stack file. .LINK https://github.com/PavelStsefanovich/LocationStack #> param ( [string]$name = $(throw 'Mandatory parameter not provided: <name>.'), [switch]$force ) $ErrorActionPreference = 'Stop' if ($name -match '[^a-zA-Z0-9]') { throw "Invalid name: '$name' (allowed characters: [a-zA-Z0-9])" } $save_directory = Join-Path (Split-Path $PROFILE) 'data' $filepath = Join-Path $save_directory "locstack_$name`.json" if (!(Test-Path $filepath)) { Write-Warning "LocationStack '$name' not found on disk" Write-Warning "Use Get-LocationStack to list available stacks." break } if (!$force) { Write-Warning "Found location stack '$name' on disk." Write-Warning "To delete, re-run with <-force>." break } Remove-Item $filepath -Force Write-Host "Removed location stack '$name'" } try { Set-Alias -Name als -Value Add-LocationToStack -Force -ErrorAction Stop Set-Alias -Name rls -Value Remove-LocationFromStack -Force -ErrorAction Stop Set-Alias -Name shls -Value Show-LocationStack -Force -ErrorAction Stop Set-Alias -Name swl -Value Switch-Location -Force -ErrorAction Stop Set-Alias -Name ole -Value Open-LocationInExplorer -Force -ErrorAction Stop Set-Alias -Name clrs -Value Clear-LocationStack -Force -ErrorAction Stop Set-Alias -Name els -Value Export-LocationStack -Force -ErrorAction Stop Set-Alias -Name ils -Value Import-LocationStack -Force -ErrorAction Stop Set-Alias -Name gls -Value Get-LocationStack -Force -ErrorAction Stop Set-Alias -Name rmls -Value Remove-LocationStack -Force -ErrorAction Stop Export-ModuleMember -Function * -Alias * } catch { Write-Warning "<Import-module LocationStack> : Failed to update aliases for imported functions. Please run the following block to set aliases manually:" Write-Host '' Write-Host 'Set-Alias -Name als -Value Remove-LocationFromStack -Force' Write-Host 'Set-Alias -Name rls -Value Add-LocationToStack -Force' Write-Host 'Set-Alias -Name shls -Value Show-LocationStack -Force' Write-Host 'Set-Alias -Name swl -Value Switch-Location -Force' Write-Host 'Set-Alias -Name ole -Value Open-LocationInExplorer -Force' Write-Host 'Set-Alias -Name clrs -Value Clear-LocationStack -Force' Write-Host 'Set-Alias -Name els -Value Export-LocationStack -Force' Write-Host 'Set-Alias -Name ils -Value Import-LocationStack -Force' Write-Host 'Set-Alias -Name gls -Value Get-LocationStack -Force' Write-Host 'Set-Alias -Name rmls -Value Remove-LocationStack -Force' Write-Host '' Write-Warning 'Use the following command to delete persistent aliases:' Write-Host '' Write-Host 'del alias:<alias_name> -Force' Write-Host '' } Export-ModuleMember -Function * |