something alliterative
Rake for Rails Developers
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.
Powered by Typo – Thème Frédéric de Villamil | Photo Glenn