Home | Ruby | SearchReplaceGlobal |     Share This Page

Listing: searchreplaceglobal.rb


Click here to download original file.

#!/usr/bin/ruby -w

=begin
/***************************************************************************
 *   Copyright (C) 2006, 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

# user-defined, platform specific viewers

EDITOR='kwrite'
VIEWER='kuickshow'

require 'searchreplaceglobalui_ui.rb'

require 'searchreplaceglobalhelp'

require 'find'

PROGRAM_VERSION = '1.4'

=begin
   The Configuration class defines program values
   to be preserved in a configuration file.
=end

class Configuration
   attr_accessor :app_xpos
   attr_accessor :app_ypos
   attr_accessor :app_xsize
   attr_accessor :app_ysize
   attr_accessor :file_filter_list
   attr_accessor :search_for_list
   attr_accessor :replace_with_list
   attr_accessor :subdirs
   attr_accessor :global
   attr_accessor :caseSens
   attr_accessor :multiLine
   attr_accessor :reverse
   attr_accessor :changed_file_list
   attr_accessor :search_path_list
   def initialize
      @app_xpos = -1
      @app_ypos = -1
      @app_xsize = -1
      @app_ysize = -1
      @file_filter_list = nil
      @search_for_list = nil
      @replace_with_list = nil
      @changed_file_list = nil
      @search_path_list = nil
      @subdirs = true
      @global = true
      @caseSens = false
      @multiline = true
      @reverse = false
   end
end

=begin
   The ConfigurationHandler class reads and writes
   the program configuration file and populates a
   Configuration class instance
=end

class ConfigurationHandler
   attr_accessor :ini_path
   def initialize(conf,parent)
      @conf = conf
      @prog_name = parent.className()
      @conf_path = File.join(ENV["HOME"], "." + @prog_name)
      @ini_path = @conf_path + "/" + @prog_name + ".ini"
      Dir.mkdir(@conf_path) unless FileTest.exists?(@conf_path)
   end
   def write_config()
      file = File.new(@ini_path,"w")
      unless file.nil?
         @conf.instance_variables.sort.each { |x|
            xi = @conf.instance_variable_get(x)
            # escape strings
            if(xi.class == String)
               xi.gsub!(/\/,"\\\\")
               xi.gsub!(/"/,"\\"")
               xi = "\"#{xi}\""
            end
            file.write("#{x}=#{xi}\n")
         }
         file.close()
      end
   end
   def read_config()
      if FileTest.exists?(@ini_path)
         file = File.new(@ini_path,"r")
         file.each { |line|
            @conf.instance_eval(line)
         }
         file.close()
      end
   end
end

# main class

class SearchReplaceGlobal < SearchReplaceGlobalUI
   attr_accessor :ini_file
   def initialize(app,argv)
      super()
      @app = app
      @argv = argv
      setCaption(self.className + " " + PROGRAM_VERSION)
      @file_list = nil
      @changed_file_list = nil
      @config = Configuration.new
      @configHandler = ConfigurationHandler.new(@config,self)
      read_config()
      @ini_file = @configHandler.ini_path
      @help_engine = SearchReplaceGlobalHelp.new(self)
      @fileFilterComboBox.setFocus()
   end

   def beep
      @app.beep
   end

   def write_combo_box(widget,data)
      if(data)
         update_combo_box(widget,data.split("\t"))
      else
         widget.insertItem("")
      end
   end

   def read_config()
      @configHandler.read_config()
      if(@config.changed_file_list && @config.changed_file_list.strip.length > 0)
         @changed_file_list = @config.changed_file_list.split("\t")
         @changedTextEdit.setText(@changed_file_list.sort.join("\n"))
      end
      write_combo_box(@searchPathComboBox,@config.search_path_list)
      write_combo_box(@fileFilterComboBox,@config.file_filter_list)
      write_combo_box(@searchComboBox,@config.search_for_list)
      write_combo_box(@replaceComboBox,@config.replace_with_list)
      @subdirsCheckBox.setChecked(@config.subdirs)
      @globalCheckBox.setChecked(@config.global)
      @caseCheckBox.setChecked(@config.caseSens)
      @reverseCheckBox.setChecked(@config.reverse)
      @multiLineCheckBox.setChecked(@config.multiLine)
      if(@config.app_xpos != -1)
         move(@config.app_xpos,@config.app_ypos)
         resize(@config.app_xsize,@config.app_ysize)
      end
   end

   def read_combo_box(widget)
      array = update_combo_box(widget)
      return array.join("\t")
   end

   def write_config()
      @config.app_xsize = width()
      @config.app_ysize = height()
      @config.app_xpos = x()
      @config.app_ypos = y()
      @config.subdirs = @subdirsCheckBox.isChecked()
      @config.global = @globalCheckBox.isChecked()
      @config.caseSens = @caseCheckBox.isChecked()
      @config.multiLine = @multiLineCheckBox.isChecked()
      @config.reverse = @reverseCheckBox.isChecked()
      @config.changed_file_list = (@changed_file_list)?@changed_file_list.join("\t"):""
      @config.search_path_list = read_combo_box(@searchPathComboBox)
      @config.file_filter_list = read_combo_box(@fileFilterComboBox)
      @config.search_for_list = read_combo_box(@searchComboBox)
      @config.replace_with_list = read_combo_box(@replaceComboBox)
      @configHandler.write_config()
   end

   # override default close() method
   def close(*x)
      if(Qt::MessageBox::question(self, self.className.to_s,"Okay to close application?",Qt::MessageBox::No,Qt::MessageBox::Yes) == Qt::MessageBox::Yes)
         write_config()
         @app.exit(0)
      end
   end

   def build_file_tree()
      update_all_combo_boxes()
      begin
         @file_list = []
         path = @searchPathComboBox.text(0).strip
         path = (path.length == 0)?".":path
         search = @fileFilterComboBox.text(0).strip
         Find.find(path) do |item|
            unless(FileTest.directory?(item))
               if(search.length == 0 || item =~ %r{#{search}})
                  @file_list << item
               end
            end
         end
      rescue Exception => err
         Qt::MessageBox::warning(self, self.className.to_s,"Error in file search:\n\n\"" + err.to_s + "\"")
      end
   end

   def build_tree()
      build_file_tree()
      @resultsTextEdit.setText(@file_list.sort.join("\n"))
      statusBar.message("Found #{@file_list.size} matching files.")
   end

   def unescape(s)
      shift = false;
      len = s.length;
      output = "";
      i = 0;
      while(i < len)
         c = s[i,1]
         if(shift)
            case(c)
            when "t" then output += "\t"
            when "n" then output += "\n"
            when "r" then output += "\r"
            when "f" then output += "\f"
            when "a" then output += "\a"
            when "e" then output += "\e"
            when "b" then output += "\b"
            when "v" then output += "\v"
            when "0" then output += "\0"
               # handle case of /c(control letter)
            when "c" then
               if(i < len-1)
                  i += 1
                  output += (s[i] & 0x1f).chr;
               end
            else output += "\" + c
            end
            shift = false;
         else # not shifted
            if(c == "\")
               shift = true;
            else
               output += c;
            end
         end
         i += 1
      end
      return output
   end

   def build_search_regex()
      search = unescape(@searchComboBox.text(0))
      options = 0
      options |= Regexp::MULTILINE if @multiLineCheckBox.isChecked()
      options |= Regexp::IGNORECASE unless @caseCheckBox.isChecked()
      return Regexp.new(search,options)
   end

   def find_matches()
      build_file_tree()
      begin
         reversed = @reverseCheckBox.isChecked()
         results = []
         @file_list.each do |path|
            data = File.read(path)
            regex = build_search_regex()
            n = data.scan(regex)
            outcome = n.size > 0
            outcome = !outcome if reversed
            if(outcome)
               results << sprintf("[%6d] ",n.size) + path
            end
         end
         mod = (reversed)?"non-":""
         # show highest occurrences near top of list,
         # but sort alphabetically within equal
         # numbers of occurrences
         sorted = results.sort { |a,b|
            if a[0 .. 8] == b[0 .. 8]
               a[8 .. -1] <=> b[8 .. -1]
            else
               b <=> a
            end
         }
         @resultsTextEdit.setText(sorted.join("\n"))
         statusBar.message("Found #{results.size} #{mod}matching files.")
      rescue Exception => err
         Qt::MessageBox::warning(self, self.className.to_s,"Error in content search:\n\n\"" + err.to_s + "\"")
      end
   end

   def search_replace()
      build_file_tree()
      begin
         reply = Qt::MessageBox::critical(self, self.className.to_s,"Warning: search & replace on " + @file_list.size.to_s + "\nfile(s). Proceed?","Yes","Rehearse","No",2)
         if(reply < 2)
            replace = unescape(@replaceComboBox.lineEdit.text())
            @changed_file_list = []
            build_tree()
            regex = build_search_regex()
            @file_list.each do |path|
               data = File.read(path)
               if(@globalCheckBox.isChecked())
                  result = data.gsub(regex,replace)
               else
                  result = data.sub(regex,replace)
               end
               if(result != data)
                  if(reply == 0)
                     backup_path = path + "~"
                     unless FileTest.exist?(backup_path)
                        File.open(backup_path,"w") { |f| f.write(data) }
                     end
                     File.open(path,"w") { |f| f.write(result) }
                  end
                  @changed_file_list << path
               end
            end
            @changedTextEdit.setText(@changed_file_list.sort.join("\n"))
            s = (reply == 0)?"Changed":"Would have changed"
            statusBar.message(s + " #{@changed_file_list.size} files.")
         end
      rescue Exception => err
         Qt::MessageBox::warning(self, self.className.to_s,"Error in search & replace:\n\n\"" + err.to_s + "\"")
      end
   end

   def revert()
      if(@changed_file_list)
         original_changed = @changed_file_list.size
         recovered = []
         lost = []
         reply = Qt::MessageBox::critical(self, self.className.to_s,"Warning: attempt to restore " + @changed_file_list.size.to_s + "\nchanged file(s). Proceed?","Yes","No","Cancel",1)
         if(reply == 0)
            @changed_file_list.each do |path|
               backup_path = path + "~"
               if FileTest.exists?(backup_path)
                  data = File.read(backup_path)
                  File.open(path,"w") { |f| f.write(data) }
                  recovered << path
                  File.delete(backup_path)
               else
                  lost << path
               end
            end
            @changed_file_list = lost
            @resultsTextEdit.setText(recovered.sort.join("\n"))
            @changedTextEdit.setText(@changed_file_list.sort.join("\n"))
            statusBar.message("Restored #{recovered.size} out of " + original_changed.to_s + " files from backups.")
         end
      end
   end

   def edit_file(k,widget)
      widget.setSelection(k[0],0,k[0],widget.paragraphLength(k[0]))
      path = widget.selectedText().chomp
      path.sub!(%r{\[.*?\]},"")
      if(path =~ /\.(jpg|jpeg|bmp|gif|png|xpm|cpt)$/i)
         system("#{VIEWER} #{path} &")
      else
         system("#{EDITOR} #{path} &")
      end
   end

   def choosePath()
      fd = Qt::FileDialog.new(@searchPathComboBox.currentText())
      fd.setMode(Qt::FileDialog::DirectoryOnly)
      if(fd.exec() == Qt::Dialog::Accepted)
         path = fd.selectedFile();
         @searchPathComboBox.insertItem(path,0)
         @searchPathComboBox.setCurrentItem(0)
         update_combo_box(@searchPathComboBox)
      end
   end

   def erase_temps()
      temp_list = []
      path = @searchPathComboBox.text(0).strip
      Find.find(path) do |item|
         unless(FileTest.directory?(item))
            if(item =~ %r{.*~$})
               temp_list << item
            end
         end
      end
      @resultsTextEdit.setText(temp_list.sort.reverse.join("\n"))
      statusBar.message("Found #{temp_list.size} backup files.")
      reply = Qt::MessageBox::critical(self, self.className.to_s,"Warning: okay to erase " + temp_list.size.to_s + "\nbackup file(s)?","Yes","No","Cancel",1)
      if(reply == 0)
         temp_list.each do |path|
            File.delete(path)
         end
         @resultsTextEdit.setText("")
      end
   end

   def html_escape(s)
      s.gsub!("<","&lt;")
      s.gsub!(">","&gt;")
      return s
   end

   def update_combo_box(widget,array = nil)
      if(array)
         array.uniq!
         widget.clear()
         array.each do |item|
            widget.insertItem(item)
         end
      else
         # this section solves the problem that
         # the user may not have pressed "Enter"
         # before selecting an action
         line_edit = widget.lineEdit()
         if(line_edit)
            text = line_edit.text()
         else
            text = widget.currentText()
         end
         array = []
         0.upto(widget.count-1) do |i|
            array << widget.text(i)
         end
         # move selected text to top of list
         array.uniq!
         array.delete(text)
         array.unshift(text)
         widget.clear()
         array.each do |item|
            widget.insertItem(item)
         end
      end
      widget.setCurrentItem(0)
      s = widget.currentText()
      # will this string be difficult to read in a combobox?
      if(s && s.length > 16)
         tip = html_escape(s)
         Qt::ToolTip.add(widget,tip)
      elsif(widget.editable)
         Qt::ToolTip.add(widget,"Enter your search string here")
      else
         Qt::ToolTip.add(widget,"Select from this list or press \"Browse\".")
      end
      return array
   end

   def update_all_combo_boxes()
      update_combo_box(@fileFilterComboBox)
      update_combo_box(@searchPathComboBox)
      update_combo_box(@searchComboBox)
      update_combo_box(@replaceComboBox)
   end

   def quitButton_clicked(*k)
      close()
   end

   def searchReplaceButton_clicked(*k)
      search_replace()
   end

   def scanButton_clicked(*k)
      build_tree()
   end

   def choosePathButton_clicked(*k)
      choosePath()
   end

   def undoPushButton_clicked(*k)
      revert()
   end

   def searchPushButton_clicked(*k)
      find_matches()
   end

   def resultsTextEdit_clicked(*k)
      edit_file(k,@resultsTextEdit)
   end

   def changedTextEdit_clicked(*k)
      edit_file(k,@changedTextEdit)
   end

   def eraseButton_clicked(*k)
      erase_temps()
   end

   def searchPathComboBox_activated(*k)
      update_combo_box(@searchPathComboBox)
   end

   def fileFilterComboBox_activated(*k)
      update_combo_box(@fileFilterComboBox)
   end

   def searchComboBox_activated(*k)
      update_combo_box(@searchComboBox)
   end

   def replaceComboBox_activated(*k)
      update_combo_box(@replaceComboBox)
   end

   def helpSearchLineEdit_textChanged(*k)
      @help_engine.search
   end

   def helpSearchLineEdit_returnPressed(*k)
      @help_engine.search
   end

   def controlTabWidget_selected(*k)
      tab = k.first
      case tab
      when "Help" then @helpSearchLineEdit.setFocus()
      end
   end

end

# create and show application

if $0 == __FILE__
   app = Qt::Application.new(ARGV)
   dialog = SearchReplaceGlobal.new(app,ARGV)
   app.mainWidget = dialog
   dialog.show
   app.exec
end

Home | Ruby | SearchReplaceGlobal |     Share This Page