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.
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.
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.
I have just created a Gist with which you can pass multiple arguments to a Rake task. Its usage resembles `Module.define_method`.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Rakefile
hosted with ❤ by GitHub
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
task.rb
hosted with ❤ by GitHub
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