#!/usr/bin/python
import commands
import sys, os
import shutil
import getopt
import re

USAGE = r"""\
Usage: remv [OPTIONS] [PATTERN [REPLACE]] FILE ...

Options:
    -r          replace PATTERN with REPLACE for each file (default)
    -m          move files matching PATTERN to REPLACE
    -d          remove all occurrences of PATTERN
    -c          capitalize all given files
    -l          lowercase all given files
    -u          uppercase all given files
    -i          ignore case when matching PATTERN
    -R          be recursive
    -D          do action on directories as well
    -I          confirm actions interactively
    -C          consider REPLACE to be a command, expanding backreferences
                before and after execution
    -f          force overwrite actions
    -v          be verbose
    --dry       don't actually do anything
    -h, --help  print this message

Examples:
    Replace spaces by underlines:
        remv ' ' '_' *

    Do the same with help from sed:
        remv -m -C '.*' 'echo "\&" | sed "s/_/ /g"' *

    Move files like 6.1.001 to vim-6.1-001.patch:
        remv -m '^6.1.(\d{3})$' 'vim-6.1-\1.patch'

    Capitalize all files:
        remv -c *

Written by Gustavo Niemeyer <niemeyer@conectiva.com>.
"""

class Error(Exception): pass
class DoubleModeError(Error):
	def __init__(self):
		Error.__init__("specify only one mode")

MOVE    = 1
REPLACE = 2
DELETE  = 3
CAPITAL = 4
UPPER   = 5
LOWER   = 6

def parse_options():
	try:
		opts, args = getopt.getopt(sys.argv[1:], "mrdcluifvhIDRC",
								   ["dry", "help"])
	except getopt.GetoptError, e:
		raise Error, e.msg
	class Options: pass
	obj = Options()
	obj.mode = None
	obj.ignorecase = 0
	obj.recursive = 0
	obj.dirs = 0
	obj.interactive = 0
	obj.command = 0
	obj.force = 0
	obj.verbose = 0
	obj.dry = 0
	obj.arg_from = None
	obj.arg_to = None
	obj.files = []
	need_from = 1
	need_to = 1
	for opt, val in opts:
		if opt == "-m":
			if obj.mode: raise DoubleModeError
			obj.mode = MOVE
		elif opt == "-r":
			if obj.mode: raise DoubleModeError
			obj.mode = REPLACE
		elif opt == "-d":
			if obj.mode: raise DoubleModeError
			obj.mode = DELETE
			need_to = 0
		elif opt == "-c":
			if obj.mode: raise DoubleModeError
			obj.mode = CAPITAL
			need_from = need_to = 0
		elif opt == "-l":
			if obj.mode: raise DoubleModeError
			obj.mode = LOWER
			need_from = need_to = 0
		elif opt == "-u":
			if obj.mode: raise DoubleModeError
			obj.mode = UPPER
			need_from = need_to = 0
		elif opt == "-i":
			obj.ignorecase = 1
		elif opt == "-D":
			obj.dirs = 1
		elif opt == "-R":
			obj.recursive = 1
		elif opt == "-I":
			obj.interactive = 1
		elif opt == "-C":
			obj.command = 1
		elif opt == "-f":
			obj.force = 1
		elif opt == "-v":
			obj.verbose = 1
		elif opt == "--dry":
			obj.dry = 1
		elif opt in ["-h", "--help"]:
			sys.stdout.write(USAGE)
			sys.exit(0)
	if obj.mode is None:
		obj.mode = REPLACE
	try:
		if need_from:
			obj.arg_from = args.pop(0)
		if need_to:
			obj.arg_to = args.pop(0)
		elif obj.command:
			raise Error, "replace pattern is not needed in this mode"
	except IndexError:
		raise Error, "couldn't find all needed arguments"
	obj.files = args
	if not obj.files:
		raise Error, "no filenames provided"
	return obj

REGROUP = re.compile(r"\\(\d|&)")

def replace_backrefs(pattern, filename, template):
	m = pattern.search(filename)
	if m:
		def func(x):
			ref = x.group(1)
			if ref == "&":
				return filename
			else: 
				try:
					return m.group(int(ref))
				except IndexError:
					raise Error, r"backref \%s not found in template '%s'" % \
								 (ref, template)
		return REGROUP.subn(func, template)[0]
	return None

def recurse(opt, dir):
	if opt.recursive:
		opt.files.extend([os.path.join(dir,x) for x in os.listdir(dir)])

def main():
	opt = parse_options()
	if opt.arg_from:
		if opt.ignorecase:
			pat_from = re.compile(opt.arg_from, re.I)
		else:
			pat_from = re.compile(opt.arg_from)
	for path_from in opt.files:
		while path_from and path_from[-1] == "/":
			path_from = path_from[:-1]
		if path_from == "":
			raise Error, "empty filename"
		elif not os.path.exists(path_from):
			raise Error, "file not found: "+path_from
		elif os.path.isdir(path_from):
			if not opt.dirs:
				recurse(opt, path_from)
				continue
			isdir = 1
		else:
			isdir = 0
		dir_from, file_from = os.path.split(path_from)
		file_to = file_from
		arg_to = opt.arg_to

		# Prepare file_to if necessary:
		if opt.command:
			cmd = replace_backrefs(pat_from, file_from, arg_to)
			if cmd:
				status, output = commands.getstatusoutput(cmd)
				if status != 0:
					raise Error, "command '%s' returned status %d" % \
								 (cmd, status)
				output = output.strip()
				if not output:
					raise Error, "command '%s' returned empty output" % \
								  cmd
				arg_to = output
		if opt.mode == CAPITAL:
			l = len(file_from)
			file_to_list = [""]*l
			extension = file_from.rfind(".")
			if extension == -1:
				extension = l
			capitalize = 1
			for i in range(l):
				letter = file_from[i]
				if capitalize and i < extension:
					if letter.isalpha():
						letter = letter.upper()
						capitalize = 0
				else:
					if letter.isalpha():
						letter = letter.lower()
					else:
						if letter != "'":
							capitalize = 1
				file_to_list[i] = letter
			file_to = "".join(file_to_list)
		elif opt.mode == LOWER:
			file_to = file_from.lower()
		elif opt.mode == UPPER:
			file_to = file_from.upper()
		elif arg_to is not None:
			arg_to = replace_backrefs(pat_from, file_from, arg_to)
			if arg_to is not None:
				if opt.mode == REPLACE:
					file_to = pat_from.subn(arg_to, file_from)[0]
				else:
					file_to = arg_to

		path_to = os.path.join(dir_from, file_to)

		# Do the actions:
		if opt.mode == DELETE:
			if opt.interactive:
				type = isdir and "directory" or "file"
				answer = raw_input("Remove %s '%s'? " % (type, path_from))
				if answer.lower().strip() not in ["y", "yes"]:
					isdir and recurse(opt, path_from)
					continue
			if opt.verbose:
				print "Removing '%s'." % path_from
			if not opt.dry:
				if isdir:
					shutil.rmtree(path_from)
				else:
					os.unlink(path_from)
		elif file_to != file_from:
			type = isdir and "directory" or "file"
			if os.path.isdir(path_to):
				raise Error, "can't replace directory '%s' with %s '%s'" % \
							 (path_to, type, path_from)
			elif not opt.force and os.path.isfile(path_to):
				answer = raw_input("Overwrite file '%s' with %s '%s'? " %
								   (path_to, type, path_from))
				if answer.lower().strip() not in ["y", "yes"]:
					isdir and recurse(opt, path_from)
					continue
			elif opt.interactive:
				answer = raw_input("Move %s '%s' to '%s'? " %
								   (type, path_from, path_to))
				if answer.lower().strip() not in ["y", "yes"]:
					isdir and recurse(opt, path_from)
					continue
			if opt.verbose:
				print "Moving '%s' to '%s'." % (path_from, path_to)
			if not opt.dry:
				os.rename(path_from, path_to)
				# That's the only reason why we can't do a global recurse()
				# just after the loop start.
				isdir and recurse(opt, path_to)
			else:
				isdir and recurse(opt, path_from)
		elif opt.verbose:
			print "Skipping '%s'." % path_from
			isdir and recurse(opt, path_from)

if __name__ == "__main__":
	try:
		main()
	except Error, msg:
		sys.stderr.write("error: %s\n" % str(msg))
		sys.exit(1)

# vim:ts=4:sw=4
