Protip: Passing Parameters to Your Rake Tasks

Patrick Reagan, Former Development Director

Article Category: #Code

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

Related Articles