什麼是 Rake?
Rake 是由大師 Jim Weirich 所開發的任務程式工具,就像是 Ruby 版的 Make,可以用來執行各式任務。
Rake 是以 Ruby 語法編寫任務於 Rakefile
檔案中,並用 command-line interface(CLI)輸入指令 $ rake
來執行任務。Rake 可以使用在任何環境,並非只有 Ruby 專案可以使用,只要有 Rakefile 就可以使用 Rake 來執行任務。
文章之後會簡稱 command-line interface 為 CLI,並稱任務為 task。
開始練習寫 Rake!
既然 Rake 是以 Ruby 語法編寫 task,那我們先練習用 Ruby 寫一個可以用 rake 執行的 task 吧!
一起了解如何編寫 Rakefile 和 Rake 提供哪些方法及其用途。
首先要確認 rake 是否存在,請在 CLI 輸入指令 $ gem list --local
來進行確認,若 gem list 中沒有 rake,請先執行 $ gem install rake
。
Task
確認 rake 存在後,就來隨意建立一個新的資料夾 rake_practicing
,接著用 CLI 執行 $ rake
。
$ cd rake_practicing
$ rake
rake aborted!
No Rakefile found (looking for: rakefile, Rakefile, rakefile.rb, Rakefile.rb)
/Library/Ruby/Gems/2.3.0/gems/rake-13.0.1/exe/rake:27:in `<top (required)>'
(See full trace by running task with --trace)
當我們執行 rake 時,他會去找 rakefile
、Rakefile
、rakefile.rb
、Rakefile.rb
中任一種 rake 可執行的檔案,既然原本就沒有,那就直接新增一個 Rakefile,並再次執行 $ rake
。
$ touch Rakefile
$ rake
rake aborted!
Don't know how to build task 'default' (See the list of available tasks with `rake --tasks`)
/Library/Ruby/Gems/2.3.0/gems/rake-13.0.1/exe/rake:27:in `<top (required)>'
(See full trace by running task with --trace)
查看錯誤訊息可以發現是找不到 task,缺什麼就補什麼,接下來我們就在 Rakefile 中簡單寫一個輸出報表的 task 吧!你可以試著實作輸出報表的流程,或是查看本文之後的示範,這邊我們先用 puts
方法印出結果。
# Rakefile
desc 'Export report with result'
task :export_report do
# you can do some process here to export report
puts 'Already exported report with result, please check tmp folder.'
end
task default: [:export_report]
desc
是對任務的描述,實際上並不會執行任何動作,在輸入指令$ rake -T
列出 rake 帶描述的 task 有哪些時,會顯示描述文字在#
後面,若省略不寫desc
,則在執行$ rake -T
時,就不會列出 task,但不代表 task 不存在,沒有寫desc
還是可以執行 task 喔!
$ rake -T
rake export_report # Export report with result
task
是在 Rakefile 中主要執行的部分,透過task
方法給予任務一個名字(通常型態為 Symbol 或 String),在輸入 rake 指令時,可以指定要跑的 task 名稱,而 block 裡的程式碼就是 task 要執行的內容。Rakefile 中有兩個 task
export_report
task:輸出報表的 task。defualt
task:default task 本身並不會有什麼動作,但可以指定相依(dependency)的 task,等同於執行$ rake
指令不帶任何選項(option)時,預設會去執行$ rake export_report
。- 由此可知,一個 Rakefile 中可以有多個 tasks。
我們可以直接指定執行的 task,輸入 $ rake export_report
,就可以看到執行 task 後輸出的結果,或執行 $rake
指令,因 default task 的關係,也會得到相同的結果。
$ rake export_report
Already export report with result, please check tmp folder.
$ rake
Already export report with result, please check tmp folder.
建立目錄 Task
開發者會根據規格需求而建立所需目錄,在 Rake 中提供非常方便的 directory
方法。
使用時機通常是在 rake 需要執行具有先決條件(Prerequisites)的 task 時,而 directory 任務會擺放在該 task 的先決條件中。
# Rakefile
desc "Export report with result under directory 'report/exporting_report'"
task :export_report => "report/exporting_report" do
# go to directory which create from prerequisite task
cd 'report/exporting_report'
# export report under specify directory with result, here only create file
touch 'report_with_result.csv'
end
desc "Create directory 'report' and 'report/exporting_report'"
directory "report/exporting_report"
- 使用
directory
方法取代task
方法,並以 String 的型態呈現所需的目錄結構和名稱。
$ rake -T
rake export_report # Export report with result under directory 'report/exporting_report'
rake report/exporting_report # Create directory 'report' and 'report/exporting_report'
$ rake export_report
mkdir -p report/exporting_report
cd report/exporting_report
touch report_with_result.csv
- 執行帶先決條件的
$ rake export_report
,會在執行$ rake export_report
之前先執行$ rake report/exporting_report
來產生所需目錄,之後將產出的報表放在該目錄下。
給予 Task 參數(parameters)
開發中,有時會因需求問題而期望在執行 rake 時給予 task 參數。
例如:應需求關係要在 task 中加入時間判斷,但開發階段得知時間條件區間可能隨時會因商業需求而更改,為避免需求變動一次就要改一次程式碼,可以用給予 task 參數的方式來保持執行 rake 的彈性。
# Rakefile
# directory "report/exporting_report" ...
desc 'Give coupon if user replies between activity period'
task :give_coupon, [:activity_started_at, :activity_finished_at] do |task, args|
puts "Give coupon if user replies date between #{args.activity_started_at} to #{args.activity_finished_at}."
end
task
方法後面給予的第一個引數(argument)為任務名稱,而 rake 給予 task 的參數則是task
接收的第二個引數(Array 型態),陣列中的兩個物件就是執行 rake 時要保持彈性而給予 task 的參數。- block 裡的
|task, args|
對應前面 task 的兩個引數:task
為第一個引數:give_coupon
。args
為第二個引數[:activity_started_at, :activity_finished_at]
,因此,若要在 block 中使用 rake 執行時所帶入的參數,則需使用#{args.activity_started_at}
及#{args.activity_finished_at}
。
- 若執行 rake 時帶入的參數超過在 task 中宣告的第二個引數數量,則多出來的參數會被忽略(ignored)。
給予 Task 參數預設值
Rake 也提供給予參數預設值的方法 with_defaults
,假設商業需求為「有給予時間條件區間,但不確定事後是否會再更改區間」時,可以給予預設值保留彈性。
當執行 $ rake give_coupon
不給予參數,條件判斷則以預設值為主;坦若之後時間條件區間有所變更,也毋須擔心,只需要再執行 rake 時給予參數覆寫預設值即可。
# Rakefile
# directory "report/exporting_report" ...
desc 'Give coupon if user replies between activity period'
task :give_coupon, [:activity_started_at, :activity_finished_at] do |task, args|
args.with_defaults(activity_started_at: '2019-12-01', activity_finished_at: '2019-12-15')
puts "Give coupon if user replies date between #{args.activity_started_at} to #{args.activity_finished_at}."
end
執行 rake 時需留意
- 執行 rake 給予 task 參數時不可以有空格(space),例如:
$rake give_coupon['2019-12-01', '2019-12-31']
或是$ rake name[lesley wu, chao]
。 - 如果必須要有空格,請使用雙引號(quoted)將 task 的名稱和引數(arguments)包起來,例如:
$rake "give_coupon['2019-12-01', '2019-12-31']"
或是$ rake "name[lesley wu, chao]"
。
$ rake -T
rake export_report # Export report with result
rake give_coupon[activity_started_at,activity_finished_at] # Give coupon if user replies between activity period
rake report/exporting_report # Create directory 'report' and 'report/exporting_report'
$ rake give_coupon['2019-12-01','2019-12-31']
Give coupon if user replies date between 2019-12-01 to 2019-12-31.
$ rake give_coupon
Give coupon if user replies date between 2019-12-01 to 2019-12-15.
$ rake "give_coupon['2019-12-01', '2019 12 31']"
Give coupon if user replies date between '2019-12-01' and '2019 12 31'
給予 Task 參數並帶先決條件(Prerequisites)
當 task 使用參數時,也可以加入先決條件,也就是在執行帶參數的 task 之前,rake 會先執行先決條件指定的 task,而表示先決條件是以箭頭符號 =>
來呈現。
# Rakefile
# ...
task :name, [:first_name, :last_name] => [:pre_name] do |task, args|
puts "First name is #{args.first_name}."
puts "Last name is #{args.last_name}."
end
task :pre_name, [:pre_name] do |task, args|
args.with_defaults(pre_name: 'Timana')
puts "Forename is #{args.pre_name}."
end
$ rake name['Luis','Filipe']
Forename is Timana.
First name is Luis.
Last name is Filipe.
使用 Namespace 為 task 命名
當有多個 task 是具相關性或是專案成長到一定程度時,可能會發生任務名稱命名上的衝突。
例如:現在我們有 main program 和 samples program 都要建立各自的 Rakefile,而在「建立」這個行為的 task 命名上都是 build,為了避免命名衝突,可以使用 namespace
將 main program 和 samples program 做區分,並在 namespace
中各自建立屬於該 program 的 build
任務。
namespace 'main' do
task :build do
# Build the main program rakefile
end
end
namespace 'samples' do
task :build do
# Build the sample programs rakefile
end
end
task build: %w[main:build samples:build]
有接觸過 Rails 的朋友,對於指令 $ rake db:migrate
應該很熟悉,而這樣 rake 指令就是透過 namespace
產生而成的。
小結
介紹完在 Rakefile 中常使用的幾個方法及用途,以及如何使用 rake 指令執行 task 後,我相信你對於編寫 Rakefile 已經有基本的概念了!也許你已經發現我們能透過 Rake 製作許多不同的 task 來完成需求。
而實際開發專案時,我們並不會只在同一個 Rakefile 中做事,依需求可能會使用到其他的 Rakefile 或是 Ruby Module,這時可以透過 require
方法將其他 Rakefile
、Ruby Module
、Ruby Library
引入到正在實作的 Rakefile 中,比較常見的像是 require 'yaml'
、require 'erb'
等等。
接下來,會介紹如何在 Ruby 的好朋友 Rails 中來使用 Rake,並 Step by Step 示範在 Rails 中如何用 Rake 寫一隻輸出含簡單結果的報表喔!
Rails 中的 rake 指令
在 Rails 專案中,我們一樣用 CLI 輸入 $ rake -T
來查看在 Rails 專案中可以使用的 rake 指令有哪些,如前面所介紹,有些沒有 desc
的 task 並不會陳列在其中,但這並不代表那些 task 不存在喔!
$ rake -T
rake db:create # Creates the database from DATABASE_URL or config/database.yml for the ...
rake db:drop # Drops the database from DATABASE_URL or config/database.yml for the cu...
rake db:migrate # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)
rake routes # Print out all defined routes in match order, with names
# ...
輸入 $ rake -T
後,可以看到許多我們平常在 Rails 專案使用的指令。
而在 Rails 5.0 以後的版本,Rails 提供可以使用 $ rails
代替 $ rake
多數指令的操作,所以你也能使用 $ rails db:migrate
、$ rails routes
來執行,會得到與 rake 相同的結果。
一起動手寫隻能輸出報表的 Rake 吧!
接下來會簡單示範如何透過 rake 指令傳入 CSV 檔,並藉由資料庫現有資料與簡單的判斷條件,將判斷結果輸出在 CSV 檔上,不用再人工一行一行慢慢對資料填結果!
情境與需求
活動部門寄來一份 CSV 檔,內含多筆使用者 Email 資訊,要判斷這些 Email 的註冊時間是否在 12 月聖誕節期間內(2019-12-24 ~ 2019-12-31),活動部門希望回傳的 CSV 檔案能包含簡單的結果:「此 Email 註冊於期間內」、「此 Email 非在期間內註冊」,以利活動部門能根據結果來進行抽籤贈獎的活動。
事前準備
- 準備一個 Rails 專案,並使用 gem Devise 做一個簡單可註冊的會員功能。 你也可以直接到 GitHub clone 示範專案 export report by running rake,輕鬆跟我一起用 rake 一行指令輸出報表!
- 準備一份 CSV 檔,header 需包含 Email 及 Result 兩個欄位,而資料需有多筆 Email 資訊。
開始寫能滿足需求的任務
Step 1. 新增 Rake 執行檔案
在 Rails 專案中,rakefile 會統一放在目錄 lib/tasks
下,並且檔名使用 .rake
,那麼我們先新增 lib/tasks/inspect_registration_date.rake
檔案。
Step 2. 給予 task 名字和簡單的說明
給予 task 一個執行名字和簡短的說明,以利多年後回頭執行 $ rake -T
時,能根據描述清楚知道這個 task 主要操作的行為或欲得到的結果。
# inspect_registration_date.rake
desc "Inspect user registration date and export CSV file with result"
task :inspect_registration_date do
# do something magic here
end
$ rake -T
rake inspect_registration_date # Inspect user registration date and export CSV file with result
# ...
Step 3. 依需求給予參數
根據前面提到的需求,我們現在要做的 task 是對輸入的 CSV 檔案的資料做檢查,所以期望在輸入 rake 指令時能夠給予參數,以本次示範而言,這個參數就是要輸入的 CSV 檔案的路徑。
# inspect_registration_date.rake
desc "Inspect user registration date and export CSV file with result"
task :inspect_registration_date, [:file] => :environment do |task, args|
# You can use args from here
end
使用 $ rake -T
查看指令的變化已為 rake inspect_registration_date[file]
,後面多了 [file]
作為指令接收參數。
在 Rails 專案中執行 Rake 不可或缺的 environment task
我們可以看到在這個 task 中有先決條件 :environment
,複習一下,若 task 具先決條件,則以 rake 執行 $ rake inspect_registration_date[file]
前會先執行 $ rake environment
,但使用 $ rake -T
又找不到它,這並不代表 environment task 不存在,那這個 environment task 又是什麼呢?
environment task 是讓其他 tasks 能夠以
:environment
作為先決條件,在其他 tasks 中重現整個 Rails 環境,而這提供我們能夠執行$ rake RAILS_ENV = staging db:migrate
這樣的操作。
用翻譯蒟蒻轉成人話就是,environment task 提供給我們可以在 Rakefile 操作 Model 的權利,像是 User.find_by(email: 'test@gmail.com')
這樣的操作,若沒有加入先決條件 :environment
而執行 $ rake inspect_registration_date['FILE_PATH']
,則會得到錯誤訊息 NameError: uninitialized constant User
。
示範錯誤行為,欲使用 Model 操作但無設定環境先決條件。
desc "Inspect user registration date and export CSV file with result"
task :inspect_registration_date, [:file] do |task, args|
User.find_by(email: 'test@gmail.com')
end
$rake inspect_registration_date['/tmp/sample.csv']
rake aborted!
NameError: uninitialized constant User
/Users/mac/Documents/chao-chao-wu/5xruby/export_report_with_rake_sample/lib/tasks/inspect_registration_date.rake:3:in `block in <top (required)>'
Tasks: TOP => inspect_registration_date
(See full trace by running task with --trace)
因此,在 Rails 專案中,若要使用 Model 相關操作,就必須要設定先決條件 :environment
。
Step 4. 將任務需求寫在 block 裡
依照前提的需求說明,我們會在這個 task 做以下三件事:
- 輸入 CSV 檔案
- 檢查 CSV 資料並在 Result 欄位加入判斷結果
- 輸出帶有結果的 CSV 檔案
接下來我們要將這三項要做的事情編寫成程式碼寫進 block 裡,當我們輸入 rake 指令,呼叫 task inspect_registration_date
時,就會自動執行 block 裡的程式碼。
# inspect_registration_date.rake
desc "Inspect user registration date and export CSV file with result"
task :inspect_registration_date, [:file] => :environment do |task, args|
# Step 1. read importing CSV data
csv_contents = CSV.read(args[:file].pathmap, headers: true)
# Step 2. inspect user registration date and fill in result column
csv_contents.each do |row|
email = row['Email']
# activity period
activity_start_date = DateTime.new(2019, 12, 24)
activity_end_date = DateTime.new(2019, 12, 31, 23).end_of_hour
user_registration_date = User.find_by(email: email).created_at
next row['Result'] = "此 Email 非在期間內註冊" unless user_registration_date.between?(activity_start_date, activity_end_date)
row['Result'] = "此 Email 註冊於期間內"
end
# Step.3 export CSV file with result
file_name = File.basename(args[:file], '.csv')
File.open("/tmp/#{file_name}_result.csv", 'w') do |file|
file.write(csv_contents)
end
end
- 在 rake 檔案中沒有
return
可以使用,但可以使用相同效果的next
。以示範為例,用next
搭配判斷條件,若條件不符,則會直接在 result 欄位填入「此 Email 非在期間內註冊」,並且程式會跳出 Step 2 的 block 不再往下執行。
Step 5. 準備輸入用的 CSV 檔案並執行 rake 任務
完成 rake 檔案後,請將事前準備好的 CSV 檔案放在 /tmp
資料夾下,接著在 Terminal 輸入一行指令 rake inspect_registration_date['/tmp/sample.csv']
,當任務執行完畢後,開啟 /tmp/sample_result.csv
檔案,此檔案就包含活動部門期望的結果,趕緊把這份檔案寄給活動部門就可以囉!🎉
關於輸入 CSV 的檔案位置,你也可以放在任何你想放的位置,只要記得更改程式中輸出的路徑為你想找到含結果的檔案位置,因為範例中輸出檔案路徑是放在
/tmp
資料夾下。若在執行 rake 時發生錯誤
NameError: uninitialized constant CSV
,請先確認config/application.rb
是否有require 'csv'
library 到專案中。
總結
當你完成本篇文章的範例後,可以想一下,若情境變成要在不同時間點,多次使用相同條件來判斷需求是否符合時,那寫完一隻 rake 後,每次都只需要用一行指令就可以完成任務囉!🎉
也建議將 Rake 搭配排程工具一起使用,在特定的時間執行 task,像是寄發信件、備份等等行為都可以用 Rake 編寫成一個 task,在特定的時間執行,就不用再人工處理囉!
坦若你想對 rake 檔案中的程式碼進行測試,可以將 task
block 裡的程式碼改寫成一個 Service Object,之所以要用 Service Object 改寫的原因在於我們無法測試 rake 檔案中的程式碼,但若改寫成 Service Object 這樣的 PORO(Plain Old Ruby Object),就可以對這段程式碼編寫測試囉!
參考資料
- Rake (software) From Wikipedia
- RAKE - Ruby Make docs
- The Rails Command Line
- RAILS CASTS #66 Custom Rake Tasks
- Creative Commons Wooden Tile Report Image
- Ruby 語法放大鏡之「常在終端機裡下 rake db:migrate 指令,這個 rake 是什麼,後面那個 db:migrate 又是怎麼回事?」
👩🏫 課務小幫手:
✨ 想掌握 Ruby on Rails 觀念和原理嗎?
我們有開設 🏓 Ruby on Rails 實戰課程 課程唷 ❤️️