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!("<","<")
s.gsub!(">",">")
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