Passing parameters to a Rake task

I’ve been building a few custom rake tasks of late and I wanted to pass a parameter to one of my tasks. Typically you would use a named variable to do this. However, I wanted a cleaner approach, something which was closer to the syntax used for a Rails generator.

So instead of this…

$ rake say_hello NAME=eddie

…or, god forbid, this…

$ rake say_hello[eddie]

…I wanted something more like this…

$ rake say_hello eddie

Hmm. Seems easy enough, but there’s a catch. Let’s look at an example.

Method 1: Using a named variable

task :say_hello do
  name = ENV['NAME']
  puts "Hello, #{name}."
end

This is the typical approach, examples of which you will find everywhere. Running the task is easy and the intention of the command is clear:

$ rake say_hello NAME=eddie
=> Hello, eddie.

Nothing new here.

Method 2: Using arguments

task :say_hello, :name do |t, args|
  name = args.name
  puts "Hello, #{name}."
end

You would run this task like so…

$ rake say_hello[eddie]
=> Hello, eddie.

This is a slightly more cryptic approach, which I don’t like at all. The syntax is unintuitive and totally obscures the intention of the command, not to mention the use of square parenthesis to wrap the argument. Ugly!

Method 3: Custom black magic!

task :say_hello do
  name = ARGV.last
  puts "Hello, #{name}."
  task name.to_sym do ; end
end

For me, this method gives us a much more DSL-like syntax, which seems cleaner and more intuitive (this is obviously entirely dependant on your own set of circumstances). To run the task using this method, you would do this…

$ rake say_hello eddie
=> Hello, eddie.

A few things are happening here that are worth an explanation.

Firstly, we are using the ARGV collection of command line arguments to grab the value for our argument. Standard Ruby stuff. Beware though, that this collection will contain all the arguments for Rake, not our task. That’s why we grab the last element in the collection because the first element is the name of our rake task. Modifying the task as follows will illustrate this point:

task :say_hello do
  puts ARGV.inspect
  name = ARGV.last
  task name.to_sym do ; end
end
$ rake say_hello eddie
=> ["say_hello", "eddie"]

As you can see, the ARGV collection contains two elements: the name of our rake task and the value of the argument we are trying to set. Grab the last one and we’re halfway there.

Secondly, we have to define a new rake task on the fly with the same name as the value of our argument. If we don’t do this, rake will attempt to invoke a task for each command line argument.

$ rake say_hello eddie

By default, the above command will first invoke a task called “say_hello”, and then try to invoke a task called “eddie”. This behaviour is hard-coded into rake. So, we simply define a “no op” task with the corresponding name (“no op” means “no operation”). This avoids an exception being thrown when rake inevitably doesn’t find a defined task of that name.

There you have it. A DSL-like syntax for passing arguments to a rake task.

Simples.

6 thoughts on “Passing parameters to a Rake task

  1. The problem with this method is that it gets ugly if you need more than one parameter. Using the square brackets is unfortunately the best way I’ve seen of being able to include more than one parameter in a Rake task call. It might be ugly but the focus should be on what the task does and not how the command looks.

    1. I don’t agree that the focus should exclusively be on what the task does. Every API is important, and making it more intuitive is always a win. However, this example is a specific use-case in which the rake tasks in question were being built for people with limited technical knowledge.
      Thanks for reading.

  2. I have just created a Gist with which you can pass multiple arguments to a Rake task. Its usage resembles `Module.define_method`.


    require_relative "task"
    desc "Send an invite"
    task :invite do |name, email|
    puts "Invitation sent to '#{name} <#{email}>'"
    end
    # Example:
    #
    # $ rake invite "Paul Engel" paul@engel.com
    # Invitation sent to 'Paul Engel <paul@engel.com>'
    #

    view raw

    Rakefile

    hosted with ❤ by GitHub


    module Rake
    class Task
    class << self
    alias :original_define_task :define_task
    end
    def self.define_task(*args, &block)
    original_define_task *args do |task|
    if block_given?
    arguments = ARGV.select do |arg|
    !arg.include?(task.name) && original_define_task(arg.to_sym) do; end
    end
    block.call *arguments
    end
    end
    end
    end
    end

    view raw

    task.rb

    hosted with ❤ by GitHub

  3. Thanks. Really useful.

    I think this poses a dilemma though. Now that I’ve understood that the rake actually runs a series of tasks passed as *it’s* arguments (which I hadn’t realised before), this technique overrides default behaviour for one’s bespoke tasks.

    I think this is a dangerous habit to adopt (despite the fact that I agree it’s more intuitive DSL-like syntax which is exactly what I was looking for).

    It would be too easy to call other rake tasks forgetting that your parameter isn’t really a parameter this time, but another task.

    Hmm. I have to think carefully about whether to adopt it or not. Anyway, love the black magic of the no-op! Thanks!

    Gruff

  4. Pingback: Sinatra.rake

Leave a reply to Bill Cancel reply