在「如何在 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,分別用上了 invoke
和 execute
:
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:man
和 hello:earth
更多實驗
rake 其實是可以一次 invoke 多個的,例如:
bundle exec rake db:create db:migrate db:seed
所以我們也可以分別將剛剛的 invoke
和 excute
排列組合一下:
$ 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 都放在這個 repo 的 hello.rake
檔案內: