Home | Linux | Bash Script Beautifier |     Share This Page

#!/usr/bin/ruby -w


=begin
/***************************************************************************
 *   Copyright (C) 2008, Paul Lutus                                        *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/
=end

PVERSION = "Version 1.2, 09/10/2009"

module BeautifyBash

   # user-customizable values

   BeautifyBash::TabStr = " "
   BeautifyBash::TabSize = 3

   def BeautifyBash.beautify_string(data,path)
      tab = 0
      case_count = 0
      case_stack = []
      case_stack[0] = 0
      in_here_doc = false
      defer_ext_quote = false
      in_ext_quote = false
      ext_quote_string = ""
      here_string = ""
      output = []
      data.each do |record|
         record.chomp!
         stripped_record = record.strip
         if(in_here_doc)
            if(stripped_record =~ %r{#{here_string}})
               in_here_doc = false
            end
         else # not in here_doc
            if(stripped_record =~ %r{<<-?})
               here_string = stripped_record.sub(%r{.*<<-?\s*['|"]?([_|\w]+)['|"]?.*},"\\1")
               in_here_doc = true if (here_string.size > 0)
               # puts "here string: [#{here_string}]"
            end
         end
         if(in_here_doc) # pass unchanged
            output << record
         else # not in here_doc
            test_record = stripped_record.gsub(/\\./,"")
            # collapse multiple quotes between ' ... '
            test_record = test_record.gsub(/'.*?'/,"")
            # collapse multiple quotes between " ... "
            test_record = test_record.gsub(/".*?"/,"")
            # remove '#' comments
            test_record = test_record.sub(/(\A|\s)(#.*)/,"")
            if(in_ext_quote)
               if(test_record =~ %r{#{ext_quote_string}})
                  # provide line after quote
                  test_record = test_record.sub(%r{.*#{ext_quote_string}(.*)},"\\1")
                  in_ext_quote = false
               end
            else # not in ext_quote
               if(test_record =~ %r{[^\\]('|")})
                  # apply only after this line has been processed
                  defer_ext_quote = true
                  ext_quote_string = test_record.sub(%r{.*(['|"]).*},"\\1")
                  # provide line before quote
                  test_record = test_record.sub(%r{(.*)#{ext_quote_string}.*},"\\1")
               end
            end
            if(in_ext_quote) # pass unchanged
               output << record
            else
               inc = test_record.scan(/(\s|\A|;)(case|then|do)(;|\Z|\s)/).length
               inc += test_record.scan(/(\{|\(|\[)/).length
               outc = test_record.scan(/(\s|\A|;)(esac|fi|done|elif)(;|\)|\||\Z|\s)/).length
               outc += test_record.scan(/(\}|\)|\])/).length
               if(test_record =~ /\besac\b/)
                  outc += case_stack[case_count]
                  case_count -= 1
               end
               # sepcial handling for bad syntax within case ... esac
               if(case_count > 0)
                  if(test_record =~ /\A[^(]*\)/)
                     # avoid overcount
                     outc -= 2;
                     case_stack[case_count] += 1
                  end
                  if test_record =~ /;;/
                     outc += 1
                     case_stack[case_count] -= 1
                  end
               end
               # an ad-hoc solution for the "else" keyword
               else_case = (test_record =~ /^(else)/)?-1:0
               net = inc - outc
               tab += (net < 0)?net:0
               extab = tab + else_case
               extab = (extab > 0)?extab:0
               output << (TabStr * TabSize * extab) + stripped_record
               tab += (net > 0)?net:0
            end # not in ext quote
            if(defer_ext_quote)
               in_ext_quote = true
               defer_ext_quote = false
            end # no deferred ext quote flag
            if(test_record =~ /\bcase\b/)
               case_count += 1
               case_stack[case_count] = 0
            end
         end # not in here doc
      end # each record
      error = (tab != 0)
      STDERR.puts "Error: indent/outdent mismatch: #{tab}." if error
      return output.join("\n") + "\n",error
   end # beautify_string

   def BeautifyBash.beautify_file(path)
      error = false
      if(path == '-') # stdin source
         source = STDIN.read
         dest,error = beautify_string(source,"stdin")
         print dest
      else # named file source
         source = File.read(path)
         dest,error = beautify_string(source,path)
         if(source != dest)
            # make a backup copy
            File.open(path + "~","w") { |f| f.write(source) }
            # overwrite the original
            File.open(path,"w") { |f| f.write(dest) }
         end
      end
      return error
   end # beautify_file

   def BeautifyBash.main
      error = false
      if(!ARGV[0])
         STDERR.puts "usage: shell script filenames or \"-\" for stdin."
         exit 0
      end
      ARGV.each do |path|
         error = (beautify_file(path))?true:error
      end
      error = (error)?1:0
      exit error
   end # main
end # module BeautifyBash

# if launched as a standalone program, not invoked as a module
if __FILE__ == $0
   BeautifyBash.main
end

Home | Linux | Bash Script Beautifier |     Share This Page