Jacob Swanner Development Blog

Rake: File Tasks

This is the second in a series on Rake, see previous post for introduction on the Rakefile format and about global tasks with Rake.

In this post we’re going to look at another capability of Rake: file tasks. We’ll cover how create them, how they work, and then create a useful example. But, before we get into file tasks, we need to have a better understanding of another aspect of the Rakefile format: prerequisites.

Task Prerequisites

Any Rake task can optionally have one, or more, prerequisite tasks – also referred to as dependencies. As with any other Rake task, a prerequisite task is only executed if it is needed, and if it is executed it is only ever done so once.

Let’s start by declaring a couple tasks called one and two in our Rakefile:

task 'one' do
  puts 'one'
end

task 'two' do
  puts 'two'
end

We can run the tasks in a shell, as we’ve seen before:

$ rake one
one
$ rake two
two

Now let’s declare one as a prerequisite for two:

task 'one' do
  puts 'one'
end

task 'two' => ['one'] do
  puts 'two'
end

As you can see, we haven’t changed how one was defined at all. But, for two, we added => ['one'] after the task name: this is how you declare a task’s prerequisites. At first this format may seem foreign, but keep in mind, this is just Ruby code; let’s see how our Rakefile would look like if we added in all of Ruby’s optional syntax:

task('one') do
  puts 'one'
end

task({'two' => ['one']}) do
  puts 'two'
end

As you can see, in both cases, we’re just passing 1 argument and a block to the task method. For one, we pass in just the task name, 'one'; for two, we’re passing in a hash, {'two' => ['one']}, the hash key is the task name 'two' and the hash value is an array of prerequisites ['one'].

Aside: If your task only has one prerequisite, the hash value doesn’t need to be an array:

task 'two' => 'one' do
  puts 'two'
end

Let’s see what happens when we run each of those tasks now:

$ rake one
one
$ rake two
one
two

As you can see above, when we ran two, we get the output from both the one and two tasks. Now that we have a foundation for prerequisite tasks to build on, let’s look at file tasks.

File Tasks

Thus far, all of the tasks we’ve created have used Rake’s task method to declare the task, but for file tasks Rake has a special method: file. File tasks in Rake are very similar to normal 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. Now, the twist is that those things get modified to be file related: the name of the task is the same as the file’s name, Rake determines that a file task needs to be run if the file doesn’t exist or if any of the prerequisite file tasks are newer.

That’s a bit to wrap your head around, so let’s look at some examples:

file 'foo.txt' do
  touch 'foo.txt'
end

Even though file tasks are meant for dealing with files, you are still responsible for creating the file in the task’s action if the file doesn’t exist.

Aside: Rake includes a modified version of the FileUtils module so that you have access to its methods in your task actions, which is where that touch method above is from.

Aside: FileUtils.touch works like the Unix touch program: updates a file’s timestamps and creates nonexistent files.

And, now when we run that task:

$ rake foo.txt
touch foo.txt   # output from our file task when it's run
$ ls            # showing file was created
foo.txt

Earlier, I mentioned that Rake will not run a file task if the file exists; so let’s see what happens when I delete the file and then run the task twice:

$ rm foo.txt    # deleting file
$ rake foo.txt  # running file task
touch foo.txt   # output from our task
$ rake foo.txt  # running file task again, but no output
$ ls            # showing file was created
foo.txt

As we can see, the first time we ran the foo.txt task we see the touch foo.txt output from the file being created, but the second time we ran the task we get no such output. But, things behave a bit different if we add a prerequisite to our file task:

file 'foo.txt' => 'bar.txt' do
  touch 'foo.txt'
end

Aside: If the prerequisite for a file task is another file, you do not need to create an explicit file task for the prerequisite, just using the name of the file is enough.

$ ls              # showing foo.txt does not exist
bar.txt
$ rake foo.txt    # running file task
touch foo.txt     # output from file task
$ rake foo.txt    # running file task again, but no output
$ ls              # showing file was created
bar.txt foo.txt

So far, things don’t seem much different: our file task creates the file if it doesn’t exist, and if we run the task again nothing happens.

$ touch bar.txt   # update timestamp of prerequisite file
$ rake foo.txt    # running file task again
touch foo.txt     # output! the file was updated!

Because the timestamp for the bar.txt file was newer than that of the foo.txt file, Rake executes the actions for the foo.txt task.

Useful Example

With this series, I’m trying to show you a feature of Rake, then show a useful example of using that feature, hoping that it’ll spark an idea for how you can use Rake in your normal development process; this post is no exception.

In our Rails applications, we typically have a number of configuration files that are critical for the application to run correctly. But, because these files contain either sensitive information or settings specific to where it’s being run, we do not put these files in source control; instead we usually add an “example” file with dummy data, so those who begin working on our application later know what needs to be set. Well, we can use Rake to simplify the creation of our configuration files from these “example” files.

So, let’s say we want to create the config/database.yml file from the config/database.yml.example file:

file 'config/database.yml' => 'config/database.yml.example' do
  cp 'config/database.yml.example', 'config/database.yml'
end

Aside: Here we are using FileUtils’s cp (short for: copy) method.

$ ls config/                          # showing database.yml does not exist
database.yml.example
$ rake config/database.yml            # running file task
cp database.yml.example database.yml  # output from file task
$ ls config/                          # showing file was created
database.yml database.yml.example

Okay, so our task is working properly, and copying the example file as expected. I’m not a huge fan of our task definition as it stands: there’s too much repetition, and we can clean that up. When creating a task, the block that you pass to the task method can also take an argument, which will be the task itself. We can use this task argument to DRY up our task definition:

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

That looks much better, and it still behaves the same. With this task in place, anyone who joins the project can run the task and will then have a config/database.yml to use. If they happen to run it again, nothing will happen; until someone updates the config/database.yml.example file, at which point you can then run this task again and get the latest changes.

That means you can think of these “example” files as templates for the actual files we need. Granted, it would be nice if the task didn’t just overwrite our config/database.yml with the contents of the example file and instead allowed it to merge the two files together; in future posts in this series we’ll be looking at expanding this task to do just that!