Ruby
Gem
bundler
rbenv

bundle exec を理解したい。

$ bundle exec rails s

bundle exec ってなんぞ?

前提

  • rbenv
  • ruby 2.3.7

結論

rbenvが下準備を整えてから、bundler gem の bin にあるbundle を実行。
その bundle が引数のコマンドを exec する!
(完全な理解はしてない。)

Outline

  1. bundle
  2. exec
  3. bundle exec

bundle

これはシェルのコマンド。シェルのコマンドってなんぞ?
組込コマンド or $PATH にいるファイルの名前(厳密には違うだろう <-)
bundle は後者であると予想されるため、中身を見てみる。

% which echo
echo: shell built-in command
% which bundle
/Users/nishigami.ryosuke/.rbenv/shims/bundle
% cat `which bundle`
#!/usr/bin/env bash
set -e
[ -n "$RBENV_DEBUG" ] && set -x

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

export RBENV_ROOT="/Users/nishigami.ryosuke/.rbenv"
exec "/Users/nishigami.ryosuke/.rbenv/libexec/rbenv" exec "$program" "$@"

注目すべきは最後の行。これが bundle のメイン処理とにらみをつける。
exec "/Users/nishigami.ryosuke/.rbenv/libexec/rbenv" exec "$program" "$@"

つまりこの、exec のシンタックスを理解しなければ自分は次のステージに進めない。。
exec hogehoge exec hoge $@

exec

% which exec
exec: shell built-in command
% help exec
exec [ -cl ] [ -a argv0 ] [ command [ arg ... ] ]
       Replace  the current shell with command rather than forking.  If
       command is a shell builtin command  or  a  shell  function,  the
       shell executes it, then immediately exits.

       ...

ほう。
とりあえず試してみる。

% exec echo hoge
hoge

[プロセスが完了しました]

なるほど。

Replace the current shell with command rather than forking.
then immediately exits.

多分zshのプロセスが echo に置き換えられ、exit した。
子プロセスが exit した訳ではないのでこうなる。
気を取り直してもう一回。

% exec echo hoge | cat
hoge
% exec ruby -e 'puts %i[hoge fuga]' | cat
hoge
fuga

exec はただ引数のコマンドを今のプロセスで実行してexitするもの(らしい)という理解を得た!

bundle exec

exec "/Users/nishigami.ryosuke/.rbenv/libexec/rbenv" exec "$program" "$@"

これを解いてゆく。

bundle exec rails s
の場合上記はこうなる。

exec "/Users/nishigami.ryosuke/.rbenv/libexec/rbenv" exec "bundle" "exec rails s"

exec サンドイッチ祭りだ。

% cat /Users/nishigami.ryosuke/.rbenv/libexec/rbenv
...

command="$1"
case "$command" in

...

* )
  command_path="$(command -v "rbenv-$command" || true)"

  ...

  shift 1
  if [ "$1" = --help ]; then
    if [[ "$command" == "sh-"* ]]; then
      echo "rbenv help \"$command\""
    else
      exec rbenv-help "$command"
    fi
  else
    exec "$command_path" "$@"
  fi
  ;;
esac

/Users/nishigami.ryosuke/.rbenv/libexec/rbenv-exec が実行される。
つまりこうゆう状況。
exec /Users/nishigami.ryosuke/.rbenv/libexec/rbenv-exec bundle exec rails s

この辺りで気づく。自分が rbenv を使っているので bundlerbenv 下でいい感じに実行されることに。
そしてこの記事が rbenv の場合に限定された話に絞られていたことに。。
構わず続ける。

% cat /Users/nishigami.ryosuke/.rbenv/libexec/rbenv-exec
...

RBENV_COMMAND="$1"

...

RBENV_COMMAND_PATH="$(rbenv-which "$RBENV_COMMAND")"

...

shift 1

...

exec -a "$RBENV_COMMAND" "$RBENV_COMMAND_PATH" "$@"

最後の行はこう。

exec -a bundle /Users/nishigami.ryosuke/.rbenv/versions/2.3.7/bin/bundle exec rails s

% cat /Users/nishigami.ryosuke/.rbenv/versions/2.3.7/bin/bundle
#!/Users/nishigami.ryosuke/.rbenv/versions/2.3.7/bin/ruby
#
# This file was generated by RubyGems.
#
# The application 'bundler' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0.a"

if ARGV.first
  str = ARGV.first
  str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
  if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
    version = $1
    ARGV.shift
  end
end

load Gem.bin_path('bundler', 'bundle', version)

これはほぼこう

#!/Users/nishigami.ryosuke/.rbenv/versions/2.3.7/bin/ruby

require 'rubygems'

load Gem.bin_path('bundler', 'bundle', version)

一行目をご覧の通り、シェルの守備範囲を抜けていつの間にかrubyになっている!!
bundler gemの bundle ってbinをload(実行)してる。(と予想)
シェルの守備範囲を抜けたということで今回は一旦ここまで。