#!/usr/bin/env python

### Curby's Batch File Mover/Renamer
### by Michael Lee
### v1.00.00, 2006-09-03

###   Installation
#
#  1. Place this script somewhere in your executable PATH.  
#  2. Make sure that python is installed (this was tested with python 2.4).
#

###   License
#
#  Copyright (C) 2006  Michael Lee <kirbysdl@yahoo.com>
#
#  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.
#

import os               # File system and path mangling
import sys              # General interface (argv, stdio, exit)
import re               # Regular expressions
import getopt           # CLI argument parsing
import string           # String mangling for interactive mode



# --------------------------------------------------------------------
#   Utility Functions
# --------------------------------------------------------------------

# Display verbose help with usage and examples then exit
def usage(errcode=0):
  print """Usage: %s [OPTION]... SEARCH_PATTERN REPLACE_PATTERN
Renames files using regular expressions.

By default, all options are off.  Specifying an option enables its 
associated feature.

Display Options:
  -h, --help         Display this help text and exit
  -t, --test         Display planned changes without performing them
  -v, --verbose      Display every name change as it is made 
                       (use twice to see debugging output)

Selection Options:
  -d, --directories  Rename directories as well as files
  -r, --recursive    Recursively process files in subdirectories

Conflict Resolution Options:
  If a destination already exists, an error is reported and the 
  program exits. This behavior can be modified as follows:
 
  -i, --interactive  Prompt to force deletion on changes
  -f, --force        Attempt to force changes without confirmation
                       (this option overrides the -i option)
 
PATTERNS must be compatible with python's re.sub() function. In
general, Perl-compatible regular expressions will work.

For further help, see the README file distributed with this script,
and also available at http://curby.net/filelib/cbfmr/README

Report bugs to <kirbysdl@hotmail.com> or at http://curby.net/
"""%os.path.basename(sys.argv[0])
  sys.exit(errcode)

# Display "get help" message and exit
def gethelp(errcode=0):
  sys.stderr.write("Try `%s --help' for more information\n"%os.path.basename(sys.argv[0]))
  sys.exit(errcode)



# --------------------------------------------------------------------
#   Renaming Functions
# --------------------------------------------------------------------

# Rename one file or directory
def rename_one(conf, path, name, srch_re, repl_re):
  newname = re.sub(srch_re, repl_re, name)

  # Nothing to do. Bored now.
  if name == newname:
    return
  
  name = os.path.join(path, name)
  newname = os.path.join(path, newname)

  # Output the intended change to the user
  if conf["test"] or conf["verbose"]:
    print "%s --> %s" % (name,newname)
  if conf["test"]:
    return

  # Conflict, drama, etc.
  if os.path.exists(newname):
    if conf["force"]:
      clobber = True
    elif conf["interactive"]:
      if conf["verbose"] == 0:
        print "%s --> %s" % (name,newname)
      s = string.lower(raw_input("Overwrite existing destination (Yes/No/All)? "))
      while len(s) < 1 or not s[0] in "yna":
        s = string.lower(raw_input("Overwrite existing destination (Yes/No/All)? "))
      if s[0] == "n":
        return
      clobber = True
      if s[0] == "a":
        if conf["verbose"] > 1:
          print "%s: Overwriting ALL future name conflicts" % conf["name"]
        conf["force"] = True
        conf["interactive"] = False
    else:
      sys.stderr.write("%s: ERROR: Moving %s to existing destination %s.\n" % (conf["name"],name,newname))
      sys.exit(4)
    
  # Ok now we're gonna rename something.  Seriously.
  try:
    os.rename(name, newname)
  except OSError, message:
    sys.stderr.write("%s: ERROR: (while moving %s --> %s) %s\n" % (conf["name"],name,newname,message))
    sys.exit(5)

# Batch renamer
def main(conf, srch_re, repl_re):
  for root,dirs,files in os.walk(".", conf["filesonly"]):
    if not conf["recursive"] and root != ".":
      continue
    if conf["verbose"] > 1:
      print "%s: Processing %s" % (conf["name"],root)
    if not conf["filesonly"]:
      for name in dirs:
        rename_one(conf, root, name, srch_re, repl_re)
    for name in files:
      rename_one(conf, root, name, srch_re, repl_re)



# --------------------------------------------------------------------
#   Launcher
# --------------------------------------------------------------------

if __name__ == "__main__":

  # Initialize configuration dictionary for this execution
  # XXX This whole thing should be a class
  conf = {"name":os.path.basename(sys.argv[0]), 
          "verbose":0,       "force":False, "interactive":False, 
          "recursive":False, "test":False,  "filesonly":True
         }

  # Require at least two args
  if len(sys.argv) < 3:
    if len(sys.argv) == 2 and (sys.argv[1] == "-h" or sys.argv[1] == "--help"):
      usage(0)
    sys.stderr.write("Usage: %s [OPTION]... SEARCH_PATTERN REPLACE_PATTERN\n" % conf["name"])
    gethelp(1)

  # Try to grab options
  if len(sys.argv) > 3:
    try:
      options = getopt.getopt(sys.argv[1:-2], "hvifrtd", ("help","verbose","interactive","force","recursive","test","directories"))
    except getopt.GetoptError, message:
      sys.stderr.write("%s: ERROR: %s\n" % (conf["name"],message))
      gethelp(2)
    if len(options[1]) > 0:
      sys.stderr.write("%s: ERROR: Expecting 2 non-option arguments (got %d)\n"%(conf["name"],len(options[1])+2))
      gethelp(3)
    for arg, param in options[0]:
      if arg == "-h" or arg == "--help":
        usage(0)
      elif arg == "-v" or arg == "--verbose":
        conf["verbose"]    += 1
      elif arg == "-i" or arg == "--interactive":
        conf["interactive"] = True 
      elif arg == "-f" or arg == "--force":
        conf["force"]       = True
      elif arg == "-r" or arg == "--recursive":
        conf["recursive"]   = True
      elif arg == "-t" or arg == "--test":
        conf["test"]        = True
      elif arg == "-d" or arg == "--directories":
        conf["filesonly"]   = False

  # Grab regular expression patterns
  srch_re = sys.argv[-2]
  repl_re = sys.argv[-1]

  # Resolve conflicts with mutually-exclusive options
  if conf["force"]:
    conf["interactive"] = False

  # Tell the user what we're gonna do
  if conf["verbose"] > 1:
    vstr=", and displaying debugging information as well as name changes"
    if conf["test"]:          tstr=", only displaying changes instead of making changes"
    else:                     tstr=""
    if conf["force"]:         cstr=", attempting to overwrite on name conflicts"
    elif conf["interactive"]: cstr=", prompting for input to resolve name conflicts"
    else:                     cstr=", exiting on name conflicts"
    if conf["recursive"]:     rstr=" and subdirectories"
    else:                     rstr=""
    if not conf["filesonly"]: dstr=" and directory"
    else:                     dstr=""

    print "%s: Replacing `%s' with `%s' in file%s names in the current directory%s%s%s%s\n" \
          % (conf["name"],srch_re,repl_re,dstr,rstr,cstr,tstr,vstr)

  # Go go go!
  main(conf, srch_re, repl_re)
