Rails コマンドの実行の流れをたどる旅

Rails コマンド群がどのように呼び出されているのか気になったので、ソースコードを追いながらその流れを整理してみました。うーんとっても長い。

とっても長いので、ざっくり要約を載せておきます。

- Rails コマンドはそれぞれコマンド名に対応したファイルを実行する
    - 例） rails generate → /rails/railties/lib/rails/commands/generate/generate_command.rb

もう、これだけ！後はホントに沼なので書く気にならん！ｗ

さて、ここから先は自分の頭の中の思考をダダ漏らしにした感じでお送りするので、暇な方で覗いてみてください。

まずは `rails` コマンドの実行パスを確認します。こいつが何をしてるかっていう話ですよね。

```
$ which rails
=> /usr/local/bundle/bin/rails
```

実行パスが分かったので `cat /usr/local/bundle/bin/rails` を実行して中身を確認します。

```ruby:/usr/local/bundle/bin/rails

#!/usr/bin/env ruby
#
# This file was generated by RubyGems.
#
# The application 'railties' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0.a"

str = ARGV.first
if str
  str = str.b[/\A_(.*)_\z/, 1]
  if str and Gem::Version.correct?(str)
    version = str
    ARGV.shift
  end
end

if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('railties', 'rails', version)
else
gem "railties", version
load Gem.bin_path("railties", "rails", version)
end
```

- `Gem.activate_bin_path('railties', 'rails', version)`
- `Gem.bin_path("railties", "rails", version)`

このファイルでは上記のどちらかを実行して終わってるんですけど、どちらも同じ値 `"/usr/local/bundle/gems/railties-5.2.3/exe/rails"` を返してます。

```ruby
Gem.activate_bin_path('railties', 'rails', '>= 0.a')
# => "/usr/local/bundle/gems/railties-5.2.3/exe/rails"
Gem.bin_path('railties', 'rails', '>= 0.a')
# => "/usr/local/bundle/gems/railties-5.2.3/exe/rails"
```

ということなので、このパスでロードして読み込まれるコードの中身を見てみます。

```ruby:/railties/exe/rails
#!/usr/bin/env ruby
# frozen_string_literal: true

git_path = File.expand_path("../../.git", __dir__)

if File.exist?(git_path)
  railties_path = File.expand_path("../lib", __dir__)
  $:.unshift(railties_path)
end
require "rails/cli"
```



```ruby:/railties/lib/rails/cli.rb
# frozen_string_literal: true

require "rails/app_loader"

# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::AppLoader.exec_app

require "rails/ruby_version_check"
Signal.trap("INT") { puts; exit(1) }

require "rails/command"

if ARGV.first == "plugin"
  ARGV.shift
  Rails::Command.invoke :plugin, ARGV
else
  Rails::Command.invoke :application, ARGV
end
```

ここでコメントアウトに
> If we are inside a Rails application this method performs an exec and thus
the rest of this script is not run.

と書いてあるので、 Rails アプリケーション内から読み出した場合は、 `Rails::AppLoader.exec_app` 以降のコードは実行されないようです。ので、この #exec_app メソッドの中身を見てみます。

```ruby:/railties/lib/rails/app_loader.rb
# frozen_string_literal: true

require "pathname"
require "rails/version"

module Rails
  module AppLoader # :nodoc:
    extend self

    RUBY = Gem.ruby
    EXECUTABLES = ["bin/rails", "script/rails"]
    BUNDLER_WARNING = <<EOS
…(長いので省略)…
EOS

    def exec_app
      original_cwd = Dir.pwd

      loop do
        if exe = find_executable
          contents = File.read(exe)

          if contents =~ /(APP|ENGINE)_PATH/
            exec RUBY, exe, *ARGV
            break # non reachable, hack to be able to stub exec in the test suite
          elsif exe.end_with?("bin/rails") && contents.include?("This file was generated by Bundler")
            $stderr.puts(BUNDLER_WARNING)
            Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd))
            require File.expand_path("../boot", APP_PATH)
            require "rails/commands"
            break
          end
        end

        # If we exhaust the search there is no executable, this could be a
        # call to generate a new application, so restore the original cwd.
        Dir.chdir(original_cwd) && return if Pathname.new(Dir.pwd).root?

        # Otherwise keep moving upwards in search of an executable.
        Dir.chdir("..")
      end
    end

    def find_executable
      EXECUTABLES.find { |exe| File.file?(exe) }
    end
  end
end
```

ここちょっとビックリしたんですけど、 `extend self` を書いとくと自身に特異メソッドを生やすことできるっぽい。だから `Rails::AppLoader.exec_app` を直接実行できるのね。へー…。

ここでは、 `EXECUTABLES = ["bin/rails", "script/rails"]` このどちらかのパスが存在したらそれを実行というロジックになってます。 `bin/rails` の中身を見てみます。

```ruby:/path/to/work_dir/bin/rails
#!/usr/bin/env ruby
begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
```

みたところ、 `APP_PATH` か `ENGINE_PATH` がなかった場合でも定数 `APP_PATH` の初期化と `config/application.rb` と `config/boot.rb` の読み込み、そして最終的に `rails/commands` を読み込んでいるっぽい。

```ruby:/railties/lib/rails/app_loader.rb

Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd))
require File.expand_path("../boot", APP_PATH)
require "rails/commands"
```

ということで、 `rails/commands` の中身を見てみます。

```ruby:/railties/lib/rails/commands.rb
# frozen_string_literal: true

require "rails/command"

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

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

Rails::Command.invoke command, ARGV
```

うおお、ここで Rails コマンドのエイリアスが出てきたぞ…。ここで `ARGV` に含まれていた値を取り出して、それを引数に `Rails::Command.invoke command, ARGV` を実行してます。

ここで言う `ARGV` は「 Ruby スクリプトに与えられた引数を表す配列」とのことで、実行ファイル名より後の値を配列でファイル内に渡してます。
https://docs.ruby-lang.org/ja/2.6.0/method/Object/c/ARGV.html

たとえば、

```
$ rails generate model
```
の実行時の引数 `ARGV` は以下のように保存されます。

```
['generate', 'model'] 
```

さて、 `Rails::Command.invoke command, ARGV` の中身を見てみます。
たとえば、 `rails generate model` を実行する場合、

```ruby
full_namespace
# => 'generate'
args
# => ['model']
```

がそれぞれ代入されているはずです。
なお、これ以上の処理は、すべて以下のコマンドを実行する場合を前提とします。

```
$ rails generate model Foo foo:string
```

よし、 #invoke メソッドの中身見ていきます。

```ruby:/railties/lib/rails/command.rb
def invoke(full_namespace, args = [], **config)
  namespace = full_namespace = full_namespace.to_s

  if char = namespace =~ /:(\w+)$/
    command_name, namespace = $1, namespace.slice(0, char)
  else
    command_name = namespace
  end

  command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
  command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)

  command = find_by_namespace(namespace, command_name)
  if command && command.all_commands[command_name]
    command.perform(command_name, args, config)
  else
    find_by_namespace("rake").perform(full_namespace, args, config)
  end
end
```

前段にいろいろとコマンド名の処理がありますが、最終的には `command.perform(command_name, args, config)` の中身が分かればいいので、まずは変数 `command` に代入している #find_by_namespace メソッドを見ます。

```ruby:/railties/lib/rails/command.rb
def find_by_namespace(namespace, command_name = nil) # :nodoc:
  lookups = [ namespace ]
  lookups << "#{namespace}:#{command_name}" if command_name
  lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

  lookup(lookups)

  namespaces = subclasses.index_by(&:namespace)
  namespaces[(lookups & namespaces.keys).first]
end
```

冒頭 3 行の処理で変数 `lookups` には以下の値が代入され、 #lookup メソッドに渡されます。

```ruby
lookups
# => ["generate", "generate:generate", "rails:generate", "rails:generate:generate"]
```

それでは #lookup メソッドの中身を見てみます。どうやらここでは `namespaces` を元に各コマンドの処理が入ってるファイルを require してるようです。なるほど、メソッド名通りの処理だ。

```ruby:/railties/lib/rails/command/behavior.rb
def lookup(namespaces)
  paths = namespaces_to_paths(namespaces)

  paths.each do |raw_path|
    lookup_paths.each do |base|
      path = "#{base}/#{raw_path}_#{command_type}"

      begin
        require path
        return
      rescue LoadError => e
        raise unless e.message =~ /#{Regexp.escape(path)}$/
      rescue Exception => e
        warn "[WARNING] Could not load #{command_type} #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"
      end
    end
  end
end
```
ここでは、 #namespaces_to_paths / #lookup_paths / #command_type 3 つのプライベートメソッドが呼ばれています。各メソッドの中身は単純なので割愛しますが、それぞれ以下の値を返します。

```ruby
namespaces_to_paths
# => ["generate/generate", "generate", "generate/generate/generate", "rails/generate/generate", "rails/generate", "rails/generate/generate/generate"]
lookup_paths
# => ["rails/commands", "commands"]
command_type
# => "command"
```

ほんで、 `require` が true を返すのは `"rails/commands/generate/generate_command"` の時ですね。なんなんだろう、すごいトリッキーなことやってる気がするｗ

そうこうして #find_by_namespace メソッドに戻ってきました。

```ruby:/railties/lib/rails/command.rb
def find_by_namespace(namespace, command_name = nil) # :nodoc:
  lookups = [ namespace ]
  lookups << "#{namespace}:#{command_name}" if command_name
  lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

  lookup(lookups)

  namespaces = subclasses.index_by(&:namespace)
  namespaces[(lookups & namespaces.keys).first]
end
```

さて、次に実行される #subclasses メソッドが謎を呼ぶんですが、中身はこうなっています。

```ruby
def subclasses
  @subclasses ||= []
end
```

これ pry でデバッグ中に試しに実行してみると分かるんですが、なんともう値 `[Rails::Command::GenerateCommand]` が入ってます。い、いつ代入されたの…？？？？？？って話なんですが、実は `require path` の時点で代入されていました。

そもそも `xxxx_command.rb` の中身はざっくり以下のような構成になっており、必ず `Rails::Command::Base` クラスを継承するようになっています。

```ruby
module Rails
  module Command
    class HogehogeCommand < Base
    end
  end
end
```

この ` < Base` のタイミングで Rails::Command::Base.inherited が実行されます。メソッドの中身を見ると分かりますが、 `@subclasses` に `base` が追加されてるのが分かるかと思います。

```ruby
module Rails
  module Command
    class Base < Thor
      class << self
        …
        def inherited(base) #:nodoc:
          super
          if base.name && base.name !~ /Base$/
            Rails::Command.subclasses << base
          end
        end
　　　　　…
      end
    end
  end
end
```

ソースコードリーディングっていろんなファイルを飛び回るからアタマ混乱するなあ…。
さて、その `@subclasses` に #index_by をかけて Hash 化します。

```ruby:/railties/lib/rails/command.rb
def find_by_namespace(namespace, command_name = nil) # :nodoc:
  lookups = [ namespace ]
  lookups << "#{namespace}:#{command_name}" if command_name
  lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

  lookup(lookups)

  namespaces = subclasses.index_by(&:namespace)
  namespaces[(lookups & namespaces.keys).first]
end
```

ここで最後の処理に使う `namespaces` と `lookups` の値を確認しておきましょう。現時点でこんな感じになっています。

```ruby
namespaces
# => {"rails:generate"=>Rails::Command::GenerateCommand}
lookups
# => ["generate", "generate:generate", "rails:generate", "rails:generate:generate"]
```

なので、最後の行の返り値はこんな感じになります。

```ruby
namespaces[(lookups & namespaces.keys).first]
# => Rails::Command::GenerateCommand
```

やっと #find_by_namespace(namespace, command_name) の返り値がわかったので、 #invoke メソッドに戻ります。今まで見てきたのは変数 `command` にどんな値が代入されるかということでした。上記で見た通り、 `Rails::Command::GenerateCommand` が代入されることが分かりました。

```ruby:/railties/lib/rails/command.rb
def invoke(full_namespace, args = [], **config)
  namespace = full_namespace = full_namespace.to_s

  if char = namespace =~ /:(\w+)$/
    command_name, namespace = $1, namespace.slice(0, char)
  else
    command_name = namespace
  end

  command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
  command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)

  command = find_by_namespace(namespace, command_name)
  if command && command.all_commands[command_name]
    command.perform(command_name, args, config)
  else
    find_by_namespace("rake").perform(full_namespace, args, config)
  end
end
```

仮に `command` が nil だった場合は `Rails::Command::RakeCommand` を元に rake コマンドが走るようですが一旦それは置いておきます。 `command.perform(command_name, args, config)` の中身を見ていきます。なお引数の値はこんな感じになっています。

```ruby
command_name
# => "generate"
args
# => ["model"]
config
# => {}
```


```ruby:/railties/lib/rails/command/base.rb
def perform(command, args, config) # :nodoc:
  if Rails::Command::HELP_MAPPINGS.include?(args.first)
    command, args = "help", []
  end

  dispatch(command, args.dup, nil, config)
end
```

ここの #dispatch は Rails ではなく Thor のメソッドです。Thor 内部の動きについてはまた色々ありますが、ここでは割愛します。なんだかんだあった後に Rails::Command::GenerateCommand#perform メソッドが呼び出されます。

```ruby:/railties/lib/rails/commands/generate/generate_command.rb
def perform(*)
  generator = args.shift
  return help unless generator

  require_application_and_environment!
  load_generators

  ARGV.shift

  Rails::Generators.invoke generator, args, behavior: :invoke, destination_root: Rails::Command.root
end
```

さて、ここから沼に入っていきます。しばらく自分がどこにいるか分からなくなります。ちょっと最後の方まで書いてたんですが説明するのがめんどくさくなったので、興味のある方は覗いてみてください。 Rails って大きいなあ（小並）ってのがひしひしと分かります。

要するに Rails コマンドとそれを実行するファイル名は対応していて、 rails generate なら generate_command.rb、 rails console なら console_command.rb を見に行けばなんとなく中で何をやっているかが分かる、というしくみです。
いつもやっているソースコードリーディングの思考をそのまま書いたみたいな感じでまとまりないのは申し訳ないですが、備忘録程度に書いてるので、まあそんなもんだと思ってください。

#### Thor について（おまけ）
なお、各コマンドのオプションについては、 class_option で大半を定義しています。これは Thor の機能で http://whatisthor.com/#class-options 、Thor クラスを継承したクラスにこのメソッドを書くと、クラス全体で定義しておきたいオプションを設定することができる、というものです。

たとえば、

```ruby:sample.rb
#!/usr/bin/env ruby
require 'thor'

class Nya < Thor
  class_option :nyanchu, type: :boolean, default: false

  desc 'hello NAME', 'say hello to NAME'
  def hello(name)
    if options[:nyanchu]
      puts "#{name}, nyanchu~!"
    else
      puts "#{name}, nya~!"
    end
  end
end

Nya.start(ARGV)
```

としておくと、勝手に `[--nyanchu], [--no-nyanchu]` オプションをつけてくれるようになります。コマンド名を指定せずにファイルを実行すると、よく見かける README を出力してくれます。なにこれ便利。

```
root@10161f5ac926:/hoge# ./bin/sample
Commands:
  sample hello NAME      # say hello to NAME
  sample help [COMMAND]  # Describe available commands or one specific command

Options:
  [--nyanchu], [--no-nyanchu]  
```

なので、このオプションにしたがって以下のように実行してみると、ちゃんと引数を解釈してくれます。

```
root@10161f5ac926:/hoge# ./bin/sample hello Waku --nyanchu
Waku, nyanchu~!
```

もっと詳しく Thor について知りたい方は公式ドキュメントか GitHub のリポジトリを見に行っても良いかもしれません。
http://whatisthor.com/
https://github.com/erikhuda/thor