Skip to content

Instantly share code, notes, and snippets.

@vrdhn
Last active July 18, 2025 06:50
Show Gist options
  • Select an option

  • Save vrdhn/1ca6bb16cb3ca388703ab7b0c9cb4cb4 to your computer and use it in GitHub Desktop.

Select an option

Save vrdhn/1ca6bb16cb3ca388703ab7b0c9cb4cb4 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# Created by ChatGPT, prompted by Vardhan
#
# Enhanced cd wrapper for Bash with stack, bookmarks, and pattern matching
#
# β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
# β”‚ Features β”‚
# β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
# πŸ“ Directory Stack (max 10 entries)
# - cd (no args) : print the directory stack
# - cd - : rotate top 2 entries (swap top and next)
# - cd -- : rotate top 3 entries (cycle top 3)
# - cd --- : rotate top 4 entries, etc.
# - cd -N (N = digit) : rotate top N+1 entries left by N to bring index N to top
# - cd +N : swap top and entry N in stack
#
# πŸ“Œ Bookmarks
# - cd + : list bookmarks
# - cd +name /path : save bookmark 'name' with given path (path optional, defaults to current)
# - cd +name : set bookmark 'name' to current directory
# - cd -name : jump to bookmark 'name'
#
# πŸ“‚ Directory shortcuts
# - cd ... : cd ../..
# - cd .... : cd ../../..
#
# 🧭 Pattern Matching (set by match_define)
# - match_define name pattern (e.g. "/data/@_user/@_proj")
# - On cd, sets _name=name, and variables like _user, _proj, _rest
#
# πŸ’¬ Example PS1 usage:
# export PS1='\u@\h:\w$([[ "$_name" != "nomatch" ]] && echo " ($_name $_user $_proj)")\$ '
#
# ────────────────────────────────────────────────────────────────────────────
# Stack and bookmarks initialization
[[ -z "${CD_STACK+x}" ]] && declare -a CD_STACK=()
[[ -z "${CD_BOOKMARKS+x}" ]] && declare -A CD_BOOKMARKS=()
[[ -z "${CD_PATTERNS+x}" ]] && declare -A CD_PATTERNS=()
# Clear all _* variables from pattern matching
clear_pattern_vars() {
for var in $(compgen -v); do
[[ "$var" == _* ]] && unset "$var"
done
}
# Define a pattern for matching in form: match_define name pattern
match_define() {
local name="$1" pattern="$2"
if [[ -z "$name" || -z "$pattern" ]]; then
echo "Usage: match_define name pattern" >&2
return 1
fi
CD_PATTERNS["$name"]="$pattern"
}
# Match $PWD against patterns and set variables accordingly
match_patterns() {
local pwd_path="$PWD"
clear_pattern_vars
_name="nomatch"
for pname in "${!CD_PATTERNS[@]}"; do
local pattern="${CD_PATTERNS[$pname]}"
IFS='/' read -ra pat_parts <<< "$pattern"
IFS='/' read -ra pwd_parts <<< "$pwd_path"
declare -A captures=()
local i=0 j=0 match=1
while (( i < ${#pat_parts[@]} && j < ${#pwd_parts[@]} )); do
local p="${pat_parts[i]}"
local w="${pwd_parts[j]}"
if [[ "$p" =~ ^@_([a-zA-Z_][a-zA-Z0-9_]*)$ ]]; then
captures["${BASH_REMATCH[1]}"]="$w"
((i++, j++))
elif [[ "$p" == "$w" ]]; then
((i++, j++))
else
match=0
break
fi
done
if (( match )) && (( i == ${#pat_parts[@]} )); then
_name="$pname"
for k in "${!captures[@]}"; do
export "_$k"="${captures[$k]}"
done
# Join remainder parts with / for _rest or empty string if none
if (( j < ${#pwd_parts[@]} )); then
_rest="${pwd_parts[@]:j}"
# Convert array to slash-separated string
_rest="${_rest// /\/}"
else
_rest=""
fi
export _rest
export _name
return 0
fi
done
# No match found
_name="nomatch"
export _name
unset _rest
return 1
}
# Update directory stack, keeping max 10 entries, no duplicates (top is $PWD)
update_stack() {
local current="$PWD"
local -a newstack=("$current")
for d in "${CD_STACK[@]}"; do
[[ "$d" != "$current" ]] && newstack+=("$d")
done
CD_STACK=("${newstack[@]:0:10}")
}
# Print the directory stack nicely
print_stack() {
echo "πŸ“ Directory stack (top is current):"
local i=0
for d in "${CD_STACK[@]}"; do
echo " [$i] $d"
((i++))
done
}
# Rotate left the top n entries by k places (default k=1)
rotate_stack_left_n() {
local n="$1"
local k="${2:-1}"
(( n > ${#CD_STACK[@]} )) && n=${#CD_STACK[@]}
(( n < 2 )) && return 0
k=$((k % n))
local -a slice=("${CD_STACK[@]:0:n}")
local -a rotated=("${slice[@]:k}" "${slice[@]:0:k}")
CD_STACK=("${rotated[@]}" "${CD_STACK[@]:n}")
builtin cd "${CD_STACK[0]}" || return 1
update_stack
match_patterns
}
# Rotate top n entries to bring index to top (rotate left by index)
rotate_stack_to_index() {
local index="$1"
(( index < 0 || index >= ${#CD_STACK[@]} )) && {
echo "❌ Invalid stack index $index" >&2
return 1
}
local n=$((index + 1))
rotate_stack_left_n "$n" "$index"
}
# Swap top and entry at given index in stack
swap_stack_top_with() {
local index="$1"
(( index >= ${#CD_STACK[@]} )) && {
echo "❌ Invalid stack index $index" >&2
return 1
}
local tmp="${CD_STACK[0]}"
CD_STACK[0]="${CD_STACK[$index]}"
CD_STACK[$index]="$tmp"
builtin cd "${CD_STACK[0]}" || return 1
update_stack
match_patterns
}
# Resolve relative path to absolute path safely
resolve_path() {
local path="$1"
if [[ "$path" == /* ]]; then
echo "$path"
else
echo "$(cd "$path" 2>/dev/null && pwd)"
fi
}
# The main cd wrapper function
cd_wrapper() {
local arg1="$1"
local arg2="$2"
# No args: print stack
[[ -z "$arg1" ]] && { print_stack; return 0; }
# List bookmarks: cd +
if [[ "$arg1" == "+" ]]; then
echo "πŸ”– Bookmarks:"
if (( ${#CD_BOOKMARKS[@]} == 0 )); then
echo " (none)"
else
for k in "${!CD_BOOKMARKS[@]}"; do
echo " +$k β†’ ${CD_BOOKMARKS[$k]}"
done
fi
return 0
fi
# Save bookmark: cd +name [path]
if [[ "$arg1" =~ ^\+([A-Za-z_][A-Za-z0-9_]*)$ ]]; then
local key="${BASH_REMATCH[1]}"
local path="${arg2:-$PWD}"
local fullpath
fullpath=$(resolve_path "$path")
if [[ -z "$fullpath" ]]; then
echo "❌ Invalid path: $path" >&2
return 1
fi
CD_BOOKMARKS["$key"]="$fullpath"
echo "βœ… Bookmark +$key saved as $fullpath"
return 0
fi
# Jump to bookmark: cd -name
if [[ "$arg1" =~ ^-([A-Za-z_][A-Za-z0-9_]*)$ ]]; then
local key="${BASH_REMATCH[1]}"
if [[ -z "${CD_BOOKMARKS[$key]}" ]]; then
echo "❌ Bookmark not found: $key" >&2
return 1
fi
builtin cd "${CD_BOOKMARKS[$key]}" || return 1
update_stack
match_patterns
return 0
fi
# Rotate stack: cd - , --, ---
if [[ "$arg1" =~ ^-+$ ]]; then
local count=$(( ${#arg1} + 1 )) # cd - means rotate top 2, -- means 3 etc
rotate_stack_left_n "$count" 1
return 0
fi
# Rotate to index: cd -N (N digit)
if [[ "$arg1" =~ ^-([0-9]+)$ ]]; then
local idx="${BASH_REMATCH[1]}"
rotate_stack_to_index "$idx"
return $?
fi
# Swap top with stack index: cd +N
if [[ "$arg1" =~ ^\+([0-9]+)$ ]]; then
local idx="${BASH_REMATCH[1]}"
swap_stack_top_with "$idx"
return $?
fi
# Handle ... , .... etc as ../.. , ../../..
if [[ "$arg1" =~ ^\.{2,}$ ]]; then
local ups=$(( ${#arg1} - 1 ))
local path=""
for ((i=0; i<ups; i++)); do
path+="../"
done
builtin cd "$path" || return 1
update_stack
match_patterns
return 0
fi
# Normal cd
builtin cd "$arg1" || return 1
update_stack
match_patterns
}
# Override built-in cd
cd() {
cd_wrapper "$@"
}
# Initialize stack with current directory on script load
update_stack
match_patterns
# Export PS1 example usage:
#
# export PS1='\u@\h:\w$([[ "$_name" != "nomatch" ]] && echo " ($_name $_user $_proj $_rest)")\$ '
#
# This will display pattern matching variables automatically in prompt when you cd.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment