Jacob Swanner Development Blog

Rake: Rule Tasks

This is the third post in a series on Rake, see previous posts for an introduction of the Rakefile format along with global tasks , and task dependencies along with file tasks.

At one point, I thought about writing about something else besides Rake (Rails 4.1 perhaps?). But, with the recent passing of Jim Weirich, I felt it better to continue showcasing one of his many contributions to the Ruby community.

So, to that end, in this post we’re going to look at another capability of Rake: rule tasks. We’ll cover how create them, how they work, and then we’ll expand on the example from the previous post to make it usable for more of our configuration files.

We’ll start off with this:

file 'config/database.yml' => 'config/database.yml.example' do |task|
  cp task.prerequisites.first, task.name
end

And, end up with this:

rule '.yml' => '.yml.example' do |task|
  cp task.source, task.name
end

Which is not only less code, but as you’ll see, more useful.

Rule Tasks

Rule tasks, also known as synthesized tasks, have the same characteristics as all other kinds of tasks: they have a name, they can have zero or more actions, they can have prerequisites, and if Rake determines the task needs to be run it will only be run once.

What makes rule tasks different is that you don’t actually give them a name – I know, I just said that rule tasks have names, just bear with me – instead when you declare the task you give it a pattern in place of a name.

Task Declaration

If that made no sense, hopefully looking at some code will clear things up. First, we declare the rule task, and for that we use the rule method:

rule /foo/ do |task|
  puts 'called task named: %s' % task.name
end

In the above example, we used a Regexp pattern to create a rule task that will match any task name with foo in it, and it’s action will report the task’s name. Let’s see it in action:

$ rake foobar
called task named: foobar

As you can see above, once executed, the task’s name was foobar, as that was the name given to the rake command, but the rule’s pattern is /foo/.

A common use for rule tasks is when dealing with files, especially when we don’t necessarily know what the file’s name will be, but we know what to do based on part of the file’s name, like its extension:

rule /\.txt$/ do |task|
  puts 'creating file: %s' % task.name
  touch task.name
end

In the above example, we used a Regexp pattern to create a rule task that will match any task name ending in .txt, and it’s action will create a file with that name. Let’s see how this task works in action:

$ rake hello.txt
creating file: hello.txt
touch hello.txt
$ ls
hello.txt

This approach of matching based on the end of the task’s name is so common in Rake, we actually don’t need to use a Regexp for it, as just a String of what the end of the task’s name will be will suffice:

rule '.txt' do |task|
  puts 'creating file: %s' % task.name
  touch task.name
end
$ rake world.txt
creating file: world.txt
touch world.txt
$ ls
hello.txt world.txt

Dependencies

As with any other kind of Rake task, rule tasks can have dependencies. These dependency tasks can be either regular, file, or other rule tasks. And, they are declared using the same Hash syntax we’ve seen before:

rule '.dependency' do |task|
  puts 'called task: %s' % task.name
end

rule '.task' => '.dependency' do |task|
  puts 'called task: %s' % task.name
end
$ rake rule.task
called task: rule.dependency
called task: rule.task

Rules for Files

Rule tasks don’t need to be about files, as some of the examples above have shown, but if there is a file with the same name as the task’s name, then that task will have the characteristics of a file task. There’s an entire post in this series about file tasks, if you need a reminder of their characteristics and how they are used. But, in a gist, it means: the task will only be executed if the file does not exist, or unless it has a file task dependency with a newer timestamp than itself. Also like file tasks, if the dependency is a rule task matching existing files, you do not need an explicit declaration for the dependency task itself.

rule '.txt' => '.template' do |task|
  cp task.prerequisites.first, task.name
end

For rule tasks, the Task object has an additional source method which, I believe, can improve the readability of our task declaration:

rule '.txt' => '.template' do |task|
  cp task.source, task.name
end

Let’s see this task in action:

$ ls
file.template               # file.txt does not exist
$ rake file.txt
cp file.template file.txt   # output from running task
$ ls
file.template file.txt      # new file has been created
$ rake file.txt
$                           # attempting task again, produces no output

Useful Example

In the prevous post of this series, we used a file task to copy example configuration files to their desired location:

file 'config/database.yml' => 'config/database.yml.example' do |task|
  cp task.prerequisites.first, task.name
end
$ rake config/database.yml
cp database.yml.example database.yml

And, while this certainly works, things start to get a bit unruly once we want to apply this technique to a number of files:

file 'config/database.yml' => 'config/database.yml.example' do |task|
  cp task.prerequisites.first, task.name
end

file 'config/newrelic.yml' => 'config/newrelic.yml.example' do |task|
  cp task.prerequisites.first, task.name
end

file 'config/sidekiq.yml' => 'config/sidekiq.yml.example' do |task|
  cp task.prerequisites.first, task.name
end

This is a great example of where a rule task really shines. All we need to do is create a single task with the correct patterns:

rule '.yml' => '.yml.example' do |task|
  cp task.source, task.name
end

And, we can apply it to any number of files:

$ rake config/database.yml
cp database.yml.example database.yml
$ rake config/newrelic.yml
cp newrelic.yml.example newrelic.yml
$ rake config/sidekiq.yml
cp sidekiq.yml.example sidekiq.yml