Making chruby and binstubs play nice

Like a gentleman I use chruby and Bundler to manage Ruby versions and gems in my projects.

Instead of typing bundle exec to run gem executables within a project, I prefer saving keystrokes and using an executable’s name on its own1. I also want to avoid installing another tool like Gem home.

So off to binstubs land I go. Bundler generates them for you and these days Rails even ships with a few as standard. These stub files live in your project and ensure the right set of gems for your project are loaded when they’re executed.

Security risks aside I could just prepend my path with ./bin: and walk away—except that chruby auto-switching spoils the party. When I enter a project directory with a .ruby-version file, chruby prepends the current Ruby version paths at the beginning of PATH thereby matching before my previously prepended ./bin:.

Chruby recommends using rubygems-bundler but I don’t want to install another gem to get this to work. So I tweaked my zsh setup to use preexec_functions like chruby to patch my PATH. I add my function to preexec_functions after chruby loads so that my code patches the PATH after chruby does its work.

As for security I use the same scheme as Tim Pope. Add a git alias for marking a git repository as trusted and then only add a project’s bin directory to PATH if it is marked as such.

Now I just mark a repo as trusted via git trust, and its local binstubs are automatically added to my path.

Changes in my .zshenv:

# Remove the need for bundle exec ... or ./bin/...
# by adding ./bin to path if the current project is trusted

function set_local_bin_path() {
  # Replace any existing local bin paths with our new one
  export PATH="${1:-""}`echo "$PATH"|sed -e 's,[^:]*\.git/[^:]*bin:,,g'`"
}

function add_trusted_local_bin_to_path() {
  if [[ -d "$PWD/.git/safe" ]]; then
    # We're in a trusted project directory so update our local bin path
    set_local_bin_path "$PWD/.git/safe/../../bin:"
  fi
}

# Make sure add_trusted_local_bin_to_path runs after chruby so we
# prepend the default chruby gem paths
if [[ -n "$ZSH_VERSION" ]]; then
  if [[ ! "$preexec_functions" == *add_trusted_local_bin_to_path* ]]; then
    preexec_functions+=("add_trusted_local_bin_to_path")
  fi
fi

The git trust alias from my .gitconfig:

[alias]
  # Mark a repo as trusted
  trust = "!mkdir -p .git/safe"

  1. Even though I’ve aliased bundle exec to be in my shell I still feel like an animal when I have to type it. ↩︎