Jake Teton‑Landis

Perfection enthusiast and byte craftsman splitting time between Miami, FL and New York, NY.

Interested in human productivity, big levers, and design.

GithubMastodon
TwitterLinkedIn

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.