[[Ruby on Rails4を読む]] #contents *はじめに [#y72fe653] Rails4では以下のようにアプリケーションのベースとなるファイルを生成するようです。 C:\Sites>rails new foo C:\Sites>cd foo C:\Sites\foo>rails generate scaffold user name:string email:string アプリケーション名やらなんやらはRails入れるときに[[参考にしたサイト>http://www.catch.jp/wiki/index.php?windows_rails]]のサンプルそのまんまです。深い意味はありません。 さて、以前に読んだときはファイル生成周りを読んでませんが興味がわかなかったのか読んで萌えポイントがなかったのか、ともかく今回は読みます。ちらっと見ただけで萌えポイントがあったので:-) *PATH上にあるrailsコマンド [#h348e558] まずはエントリーポイント、railsコマンドを見ていきます。 #code(Ruby){{ require 'rubygems' version = ">= 0" # 引数でバージョン指定があったらversionを更新するコード gem 'railties', version load Gem.bin_path('railties', 'rails', version) }} というわけでrailtiesのrailsに進みます。「.gitがあったら」のコードを除くと以下の一行です。 #code(Ruby){{ require "rails/cli" }} *railties/lib/rails/cli.rb [#ieb0f093] rails/cli.rb。 #code(Ruby){{ require 'rails/app_rails_loader' # If we are inside a Rails application this method performs an exec and thus # the rest of this script is not run. Rails::AppRailsLoader.exec_app_rails require 'rails/ruby_version_check' Signal.trap("INT") { puts; exit(1) } if ARGV.first == 'plugin' ARGV.shift require 'rails/commands/plugin' else require 'rails/commands/application' end }} 妙なことが書いてありますがいったん無視して下の方に注目します。(気になる人のために書いておくと「If we are inside Rails application」というのは「Railsアプリケーションディレクトリ内でコマンドが実行されたら」ということです。アプリディレクトリを作るときは当然、アプリディレクトリ内にはいないので下のコードが実行されます) *railties/lib/rails/commands/application.rb [#ja65951d] rails/commands/application.rb。 #code(Ruby){{ require 'rails/generators' require 'rails/generators/rails/app/app_generator' (中略) args = Rails::Generators::ARGVScrubber.new(ARGV).prepare! Rails::Generators::AppGenerator.start args }} ARGVScrubberはapp_generator.rbの後半に書かれています。いろいろややこしく書いてありますがやっていることは以下の2点です。 -変なコマンドが指定されてたら--helpが指定されてたということにする -railsrcを読み込んでオプションに加える さて、AppGenerator.startですがこれを追っかけるのはなかなか面倒です。Railsのソース読むためにはこの追っかけ方法を会得する必要があります:-) rails/generators/rails/app/app_generator.rb #code(Ruby){{ require 'rails/generators/app_base' module Rails module Generators class AppGenerator < AppBase # :nodoc: }} rails/generators/app_base.rb #code(Ruby){{ require 'rails/generators' module Rails module Generators class AppBase < Base # :nodoc: }} base.rbはrequireしていない。しかし、generators.rbで以下のように書かれている。 rails/generators.rb #code(Ruby){{ module Rails module Generators autoload :Base, 'rails/generators/base' }} rails/generators/base.rb #code(Ruby){{ require 'thor/group' module Rails module Generators class Base < Thor::Group }} というわけで、gemが変わってthorに移動します。 thor/group.rb #code(C){{ require "thor/base" class Thor::Group # rubocop:disable ClassLength include Thor::Base }} thor/base.rb #code(Ruby){{ class Thor module Base class << self def included(base) #:nodoc: base.extend ClassMethods base.send :include, Invocation base.send :include, Shell end module ClassMethods def start(given_args = ARGV, config = {}) config[:shell] ||= Thor::Base.shell.new dispatch(nil, given_args.dup, nil, config) end }} というわけでようやくstartはけ〜んです。 *Thor::Group.dispatch (thor/lib/thor/group.rb) [#v72b37fc] さてというわけでdispatchというなんかコマンド実行してそうな雰囲気のメソッドにたどり着きました。見出しにもう書いてますがBaseのdispatchは例外投げるだけで実際にはGroupに定義されているdispatchが呼ばれます。 #code(Ruby){{ class Thor::Group # rubocop:disable ClassLength class << self def dispatch(command, given_args, given_opts, config) #:nodoc: if Thor::HELP_MAPPINGS.include?(given_args.first) help(config[:shell]) return end args, opts = Thor::Options.split(given_args) opts = given_opts || opts instance = new(args, opts, config) yield instance if block_given? if command instance.invoke_command(all_commands[command]) else instance.invoke_all end end }} オプションは付けてないので飛ばします。大したことはやってないので興味があったら見てみてください。 で、newされてます。やや見慣れない感じですが、selfはクラスオブジェクトなので通常通りインスタンス生成が行われます。クラスオブジェクトはAppGeneratorなのでrailtiesの方に戻りましょう。 railties/lib/rails/generators/rails/app/app_generator.rb #code(Ruby){{ class AppGenerator < AppBase # :nodoc: def initialize(*args) super unless app_path raise Error, "Application name should be provided in arguments. For details run: rails --help" end if !options[:skip_active_record] && !DATABASES.include?(options[:database]) raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." end end }} 「app_pathってなに〜!?」って心の叫びを今から解決します。superしてるのでスーパークラスのinitialize呼ばれます。実際にはAppBaseのinitializeが呼ばれてさらにそこからsuperされてますが飛ばしてThor::Baseのinitializeに移ります。 thor/lib/thor/base.rb #code(Ruby){{ def initialize(args = [], local_options = {}, config = {}) # rubocop:disable MethodLength # オプションの処理。さくっと省略します # Add the remaining arguments from the options parser to the # arguments passed in to initialize. Then remove any positional # arguments declared using #argument (this is primarily used # by Thor::Group). Tis will leave us with the remaining # positional arguments. to_parse = args to_parse += opts.remaining unless self.class.strict_args_position?(config) thor_args = Thor::Arguments.new(self.class.arguments) thor_args.parse(to_parse).each { |k, v| __send__("#{k}=", v) } @args = thor_args.remaining end }} また謎来ました。見た感じ、渡された引数を定義されている引数リスト(arguments)に基づいてsetter呼び出してる雰囲気です。とするとapp_pathという引数(argument)を定義しておけば先ほどのAppGenerator#initializeは問題がなくなりそうです。 ところで・・・、新しくアプリケーション作るときって、「rails new foo」でしたよね。ところがぎっちょん、今のargsには['foo']と'new'は除けられた配列になっています。いつの間にそうなったのかというと先ほどのARGVScrubber、 railties/lib/rails/generators/rails/app/app_generator.rb #code(Ruby){{ def prepare! handle_version_request!(@argv.first) handle_invalid_command!(@argv.first, @argv) do handle_rails_rc!(@argv.drop(1)) end end def handle_invalid_command!(argument, argv) if argument == "new" yield else ['--help'] + argv.drop(1) end end def handle_rails_rc!(argv) if argv.find { |arg| arg == '--no-rc' } argv.reject { |arg| arg == '--no-rc' } else railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) } end end def railsrc(argv) if (customrc = argv.index{ |x| x.include?("--rc=") }) fname = File.expand_path(argv[customrc].gsub(/--rc=/, "")) yield(argv.take(customrc) + argv.drop(customrc + 1), fname) else yield argv, self.class.default_rc_file end end def insert_railsrc_into_argv!(argv, railsrc) return argv unless File.exist?(railsrc) extra_args = read_rc_file railsrc argv.take(1) + extra_args + argv.drop(1) end }} 渡された引数配列をdrop(1)しているのでしれっとnewが除けられています。はじめ見たときは気づきませんでした。(argumentsまで来たところで、「あれ?newどこ行ったん?」と思いました) 話を戻してargumentです。AppGeneratorにはapp_pathの定義はありませんがスーパークラスのAppBaseにありました。 railties/lib/rails/generators/app_base.rb #code(Ruby){{ class AppBase < Base # :nodoc: argument :app_path, type: :string }} argumentメソッドを貼るのは省略します。attr_accessorでsetter/getter作ってるのとargumentsにThor::Argumentインスタンスを追加しています。 *Thor::Invocation::invoke_all (thor/lib/thor/invocation.rb) [#ff1139d1] さて。ここまででやっとインスタンス生成が終わりました。ようやくコマンド実行です。invoke_allが呼ばれます。group.rbやbase.rbにinvoke_allない、と思ったら同ディレクトリにinvocation.rbがあってそこにありました。ちなみに、InvocationはBaseのincludedメソッドでincludeされてます・・・ #code(Ruby){{ class Thor module Invocation def invoke_all #:nodoc: self.class.all_commands.map { |_, command| invoke_command(command) } end }} またまた謎のall_commandsが登場です。こちらはbase.rbに書かれています。 thor/lib/thor.base.rb #code(Ruby){{ def all_commands @all_commands ||= from_superclass(:all_commands, Thor::CoreExt::OrderedHash.new) @all_commands.merge(commands) end def commands @commands ||= Thor::CoreExt::OrderedHash.new end }} まあ想像通りかなと思います。OrderedHashってのはまあOrderedHashなのでしょう。 で・・・、commandsはいつ設定されるの?と先ほどのargument的なのを小一時間探したのですが見当たりませんでした。app_generator.rbを見ると railties/lib/rails/generators/rails/app/app_generator.rb #code(Ruby){{ module Rails module Generators class AppGenerator < AppBase # :nodoc: def create_root_files build(:readme) build(:rakefile) build(:configru) build(:gitignore) unless options[:skip_git] build(:gemfile) unless options[:skip_gemfile] end }} 明らかにcreate_root_files実行されてる(commandsに入れられてる)よなぁと思ってたらthor/base.rbにいい感じに邪悪なコードがありました:-< thor/lib/thor/base.rb #code(Ruby){{ def method_added(meth) meth = meth.to_s if meth == "initialize" initialize_added return end # Return if it's not a public instance method return unless public_method_defined?(meth.to_sym) @no_commands ||= false return if @no_commands || !create_command(meth) is_thor_reserved_word?(meth, :command) Thor::Base.register_klass_file(self) end }} というわけで、publicメソッドはcommandsとして登録されるようです(゜Д゜)。publicだけどcommandsに入れたくない場合はno_commands { commandsに入れたくないメソッドを定義 }とかおなか一杯過ぎなので省略します。なお、commandsはOrderedHashなので、commandsはメソッドを定義した順に実行されます。 ここまで来たら後は淡々とテンプレートのファイルをコピーするだけです。正確に言うと、buildメソッド→builder(デフォルトapp_generator.rbに定義されているAppBuilder)のメソッド呼び出し→templateとか(thor/lib/actionsディレクトリにある*.rbに定義されています)を呼び出してファイルコピーを行っています。 *おわりに [#ce896bb4] 今回はRailsアプリケーションのスタート地点、rails newコマンドの実装を見てきました。autoload、include・included・extendと初心者お断り!感満載なコードでした。特にmethod_addedを使ったコマンドの定義はやりすぎかなと思います・・・ rails generateまで読む予定でしたが長くなるので今回はここまで、別ページに改めて書くことにします。 そういえばbundle install忘れてた。けど、長くなるのでやめます。bundleもThor使ってるみたいです。