たごもりすメモ

コードとかその他の話とか。

Ruby 1.9.2p0 で requireとrequire_relativeについて調べてみた

先日のこのエントリの続き。
http://d.hatena.ne.jp/tagomoris/20100924/1285322426

Ruby内部のコードを追っかけるのは時間かかるのでとりあえず置いておいて、1.9.2になってLOAD_PATHの仕様変更(カレントディレクトリが除外された)にともなって動作が変わった require と、追加された require_relative の実際の動作について追ってみた。手がかりは前のエントリにもらったコメント。

まず大前提

eval の第3引数にファイルパスを渡すことで、評価される文字列の中に __FILE__ (など)がある場合は第3引数(および第4引数の行数)を展開してくれる。
これがわかっておりませんでした。rackupすると最終的にconfig.ruをevalしてるのは以下のコード。

/* rack-1.2.1/lib/rack/builder.rb */
module Rack
  /* snip */
  class Builder
    def self.parse_file(config, opts = Server::Options.new)
      options = {}
      if config =~ /\.ru$/
        cfgfile = ::File.read(config)
        if cfgfile[/^#\\(.*)/] && opts
          options = opts.parse! $1.split(/\s+/)
        end
        cfgfile.sub!(/^__END__\n.*/, '')
        app = eval "Rack::Builder.new {( " + cfgfile + "\n )}.to_app",
          TOPLEVEL_BINDING, config
      else
        require config
        app = Object.const_get(::File.basename(config, '.rb').capitalize)
      end
      return app, options
    end

ということで、evalするときにconfig.ruのパスが渡ってるので、ファイル内に __FILE__ 書いといたら正常に config.ru のパスに展開されるわけですね。これならポータブルなやりかたと言える。

require_relativeは?

で、じゃあ eval に第3引数を渡してるなら require_relative が使えてもおかしくない気がするんですけど。いちおう追試してみましたが、やっぱりダメでした。こんな感じ。

dhcp79:hoge tagomoris$ irb19
irb(main):001:0> data = IO.read('/Users/tagomoris/tmp/hoge/evalscript.rb')
=> "require_relative 'required_module'\n"
irb(main):002:0> data
=> "require_relative 'required_module'\n"
irb(main):003:0> returned = eval data, TOPLEVEL_BINDING, '/Users/tagomoris/tmp/hoge/evalscript.rb'
LoadError: cannot infer basepath
	from /Users/tagomoris/tmp/hoge/evalscript.rb:1:in `require_relative'
	from /Users/tagomoris/tmp/hoge/evalscript.rb:1:in `<main>'
	from (irb):3:in `eval'
	from (irb):3
	from /usr/local/bin/irb19:12:in `<main>'
irb(main):004:0>

ruby-dev MLをひと通り漁ってみたけど誰も何も言ってないなー。投稿してみようかなあ。でもどうなんだろうな。

requireのやりかた

前のエントリにコメントで require './app' でいいじゃない、というのがありました。
いやそれって '.' からの相対パス*1なんだからダメでしょ……と思ったら、なんと 1.9.2p0 でも通ってしまった。これってバグなんじゃねーの? リファレンスの記述でも、はっきりと $: ($LOAD_PATHね)から探す、と書いてあるし。
ちなみにカレントディレクトリに app.rb がないと動かないので、rackup コマンドだけならともかくアプリケーションサーバにデプロイすると動かないんじゃないかと思います。だから実際に今の用途としては使えないかな。

で __FILE__ が使えるんでこれを使いましょう、ということですが、せっかく require_relative で全部書き直したのにアプリケーションのディレクトリを LOAD_PATHに入れるのはなー、というのが個人的な意見。ちなみにその場合のコードは前のエントリで id:aquarla に書いてもらったこちらのコード
config.ru 内で読み込むファイルが多いとLOAD_PATHに入れないとやってられないような気もするけど、普通はアプリケーション本体のファイルひとつだけだと思うので、それだったらrequire用の絶対パスを生成してもまあいいんじゃないでしょうか、ということでこっちにしてみた。

app_dir = File.expand_path(File.dirname(__FILE__))
require app_dir + '/app'
run Sinatra::Application

これならどこに持っていっても動く。やれやれ。

さて、requireとrequire_relativeの動作についてはどうしようかな……。なんとなく他にもハマる人はいそうな気がするんだけど。

*1:もしくは期待してるのは __FILE__ からの相対パス