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