Optimizing chruby for Zsh

ruby shell
Posted on: 2015-01-30

I use chruby and it's auto-switching behavior to switch between Ruby versions. I also use Zsh instead of bash.

Not long ago, I noticed some errors that didn't make sense to me, like chruby: unknown Ruby: ruby-1.9.3. But if I checked the .ruby-version file in that directory, it would specify something else.

Eventually I figured out what I was doing. I'd open a terminal window in a folder with an outdated .ruby-version and see no error. Then I'd cd to a folder with a good .ruby-version and see the error.

This is because chruby's way of auto-switching is "right before you run a command, check for .ruby-version and switch to what it specifies." (A terrible hack is needed for this in bash.) In my case, it was running before cd, which meant it was running in the wrong directory.

To me, it seemed better to use this strategy: "When the shell starts, and after any change of directory, check for .ruby-version and switch to what it specifies".

I tried to fix this, but the bottom line is that it's seemingly impossible in bash, and postmodern wants chruby to work the same way for both shells. Which is admirable.

However, I only use Zsh, and Zsh provides a better way: hook functions. Specifically, chpwd_functions+=("chruby_auto") means "run the function called chruby_auto whenever you change directories."

So in my zsh configuration, I now source a file that contains this:

# Enable chruby
source /usr/local/opt/chruby/share/chruby/chruby.sh

# Enable auto-switching of Rubies specified by .ruby-version files
# in current or parent directory
# Taken from https://raw.githubusercontent.com/postmodern/chruby/c260570c49fedb77b30b9948b798ac0e5046b63d/share/chruby/auto.sh
unset RUBY_AUTO_VERSION

function chruby_auto() {
  local dir="$PWD/" version

  until [[ -z "$dir" ]]; do
    dir="${dir%/*}"

    if { read -r version <"$dir/.ruby-version"; } 2>/dev/null || [[ -n "$version" ]]; then
      if [[ "$version" == "$RUBY_AUTO_VERSION" ]]; then return
      else
        RUBY_AUTO_VERSION="$version"
        chruby "$version"
        return $?
      fi
    fi
  done

  if [[ -n "$RUBY_AUTO_VERSION" ]]; then
    chruby_reset
    unset RUBY_AUTO_VERSION
  fi
}

# Auto-detect whenever we change directories. 
# It was too hard to contribute
# this back because bash just can't do it.
chpwd_functions+=("chruby_auto")

# Run once as the shell starts up
chruby_auto

It's a small difference, but it makes me happy; I never get an error about an unknown Ruby unless it applies to my current directory.

Update and a Warning

I found one way that this can cause trouble. I moved into a project directory, saw that its .ruby-version called for a Ruby I didn't have, and did ruby-install to get it. Then I ran gem install bundler, followed by bundle. I assumed that I was running ~/.gem/ruby/2.2.2/bin/bundle, the right one for the new version of Ruby. But I was actually running /usr/bin/bundle, which compiled some C gems like json against the wrong version of Ruby and caused me mysterious segfaults until I figured it out.

This wouldn't have happened with the typical chruby configuration, because chruby calls hash -r, which means "update your list of what command lives where", and the typical config does that before every command. (This effectively renders the hash of command locations useless, since it's cleared before every lookup. On the other hand, it prevents the problem I created for myself, and computers are fast, so...)

In my case, cd ..; cd - or just hash -r does the trick... but only once I knew to do it. Which I learned through much pain. :)

To prevent future mishaps, I edited /usr/bin/bundle to yell at me and exit, because I never want it anyway.

So the moral is: never try anything new, and always follow the crowd. No, wait, the moral is: take risks, get hurt, and learn stuff from it.

Anyway, you've been warned.