rbenvの切り替えの仕組み…と、他言語での実験

rbenvを使ってみる - すぎゃーんメモの続き。
現時点でのrbenvのバージョンは0.2.1。
rbenvを使っていると.rbenv-versionファイルの有無でrubyコマンド打ったときに実行されるrubyが違うものになる、というのがちょっと新鮮で、これはどういう仕組みで動いているのだろう?と思って少し調べてみた。
上記記事のようにrbenvの設定をした環境では、

$ which ruby
/Users/sugyan/.rbenv/shims/ruby

となり、${RBENV_ROOT}/shims以下のrubyを指すことになる。ここへのPATHは$HOME/.rbenv/libexec/rbenv-initに

echo 'export PATH="'${RBENV_ROOT}'/shims:${PATH}"'

と書かれているので、eval "$(rbenv init -)"してあれば優先して通っているはず。


で、この${RBENV_ROOT}/shims/rubyの中身を見てみると…

#!/usr/bin/env bash
set -e
export RBENV_ROOT="/Users/sugyan/.rbenv"
exec rbenv exec "${0##*/}" "$@"

とだけ書いてある。これは同ディレクトリにあるgemやirbやrakeなども全部同じ内容。結局このPATHに入っているコマンドを呼ばれた場合はすべて同じように処理される、ということになる。
このシェルスクリプトの内容としては

set -e

でエラー時の処理を設定。全然知らなかったけどbashのドキュメントによると

Exit immediately if a pipeline (see Pipelines), which may consist of a single simple command (see Simple Commands), a subshell command enclosed in parentheses (see Command Grouping), or one of the commands executed as part of a command list enclosed by braces (see Command Grouping) returns a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command's return status is being inverted with !. A trap on ERR, if set, is executed before the shell exits.

http://www.gnu.org/software/bash/manual/bashref.html#The-Set-Builtin

というものらしい。例えば

#!/usr/bin/env bash
set -e
test -e $0
echo 'finish!'

とかだと正常に最後の"finish!"が出力されるが、

#!/usr/bin/env bash
set -e
test -d $0
echo 'finish!'

だと3行目のtestコマンドが0でない終了コマンドを返すので、その時点でこのシェルスクリプトが終了して"finish!"は出力されない。
毎行ごとにエラーチェックと終了処理を挟むような場合はset -eをしておくと良い、ということか。


ちょっと脱線した。肝心なのは

export RBENV_ROOT="/Users/sugyan/.rbenv"
exec rbenv exec "${0##*/}" "$@"

の部分。変数展開${0##*/}は

The word is expanded to produce a pattern just as in filename expansion (see Filename Expansion). If the pattern matches the beginning of the expanded value of parameter, then the result of the expansion is the expanded value of parameter with the shortest matching pattern (the ‘#’ case) or the longest matching pattern (the ‘##’ case) deleted. If parameter is ‘@’ or ‘*’, the pattern removal operation is applied to each positional parameter in turn, and the expansion is the resultant list. If parameter is an array variable subscripted with ‘@’ or ‘*’, the pattern removal operation is applied to each member of the array in turn, and the expansion is the resultant list.

http://www.gnu.org/software/bash/manual/bashref.html#Shell-Parameter-Expansion

ということでフルパスから最後の"/"までの部分を取り除いたものを取得することになる。${RBENV_ROOT}/shims/rubyを

#!/usr/bin/env bash
set -e
export RBENV_ROOT="/Users/sugyan/.rbenv"
echo $0
echo ${0##*/}
# exec rbenv exec "${0##*/}" "$@"

と書き換えて${RBENV_ROOT}/shims/rubyを呼んでみると、

$ ruby
/Users/sugyan/.rbenv/shims/ruby
ruby

となる。つまり、${RBENV_ROOT}/shims/rubyを呼ばれた場合は最終的には

exec rbenv exec ruby "$@"

と実行される形になる。rbenv execは内部的には${RBENV_ROOT}/libexec/rbenv-execをexecする形になっていて、このrbenv-exec内では

...
RBENV_COMMAND="$1"
...
RBENV_COMMAND_PATH="$(rbenv-which "$RBENV_COMMAND")"
RBENV_BIN_PATH="${RBENV_COMMAND_PATH%/*}"
...
shift 1
export PATH="${RBENV_BIN_PATH}:${PATH}"
exec -a "$RBENV_COMMAND" "$RBENV_COMMAND_PATH" "$@"

となっていて、rbenv-whichから取得できた"実行すべきrubyの実体へのPATH”を取得して、execして引数をわたしてやる、ということをしているようだ。
で、${RBENV_ROOT}/libexec/rbenv-whichは

...
RBENV_VERSION="$(rbenv-version-name)"
RBENV_COMMAND="$1"
...
if [ "$RBENV_VERSION" = "system" ]; then
  PATH="$(remove_from_path "${RBENV_ROOT}/shims")"
  RBENV_COMMAND_PATH="$(command -v "$RBENV_COMMAND")"
else
  RBENV_COMMAND_PATH="${RBENV_ROOT}/versions/${RBENV_VERSION}/bin/${RBENV_COMMAND}"
fi
...

if [ -x "$RBENV_COMMAND_PATH" ]; then
  echo "$RBENV_COMMAND_PATH"
else
...

というかたちになっていて、実行すべきrubyのバージョンを$(rbenv-version-name)から取得し、そこへのパスを返している。
${RBENV_ROOT}/libexec/rbenv-version-nameを見てみると

...
if [ -z "$RBENV_VERSION" ]; then
  RBENV_VERSION_FILE="$(rbenv-version-file)"
  RBENV_VERSION="$(rbenv-version-file-read "$RBENV_VERSION_FILE" || true)"
fi

if [ -z "$RBENV_VERSION" ] || [ "$RBENV_VERSION" = "system" ]; then
  echo "system"
  exit
fi
...

のように書いてあり、${RBENV_VERSION}が設定してあればそれを見て、無ければ$(rbenv-version-file)から取ってくる、ということをしているのがわかる。つまり最優先されるのは${RBENV_VERSION}なのでそれが指定されていればglobal, localの指定にも関係なくそれを使うことになる。

$ rbenv global
1.9.3-p0
$ rbenv local
system
$ ruby -v
ruby 1.8.7 (2010-01-10 patchlevel 249) [universal-darwin10.0]
$ RBENV_VERSION=1.9.2-p290 ruby -v
ruby 1.9.2p290 (2011-07-09 revision 32553) [x86_64-darwin10.8.0]

で、${RBENV_ROOT}/libexec/rbenv-version-fileでは

...
root="$RBENV_DIR"
while [ -n "$root" ]; do
  if [ -e "${root}/.rbenv-version" ]; then
    echo "${root}/.rbenv-version"
    exit
  fi
  root="${root%/*}"
done
...

となっていて、${RBENV_DIR}から上に遡っていって最初に見つかった.rbenv-versionファイルを使うことになる。

要するに

rbenvを使用する設定をした状態でrubyコマンドを叩くと、

  1. ${RBENV_ROOT}/shims/rubyが呼ばれ、その中で
  2. rbenv-execが呼ばれ、その中では
  3. rbenv-whichで実行すべきrubyを探すが、それは中で
  4. rbenv-version-nameで実行すべきrubyのバージョンを特定し、そいつが中で
  5. rbenv-version-fileで.rbenv-versionファイルがあるかどうか調べる

という流れになる。.rbenv-versionファイルが見つかれば、

  1. rbenv-version-fileがそのファイルパスを返し、
  2. rbenv-version-nameがそこから読み取った${RBENV_VERSION}を返し、
  3. rbenv-whichがそのバージョンに対応する${RBENV_COMMAND_PATH}を返し、
  4. rbenv-execがそのコマンドを実際に実行する

というかんじ。なかなか深い…。

ということは

特にruby特有の仕組みを使っておらず、ほぼシェルスクリプトだけで実現されているので、他の似たようなスクリプト言語でもこの仕組みは適用できそう。
ということでPerlで実験してみた。
まずはrbenvとまったく同じものをplenvという名前でcloneする。

$ cd
$ git clone git://github.com/sstephenson/rbenv.git .plenv

s/rbenv/plenv/g になるよう全部置換してplenv-*を生成

$ for i in $(find .plenv/libexec -name 'rbenv*'); do perl -pe 's/rbenv/plenv/g;s/RBENV/PLENV/g' $i > ${i/rbenv/plenv}; chmod u+x ${i/rbenv/plenv}; done

シンボリックリンクを作成してやる

$ ln -s ../libexec/plenv .plenv/bin

これでだいたいオッケー。$HOME/.zshenvにPATHと設定を追記。

# plenv
path=($HOME/.plenv/bin(N) $path)
eval "$(plenv init -)"

これでplenvコマンドは使えるようになる。
versions以下から、perlbrewでinstall済みのperlにシンボリックリンクを貼ってやる(新規インストールでも良いはず)。

$ ln -s $HOME/perl5/perlbrew/perls/perl-5.12.1 $HOME/.plenv/versions/5.12.1
$ ln -s $HOME/perl5/perlbrew/perls/perl-5.14.2 $HOME/.plenv/versions/5.14.2

すると、

$ plenv versions
  5.12.1
  5.14.2

バージョン選択できるようになる。

$ plenv global 5.14.2
$ plenv versions
  5.12.1
* 5.14.2 (set by /Users/sugyan/.plenv/version)

global設定完了。最後に、$HOME/.plenv/shims/perlを

#!/usr/bin/env bash
set -e
export PLENV_ROOT="/Users/sugyan/.plenv"
exec plenv exec "${0##*/}" "$@"

と書いて実行権限を付与する。

$ perl -v

This is perl 5, version 14, subversion 2 (v5.14.2) built for darwin-2level
...

$ PLENV_VERSION=5.12.1 perl -v

This is perl 5, version 12, subversion 1 (v5.12.1) built for darwin-2level
...


$ PLENV_VERSION=system perl -v

This is perl, v5.10.1 (*) built for darwin-2level
...

$ cd ~/temp
$ ls .plenv-version
ls: .plenv-version: No such file or directory
$ perl -v

This is perl 5, version 14, subversion 2 (v5.14.2) built for darwin-2level
...

$ plenv local 5.12.1
$ ls .plenv-version
.plenv-version
$ cat .plenv-version
5.12.1
$ perl -v

This is perl 5, version 12, subversion 1 (v5.12.1) built for darwin-2level
...

同じようなかんじで切り替えできますね。