Last active
May 2, 2026 19:59
-
-
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"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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