rbenv 仕組み

 久しぶり(2年)にRailsの勉強をするためにだいぶ昔に入れたrbenvを久しぶりに使った. ただ, 仕組みがいまいちわかってないのでコード読もうと思い読んだところ, シェルスクリプトのアレコレすら色々忘れてたのでそれを含めてメモ.

まず rbenv init - について

eval "$(rbenv init -)"

rbenv init -

export PATH="/Users/ユーザ名/.rbenv/shims:${PATH}"
export RBENV_SHELL=bash
source '/usr/local/Cellar/rbenv/1.1.0/libexec/../completions/rbenv.bash'
command rbenv rehash 2>/dev/null
rbenv() {
  local command
  command="$1"
  if [ "$#" -gt 0 ]; then
    shift
  fi

  case "$command" in
  rehash|shell)
    eval "$(rbenv "sh-$command" "$@")";;
  *)
    command rbenv "$command" "$@";;
  esac
}

 rbenv(){…}の動作は "rbenv install 2.3.1"とすると, まず変数のcommand(以下$command)には$1なので"install"が入る. そして次のifで引数がshiftして$@は"install 2.3.1"から"2.3.1"となる.

 続いてcaseで$commandが"install"で"rehash", "shell"でないので, command rbenv "$command" "$@"が実行されるが, これは /usr/local/bin/rbenv(/usr/local/Cellar/rbenv/1.1.0/libexec/rbenvへのsymlink) "install" "2.3.1"ということ. このcommandはaliasやfunction以外のコマンドを実行するshellの組み込みコマンドで, これがないと上に書いてあるfunctionのrbenvが呼ばれてしまう.

 そして, $commandが"rehash"のときはeval "$(rbenv "sh-$command" "$@")"が実行されるが, これはeval $(rbenv "sh-rehash")で, まずrbenv(このrbenvはfunciton)がもう一度呼ばれ, この際のcommandは"sh-rehash"となっているため, command rbenv "$command" "$@"が実行される. そしてこの返り値がevalで評価される. "shell"の場合も同様である. つまりこのcaseはcommandが"rehash"や"shell"の場合は command rbenv "$command" "$@" を実行した返り値がコマンド(rbenv sh-rehashやrbenv sh-shell 2.3.1などと実行するとわかる)であり, それをシェルで実行させるため, このような書き方になっている.

 /usr/local/bin/rbenvでは/usr/local/Cellar/rbenv/1.1.0/libexec/をPATHに追加して, ここにあるシェルスクリプト群を呼び出すようになっている. つまり, rbenv commandで実行されるシェルスクリプトはだいたい/usr/local/Cellar/rbenv/1.1.0/libexec/にrbenv-commandとしておいてある. ただし, rbenv rehash と rbenv shellは上述したようにrbenv sh-rehash, rbenv sh-shellとなるのでrbenv-sh-rehash, rbenv-sh-shellとなる.

 次に, rbenvのキモである/Users/ユーザ名/.rbenv/shims以下にあるファイルについて. これらはrbenv rehashつまりrbenv-sh-rehash(の中で呼ばれるrbenv-rehash)によって生成されるシェルスクリプトである. つまりruby, irb, rails等を使用する際に実際に実行しているラッパーのコマンド群. まずこれらのファイルはすべて以下のようなシェルスクリプト.

#!/usr/bin/env bash
set -e
[ -n "$RBENV_DEBUG" ] && set -x

program="${0##*/}"
if [ "$program" = "ruby" ]; then
  for arg; do
    echo "$arg"
    case "$arg" in
    -e* | -- ) break ;;
    */* )
      if [ -f "$arg" ]; then
        export RBENV_DIR="${arg%/*}"
        break
      fi
      ;;
    esac
  done
fi

export RBENV_ROOT="/Users/ユーザ名/.rbenv"
exec "/usr/local/Cellar/rbenv/1.1.0/libexec/rbenv" exec "$program" "$@"

 まず$programには${0##*/}は$0のbasenameが入る. つまり/usr/local/bin/rbenv install 2.3.1では$0は/usr/local/bin/rbenvだが, ${0##*/}によってrbenvとなる.
下記のQiita記事がシェルでのbasename, dirnameについて完結にまとまってる.
bashの変数展開によるファイル名や拡張子の取得 - Qiita

 続いて, コマンドがrubyだった場合で後続の引数にファイルが含まれる場合, つまり ruby ~/Desktop/hoge.rbなどとする場合はRBENV_DIRに/User/ユーザ名/Desktopが入る(${arg%/*}はdirnameを取得する変数展開). なお, for arg; do ~ done は for arg in "$@"; do ~ doneのショートハンド.

 この変数(RBENV_DIR)をどこで使ってるか探したところ/usr/local/Cellar/rbenv/1.1.0/libexec/rbenv-version-file(rbenvのversion指定ファイルからversionを読み取るスクリプト)にて下記の記述があった.

  find_local_version_file "$RBENV_DIR" || {
    [ "$RBENV_DIR" != "$PWD" ] && find_local_version_file "$PWD"
  } || echo "${RBENV_ROOT}/version"

 つまり, rubyコマンドで実行する.rbファイルのおいてあるディレクトリ($RBENV_DIR)においてrbenv localでrubyのバージョン指定が行われている場合, そのバージョンを利用するための変数. これを見ると, ruby ~/Desktop/hoge.rb とした場合, まずhoge.rbのある~/Desktop以下にあるlocalのバージョン指定を見て, そのあとカレントディレクトリ下のlocalのバージョンを見てるが, localの指定の優先度は 実行するrbのディレクトリ下 > カレントディレクトリ下であることがわかる.

 最後に, exec "/usr/local/Cellar/rbenv/1.1.0/libexec/rbenv" exec "$program" "$@"としている. これはつまり rbenv(functionではない方)を引数exec, "$program" "$@"で実行する(execコマンド)ということ. ちなみに, ここがcommand rbenvじゃなくてversion指定してフルパスでexecしてる理由はよくわからなかった.

 そして, rbenv-execではrbenv-version-nameで指定したrubyのバージョンを取得(ここで上述したrbenv-version-fileが使われる), rbenv-whichで指定したバージョンの実行するコマンドのフルパス(RBENV_COMMAND_PATH)を作成し, 更にそのコマンドが存在するディレクトリ(RBENV_BIN_PATH)をPATHに追加し, execしている.

 ちなみによく疑問で出て来る「rbenv rehashは何してるの」は/Users/ユーザ名/.rbenv/shims以下にラッパーを(gemなどで環境に変更があった場合, 例えばrailsを入れたらrailsコマンドのラッパーを作る)用意し, シェルのコマンド検索のハッシュテーブルを空にしている. 大切なのはたぶん前者. 後者はrbenvの仕組み的に必要かどうかはよくわからない.