goto
February 2011 - February 2018
At first, you're content with cd
. It's fun and novel to navigate your
computer with paths in a terminal, and it feels faster than the double-clicking
you used to do in the Finder. Plus, you get to feel like you're on the way to
being a hacker.
But as the years go by, even with tab completion, you begin to tire of writing
the same locations over and over again. Even browsing the nice zsh completion
list barely eases your pain. How many times this semester will you type cd ~/src/cs61a
? How many times will you type vim ~/.zshrc
then . ~/.zshrc
?
Fifty times? A hundred times? Thousands of times? It's time to do something
about this.
alias webtree="clear; cd $webtree; ls"
alias oldtree="clear; cd ~/src/webtree-legacy; ls"
alias prefix="cd $PREFIX"
alias templates="clear; cd ~/src/templates; ls"
alias lib="clear; cd ~/src/lib/; ls"
From the author's .zshrc, circa 2011
This works for a while, but as you acrete aliases over the years, you begin to
notice some patterns. You've written the same characters many times. Didn't
your favorite teachers tell you, "Don't repeat yourself"? Instead of feeling
the pain when you cd
, now you feel the pain when you're in your .zshrc
adding another alias. "You know how I'd solve this in Ruby?" you say. "I'd use
Hash#map
". So you break out your Google (who could know that the syntax for a
map in zsh
is typeset -A var_name
?) and come up with a nice little loop.
typeset -A jump_dirs
jump_dirs=(
zshall "~/.zsh
webtree "~/src/webtree"
prefix "$PREFIX"
lib "~/src/lib"
stats "~/src/jarkstats.js"
gsd "~/src/GroovesharkDesktop"
)
for short in ${(k)jump_dirs}; do
alias $short="cd $jump_dirs[$short]"
done
Inspired by the author's .zshrc, circa 2014
Very cool. This is certianly a nice way to arrange things. It's not the next vim-tmux-navigator, but it has a certain utilitarian asthetic in your dotfiles. It breaks up the monotony. And, when you need to add another edit-this-thing-real-quick alais, you feel no pain.
A few years pass. You've been working at a "real company" for a while; working
on code at a scale far larger than a few CS projects here, and a few student
job repos there. There's 50 repos checked out in ~/src, and you find yourself
in a dozen of them today, and a different dozen tomorrow. It feels pointless
adding another mapping to quick_dirs
to generate an alias -- a new repo will
appear tomorrow, and you'll need another mapping.
# considered harmful
goto () {
cd ~/src/"$1"
}
The function does the trick for new files in ~/src, but now that you're writing
Golang, you can't keep your projects in ~/src. Daddy knows best, and he says to
put your files in $GOPATH
. You're used to typing goto monorail
. It seems
rude, somehow, when you type goto encabulator
, but instead of happily
agreeing, zsh
tells you "cd: no such file or directory: ~/src/encabulator".
Weren't you just there in ~/.../encabulator a moment ago? You stomp off to add
yet another Go project to jump_dirs
when you realize you just can't take it
anymore.
# paths in which to find projects or directories
# top path wins
goto_search_paths=(
$GOPATH/src/*/*
~/airlab-stable/repos
~/src
)
goto () {
for p in $goto_search_paths; do
local it="$p/$1"
if [ -e "$it" ] ; then
echo "cd $it"
cd "$it"
return 0
fi
done
echo "goto: not found: $1"
return 1
}
You've done it. You've solved the cd
problem forever. You can cd
to any go
project, be it yours or a dependency. You can cd
to any work project. And you
can cd
to the rest of the stuff in ~/src. All is right in the world. Until
you have to copy a file from one project to another. You type this in your shell:
$ goto slipnslide
$ cp slipnslide.gemspec ~/src/dep-checker/spec/fixtures/slipnslide
cp: ~/src/dep-checker/spec/fixtures/slipnslide: No suck file or directory
You've forgotten where any project is checked out. You rely entirely on goto
. So...
$ goto dep-checker
$ dep_checker="$PWD"
$ goto slipnslide
$ cp slipnslide.gemspec $dep_checker/spec/fixtures/slipnslide
(Successful silence...)
Just how many times are you gonna perform that goto-then-set-a-var-then-goto-again operation? You're a shell pro. You're not gonna do it more than 16 or 17 times before you've had enough. You've been reading "The Art of Unix Programming" because someone extremely opinionated told you to. And what you've learned, is that you should remind yourself to practice The Unix Arts. Because you've been using too much Ruby lately.
It's time to compose.
# paths in which to find projects or directories
# top path wins
goto-refresh-search-paths () {
# assign this variable in a function, so that we freshly expand any globs
# every time we run a goto- command.
goto_search_paths=(
$GOPATH/src/*/*
~/airlab-stable/repos
~/src
~/src/disabled-repos
)
}
goto-which () {
goto-refresh-search-paths
for p in $goto_search_paths; do
local it="$p/$1"
if [ -e "$it" ] ; then
echo "$it"
return 0
fi
done
echo "goto-which: not found: $1" > /dev/stderr
return 1
}
goto () {
goto-refresh-search-paths
local dir=$(goto-which "$1")
if [ -n "$dir" ]; then
echo "cd $dir"
cd "$dir"
fi
}
There we go. Even has some good touches like writing its error messages to
STDERR, so that when you use $(goto-which does-not-exist)
, you get "" back
instead of interpreting the error as a directory name. You're unstoppable.
Nothing can stop you. It's time to go all the way, and add tab completion as
long as you've got 19_goto.sh open in nvim.
_goto () {
goto-refresh-search-paths
echo "_goto: $COMP_CWORD" >&2
COMPREPLY=()
# Only complete one word
if [[ $COMP_CWORD -gt 1 ]]; then
return 0
fi
# Complete with matching paths inside our search paths
local cur
cur="${COMP_WORDS[COMP_CWORD]}"
echo "_goto: cur = ${cur}" >&2
for p in $goto_search_paths; do
COMPREPLY+=("$p/$cur"*)
done
}
if [[ "$SHELL" == *"zsh" ]]; then
autoload bashcompinit
bashcompinit
fi
complete -F _goto goto
complete -F _goto goto-which
It doesn't work. Even though you loaded the bash completion support for zsh. Well, at least you tried. You're a hacker.
Update (2019-01-23): The current version of the goto
shell function and friends has working
tab completion. You can find it online
in my dotfiles repo.