In the dark ages of web programming, we’d write various Perl, PHP, and shell scripts to maintain our application. Something to clean up the sessions, something to send out our newsletter every night, something to rename files en masse. We’d scatter them throughout our directory tree, like ninja stars thrown blindly at that old hickory down by the river. Then, one day, the brash new kid up and duplicates all of them on his own because he has no idea they exist. How could he?! We have no one to blame but ourselves.
Fortunately, this is the renaissance. We have Rake. Aside from freezing gems, managing Subversion, and rake remigrating, we can also use Rake to help make our shellin’ lives easier.
While you probably use a gooey to manhandle your database (I fancy YourSQL), sometimes you just need to hit the mysql shell and hit it hard. For those occasions I really want to not worry about the host, port, or showing my credentials. I just want to shell in and get on with my business. Something like $ rake production mysql would be nice.
Two steps, here they are.
environments.rake
So, you can chain rake tasks. Run $ rake task1 task2 and the tasks will be executed in that order. As a result, I want to create some environment tasks which set the RAILS_ENV for me. That way I can $ rake production migrate instead of having to $ rake migrate RAILS_ENV=production. I switch environments so much that this little nicety is worth it. And it has another advantage, which I’ll tell you about later.
Anyway, define a task for each of your environments and slip them into something like, say, RAILS_ROOT/lib/tasks/environments.rake.
Here’s my environments.rake (refactored by Jamie Macey):
%w[development production staging].each do |env| desc "Runs the following task in the #{env} environment" task env do RAILS_ENV = ENV['RAILS_ENV'] = env end end task :testing do RAILS_ENV = ENV['RAILS_ENV'] = 'test' end task :dev do Rake::Task["development"].invoke end task :prod do Rake::Task["production"].invoke end
Now we can prefix any task with an environment instead of having to fumble with RAILS_ENV ourselves. Joy.
mysql.rake
The second part of our $ rake production mysql task (which we want to launch us into a shell connected to our production MySQL server) is the actual mysql task.
Here’s RAILS_ROOT/lib/tasks/mysql.rake, which we use to build and execute a mysql shell command using the configuration options set in database.yml. Using the config file ensures our mysql rake task is always synced up with our Rails application’s db settings.
def sh_mysql(config) returning '' do |mysql| mysql << mysql_command << ' ' mysql << "-u#{config['username']} " if config['username'] mysql << "-p#{config['password']} " if config['password'] mysql << "-h#{config['host']} " if config['host'] mysql << "-P#{config['port']} " if config['port'] mysql << config['database'] if config['database'] end end def mysql_command 'mysql' end desc "Launch mysql shell. Use with an environment task (e.g. rake production mysql)" task :mysql do system sh_mysql(YAML.load(open(File.join('config', 'database.yml')))[RAILS_ENV]) end
If you need to use something other than the `mysql’ shell command (maybe you need to include the full path?), just replace the return value of the mysql_command method.
Lucky for us this is easy, fast, and fairly safe: the mysql shell command is nice enough to hide the password from ps aux. That part of the command will show as ‘-pXXXXX’, Fort Knox style.
That’s it. Now you can $ rake prod mysql or $ rake mysql or whatever, all day long. You barely need to code anymore.
Tab-leeting It
Since tab completion is all the rage these days, I thought it’d be nice to dig through the Internet and find out how to get it rolling on rake.
What I came across was a chain letter ending at this onrails.org post. It rules! But unfortunately, it’s slow. Real slow. Is my laptop really that old already? Damn.
The problem is that rake is being run each time we hit tab. Here’s my sped up version:
#!/usr/bin/env ruby # Save this somewhere, chmod 755 it, then add # complete -C path/to/this/script -o default rake # to your ~/.bashrc # # If you update your tasks, just $ rm ~/.raketabs* # # Adapted from # http://onrails.org/articles/2006/08/30/namespaces-and-rake-command-completion exit 0 unless File.file?(File.join(Dir.pwd, 'Rakefile')) exit 0 unless /^rake\b/ =~ ENV["COMP_LINE"] def rake_silent_tasks if File.exists?(dotcache = File.join(File.expand_path('~'), ".raketabs-#{Dir.pwd.hash}")) File.read(dotcache) else tasks = `rake --silent --tasks` File.open(dotcache, 'w') { |f| f.puts tasks } tasks end end after_match = $' task_match = (after_match.empty? || after_match =~ /\s$/) ? nil : after_match.split.last tasks = rake_silent_tasks.split("\n")[1..-1].map { |line| line.split[1] } tasks = tasks.select { |t| /^#{Regexp.escape task_match}/ =~ t } if task_match # handle namespaces if task_match =~ /^([-\w:]+:)/ upto_last_colon = $1 after_match = $' tasks = tasks.map { |t| (t =~ /^#{Regexp.escape upto_last_colon}([-\w:]+)$/) ? "#{$1}" : t } end puts tasks exit 0
You can grab it with wget http://pastie.caboo.se/17743.txt.
Next stick this into .bashrc (or whatever init script your shell hits):
complete -C path/to/script/you/downloaded -o default rake
Now restart your shell, then cd to a Rails project. Type rake db:<tab>. Wait a second. Did all the tasks show up? Awesome. Hit tab again. Then again. Then again. Tab tab tab tab tab. Only the first time should be slow. What we’re doing is caching the rake --tasks call because otherwise rake will be run each time we hit tab. To clear out your local cache, just $ rm ~/.raketabs* like the script sez.
Improvements? Bug fixes? Let’s hear ‘em.
Love It
Rake is real cool. Beyond having namespaces and method_missing, it’s got built in support for building gems. And hey, if you look in rake/lib/rake/contrib you can even find files such as rubyforgepublisher.rb for, you guessed it, publishing to Rubyforge automatically. What can’t this kid do?
Update
Yeah, I lied about my environments.rake needing to be un-DRY and Jamie Macey called me on it big time. Revised code has been posted.
You had me at “So, you can chain rake tasks.”
On the business of auto-completion – does anyone know how I can plug this into WinXP’s cmd?
So Chris, what reason do you have for not being DRY in your environments.rake? This seems to work for me:
Also, you should rename your ‘test’ task to ‘testing’, elsewise it conflicts with the default meaning of test, which is to run all the tests. On my box at least, the rails definition is the one with priority.
Great tips! Thanks!
Just for the benefit of other readers, I had to add this to mysql.rake to get it working:
require ‘active_support/core_ext/object/misc’
It might be my configuration but I suspect the more recent versions or rails don’t load the full environment when running rake.
Mat,
Good catch—at first I wasn’t sure why ‘returning’ wasn’t working, and then it made sense. You can also make the mysql task depend on :environment, but just requiring the file which defines ‘returning’ probably makes more sense anyway. (This was with edge rails, so I’m pretty sure you’re right about the environment loading with rake…)
Thanks very much for this post! I enjoyed it so much I grabbed the environment and mysql portions, adapted and refactored, and have published them in a plugin called GenerallyUseful (with full credit to this post for the basis of the adaptation).
Thanks again!
Oooh! The tab-completion for rake tasks is * ! :O
Big thanks!
Cheers!
Chime in.