Skip to content

Instantly share code, notes, and snippets.

@jongalloway
Last active May 2, 2026 19:59
Show Gist options
  • Select an option

  • Save jongalloway/332c3b15d2557dfa71575b79713ffbf1 to your computer and use it in GitHub Desktop.

Select an option

Save jongalloway/332c3b15d2557dfa71575b79713ffbf1 to your computer and use it in GitHub Desktop.
jump-utils.ps1 - Lightweight directory jump utilities for PowerShell. Install with: iwr "https://gist.githubusercontent.com/jongalloway/332c3b15d2557dfa71575b79713ffbf1/raw/jump-utils.ps1" -outf "$(Split-Path $PROFILE)\jump-utils.ps1"
# jump-utils.ps1 - Lightweight directory jump utilities for PowerShell
# https://gist.github.com/jongalloway/332c3b15d2557dfa71575b79713ffbf1
#
# Quick install - add this to your PowerShell profile (code $PROFILE):
# iwr "https://gist.githubusercontent.com/jongalloway/332c3b15d2557dfa71575b79713ffbf1/raw/jump-utils.ps1" -OutF "$(Split-Path $PROFILE)\jump-utils.ps1"
# Add-Content $PROFILE '. (Join-Path (Split-Path $PROFILE) "jump-utils.ps1")'
#
# Or manually:
# 1. Save this file next to your profile (Split-Path $PROFILE)
# 2. Add to your PowerShell profile: . (Join-Path (Split-Path $PROFILE) "jump-utils.ps1")
# 3. Set REPO_ROOT env var or you'll be prompted the first time you run "repo"
#
# Usage:
# repo <name> - pushd to a repo (supports partial match)
# repo list - list all repos
# repo - pushd to repo root
# jump <target> - pushd to a named directory (supports partial match)
# jump add <n> [path] - add a custom jump target (defaults to current dir)
# jump remove <name> - remove a custom jump target
# jump list - list all jump targets
# jump - popd (go back)
# Aliases: jr = repo, j = jump
# --- Repo root configuration (lazy — only prompts when repo/jr is first used) ---
function _EnsureRepoRoot {
if ($script:RepoRoot) { return }
if ($env:REPO_ROOT) {
$script:RepoRoot = $env:REPO_ROOT
return
}
$example = Join-Path ([Environment]::GetFolderPath('MyDocuments')) "GitHub"
$script:RepoRoot = Read-Host "Enter your repositories root directory (e.g. $example)"
[Environment]::SetEnvironmentVariable('REPO_ROOT', $script:RepoRoot, 'User')
$env:REPO_ROOT = $script:RepoRoot
Write-Host "REPO_ROOT saved to user environment variables." -ForegroundColor Green
}
# --- repo command ---
function repo {
param([string]$Name)
_EnsureRepoRoot
if ($Name -ieq 'list') {
Get-ChildItem -Path $script:RepoRoot -Directory | Select-Object Name
return
}
if ($Name) {
$target = Join-Path $script:RepoRoot $Name
if (Test-Path $target) {
Push-Location $target
} else {
$matches = Get-ChildItem -Path $script:RepoRoot -Directory | Where-Object { $_.Name -ilike "$Name*" }
if ($matches.Count -eq 1) {
Push-Location $matches[0].FullName
} elseif ($matches.Count -gt 1) {
Write-Error "Ambiguous match for '$Name': $($matches.Name -join ', ')"
} else {
Write-Error "Repository '$Name' not found in $script:RepoRoot"
}
}
} else {
Push-Location $script:RepoRoot
}
}
Register-ArgumentCompleter -CommandName repo -ParameterName Name -ScriptBlock {
param($commandName, $parameterName, $wordToComplete)
$root = if ($script:RepoRoot) { $script:RepoRoot } else { $env:REPO_ROOT }
if (-not $root) { return }
Get-ChildItem -Path $root -Directory |
Where-Object { $_.Name -ilike "$wordToComplete*" } |
ForEach-Object { $_.Name }
}
# --- jump command ---
function Get-DownloadsPath {
if ($IsWindows) {
try {
$downloadsPath = (New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path
if ($downloadsPath -and (Test-Path $downloadsPath)) {
return $downloadsPath
}
} catch {
Write-Verbose "Unable to resolve shell:Downloads; using the default Downloads path."
}
}
$defaultPath = Join-Path $HOME 'Downloads'
if (Test-Path $defaultPath) {
return $defaultPath
}
return $defaultPath
}
$script:BuiltinTargets = @{
Documents = [Environment]::GetFolderPath('MyDocuments')
Downloads = Get-DownloadsPath
Desktop = [Environment]::GetFolderPath('Desktop')
Pictures = [Environment]::GetFolderPath('MyPictures')
Videos = [Environment]::GetFolderPath('MyVideos')
Music = [Environment]::GetFolderPath('MyMusic')
}
$script:CustomTargetsFile = Join-Path (Split-Path $PROFILE) "jump-targets.json"
function _LoadCustomTargets {
if (Test-Path $script:CustomTargetsFile) {
return (Get-Content $script:CustomTargetsFile -Raw | ConvertFrom-Json -AsHashtable)
}
return @{}
}
function _SaveCustomTargets($targets) {
$targets | ConvertTo-Json | Set-Content $script:CustomTargetsFile
}
function _AllJumpTargets {
$all = @{} + $script:BuiltinTargets
$custom = _LoadCustomTargets
foreach ($key in $custom.Keys) { $all[$key] = $custom[$key] }
return $all
}
function jump {
param(
[string]$Name,
[Parameter(ValueFromRemainingArguments)][string[]]$Rest
)
if (-not $Name) {
Pop-Location
return
}
if ($Name -ieq 'add') {
if (-not $Rest -or $Rest.Count -eq 0) {
Write-Error "Usage: jump add <name> [path] (defaults to current directory)"
return
}
$targetName = $Rest[0]
$targetPath = if ($Rest.Count -gt 1) { $Rest[1] } else { (Get-Location).Path }
if ($script:BuiltinTargets.ContainsKey($targetName)) {
Write-Error "'$targetName' is a built-in target and cannot be overridden."
return
}
$custom = _LoadCustomTargets
$custom[$targetName] = $targetPath
_SaveCustomTargets $custom
Write-Host "Added jump target '$targetName' -> $targetPath" -ForegroundColor Green
return
}
if ($Name -ieq 'remove') {
$targetName = if ($Rest) { $Rest[0] } else { $null }
if (-not $targetName) {
Write-Error "Usage: jump remove <name>"
return
}
$custom = _LoadCustomTargets
if ($custom.ContainsKey($targetName)) {
$custom.Remove($targetName)
_SaveCustomTargets $custom
Write-Host "Removed jump target '$targetName'" -ForegroundColor Yellow
} else {
Write-Error "'$targetName' is not a custom jump target. (Built-in targets cannot be removed.)"
}
return
}
if ($Name -ieq 'list') {
$all = _AllJumpTargets
$custom = _LoadCustomTargets
$all.GetEnumerator() | Sort-Object Name | ForEach-Object {
$label = if ($custom.ContainsKey($_.Name)) { '*' } else { ' ' }
[PSCustomObject]@{ ' ' = $label; Name = $_.Name; Path = $_.Value }
} | Format-Table -AutoSize
return
}
$all = _AllJumpTargets
if ($all.ContainsKey($Name)) {
Push-Location $all[$Name]
} else {
$matches = $all.Keys | Where-Object { $_ -ilike "$Name*" }
if (@($matches).Count -eq 1) {
Push-Location $all[$matches]
} elseif (@($matches).Count -gt 1) {
Write-Error "Ambiguous match for '$Name': $($matches -join ', ')"
} else {
Write-Error "Unknown jump target '$Name'. Run 'jump list' to see available targets."
}
}
}
Register-ArgumentCompleter -CommandName jump -ParameterName Name -ScriptBlock {
param($commandName, $parameterName, $wordToComplete)
$all = _AllJumpTargets
@('add', 'remove', 'list') + @($all.Keys) | Where-Object { $_ -ilike "$wordToComplete*" } | Sort-Object
}
# --- Aliases ---
Set-Alias -Name j -Value jump
Set-Alias -Name jr -Value repo
Register-ArgumentCompleter -CommandName j -ParameterName Name -ScriptBlock {
param($commandName, $parameterName, $wordToComplete)
$all = _AllJumpTargets
@('add', 'remove', 'list') + @($all.Keys) | Where-Object { $_ -ilike "$wordToComplete*" } | Sort-Object
}
Register-ArgumentCompleter -CommandName jr -ParameterName Name -ScriptBlock {
param($commandName, $parameterName, $wordToComplete)
$root = if ($script:RepoRoot) { $script:RepoRoot } else { $env:REPO_ROOT }
if (-not $root) { return }
Get-ChildItem -Path $root -Directory |
Where-Object { $_.Name -ilike "$wordToComplete*" } |
ForEach-Object { $_.Name }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment