Erik's Engineering

something alliterative

One of the most common tasks on a big rails project is to add a new rake task. Rake is an important tool, used by lots of different groups in the ruby community. It's used by rails, but I'm pretty sure it pre-dates rails. It certainly doesn't feel like rails.

Versions

For this blog post, I'm using rake 0.9.2.2, rails 3.1.3 and ruby 1.9.3-p0. I hope that's all current enough for you. Newer versions of rake and rails changed a few things, deprecating some old conventions. I think the new ways still work on the older versions, but I haven't tested.

During development it's really easy to wind up with multiple versions of rake installed. It's safest to specify a version in your Gemfile and then use bundle exec to run the rake binary.

Listing Tasks

The first thing to do with rake, is run 'bundle exec rake -T'. This will give you a list of available rake tasks. Do it on your app right now, just to help orient yourself. Every rails app comes with a bunch of tasks, and most apps add new ones of their own.

rake_demo > bundle exec rake -T
rake about              # List versions of all Rails fr...
rake assets:clean       # Remove compiled assets
rake assets:precompile  # Compile all the assets named ...
rake db:create          # Create the database from conf...
rake db:drop            # Drops the database for the cu...
rake db:fixtures:load   # Load fixtures into the curren...
rake db:migrate         # Migrate the database (options...
rake db:migrate:status  # Display status of migrations
rake db:rollback        # Rolls the schema back to the ...
rake db:schema:dump     # Create a db/schema.rb file th...
rake db:schema:load     # Load a schema.rb file into th...
rake db:seed            # Load the seed data from db/se...
rake db:setup           # Create the database, load the...
rake db:structure:dump  # Dump the database structure t...
rake db:version         # Retrieves the current schema ...
rake doc:app            # Generate docs for the app -- ...
rake log:clear          # Truncates all *.log files in ...
rake middleware         # Prints out your Rack middlewa...
rake notes              # Enumerate all annotations (us...
rake notes:custom       # Enumerate a custom annotation...
rake rails:template     # Applies the template supplied...
rake rails:update       # Update configs and some other...
rake routes             # Print out all defined routes ...
rake secret             # Generate a cryptographically ...
rake stats              # Report code statistics (KLOCs...
rake test               # Runs test:units, test:functio...
rake test:recent        # Run tests for {:recent=>"test...
rake test:single        # Run tests for {:single=>"test...
rake test:uncommitted   # Run tests for {:uncommitted=>...
rake time:zones:all     # Displays all time zones, also...
rake tmp:clear          # Clear session, cache, and soc...
rake tmp:create         # Creates tmp directories for s...
rake_demo > 

There are a couple things to notice here.

First, tasks are listed in alphabetical order. In order to get 'create', 'migrate', and 'seed' anywhere close to each other, they have to be grouped together in a namespace ('db' in this case).

Second, tasks have friendly descriptions that go with them, to help you figure out what something like 'secret' is.

Third, rake will conveniently truncate the output to match the width of your terminal window. Make that window really wide if you want to read more of the description. Keep this in mind when you make your own tasks - put the really important description first.

Fourth, if a task doesn't have a description it won't show up. There are more tasks than just the ones rake -T shows you. Think of the others as private tasks.

You can add an extra argument after -T and rake will filter the output down to only tasks that include it. Very handy if you want to see all the test tasks, or if you can't remember if it's test:unit or test:units.

Running Tasks

You run task 'foo' with the command line 'bundle exec rake foo'.

You can run both tasks foo and bar using 'bundle exec rake foo bar'. This can save you a lot of time if you have a big rails app that takes a long time to start. I cut 3 minutes off our deploy time by bundling multiple rake commands together to avoid repeated startup cost.

You pass arguments to a task by enclosing them in square brackets with NO SPACES. 'bundle exec rake foo[bar,1]'. If you put spaces anywhere between the start of the task name and the ending square bracket, rake will get confused and probably error out.

The rake tasks that come with rails usually get their arguments from environment variables instead. 'bundle exec rake db:migrate VERSION=0'

Code Organization

Rake tasks go in the lib/tasks directory, in files with a .rake extension. These aren't loaded during normal application boot, so whenever possible you should minimize the amount of code that goes into those .rake files and instead have them call code that's in other parts of the codebase. Think of rake tasks as actions for a command line application. They should do some argument processing and then hand things off to business logic that lives in your models or other classes.

Try not to let your .rake files get too big. Code in them is hard to test and inaccessible from the rest of your app.

Basic Rake Task

Here's a very basic .rake file, with a simple task.

namespace :demo do

  desc "a basic task"
  task :basic do |t, args|
    puts "I'm a basic task"
  end

end

You'd call this with 'bundle exec rake demo:basic'.

First off, notice the namespace :demo line. I've named this to match the filename (demo.rake). Tasks within this namespaces will be called with demo: before their names. This gives you a way to indicate that some tasks are related to each other. This gets important when you have a lot of them. The naming convention helps you find the right file.

Next, you see the desc line. This gives a description that will be shown when you run rake -T. Keep it short - rake will truncate it to fit it on a single line in the user's terminal.

If you do not include a desc line, the task will not show up when you run rake -T. You can make use of that to create "private" tasks that aren't advertised.

The task line is the start of the actual task declaration. It names the task, declares args and dependencies and starts up the block of code that is executed when the task is run. The |t,args| on the block are optional, but I'd recommend always including them to help remind you about how to pass args. Consider them boilerplate.

Try to keep your tasks short and sweet, just like methods. These are every bit as much application code as anything in a controller or model, and the same rules about clear naming, documentation and short methods that apply to other code apply equally to rake tasks.

Dependencies

Rake tasks can depend on other things. You do this by putting '= > [:list, :of, :dependencies]' between the task name and the do that starts the task block. You can list dependencies as either symbols or strings. Most people use symbols and fall back to strings when forced to by syntax. If a dependency is in the same namespace you don't have to include the namespace part. For dependencies outside your current namespace you pretty much need to list them as strings.

Dependencies are a great way to break longer rake tasks down into shorter tasks and make it easier to re-use code. All dependencies will be run before the body of a task is executed, so any setup code will itself have to go in a dependency.

You'll notice that I never accessed any of my application's models in the basic demo task. That's because by default your rails application isn't loaded. The powers that be know you need this a lot, so they have a handy rake task called :environment that will do it for you. Just list it as a dependency. This is a great example of extracting some commonly used code into a separate task that's brought in as a dependency.

Here are a couple rake tasks that make use of dependencies:

namespace :deps do
  desc "uses the environment"
  task :with_environment => [:environment] do |t, args|
    puts "User count: #{User.count}"
  end

  desc "run both one and two"
  task :both => [:one, :two] do |t, args|
    puts "both"
  end

  desc "print 'one'"
  task :one do |t, args|
    puts "one"
  end

  desc "print 'two'"
  task :two do |t, args|
    puts "two"
  end

end

Basically, anything on the task line in the array after the hash rocket will be run first. You can add any number of dependencies by adding them to array, but most of the time you'll just want the environment.

Multiple dependencies will be satisfied in the order specified, but will only be called once for the entire invocation of rake. This means that each task will only run once, even if it's a dependency of multiple other tasks.

rake_demo > bundle exec rake deps:with_environment deps:both deps:one
User count: 0
one
two
both
rake_demo > 

Loading the rails environment will make your rake task take a lot longer to run. If possible, avoid loading rails. Your fellow developers will thank you for it.

Arguments

Sometimes, you want to pass in an argument or two to your task. The tasks that come with rails usually pass arguments via environment variables. This gives you named arguments, but you can't differentiate arguments for different tasks and you can't use the very handy argument handling code that comes with rake.

Rake's built-in argument handling code gets arguments like this: 'bundle exec rake db:dump[filename]'.

Arguments are listed in an array on the task line, after the task is named, but before the declaration of dependencies (if any). task :name, [:arg1, :arg2] => [:dep1, :dep2] do |t, args|

Here's a simple rake task that accepts some arguments.

namespace :args do
  desc "takes dimensions as arguments"
  task :dimensions, [:x,:y] do |t, args|
    args.with_defaults(:x => 50, :y => 100)

    puts "dimensions are #{args[:x]}x#{args[:y]}"
  end
end

Arguments are ordered, not named. However, rake automatically parses them into a hash for you based on the order you declare them on the task line. It also gives you a handy way to set defaults for those arguments. Check out the #with_defaults method. It will make your life easier.

rake_demo > bundle exec rake -T args
rake args:dimensions[x,y]  # takes dimensions as arguments
rake_demo > bundle exec rake args:dimensions
dimensions are 50x100
rake_demo > bundle exec rake args:dimensions[25]
dimensions are 25x100
rake_demo > bundle exec rake args:dimensions[25,75]
dimensions are 25x75
rake_demo > 

You don't get a lot of guarantees about what type the arguments will show up as (e.g. String vs Integer), so don't forget to do appropriate conversions to keep yourself safe. Likewise, rake doesn't consider arguments mandatory, so you'll need to enforce that manually.

Calling Other Tasks

Sometimes, you want to call another rake task, but for some reason you don't want to just list it as a dependency. Perhaps you need to calculate some values to pass to it as arguments, or you want to force it to run even if it was already invoked as a dependency by some other task.

You can do this by looking up the task using Rake::Task['task_name_as_string'] and calling #invoke on it.

Pass your arguments to the #invoke call just like any normal method call.

namespace :invoke do

  desc 'invoke bar with random argument'
  task :foo do |t, args|
    n = rand(5)

    Rake::Task['invoke:bar'].invoke n
  end

  desc 'default is 3'
  task :bar, [:n] do |t, args|
    args.with_defaults(:n => 3)
    puts "n is #{args['n']}"
  end

end
rake_demo > bundle exec rake invoke:foo
n is 0
rake_demo > 

File Tasks

Rake is based on the traditional make command. Make is all about creating files based on dependencies - it's a helper for compiling things.

Naturally, rake gives you a handy tool for building files. Here's a quick example of using that capability as part of a larger task.

desc "set up a fresh git clone for dev work"
task :setup => ['config/database.yml', 'setup:bundle_install', 'db:migrate'] do |t,args|
end

namespace :setup do

  desc "set up database.yml"
  file "config/database.yml" => ['config/database.yml.example'] do

    cp "config/database.yml.example", "config/database.yml"

  end


  desc "install gems"
  task :bundle_install do |t, args|

    system('gem install bundler')
    system('bundle install')

  end

end

Rake is smart enough to not overwrite database.yml if one already exists. If a file depends on other files, it will take their timestamps into account when deciding whether or not they need to be updated. In this case, if I update database.yml.example, bundle exec rake setup will overwite my database.yml, picking up any new settings that were added. That means you can use it to conditionally build assets, etc.

If you don't declare any file dependencies, rake will only build the file if it doesn't already exist. Sometimes this is safer - maybe I don't want to risk nuking my database.yml and it's better to leave it alone if one already exists.

Summary

Rake is a really DSL for adding command line functions to your rails apps. It offers some distinct advantages over plain old scripts but like any other code you write you do need to think about what you're doing first so that you can make the most of its features without creating unmaintainable code.

  • Organize your tasks into relevant namespaces. Don't just throw them all in a junk drawer.
  • Keep tasks short, with business logic in other places. Think of tasks as actions in a controller.
  • Don't load the Rails environment if you don't have to.
  • Use dependencies to build more powerful tasks via composition.

Edit:

Brook Riggio has posted a really excellent mind-map of the stock tasks that come with a Rails app. Check it out here. I love how it breaks everything down hierarchically in a way you just can't do in text.

Published on 04/12/2011 at 17h04 under , . Tags , ,

I released a side project last weekend and it was featured on the front page of Slashdot on Monday.

Published on 23/12/2010 at 17h10 under , . Tags , , ,

If you don't recharge your batteries, you'll run slow and not work very well.

Published on 14/10/2010 at 02h01 under , . Tags ,

I walk through the steps to create a new Rails app, using Ruby 1.9, Rails 3, Rspec and Cucumber.

Published on 11/09/2010 at 13h59 under , . Tags , , , ,

For less than $50 in parts, I built a controller that let me use my crock pot for sous vide cooking.

Published on 31/07/2010 at 16h46 under , . Tags , ,

If you're staring at some code and you just can't find the bug... chances are that it's not there.

Published on 25/07/2010 at 23h18 . Tags

A very brief introduction to using RabbitMQ with Carrot to send data between processes asynchronously.

Published on 15/05/2010 at 13h44 under , . Tags , , , ,

Powered by Typo – Thème Frédéric de Villamil | Photo Glenn