読者です 読者をやめる 読者になる 読者になる

gaaamiiのブログ

悪気なく間違ったことを書いているときがあります。コメントやTwitter、ブコメなどでご指摘ください

railsコマンドを追う

Ruby on Rails Ruby

Ruby on Railsというフレームワークを使うとrails new Hogeとかでアプリケーションのひな形ができちゃって、rails serverでサーバーが立ち上げられたりするわけですが、これは一体どうなってるんだというのを追っていけたらなと思います。誰にでもわかるように書きたいです。今回こそはくじけずに書ききりたい。

railsとbin/railsの違い

railsはシステムにインストールされたrailsコマンドを呼ぶ(/Users/ユーザー名/.rbenv/shims/railsみたいな)。

bin/railsはそのプロジェクト下のbin/railsのコマンドを呼ぶ。

bin/rails

Railsプロジェクトを作ると、binというディレクトリの中にrailsというファイルがある。これをエディタで開いてみる。

$ vim bin/rails

中身はこんな感じ。

#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application',  __FILE__)
require_relative '../config/boot'
require 'rails/commands'

たった4行だ。

1行目

#!/usr/bin/env ruby

いきなり意味がわからない。

シバン

この記述、「シバン」(shebang)と呼ばれるもので、インタプリタを指定するために書かれるらしい。普通にRubyスクリプト実行するときはruby rails.rbみたいに実行するんだけど、今回はrailsというファイルをrubyで解釈してもらいたいわけなので、ファイル内で「Ruby使うよ」っていうのを言ってあげる必要がある。そのため、このshebangが必要になっている。

要は、#!という2文字でOSに対して「これshebang行だからね」と伝え、/usr/bin/env rubyrubyインタプリタを使うと伝えてる。

2行目

APP_PATH = File.expand_path('../../config/application',  __FILE__)

APP_PATHに入るのがなにか分からない場合はこの行の後にp APP_PATHというコードを挿入してbin/railsを実行すれば見れる。

"/Users/ユーザー名/プロジェクト名/config/application"

このAPP_PATH、どこで使うことになるんだろう...?

3行目

require_relative '../config/boot'

これは普通に、config/boot.rbをrequireしてる。require_relativeを使うと、相対パスを指定してファイルを読み込むことができる。

boot.rb

では、このboot.rbの中身を見てみる。

1行目
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

ENVというのはRuby環境変数を表すオブジェクト。ハッシュと違う点はキーにも値にも文字列しか指定できないというところ。

何やらプロジェクトのGemfileのパスをENV['BUNDLE_GEMFILE']という環境変数に入れているようだ。||=という演算子は「無かったら(厳密には「偽か未定義なら」)入れてね」という意味。

2行目
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])

ENV['BUNDLE_GEMFILE']にはGemfileのパスが入っている。「そこにGemfileあったらbundler/setupをrequireしてね」というような意味。

requireはどこを探しに行くのか

この記事がわかりやすい。

requireは引数のファイル名のRubyファイルを読み込んで実行するメソッドです。引数が絶対パスだったときはそのファイルを、そうでない場合はロードパスを優先順位上位から辿って最初に見つかったファイルをロードする。

じゃあ上記のrequire 'bundler/setup'はどうなってるのか。

rails consoleirbを立ち上げて、以下を実行する。

$LOAD_PATH.each {|path| p path };nil

ぐあーっとパスが表示されるので、bundlerがありそうなところを探す。ちなみに表示する際、返り値として$LOAD_PATHが返ってくるととても見づらいのでここではセミコロンで区切ってnilを返り値にしてる。ぐあーっと表示されるのはほとんどgemのファイルなので、そういうの(kaminariとかjquery-railsとか)は無視してざっと見ると、bundlerのディレクトリが見つかる。

"/Users/ユーザー名/.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/bundler-1.6.3/lib"

ここに移動する。

cd /Users/ユーザー名/.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/bundler-1.6.3/lib

で、lsで見ると、bundlerというディレクトリとbundler.rbというファイルが見つかる。 ls bundlerでbundlerディレクトリの中身を表示。すると、setup.rbというファイルが見つかる。

setup.rb

setup.rbを開くと、430行くらいのコードが書かれている。全部見るのきついので「ああセットアップするんだろーな」という雰囲気だけ感じて、ファイルを閉じる。参照元のboot.rbが、GemfileがあればBundlerをセットアップするというのはわかった。setup.rbももう少し読んだ方がいいんじゃないかと思いつつ逃げるようにbin/railsに戻る。

4行目

4行目はなんだかとても楽しそう。commandsっていう名前からして明らかにこのエントリの本筋に迫る感じだ。

require 'rails/commands'

Railsrailtiesっていうライブラリにぎっしりと機能が詰まっているというのが記憶のどこかにあったので、それを見てみる。

cd /Users/ユーザー名/プロジェクト名/vendor/bundle/ruby/2.1.0/gems/railties-4.1.4/lib

案の定、railsというディレクトリとrails.rbというファイルがある。今はcommands.rbを探しているので、それを見てみる。

vim rails/commands.rb

こんな感じ。

ARGV << '--help' if ARGV.empty?

aliases = { 
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner"
}

command = ARGV.shift
command = aliases[command] || command

require 'rails/commands/commands_tasks'

Rails::CommandsTasks.new(ARGV).run_command!(command)

おお!実行してる!コマンド実行してる! rails serverとタイプした時の流れとしては、このcommandという変数に"server"が入り、CommandsTasksインスタンスメソッドがこれを受け取って処理してくれるみたい。インスタンス初期化の時に渡してるARGVの中身は、コマンド後のオプション。


というわけで、railsコマンドをタイプした時に内部で何が起きてるのかをざっくりと見てみました。ちょっとざっくりしすぎている部分はまた書き足すかもしれません。