Friday, October 06, 2006

Beautify Ruby in Rails with Rake

I was looking for a way to automatically beautify Ruby code, and found a nice Ruby script (rbeautify.rb - version 2.1) with a little searching. However, I wanted to beautify all the Ruby code in my Rails project with a single command, so I wrote a Rake script.

My Rake script seems to work well, but some files do cause the beautifier to report Indentation errors (which I haven't looked into yet). Also, "here documents" in the code caused problems. I modified the beautifier to correctly skip over them, but it now also skips over code it shouldn't in rare cases (which should be harmless). I could not make the fix perfect because the beautifier seems to parse code on a line-by-line instead of token level. This makes it more difficult to recognize some things. As a safety measure, I also prevented the script from overwriting files on Indentation errors.


Installing:
1. Create /lib/rbeautify.rb
2. Create /lib/tasks/beautify.rake
3. Copy in the following:

beautify.rake:
require 'rbeautify'

namespace :rails do
desc "Beautifies all Ruby files in the project"
task :beautify do
_folders = Array.new
_folders.push(Dir.new(pwd)) # populate folder list with project dir
while _folder = _folders.pop # while folder list is populated
for _name in _folder # iterate through items in folder
_path = File.join(_folder.path, _name) # create full path
next if _name[0,1] == '.' # skip ".", "..", ".svn", etc.
if File.directory? _path
_folders.push(Dir.new(_path)) # add dirs to stack
elsif (_name[-3,3] == '.rb' || # only process Ruby files
_name[-5,5] == '.rake' ||
_name[-4,4] == '.rjs')
Beautify.beautifyRuby(_path) # call beatify
end
end
end
end
end



rbeautify.rb:
#!/usr/bin/ruby -w

# Ruby beautifier, version 2.1, 09/11/2006
# Copyright (c) 2006, P. Lutus
# Released under the GPL
#
# NOTE (by P. Tseng):
# Modified original to NOT back up any files, and to abort if any indentation
# errors are detected.
#
# KNOWN BUGS:
# Does not handle all cases of /<<[-'"\w]/

class Beautify
@@tabSize = 2
@@tabStr = " "

# indent regexp tests
@@indentExp = [
/^module\b/,
/^if\b/,
/(=\s*|^)until\b/,
/(=\s*|^)for\b/,
/^unless\b/,
/(=\s*|^)while\b/,
/(=\s*|^)begin\b/,
/(^| )case\b/,
/\bthen\b/,
/^class\b/,
/^rescue\b/,
/^def\b/,
/\bdo\b/,
/^else\b/,
/^elsif\b/,
/^ensure\b/,
/\bwhen\b/,
/\{[^\}]*$/,
/\[[^\]]*$/
]

# outdent regexp tests
@@outdentExp = [
/^rescue\b/,
/^ensure\b/,
/^elsif\b/,
/^end\b/,
/^else\b/,
/\bwhen\b/,
/^[^\{]*\}/,
/^[^\[]*\]/
]

def self.makeTab(tab)
return (tab <>
end

def self.addLine(line,tab)
line.strip!
line = makeTab(tab)+line if line.length > 0
return line + "\n"
end

def self.beautifyRuby(path)
hereDocumentTerminators = Array.new
hereDocumentStart = false
commentBlock = false
programEnd = false
multiLineArray = Array.new
multiLineStr = ""
tab = 0
source = File.read(path)
dest = ""
source.split("\n").each do |line|
if(!programEnd && hereDocumentTerminators.size == 0)
# detect program end mark
if(line =~ /^__END__$/)
programEnd = true
else
# combine continuing lines
if(!(line =~ /^\s*#/) && line =~ /[^\\]\\\s*$/)
multiLineArray.push line
multiLineStr += line.sub(/^(.*)\\\s*$/,"\\1")
next
end

# add final line
if(multiLineStr.length > 0)
multiLineArray.push line
multiLineStr += line.sub(/^(.*)\\\s*$/,"\\1")
end

tline = ((multiLineStr.length > 0)?multiLineStr:line).strip
if(tline =~ /^=begin/)
commentBlock = true
# Check for here document.
# Tries to skip some uses of <<>
# TODO: currently may wrongly assume other uses of <<>
# documents, if the <<>
# but should be safe (skips over them)
elsif(!(tline =~ /^#/) && (end_index = tline.index(/<<[-"'\w]/)) &&
!(tline =~ /class *<<[-"'\w]/) && !(tline =~ /['"\d\]] *<<[-"'\w]/))
# check if we're inside a string
current_index = 0
single_count = 0
double_count = 0
previous_c = -1
tline.each_byte do |c|
break if (current_index == end_index)
current_index += 1
# unescaped single quote
if (c == 39 &&amp;amp;amp;amp;amp;amp; !(double_count == 1) && previous_c != 92)
if single_count == 0
single_count = 1
else
single_count = 0
end
elsif (c == 34 && !(single_count == 1) && previous_c != 92)
if double_count == 0
double_count = 1
else
double_count = 0
end
end
previous_c = c
end

# if we're not in a string, get here doc terminators
if (single_count == 0 && double_count == 0)
hereDocumentTerminators = line.split('<<')
hereDocumentTerminators.delete_at(0)
for x in hereDocumentTerminators
hereDocumentTerminators.delete(x)
if (x.size != 0 && x =~ /^[-"'\w]/)
x.sub!(/-/, '')
if (x[0,1] == "'" || x[0,1] == '"')
x = x.sub(Regexp.new(x[0,1]), '').
sub(Regexp.new(x[0,1] + '.*'), '')
else
x = x.slice(/\w*/)
end
hereDocumentTerminators.push(x)
end
end
end

if hereDocumentTerminators.size != 0
hereDocumentStart = true
end
end
end
end
if((commentBlock || programEnd || hereDocumentTerminators.size > 0) &&
!hereDocumentStart)
# add the line unchanged
dest += line + "\n"
else
commentLine = (tline =~ /^#/)
if(!commentLine)
# throw out sequences that will
# only sow confusion
while tline.gsub!(/'.*?'/,"")
end
while tline.gsub!(/".*?"/,"")
end
while tline.gsub!(/\`.*?\`/,"")
end
while tline.gsub!(/\{[^\{]*?\}/,"")
end
while tline.gsub!(/\([^\(]*?\)/,"")
end
while tline.gsub!(/\/.*?\//,"")
end
while tline.gsub!(/%r(.).*?\1/,"")
end
tline.gsub!(/\\\"/,"'")
@@outdentExp.each do |re|
if(tline =~ re)
tab -= 1
break
end
end
end
if (multiLineArray.length > 0)
multiLineArray.each do |ml|
dest += addLine(ml,tab)
end
multiLineArray.clear
multiLineStr = ""
else
dest += addLine(line,tab)
end
if(!commentLine)
@@indentExp.each do |re|
if(tline =~ re && !(tline =~ /\s+end\s*$/))
tab += 1
break
end
end
end
end
if(tline =~ /^=end/)
commentBlock = false
elsif hereDocumentStart
hereDocumentStart = false
elsif(hereDocumentTerminators.size > 0)
for terminator in hereDocumentTerminators
if line.strip == terminator
hereDocumentTerminators.delete(terminator)
break
end
end
end
end
if(source != dest && tab == 0)
# make a backup copy
#File.open(path + "~","w") { |f| f.write(source) }
# overwrite the original
File.open(path,"w") { |f| f.write(dest) }
puts 'Beautified: ' + path
end
if(tab != 0)
STDERR.puts "Indentation error (#{tab}): #{path}"
end
end

#if(!ARGV[0])
# STDERR.puts "usage: Ruby filenames to beautify."
# exit 0
#end
#
#ARGV.each do |path|
# beautifyRuby(path)
#end

end

2 comments:

Nano said...

I just tried your rake task and I belive that you have some errors in your code:

1. In the method self.makeTab of rbeautify.rb you have an incomplete line, should be this: (or something like that)

return (tab < 0) ? '': @@tabStr * @@tabSize * tab


2. In the rake task in the line
_path = File.join(_folders.path, _name) # create full path

must be

_path = File.join(_folder.path, _name) # create full path


Now everything works. Please correct me if I am wrong.

Thanks!

PS. I am planning to update your task to the new rbeautify version. Are you interested?

Peter Tseng said...

1. Yes, I believe it should be:

return (tab < 0) ? "" : @@tabStr * @@tabSize * tab

2. I don't see the mistake (_folders instead of _folder)