Close and Go BackBack to Viget

Protip: Passing Parameters to Your Rake Tasks

Patrick Reagan
Patrick Reagan, Development Director, December 14, 2009 11

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.

Brandon Hauff said on 12/15 at 11:08 AM

Amazing timing.  I just figured out all of this yesterday, this blog would have saved me some time.  Jim Weirich did provide a helper on the args to provide defaults.  Instead of using || you can use args.with_defaults(:my_arg => “default value").

Ryan Sobol said on 12/15 at 02:10 PM

Thank you for shining the light on this syntactic sugar!

Patrick Reagan said on 12/15 at 02:51 PM

@Brandon: Thanks for the tip, I updated the post to use that feature.
@Ryan: You’re welcome.

Sam Kong said on 12/16 at 03:34 AM

It’s good to know that. Thanks.

On rails, I usually write a rake like the following.
task :import => :environment do
...
end

How do I apply args here? :environment is not default value but another task.

Patrick Reagan said on 12/16 at 09:48 AM

@Sam: Check out the examples above, namely those tasks that use “:needs => :microphone” above - that is how you need to specify the dependencies in those cases.  For your example task, you can supply parameters like this:

task :import, :path, :needs => :environment do |t, args|
...
end

Now :path will be available as a key in the args hash.

Paul Brannan said on 12/16 at 09:55 AM

Even if rake provides this feature by default, IMO it’s not a good syntax for parameter passing, because [] gets expanded by the shell:

pbrannan@random:~/tmp> cat Rakefile
task :foo do
puts “FOO!”
end
pbrannan@random:~/tmp> rake foo
(in /home/pbrannan/tmp)
FOO!
pbrannan@random:~/tmp> rake foo[1]
(in /home/pbrannan/tmp)

But if you are going to use this syntax, make sure you properly quote your parameters:

pbrannan@random:~/tmp> rake ‘foo[1]’
(in /home/pbrannan/tmp)
FOO!

Sam Kong said on 12/16 at 12:44 PM

@Patrick: I tried that but got an error.

namespace :ssk do
task :one, :two, :three => :environment do |t, args|
puts args
end
end

rake ssk:one[two,three]

rake aborted!
undefined method `empty?’ for :three:Symbol
/Users/ssk/Temp/tiny/Rakefile:10
(See full trace by running task with --trace)

If I remove “=> :environment”, I get the following.
{:two=>"two", :three=>"three"}

Am I missing something?

Patrick Reagan said on 12/16 at 01:11 PM

@Sam: The last parameter to the task definition needs to be a hash, so it relies on using the special key :needs to define the dependencies.  Try your example again with this:

namespace :ssk do
task :one, :two, :three, :needs => :environment do |t, args|
puts args.inspect
end
end

Sam Kong said on 12/16 at 02:00 PM

@Patrick: Yeah, it works! Thank you very much. I learned something today.

Michael Erb said on 12/16 at 02:41 PM

wow, nice tip! perfect timing too. i was just dealing with this!

Jonathan R Wallace said on 12/16 at 02:49 PM

Also, if using zsh, you can prefix the rake command with noglob to have zsh ignore the square brackets instead of quoting.

Commenting is not available in this weblog entry.

We're the Developers

at Viget Labs. We write about web development trends, tips, best practices, industry events, and our projects — all with an emphasis on Ruby on Rails.

Recent Comments

Your post has made me think!

We people get used to that what we daily do. And normally we forget that we have to evolve.

Contact Us

Have any questions, comments, ideas, or secrets to share? Let us know.


How many days in a non-leap year?

Sorry, you need to have Javascript enabled to use this form. (Don't blame us, blame the spammers!) If you'd like to contact us, please visit our Contact page.