Bad UI design in Rails

A minor peeve about the rails commandline tool.

When trying to upgrade an existing app using the new Rails 2.0 preview release you’ll inevitably be presented with a choice like this:

      exists  vendor/plugins
      exists  tmp/sessions
      exists  tmp/sockets
      exists  tmp/cache
      exists  tmp/pids
   identical  Rakefile
overwrite README? [Ynaqd]

What do the options mean here? Y most likely means "yes", n "no", and a probably means "overwrite all". But q and d? It turns out that they mean "quit" and "show a diff" respectively. Perhaps I could have guessed this, but I had to dig into the Rails source code to find out, and you just shouldn’t have to do that.

This is bad UI design. My first attempt at finding out what the options meant was doing a man rails but Rails doesn’t have a man page. My next try was rails --help but that provides no information about the abbreviations that might crop up. Finally the README that is installed with new Rails apps doesn’t appear to make any mention of this either.

I wondered if h might provide "help", so I tried it and boom, what do I see?

       force  README

So h forces an overwrite. Not very helpful.

So one is forced to dig into /usr/local/lib/ruby/gems/1.8/gems/rails-1.2.4.7794/lib/rails_generator/commands.rb and inspect the source code in order to figure out what’s going on.

Here’s the code:

def force_file_collision?(destination, src, dst, file_options = {}, &block)
  $stdout.print "overwrite #{destination}? [Ynaqd] "
  case $stdin.gets
    when /d/i
      Tempfile.open(File.basename(destination), File.dirname(dst)) do |temp|
        temp.write render_file(src, file_options, &block)
        temp.rewind
        $stdout.puts `#{diff_cmd} #{dst} #{temp.path}`
      end
      puts "retrying"
      raise 'retry diff'
    when /a/i
      $stdout.puts "forcing #{spec.name}"
      options[:collision] = :force
    when /q/i
      $stdout.puts "aborting #{spec.name}"
      raise SystemExit
    when /n/i then :skip
    else :force
  end
rescue
  retry
end

So here we see what the various options do and we also see why h caused things to be overwritten: the default action is to force an overwrite. Whoever thought that that was a bright idea? Good thing we all use version control, eh? Also note that the regular expressions used in the case statement will match the letters anywhere they appear in the input string, so if you type "xxxazzz" then that’s equivalent to typing "a".

How could this have been done better? Well, first up it should display a more helpful prompt:

overwrite README? (enter "h" for help) [Ynaqdh]

Secondly it should show detailed help for unknown options instead of just forcing the overwrite:

Y - yes, overwrite
n - no, do not overwrite
a - all, overwrite this and all others
q - quit, abort
d - diff, show the differences between the old and the new
h - help, show this help
overwrite README? (enter "h" for help) [Ynaqdh]

And thirdly it shouldn’t use such sloppy regexes.

I’ve submitted a patch with these fixes which has now been applied to the trunk.