I never did like pushd
and popd
because they require foresight. Screw that, let's just keep track of the last several directories on our own and then make them available with a cd-to-old-dirs function. I call it cdo
.
Put these two code blocks into your rc file (e.g. ~/.bashrc
or ~/.zshrc
):
# cdo: smarter directory history management
function precmd() { __cdo_log; } # to install in zsh, harmless in bash
PROMPT_COMMAND="__cdo_log" # to install in bash, harmless in zsh
# Populate the array DIRHIST with the last 9 dirs visited (not for direct use)
_cdo_log() {
local d dh first=true
if [[ -d "$__DIRHIST" && "$__DIRHIST" != "$PWD" ]]; then
# $__DIRHIST is the last dir we saw, but we miss foo in `cd foo; cd bar`
# so we use $OLDPWD to catch it; so you'd need a THIRD change to fool us.
for d in "$__DIRHIST" "${DIRHIST[@]}"; do
if [[ -n "$first" ]]; then unset DIRHIST first; DIRHIST[1]="$OLDPWD"; fi
if [[ "$OLDPWD" == "$d" || "$PWD" == "$d" || ! -d "$d" ]]; then
continue
fi
dh=$((1+${#DIRHIST[@]}))
[ $dh -lt 9 ] && DIRHIST[$dh]="$d" # push up to eight directories
done
elif [ -z "$__DIRHIST" ]; then
DIRHIST[1]="$OLDPWD"
fi
__DIRHIST="$PWD"
}
# zsh completion
command -v _describe >/dev/null 2>&1 && compdef _cdo cdo && _cdo() {
local -a dh
local d i=1
for d in "${DIRHIST[@]}"; do dh+=("$((i++)):$d"); done
_describe 'directory history' dh
}
This maintains a list of your eight more recently used directories (plus the current directory makes nine). I chose 8 because it wraps more nicely when you request a listing.
Bash completion doesn't have descriptions and there's nothing to complete since these are all a single character (do you really want a list of 1-8 digits?) but Z-shell completion is different. See below the code for an example.
Now the function itself:
cdo() {
local d n=0 t='~' error="Usage: cdo [NUM|ls]"
if [[ "$1" == ls ]]; then
for d in "${DIRHIST[@]}"; do echo "$((n=n+1)) <${d/$HOME/$t}>"; done \
|column |GREP_COLORS='ms=0;32' grep --color -e '\b[0-9] <' -e '>'
elif [[ 0 -eq "$#" || 0 -eq ${#DIRHIST[@]} ]]; then
cd ${OLDPWD+"$OLDPWD"}
elif test "$1" -le "${#DIRHIST[@]}" 2>/dev/null; then # zsh [[ ]] breaks here
cd "${DIRHIST[$1]}"
elif [[ -d "$1" ]]; then
cd "$1"
else
test "$1" -gt "${#DIRHIST[@]}" 2>/dev/null \
&& error="cdo log only has ${#DIRHIST[@]} items, try \`cdo ls\`"
echo "$error" >&2 && return 2
fi
}
This is basically a switch statement. First, we see if it's a listing request (cdo ls
). In that case, we'll loop through the list and number them, then we use column
to push it into columns. Coloring the numbers and braces dark green (0;32
) with grep
is convenient because it has to run after column
(otherwise the widths will be off from the colors' control characters).
Second, if we're given zero arguments or there is no saved history, just use $OLDPWD
or else go to the home directory if $OLDPWD
is blank.
Third, see if we have a number that's no higher than the history's size (errors from being given a directory will silently fail this condition). Go to that directory number.
If it's a directory, go there. Order is important; given cdo 8
with a history of six items but a directory named 8
will just go to that directory.
Finally, we have our errors. Either we say the number is too high or we give the usage, returning with error code 2 (typically used to denote invalid arguments).
The following example uses a prompt of hash directory dollar (# dir $
), chosen because Stack Overflow will make prompts gray:
# ~ $ cd /tmp
# /tmp $ cdo
# ~ $ cdo ls
1 </tmp>
# ~ $ cd /
# / $ cdo ls
1 </home/adam> 2 </tmp>
# / $ cdo
# ~ $ cdo ls
1 </> 2 </tmp>
# ~ $ cdo 2
# /tmp $ cd test
# /tmp/test $ cd ../test2
# /tmp/test2 $ cdo ls
1 </tmp/test> 3 </home/adam>
2 </tmp> 4 </>
# /tmp/test2 $ cdo 4
# / $ cd
# ~ $ cdo ls
1 </> 3 </tmp/test>
2 </tmp/test2> 4 </tmp>
Here's what the zsh tab completion looks like (pretend <tab>
is a Tab keypress):
# ~ $ cdo <tab>
directory history
1 -- /
2 -- /tmp/test2
3 -- /tmp/test
4 -- /tmp
It's really neat to see that other people have had the same general idea as me (I thought I had been original!). @SamTroliman's mention of fastcd, @null's xd, and @phildobbin's j2 all look quite similar. See also duplicate question 84445, which has a zsh answer using setopt auto_pushd
. I still prefer my own code, but ymmv.