Protip: Passing Parameters to Your Rake Tasks
Patrick Reagan, Former Development Director
Article Category:
Posted on
For the times I have needed to pass parameters to my Rake tasks from the command-line, I have always used environment variables (as Ryan describes in this post from 2007). I recently checked out the parallel_specs plugin to see if I could get some noticeable performance improvements when running the test suite for one of my current projects. I didn't in this case, but I saw something in the documentation that caught my eye:
$ rake parallel:spec[1]
$ rake parallel:spec[models]
$ rake parallel:test[something/else]
I have been using Rake forever, but those bracketed options were something new. I thought that it was something Michael added as part of his plugin, but it turns out that this feature is built directly into Rake itself. It was first introduced in this commit and made available as part of version 0.8.1.10. Configuring your tasks to use this feature is simple. I'll show you how.
A Sample Task #
The basic task definition needs no explanation:
desc "Basic call and response"
task :call do
response = 'Task'
puts "When I say Rake, you say '#{response}'!"
sleep 1
puts "Rake!"
sleep 1
puts "#{response}!"
end
To configure the response each time the task was called, I would modify it to take the response as an argument using this syntax:
task :call, :response do |t, args|
response = args[:response]
...
end
The task description now reflects this change:
$ rake -T call
rake call[response] # Basic call and response
Now when I invoke this task with a parameter, the args
hash will contain :response
as the key and the value I supply to the parameter:
$ rake call[Task]
When I say Rake, you say 'Task'!
Rake!
Task!
However, the argument is not required so I get some strange results when I don't provide one. I can fix that quickly by setting a default value:
task :call, :response do |t, args|
response = args[:response] || 'Task'
...
end
This feature isn't limited to a single argument. In fact, I can pass as many as I want:
task :call, :response, :repeat do |t, args|
response = args[:response] || 'Task'
repeat = (args[:repeat] || 1).to_i
puts "When I say Rake, you say '#{response}'!"
repeat.times do
sleep 1
puts "Rake!"
sleep 1
puts "#{response}!"
end
end
And I call it in a similar fashion:
$ rake call[Hoe,2]
When I say Rake, you say 'Hoe'!
Rake!
Hoe!
Rake!
Hoe!
If you're used to specifying task dependencies using the hash syntax, don't worry. It's still possible to do this when passing arguments, but the syntax is a bit different:
task :microphone do
puts "Check 1, 2, 3"
end
task :call, :response, :repeat, :needs => :microphone do |t, args|
...
end
The dependency is called as expected:
$ rake call[Hoe,2]
Check 1, 2, 3
When I say Rake, you say 'Hoe'!
Rake!
Hoe!
Rake!
Hoe!
In Practice #
FeedStitch has a task that starts the update process for a subset of the feeds users have added. It looks something like this:
namespace :feedstitch do
desc "Perform a rolling update of all feeds"
task :update_feeds => :env do
hours_for_full_update = 24
Updater.rolling_update(hours_for_full_update)
end
end
The hours_for_full_update
local variable is there for clarity, but having that value hardcoded into the Rake task means a code change and deploy each time we need to change it. Specifying this update frequency when the task is called would eliminate that complexity:
namespace :feedstitch do
desc "Perform a rolling update of all feeds"
task :update_feeds, :hours_for_full_update, :needs => :env do |t, args|
hours_for_full_update = (args[:hours_for_full_update] || 24).to_i
Updater.rolling_update(hours_for_full_update)
end
end
Now, when calling it I can configure how many feeds are updated at a time:
$ rake feedstitch:update_feeds[12]
This approach works great in those cases where you can use Rake for command-line scripts, but you need just a bit more configurability. If your project requires a custom Ruby script to be run from the command-line, I recommend Trollop for a lighter-weight solution or even Thor if you need something more advanced.
Update #
As Brandon points out in the comments, there is a with_defaults
method on args
provided by Rake::TaskArguments
that will allow you to specify default values if none are provided. Here's what the cleaned-up task would look like using this feature:
desc "Basic call and response"
task :call, :response, :repeat, :needs => :microphone do |t, args|
args.with_defaults(:response => 'Task', :repeat => 1)
puts "When I say Rake, you say '#{args[:response]}'!"
args[:repeat].to_i.times do
sleep 1
puts "Rake!"
sleep 1
puts "#{args[:response]}!"
end
end
The call to to_i
is still required as parameters are received as strings when passing them to the task.
Another Update #
Donald emailed to let me know about the new API for declaring tasks with dependencies. The above tasks should now be written as:
desc "Basic call and response"
task :call, [:response, :repeat] => [:microphone] do |t, args|
args.with_defaults(:response => 'Task', :repeat => 1)
puts "When I say Rake, you say '#{args[:response]}'!"
args[:repeat].to_i.times do
sleep 1
puts "Rake!"
sleep 1
puts "#{args[:response]}!"
end
end