Newer
Older
tren / tren.py
#!/usr/bin/env python
# tren.py
# Copyright (c) 2010 TundraWare Inc.
# For Updates See:  http://www.tundraware.com/Software/tren

# Program Information

PROGNAME = "tren.py"
PROGENV  = PROGNAME.split(".py")[0].upper()
RCSID = "$Id: tren.py,v 1.111 2010/01/26 01:23:14 tundra Exp $"
VERSION = RCSID.split()[2]

# Copyright Information

CPRT         = "(c)"
DATE         = "2010"
OWNER        = "TundraWare Inc."
COPYRIGHT    = "Copyright %s %s  %s " % (CPRT, DATE, OWNER)


#----------------------------------------------------------#
#            Variables User Might Change                   #
#----------------------------------------------------------#



#------------------- Nothing Below Here Should Need Changing ------------------#


#----------------------------------------------------------#
#                       Imports                            #
#----------------------------------------------------------#

import getopt
import os
import sys


#----------------------------------------------------------#
#                 Aliases & Redefinitions                  #
#----------------------------------------------------------#



#----------------------------------------------------------#
#                Constants & Literals                      #
#----------------------------------------------------------#



#####
# Constants
#####

MAXINCLUDES  =  50          # Maximum number of includes allowed
PADWIDTH     =  30          # Column width

#####
# Literals
#####

ALL          =  "All"       # Rename target is whole filename
COMMENT      =  "#"         # Comment character in include files
EXT          =  "Ext"       # Rename target is extension
EXTDELIM     =  "."         # Extension delimeter
INCL         =  "-I"        # Include file command line option
NAM          =  "Nam"       # Rename target is name
SEPARATOR    =   "-" *70


#----------------------------------------------------------#
#              Prompts, & Application Strings              #
#----------------------------------------------------------#


#####
# Debug Messages
#####

DEBUGFLAG     =   "-d"
dCMDLINE      =   "Command Line"
dCURSTATE     =   "Current State Of Program Options"
dDEBUG        =   "DEBUG"
dINCLUDING    =   "Including file '%s'"
dPAIR         =   "Option/Target Pair"
dPROGENV      =   "$" + PROGENV
dRESOLVEDOPTS =   "Resolved Command Line"
dTARGETS      =   "Rename Targets: %s"


#####
# Error Messages
#####

eBADARG       =  "Invalid Command Line: %s!"
eBADINCL      =  "option %s requires argument" % INCL
eERROR        =  "ERROR"
eFILEOPEN     =  "Cannot Open File '%s': %s!"
eTOOMANYINC   =  "Too Many Includes! (Max Is %d) Possible Circular Reference?" % MAXINCLUDES


#####
# Informational Messages
#####


#####
# Usage Prompts
#####

uTable = [PROGNAME + " " + VERSION + " - %s\n" % COPYRIGHT,
          "usage:  " + PROGNAME + " [-1abCcdEefghqtvXx] [-I file] [-l string] [-r old=new]... file|dir file|dir ...",
          "   where,",
          "         -1            Rename only the first instance of the specified string (Default)",
          "         -a            Rename within the entire file or directory name (Default)",
          "         -C            Do case-sensitive renaming (Default)",
          "         -c            Collapse case when doing string substitution.",
          "         -d            Dump debugging information",
          "         -e            Only perform renaming within extension portion of or directory name.",
          "         -E            Continue renaming even after an error is encountered",
          "         -f            Force renaming even if target file or directory name already exists.",
          "         -g            Replace all instances (global rename) of the old string with the new.",
          "         -h            Print help information.",
          "         -I file       Include command line arguments from file",
          "         -l string     File extension delimiter string. (Default: .)",
          "         -q            Quiet mode, do not show progress.",
          "         -r <old=new>  Replace old with new in file or directory names.",
          "         -t            Test mode, don't rename, just show what the program *would* do",
          "         -v            Print detailed program version information and exit.",
          "         -X            Treat the renaming strings literally (Default)",
          "         -x            Treat the old replacement string as a Python regular expression",
         ]

#----------------------------------------------------------#
#          Global Variables & Data Structures              #
#----------------------------------------------------------#

# Program toggle and option defaults

DEBUG             =   False        # Debugging off
CASESENSITIVE     =   True         # Search is case-sensitive
ERRORCONTINUE     =   False        # Do not continue after error
EXTDELIM          =   EXTDELIM     # Extension Delimiter
FORCERENAM        =   False        # Do not rename if target already exists
GLOBAL            =   False        # Only rename first instance of old string
QUIET             =   False        # Display progress
REGEX             =   False        # Do not treat old string as a regex
TARGET            =   ALL          # Can be "All", "Name", or "Ext"
TESTMODE          =   False


# Global data structures


#--------------------------- Code Begins Here ---------------------------------#


#----------------------------------------------------------#
#             Object Base Class Definitions                #
#----------------------------------------------------------#

    

#----------------------------------------------------------#
#             Supporting Function Definitions              #
#----------------------------------------------------------#


#####
# Turn A List Into Columns With Space Padding
#####

def ColumnPad(list, padchar=" ", padwidth=PADWIDTH):

    retval = ""
    for l in list:
        l = str(l)
        retval += l + ((padwidth - len(l)) * padchar)

    return retval.strip()

# End of 'ColumnPad()'


#####
# Print A Debug Message
#####

def DebugMsg(msg):
    PrintStderr(PROGNAME + " " + VERSION + " " + dDEBUG + ": " + msg)

# End of 'DebugMsg()'


#####
# Dump The State Of The Program
#####

def DumpState():

    DebugMsg(dCURSTATE)
    DebugMsg(SEPARATOR)

    # Names of all the state variables we want dumped
    state = [
             "DEBUG",
             "CASESENSITIVE",
             "ERRORCONTINUE",
             "EXTDELIM",
             "FORCERENAM",
             "GLOBAL",
             "QUIET",
             "REGEX",
             "TARGET",
             "TESTMODE",
            ]

    for k in state:
        DebugMsg(ColumnPad([k, eval(k)]))

    DebugMsg(SEPARATOR + "\n")

# End of 'DumpState()'


#####
# Print An Error Message
#####

def ErrorMsg(emsg):
    PrintStderr(PROGNAME + " " + VERSION + " " + eERROR + ": " + emsg)

# End of 'ErrorMsg()'


#####
# Print To stderr
#####

def PrintStderr(msg, trailing="\n"):
    sys.stderr.write(msg + trailing)

# End of 'PrintStderr()'


#####
# Print To stdout
#####

def PrintStdout(msg, trailing="\n"):
    sys.stdout.write(msg + trailing)

# End of 'PrintStdout'


#####
# Process Include Files On The Command Line
#####

def ProcessIncludes(OPTIONS):

    # Resolve include file references allowing for nested includes.
    # This has to be done here separate from the command line options so
    # that getopt() processing below will "see" the included statements.

    NUMINCLUDES = 0
    while " ". join(OPTIONS).find(INCL) > -1:
    
        # Get the index of the next include to process.
        # It cannot be the last item because this means the filename
        # to include is missing.
    
        i = OPTIONS.index(INCL)
        if i == len(OPTIONS)-1:
            ErrorMsg(eBADARG % eBADINCL)
            sys.exit(1)
    
        file = OPTIONS[i+1] ; lhs = OPTIONS[:i] ; rhs = OPTIONS[i+2:]
    
        # Keep track of- and limit the number of includes allowed
        # This is an easy way to stop circular (infinite) includes.
    
        NUMINCLUDES += 1
        if NUMINCLUDES >= MAXINCLUDES:
            ErrorMsg(eTOOMANYINC)
            sys.exit(1)
    
        # Replace insert option on the command line with that file's contents.
        # Handle comments within lines.
    
        try:
            n = []
            f = open(file)
            for l in f.readlines():
                l = l.split(COMMENT)[0]
                n += l.split()
            f.close()
    
            if DEBUG:
                DebugMsg(dINCLUDING % file)

            OPTIONS = lhs + n + rhs
    
        except IOError as e:
            ErrorMsg(eFILEOPEN % (file, e.args[1]))
            sys.exit(1)

    return OPTIONS

# End of 'ProcessIncludes()'


#####
# Break The Command Line Into Separate "Option... Target..." Pairs
#####

def ProcessOptTgtPair(OPTIONS):

    DOINGOPTS = True
    commandlines = []
    cmd = []

    while OPTIONS:

        i = OPTIONS[0]
        OPTIONS = OPTIONS[1:]

        # Process options
        if i.startswith("-"):
            if DOINGOPTS:
                cmd.append(i)

            # Starting a new "options... targets ..." pair
            else:
                commandlines.append(cmd)
                cmd = []
                DOINGOPTS = True
                cmd.append(i)

        # Process targets
        else:
            DOINGOPTS = False
            cmd.append(i)


    if cmd:
        commandlines.append(cmd)

    return commandlines
    
# End of 'ProcessOptTgtPair()'


#####
# Print Usage Information
#####

def Usage():
    for line in uTable:
        PrintStdout(line)

# End of 'Usage()'


#----------------------------------------------------------#
#                    Program Entry Point                   #
#----------------------------------------------------------#

#####
# Command Line Preprocessing
# 
# Some things have to be done *before* the command line
# options can actually be processed.  This includes:
#
#  1) Prepending any options specified in the environment variable.
#
#  2) Resolving any include file references
#
#  3) Separating the command line into [options ... filenames ..]
#     groupings so that user can interweave multiple options
#     and names on the command line.
#
#  4) Building the data structures that depend on the file/dir names
#     specified for renaming.  We have to do this first, because
#     -r renaming operations specified on the command line will
#     need this information if they make use of renaming tokens.
#
#####

# Process any options set in the environment first, and then those
# given on the command line


OPTIONS = sys.argv[1:]

envopt = os.getenv(PROGENV)
if envopt:
    OPTIONS = envopt.split() + OPTIONS


# Check for debug manually to see if we want
# debug info about includes

if DEBUGFLAG in OPTIONS:
    DEBUG = True

# Deal with include files

OPTIONS = ProcessIncludes(OPTIONS)

# Check for debug manually again before we process the options
# to get debug info on command line expansion

if DEBUGFLAG in OPTIONS:
    DEBUG = True

# Break command line into "option ... targets ..." pairs.

pairs = ProcessOptTgtPair(OPTIONS)

if DEBUG:

    # Dump what we know about the command line

    DebugMsg(ColumnPad([dCMDLINE, sys.argv]))
    DebugMsg(ColumnPad([dPROGENV, os.getenv("TREN")]))
    DebugMsg(ColumnPad([dRESOLVEDOPTS, OPTIONS]))


# Now process the command line in "opts... targets" pairs

for commandline in pairs:
    
    if DEBUG:
        DebugMsg(ColumnPad([dPAIR, " ".join(commandline)]))

    try:
        opts, args = getopt.getopt(commandline, '1abbCcdEefghl:qr:tvXx]')
    except getopt.GetoptError as e:
        ErrorMsg(eBADARG % e.args[0])
        sys.exit(1)

    for opt, val in opts:

        if opt == "-1":
            GLOBAL = False
        if opt == "-a":
            TARGET = ALL
        if opt == "-b":
            TARGET = NAM
        if opt == "-C":
            CASESENSITIVE = True
        if opt == "-c":
            CASESENSITIVE = False
        if opt == "-d":
            DumpState()
        if opt == "-E":
            ERRORCONTINUE = True
        if opt == "-e":
            TARGET = EXT
        if opt == "-f":
            FORCERENAM = True
        if opt == "-g":
            GLOBAL = True
        if opt == "-h":
            Usage()
            sys.exit(0)
        if opt == "-l":
            EXTDELIM = val
        if opt == "-q":
            QUIET = True
        if opt == "-r":
            pass
        if opt == "-t":
            TESTMODE = True
        if opt == "-v":
            PrintStdout(RCSID)
            sys.exit(0)
        if opt == "-X":
            REGEX = False
        if opt == "-x":
            REGEX = True