Rake Tasks 進階:Invoke, Execute, Enhance

在「如何在 Rails 中寫 Rake Tasks」一文中,已經紀錄了在 Ruby on Rails 中 Rake Task 的基本寫法。

這邊要來談一些最近碰到的議題:

  • 如何直接在一個 rake task 中呼叫另一個 rake task?
  • 或是如何在一個 rake task 執行時,做額外的事情?

在程式中呼叫 Rake Task: Invoke 與 Execute

如果我們想要在 ruby 的程式中執行一個 rake task,可以使用 Rake::Task['rake::name'].invoke,或是 Rake::Task['rake::name'].execute

Invoke 與 Execute 的差別

一般執行 Rake task 時,會先 invoke 該 task,然後依序 invoke 其 dependencies,然後 execute dependencies,接著才 execute 我們從外部 invoke 的 task。

也因此兩者的差別在於:

  • invoke 如同在 command line 呼叫,完整的拉起 dependencies,並且每個 task 只會執行一次。
  • execute 直接執行該 task 內容,不管該 task 有沒有被執行過。

寫點 Code 做實驗

我們利用「如何在 Rails 中寫 Rake Tasks」中定義好的 Task hello:world

namespace :hello do
  desc 'print hello world'
  task world: %w[hello:man hello:earth] do
    puts 'Hello, world!'
  end

  task earth: %w[hello:man] do
    puts 'Hello, Earth~'
  end

  task :man do
    puts 'Hello, man.'
  end
end

另外再加上兩個 tasks,分別用上了 invokeexecute

task :invoke do
  Rake::Task['hello:world'].invoke
end

task :execute do
  Rake::Task['hello:world'].execute
end

分別執行這兩個 tasks:

$ bundle exec rake invoke
Hello, man.
Hello, Earth~
Hello, world!

$ bundle exec rake execute
Hello, world!

可以發現 execute hello:world 這個 task,並不會跟著跑他的 dependencies hello:manhello:earth

更多實驗

rake 其實是可以一次 invoke 多個的,例如:

bundle exec rake db:create db:migrate db:seed

所以我們也可以分別將剛剛的 invokeexcute 排列組合一下:

$ bundle exec rake execute invoke
Hello, world!
Hello, man.
Hello, Earth~
Hello, world!

$ bundle exec rake invoke execute
Hello, man.
Hello, Earth~
Hello, world!
Hello, world!

$ bundle exec rake invoke invoke
Hello, man.
Hello, Earth~
Hello, world!

$ bundle exec rake execute execute
Hello, world!
Hello, world!

前兩種組合的結果告訴我們,先 execute 一次,該 task 下次 invoke 時還是會照常被執行。因此 execute 對於需要直接執行該 task 的 block 裡面的內容是很好用的。
invoke 則是要考慮到之前是不是有被 invoke 過,如果已經被 invoke 過,即便再次 invoke 一次,之後就不會再重複 execute

如何重複 invoke 一個 task?

使用 reenable 這個方法即可:

task :reenable do
  puts 'Reenable!'
  # because we invoke `invoke` to invoke `hello:world`, we need to re-enable both
  Rake::Task['invoke'].reenable
  Rake::Task['hello:world'].reenable
end

這樣下次跑 invoke 就會重複印出 Hello, world!

$ bundle exec rake invoke reenable invoke
Hello, man.
Hello, Earth~
Hello, world!
Reenable!
Hello, world!

Enhance:幫 Rake Task 打補丁

如果你需要在執行特定的 rake task 前後做特定的事,那使用 enhance 是不錯的選擇。enhance 就像是在某個 task 加裝 hook 一樣,執行到該 task 時,就會執行他被 enhance 的內容。

一個例子是 annotate_models 這個 gem,可以在 database migration 後,對 model 檔案加上註解,翻開其原始碼,果然是用 enhance 實現的:

# https://github.com/ctran/annotate_models/blob/v3.1.1/lib/tasks/annotate_models_migrate.rake
%w(db:migrate db:migrate:up db:migrate:down db:migrate:reset db:migrate:redo db:rollback).each do |task|
  Rake::Task[task].enhance do
    Rake::Task[Rake.application.top_level_tasks.last].enhance do
      annotation_options_task = if Rake::Task.task_defined?('app:set_annotation_options')
                                  'app:set_annotation_options'
                                else
                                  'set_annotation_options'
                                end
      Rake::Task[annotation_options_task].invoke
      Annotate::Migration.update_annotations
    end
  end
end

這樣寫之後,當「執行完」上面所窮舉跟 database migration 有關的 rake task 時,就會 block 內的 code。

那如果要在「執行前」插入 code 呢?

enhance 是可以額外增加 dependencies 的,可以將執行前要插入的 code 寫成另一個 rake task,然後將其定義為 enhance 的 dependencies 即可:

task :before do
  puts 'before task'
end

# 這裡 dependencies 一定要是 array,不然會報錯
Rake::Task['hello:world'].enhance(%w[before]) do
  puts 'after task'
end

接著重跑一次 invoke 這個 task:

$ bundle exec rake invoke
Hello, man.
Hello, Earth~
before task
Hello, world!
after task

可以發現 enhance 的 dependency 會接在原先定義的 dependencies 後面。

如果 Execute 一個被 Enhance 的 Task 會發生什麼事?

前面有討論到,如果我們直接 execute 一個 task,其 dependencies 都會被忽略。而 enhance 也包含了定義 dependencies 和後方的 block 的部分。揪竟 enhance 一個 task 會不會對其被 execute 時產生影響呢?

實驗一次就會知道了:

$ bundle exec rake execute
Hello, world!
after task

enhance 的 dependencies 也被扔掉了。
然而其 block 內定義的 code 仍然會執行到。

這個行為算是我覺得比較容易搞混的地方,老實說 enhance 給人一種 monkey patch 的感覺,我個人是認為需要謹慎使用。

Enhance 多次的影響

附帶一提,一個 task 是可以被 enhance 多次的,每次 enhance 的結果會依照先後順序疊加:

task :before do
  puts 'before task'
end

task :before2 do
  puts 'before task 2'
end

Rake::Task['hello:world'].enhance(%w[before]) do
  puts 'after task'
end

Rake::Task['hello:world'].enhance(%w[before2]) do
  puts 'after task 2'
end
$ bundle exec rake invoke
Hello, man.
Hello, Earth~
before task
before task 2
Hello, world!
after task
after task 2

本文中所有的 code 都放在這個 repohello.rake 檔案內:

參考資料