Stacking Locations in PowerShell
I’m expecting that a significant number of you use PowerShell from an interactive console prompt. You are running commands, changing directories, and generally navigating around the shell. I also expect you want to be as efficient as you can. Every sliver of time you can shave off doing something in PowerShell will add up. Or, at the very least, being efficient means reducing friction which makes running commands in the PowerShell console effortless and maybe even a little fun.
Today I want to share some tips and tricks for navigating the shell. These concepts aren’t new to PowerShell, but I want to ensure you understand how to use them.
Pushing and Popping
This idea of temporarily storing locations pre-dates PowerShell. In the legacy CMD shell, you could use the pushd and popd commands.
c:\Users\Jeff>pushd .
c:\Users\Jeff>cd c:\work\
c:\work>popd
c:\Users\Jeff>pushd .
c:\Users\Jeff>cd c:\work
c:\work>popd
c:\Users\Jeff>
Because PowerShell is also an interactive management shell, it made sense to include these commands. In PowerShell, we have the Push-Location
and Pop-Location
cmdlets. The commands maintain the legacy aliases, pushd
and popd.
I encourage you to spend a few minutes at some point to read the cmdlet help.
The behavior is the same. You push the location to a temporary stack.
PS C:\work> push-location
PS C:\work>
You can change locations.
PS C:\work> cd c:\
PS C:\>
When you need to jump back to the previous location, you can pop.
PS C:\> pop-location
PS C:\work>
This is very especially when you have a long path.
PS C:\Users\Jeff\Documents\PowerShell\Modules> pushd
PS C:\Users\Jeff\Documents\PowerShell\Modules> cd \work
PS C:\work> # do things
PS C:\work> popd
PS C:\Users\Jeff\Documents\PowerShell\Modules>
This is easy and fast. However, once you pop a location, it is gone from the stack.
PS C:\Users\Jeff\Documents\PowerShell\Modules> cd C:\Scripts
PS C:\Scripts> popd
PS C:\Scripts>
Used Named Stacks
Now for the fun stuff. You can define named stacks, pushing different locations to different stacks. Although, this will change your locations.
PS C:\work> Push-Location $home -StackName stack1
PS C:\Users\Jeff>
PS C:\Users\Jeff> Push-Location c:\scripts -StackName stack2
PS C:\Scripts> cd \work
PS C:\work>
But what did I create? I have two “saved” locations in different stacks.
PS C:\work> Get-Location -StackName stack1
Path
----
C:\work
PS C:\work> Get-Location -StackName stack2
Path
----
C:\Users\Jeff
PS C:\work>
I can now pop into the location.
PS C:\Users\Jeff> pop-Location -StackName stack1
PS C:\work>
PS C:\work> pop-Location -StackName stack2
PS C:\Users\Jeff>
As expected, once you pop locations, the location is gone.
PS C:\Users\Jeff> Pop-Location -StackName stack1
Pop-Location: Cannot find location stack 'stack1'. It does not exist or it is not a container.
PS C:\Users\Jeff>
PS C:\Users\Jeff> Get-Location -StackName stack1
Get-Location: Cannot process argument because the value of argument "stackName" is not valid. Change the value of the "stackName" argument and run the operation again.
PS C:\Users\Jeff>
Loading the Stack
So far, we’ve seen storing a single location in the stack, and when popped, it is removed. But a stack implies more than one item. How about loading a stack?
PS C:\> 1..10 | ForEach-Object { Push-Location $home -StackName stack1 }
PS C:\Users\Jeff> Get-Location -StackName stack1
Path
----
C:\Users\Jeff
C:\Users\Jeff
C:\Users\Jeff
C:\Users\Jeff
C:\Users\Jeff
C:\Users\Jeff
C:\Users\Jeff
C:\Users\Jeff
C:\Users\Jeff
C:\
Stack1
contains multiple entries for $HOME.
PS C:\Users\Jeff> Get-Location -StackName stack1 | Select-Object count
Count
-----
10
As I use the stack, saved locations are removed.
PS C:\Users\Jeff> cd d:\temp
PS D:\temp> popd -StackName stack1
PS C:\Users\Jeff> Get-Location -StackName stack1 | Select-Object count
Count
-----
9
PS C:\Users\Jeff>
I can change locations and quickly return to a “favorite” location.
Here’s another way to load the stack.
PS C:\Users\Jeff> Push-Location c:\scripts -StackName stack3
PS C:\Scripts>
PS C:\Scripts> Push-Location $home -StackName stack3
PS C:\Users\Jeff>
PS C:\Users\Jeff> Push-Location c:\work -StackName stack3
PS C:\work>
PS C:\work> Push-Location d:\temp -StackName stack3
PS D:\temp>
PS D:\temp> Push-Location T:\ -StackName stack3
PS T:\>
PS T:\> Get-Location -StackName stack3
Path
----
D:\temp
C:\work
C:\Users\Jeff
C:\Scripts
C:\Users\Jeff
Popping locations from the stack will be in order. But here’s an idea using an argument completer for Set-Location
.
Register-ArgumentCompleter -CommandName Set-Location -ParameterName Path -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
#PowerShell code to populate $wordtoComplete
(Get-Location -StackName stack3).GetEnumerator() |
ForEach-Object {
# completion text,listitem text,result type,Tooltip
[System.Management.Automation.CompletionResult]::new($_.path, $_.path, 'ParameterValue', $_.providerpath)
}
}
When I run Set-Location
or the cd
alias, I’ll get tab completion for the location drawn from the stack.
Of course, once I’ve popped into all of the locations, this is of no use unless I replenish the stack.
One way would be to add each new location to the stack in your PowerShell prompt function.
function prompt {
"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) ";
$top = (Get-Location -StackName stack3).GetEnumerator() | Select-Object -First 1
if ($top.path -ne (Get-Location).path) {
Push-Location . -StackName stack3
}
}
Every time I go to a new location, the stack is updated.
PS D:\temp> cd d:\onedrive
PS D:\OneDrive> cd drop:\work
PS Drop:\work> get-location -StackName stack3
Path
----
Drop:\work
D:\OneDrive
D:\temp
C:\work
C:\Users\Jeff
C:\Scripts
C:\Users\Jeff
PS Drop:\work>
Using this with my argument completer, I can tab complete to any previously used location.
You’ll have to consider how best to incorporate this feature into your daily PowerShell work.
PSReadLine Jump Lists
The last location trick is one I first learned from Lee Holmes. It involves a hash table of locations and a set of PSReadLine key handlers. To start, define a global variable for the hash table. You might do this in your profile script.
$PSReadlineMarks = @{
[char]"s" = "c:\scripts"
[char]"d" = "~\documents"
[char]"w" = "c:\work"
[char]"p" = "C:\Program Files\PowerShell\Modules"
}
The keys should be single characters. The value will be a directory location. Next, you need to define a PSReadlineKeyHandler. I’m defining one that uses Ctrl+j
. Think of ‘j’ as in ‘jump’.
Set-PSReadLineKeyHandler -Key Ctrl+j -BriefDescription JumpDirectory -LongDescription "Goto the marked directory." -ScriptBlock {
$key = [Console]::ReadKey()
$dir = $global:PSReadlineMarks[$key.KeyChar]
if ($dir) {
Set-Location $dir
[Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt()
}
}
Don’t use this combination in VS Code. In fact, I would only use this in the PowerShell console.
At my console prompt, I can type Ctrl+j
plus any character in my hash table, and PowerShell will “jump” to that location.
You can even update the hash table after it has been created.
Set-PSReadLineKeyHandler -Key Ctrl+Alt+j -BriefDescription MarkDirectory -LongDescription "Mark the current directory." -ScriptBlock {
#press a single character to mark the current directory
$key = [Console]::ReadKey($true)
if ($key.keychar -match "\w") {
$global:PSReadlineMarks[$key.KeyChar] = $pwd
}
else {
[Microsoft.PowerShell.PSConsoleReadLine]::Ding()
Write-Warning "You entered an invalid character."
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
}
}
When I am in the location I want to mark, I press Ctrl+Alt+j
followed by the character I want to use for the key. If the character is already being used, the key handler will update the path value.
Finally, it might be nice to see the defined key handlers. You can always look at the variable.
PS C:\> $PSReadlineMarks
Name Value
---- -----
d ~\documents
s c:\scripts
q C:\scripts\PSWorkItem\docs
p C:\Program Files\PowerShell\Modules
w c:\work
Or you can use this key handler.
Set-PSReadLineKeyHandler -Key Alt+j -BriefDescription ShowDirectoryMarks -LongDescription "Show the currently marked directories." -ScriptBlock {
$data = $global:PSReadlineMarks.GetEnumerator() | Where-Object { $_.key } | Sort-Object key
$data | ForEach-Object -Begin {
$text = @"
Key`tDirectory
---`t---------
"@
} -Process {
$text += "{0}`t{1}`n" -f $_.key, $_.value
}
if ($PSEdition -eq 'Desktop' -or $IsWindows) {
$ws = New-Object -ComObject Wscript.Shell
$ws.popup($text, 10, "Use Ctrl+J to jump") | Out-Null
[Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt()
}
else {
Write-Host "`n$text`n" -ForegroundColor Yellow
}
}
The Alt+j
key combination will create a VBScript style popup.
You could easily modify the code to use your preferred display technique.
Summary
If you are like me, you find yourself jumping around between a subset of locations. You can save some typing with the techniques I’ve shown you today. I love jumping around, especially to locations with a deep path. It removes a little friction and makes my work day that much easier.
I’d love to hear from you about how this improves your work. Or feel free to leave a follow-up question.