#!/usr/bin/env python
import os
import shutil
import logging
import sys
from optparse import OptionParser
import re
import warnings
import time
logging.basicConfig(level=logging.DEBUG)
warnings.filterwarnings('ignore', 'tmpnam')
class Config:pass
config = Config()
def parse_options():
usage = "usage: %prog [options] file_or_dir+"
parser = OptionParser(usage=usage)
parser.add_option('-s', '--search', dest='search',
help='what to search')
parser.add_option('-r', '--replace', dest='replace',
help='what replace with')
(opts, args) = parser.parse_args()
config.mode = 'search'
if not opts.search:
if opts.replace:
parser.error('Replace option could be specified only with search option')
if not args:
parser.error('No options for search')
if len(args) > 1:
parser.error('Without -s option you can specify only one positonal argument that would be the search value')
config.search = args[0]
config.targets = ['']
else:
config.search = opts.search
config.targets = args
if opts.replace:
config.replace= opts.replace
config.mode = 'replace'
def search(path):
"""
Search string in each string of file.
"""
LIMIT = 160
started = False
for count, line in enumerate(file(path)):
number = count + 1
if config.search in line:
if not started:
print config.term.highlight(path, 'GREEN')
started = True
if len(line) <= LIMIT:
print '%d:%s' % (number, config.term.highlight(line, ('BLACK', 'BG_YELLOW'), config.search))
else:
print '%d:LINE IS TOO LONG (>%d)' % LIMIT
if started:
print
def search_replace(path):
"""
Replace all search with replace and make backup copy.
"""
base, name = os.path.split(path)
while True:
tmp_path = path + str(time.time()).replace('.', '')
if not os.path.exists(tmp_path):
break
data = file(path).read()
if config.search in data:
print path
# Read and create new data
data = data.replace(config.search, config.replace)
open(tmp_path, 'w').write(data)
# Create backup
backup_path = None
for count in xrange(999):
test_path = os.path.join(base, '.%s.%03d~' % (name, count))
if not os.path.exists(test_path):
backup_path = test_path
break
if not backup_path:
logging.error('Can\'t create backup for %s. File was not modified' % path)
return
shutil.copy(path, backup_path)
shutil.copy(tmp_path, path)
def process_file(path):
if config.mode == 'search':
return search(path)
elif config.mode == 'replace':
return search_replace(path)
def main():
cwd = os.getcwd()
walked = []
# TODO: not check twice the same dir or file
for path in config.targets:
abs_path = os.path.join(cwd, path)
if not os.path.islink(abs_path) and os.path.isfile(abs_path):
walked.append(abs_path)
process_file(abs_path)
if os.path.isdir(abs_path):
walked.append(abs_path)
for root, dirs, files in os.walk(abs_path):
for fname in files:
abs_path = os.path.join(root, fname)
walked.append(abs_path)
if not os.path.islink(abs_path) and\
os.path.isfile(abs_path):
process_file(abs_path)
for dir in dirs:
abs_path = os.path.join(root, dir)
walked.append(abs_path)
class Terminal:
BOLD = DIM = REVERSE = NORMAL = ''
BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
_STRING_CAPABILITIES = """BOLD=bold DIM=dim REVERSE=rev NORMAL=sgr0""".split()
_COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
_ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
def __init__(self, term_stream=sys.stdout):
"""
Create a `TerminalController` and initialize its attributes
with appropriate values for the current terminal.
`term_stream` is the stream that will be used for terminal
output; if this stream is not a tty, then the terminal is
assumed to be a dumb terminal (i.e., have no capabilities).
"""
try:
import curses
curses.setupterm()
except:
return
if not term_stream.isatty():
return
# Look up string capabilities.
for capability in self._STRING_CAPABILITIES:
(attrib, cap_name) = capability.split('=')
setattr(self, attrib, self._tigetstr(cap_name) or '')
# Colors
set_fg = self._tigetstr('setf')
if set_fg:
for i,color in zip(range(len(self._COLORS)), self._COLORS):
setattr(self, color, curses.tparm(set_fg, i) or '')
set_fg_ansi = self._tigetstr('setaf')
if set_fg_ansi:
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
set_bg = self._tigetstr('setb')
if set_bg:
for i,color in zip(range(len(self._COLORS)), self._COLORS):
setattr(self, 'BG_'+color, curses.tparm(set_bg, i) or '')
set_bg_ansi = self._tigetstr('setab')
if set_bg_ansi:
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
def _tigetstr(self, cap_name):
# String capabilities can include "delays" of the form "$<2>".
# For any modern terminal, we should be able to just ignore
# these, so strip them out.
import curses
cap = curses.tigetstr(cap_name) or ''
return re.sub(r'\$<\d+>[/*]?', '', cap)
def highlight(self, text, colors, search=None):
"""
Highlight fragments in string or whole string with color.
"""
if not isinstance(colors, (tuple, list)):
colors = [colors]
color = ''.join(getattr(self, x) for x in colors)
norm_color = getattr(self, 'NORMAL')
if search is None:
return color + text + norm_color
else:
parts = text.split(search)
delim = color + search + norm_color
return delim.join(parts)
def prepare_backup_dir():
if config.mode == 'replace':
path = os.tmpnam()
os.mkdir(path)
config.tmp_dir = path
if __name__ == '__main__':
parse_options()
config.term = Terminal()
prepare_backup_dir()
main()