Ruby gems内でファイルをロードする際に気を付けること
おしごとでちょっぴりハマったので記す。
相対パスをそのまま書くとコケる
例えばこんな感じのGemを実装したとする。
reqiure 'yaml' module Hoge class Fuga SampleData = YAML.load_file('./sample_data.yml') def self.fuga ... end end end
sample_data
をYAMLファイルに定義しておいて、Gem内で利用することを考える。
これはRuby Gemsの開発中は特に何の問題も発生しない。
しかし、いざこのGemを公開し、bunlderでインストールして別のRubyプログラムで利用すると困ったことになる。
require 'hoge' class Foo def bar Hoge::Fuga.fuga ... end end
$ bundle exec ruby sample_code.rb #=> Errno::ENOENT (No such file or directory
これはRubyが、プロセスが実行された場所、この場合ではsample_code.rb
が存在するディレクトリからの相対パスでファイルを探しに行こうとしに行ってしまうために起こる。
そのため、何らかの方法でGemファイルのソースがある場所からの相対パスでファイルを指定してあげる必要がある。
ここで役立つのが、__dir__
メソッド。見た目がなんか特殊な変数っぽく見えるけど、実はこれKernel#__dir__
というメソッド
として実装されている。
現在のソースファイル(FILE)のあるディレクトリ名を正規化された絶対パ スで返します。シンボリックリンクも解決されます。また、FILE が nil の場合は nil を返します。
上記の通りこの__dir__
を呼び出すとソースファイルのあるディレクトリ名を正規化された絶対パスで返してくれる。
これをさらにFile#join
と組み合わせることで、Gemのディレクトリ内に存在するファイルを読み込むことができるようになる。
YAML.load_file(File.join(__dir__, 'sample_data.yml'))
ちなみに、Pythonでは__file__
や__dir__
は特殊メソッドとして実装されている。
また、PHPではマジカル定数という名前で利用されている。
https://www.php.net/manual/ja/language.constants.predefined.php
余談: __dir__
ってなんでどこでも使えるの?
__dir__
はKernel
のメソッドで、Objectが継承しているから
Kernel
はObject
が継承しており、Rubyのほとんどのオブジェクトは継承チェーンをたどってKernel#__dir__
を利用できる状態になっている。
さらに、__dir__
のレシーバを省略すると、暗黙的にself
をレシーバとするのだが、トップレベルの場合はmain
という名前のObject
オブジェクトがレシーバとなるため、トップレベルでも、そうでなくても、__dir__
をレシーバなしで変数のように呼び出すことができる。
puts "self:#{self}" puts "self.class:#{self}" puts "self.class.ancestors:#{self.class.ancestors}" puts self.private_methods.grep(/__dir__/) #=> self:main self.class:Object self.class.ancestors:[Object, Kernel, BasicObject] __dir__
Kernelモジュールには他にもいくつかメソッドが定義されていて、__dir__
の他にも__method__
や__callee__
などが使える。
上記のページを読むと分かるが、__FILE__
はKernelモジュールのメソッドではないらしい。
irb(main):001:0> Kernel.methods.grep(/__FILE__/) => [] irb(main):002:0> self.private_methods.grep(/__FILE__/) => []
main
のメソッドとしても発見できないため、特別な定数として実装されてるかもしれない。*1
おわりに
メタプログラミングRubyを最近ちょくちょく読んでるおかげで「なんでRubyではこう書かないといけないんだっけ?」が少しずつ分かる機会が増えてきたように感じる。近いうちに読書メモも残しておきたい。
*1:mainに対してprivate_methodsを呼んでいるのは、モジュール関数がprivateメソッドとして利用可能になるため