Posted at

Rakeタスクの定義を追う

More than 1 year has passed since last update.


はじめに

最近業務でRailsを触り始めました。

Rails tutorialはこなしているものの、仕事の対象は既存の大規模アプリケーションなので、カスタマイズされている部分が多く読み解くのに苦労しています。

そもそもなぜコードを読むのに苦労するのかということについて、おおよそ以下の3つの項目が重なっているためだということがわかったので、Rakeを例にとって説明します。

Rubyを読む際に苦労する点

- 豊富なシンタックスシュガー

- 定義はどこに

- gemの仕様



Rakeタスク

私が触っているアプリケーションにはこのようなタスクが定義されていました。

namespace :hoge do

desc 'Task without arguments'
task task1: :environment do
puts "This is task1"
end

desc 'Task with arguments'
task :task2, ['arg1', 'arg2'] => :environment do |arg1, arg2|
puts "This is task2 with: #{arg1}, #{arg2}"
end
end



Rakeタスク

私が触っているアプリケーションにはこのようなタスクが定義されていました。

namespace :hoge do

desc 'Task without arguments'
task task1: :environment do # :environment とは
puts "This is task1"
end

desc 'Task with arguments'
# 唐突に登場する "=>" (Hashの初期化に使用する記号では?
# 上のタスク定義とフォーマットが異なる。こちらでは :environment が "=>" で指し示されているけれど何が違うのか?
task :task2, ['arg1', 'arg2'] => :environment do |arg1, arg2|
puts "This is task2 with: #{arg1}, #{arg2}"
end
end



🤔



シンタックスシュガーを外す


  • 豊富なRubyシンタックスシュガー → シンタックスシュガーを外した場合を考える

  • 定義はどこに

  • gemの仕様



元々の定義

  task :task1 => :environment do

...
end

task :task2, ['arg1', 'arg2'] => :environment do |arg1, arg2|
...
end



引数のシンタックスシュガーを外す

  task { :task1 => :environment } do

...
end

task :task2, { ['arg1', 'arg2'] => :environment } do |arg1, arg2|
...
end



メソッド呼び出しのシンタックスシュガーを外す

  task({ :task1 => :environment }) do

...
end

task(:task2, { ['arg1', 'arg2'] => :environment }) do |arg1, arg2|
...
end

ここまでくると、 task() メソッドにタスク名その他の引数と、ひとつのブロックを与えて呼び出している、ということがわかります。

(引数ブロックについてはこのポストがわかりやすい)



引数のフォーマットが異なる 🤔

しかし、ブロックに引数がある場合とない場合で、 task() メソッドの引数のフォーマットが異なり、ここはまだよくわかりません。

  # 引数はHashのみ

# タスク名っぽいsymbolがHashのkeyに入ってる
task({ :task1 => :environment }) do
...
end

# タスク名っぽいsymbolとHash、ふたつの値を引数にとっている
task(:task2, { ['arg1', 'arg2'] => :environment }) do |arg1, arg2|
...
end



定義を辿る


  • 豊富なRubyシンタックスシュガー → シンタックスシュガーを外した場合を考える

  • 定義はどこに → RubyMineのGo To Declarationを使用する

  • gemの仕様



RubyMine

選択した要素を右クリックして Go To -> Declaration で定義に飛んでくれます。

namespaceやModuleをincludeしている部分も解決してくれるので優秀です。

task() の実装を追っていくと、最終的に Rake::TaskManager.define_task() がタスク生成処理を行っているということが分かります。



gemの実装を読む


  • 豊富なRubyシンタックスシュガー → シンタックスシュガーを外した場合を考える

  • 定義はどこに → RubyMineのGo To Declarationを使用する

  • gemの仕様 → コードを読む


Rake::TaskManager.define_task() がタスク生成処理を司っているので、見てみます

    def define_task(task_class, *args, &block) # :nodoc:

task_name, arg_names, deps = resolve_args(args) # 引数の解釈

...

task_name = task_class.scope_name(@scope, task_name)
deps = [deps] unless deps.respond_to?(:to_ary)
deps = deps.map { |d| Rake.from_pathname(d).to_s }
task = intern(task_class, task_name) # task_class.new してオブジェクト生成

# 以下taskオブジェクトのセットアップ
task.set_arg_names(arg_names) unless arg_names.empty?
...
task.enhance(deps, &block)
...
end

ここまでくるとコードを読むことができるようになり、↑の例だと resolve_args() の内容を辿ると引数の解釈部分の実装が出てきます。



定義からわかったtaskの引数

一行一行解説はしませんが、 resolve_args() を読んでいくと以下のようなフォーマットになっていることが分かりました

# dependency ( `:environment` など )がない場合

task(:task_name, :arg1_name, :arg2_name, ...) do |arg1, arg2, ...|
...
end

# dependencyがあり、ブロックに引数がない場合
task({ :task_name => [ dependency1, dependency2, ...] }) do
...
end

# dependencyがあり、ブロックに引数がある場合
task(:task_name, { [:arg1_name, :arg2_name, ...] => [ dependency1, dependency2, ...] }) do |arg1, arg2, ...|
...
end

# dependencyがひとつのみの場合は配列を外すことも可能
task({ :task_name => dependency }) do
...
end



まとめ

ソースを読むために:


  • シンタックスシュガーを外す

  • メソッド定義を突き止める

  • その上で実装を読み解く