diff --git a/tren.py b/tren.py index fb871e4..f1b1756 100755 --- a/tren.py +++ b/tren.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # tren.py -# Copyright (c) 2010-2011 TundraWare Inc. +# Copyright (c) 2010-2020 TundraWare Inc. # For Updates See: http://www.tundraware.com/Software/tren # Program Information @@ -15,7 +15,7 @@ # Copyright Information CPRT = "(c)" -DATE = "2010-2011" +DATE = "2010-2020" OWNER = "TundraWare Inc." RIGHTS = "All Rights Reserved." COPYRIGHT = "Copyright %s %s, %s %s" % (CPRT, DATE, OWNER, RIGHTS) @@ -67,7 +67,7 @@ POSIX = True # Set up Windows-specific stuff - + if WINDOWS: # Try to load win32all stuff if it's available @@ -99,7 +99,7 @@ else: sys.stderr.write("Unsupported Operating System! Aborting ...\n") sys.exit(1) - + #----------------------------------------------------------# # Aliases & Redefinitions # @@ -288,7 +288,7 @@ # Rename target keys BASE = "BASENAME" -PATHNAME = "PATHNAME" +PATHNAME = "PATHNAME" STATS = "STATS" # These literals serve two purposes: @@ -439,7 +439,7 @@ 'u' : str.upper } -CASEOPS = CASETBL.keys() +CASEOPS = list(CASETBL.keys()) CASEOPS.sort() @@ -447,7 +447,7 @@ DAYS = {0:"Mon", 1:"Tue", 2:"Wed", 3:"Thu", 4:"Fri", 5:"Sat", 6:"Sun"} - + MONTHS = {1:"Jan", 2:"Feb", 3:"Mar", 4:"Apr", 5:"May", 6:"Jun", 7:"Jul", 8:"Aug", 9:"Sep", 10:"Oct", 11:"Nov", 12:"Dec"} @@ -484,7 +484,7 @@ ALPHABETS = { BINARY : ["0", "1"], - + DECIMAL : ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], HEXLOWER : ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"], @@ -540,7 +540,7 @@ REGEX : False, # Do not treat old string as a regex RENSEP : DEFSEP, # Old, New string separator for -r TARGETSTART : False, # Entire file name is renaming target by default - TARGETEND : False, + TARGETEND : False, TESTMODE : False # Test mode off } @@ -565,7 +565,7 @@ class RenameTargets: - """ + """ This class is used to keep track of all the files and/or directories we're renaming. After the class is constructed and the command line fully parsed, this will contain: @@ -596,7 +596,7 @@ ORDERBYCDATE-date... : [fullnames in order by ctime within same 'date'] ... (repeated for each date), ORDERBYMDATE-date... : [fullnames in order by mtime within same 'date'] ... (repeated for each date) } - + self.RenRequests = [ { ASK : interactive ask flag @@ -680,7 +680,7 @@ [ST_NLINK, {}, ORDERBYNLINK], [ST_SIZE, {}, ORDERBYSIZE], [ST_UID, {}, ORDERBYUID], - ["", {}, ORDERBYUSER], + ["", {}, ORDERBYUSER], ] # Populate the data structures with each targets' stat information @@ -690,7 +690,8 @@ try: pathname, basename = os.path.split(fullname) stats = os.stat(fullname) - except (IOError, OSError), (errno, errstr): + except (IOError, OSError) as xxx_todo_changeme2: + (errno, errstr) = xxx_todo_changeme2.args ErrorMsg(eFILEOPEN % (fullname, errstr)) # Some operating systems (Windows) terminate the path with @@ -698,9 +699,9 @@ if pathname[-1] != os.sep: pathname += os.sep - + # Store fullname, basename, and stat info for this file - + if basename: self.RenNames[fullname] = {BASE : basename, PATHNAME : pathname, STATS : stats} @@ -732,7 +733,7 @@ sortkey = self.__GetFileUsername(fullname) # Save into storage - + if sortkey in storage: storage[sortkey].append(fullname) else: @@ -746,8 +747,8 @@ statflag, storage, order = seqtype - vieworder = storage.keys() - vieworder.sort() + vieworder = list(storage.keys()) + vieworder.sort() # Sort alphabetically when multiple filenames # map to the same key, creating overall @@ -774,7 +775,7 @@ FILETIMETOKS[TOKAYEAR], FILETIMETOKS[TOKAMON], FILETIMETOKS[TOKADAY]), - + (ORDERBYCDATE, ORDERBYCTIME, FILETIMETOKS[TOKCYEAR], FILETIMETOKS[TOKCMON], @@ -784,7 +785,7 @@ FILETIMETOKS[TOKMYEAR], FILETIMETOKS[TOKMMON], FILETIMETOKS[TOKMDAY])): - + lastdate = "" for fullname in self.SortViews[timeorder]: @@ -803,7 +804,7 @@ self.DateViews[key] = [fullname] lastdate = newdate - + # Add file to existing list of others sharing that date else: @@ -813,7 +814,7 @@ ##### - # Debug Dump + # Debug Dump ##### def DumpObj(self): @@ -845,7 +846,7 @@ ##### def __GetFileGroupname(self, fullname): - + if POSIX: return grp.getgrgid(self.RenNames[fullname][STATS][ST_GID])[0] @@ -874,7 +875,7 @@ retval = LookupAccountSid(fnhost, sidg)[0] # On any error, just act like win32all isn't there - + except: retval = WINGROUPNOT @@ -894,7 +895,7 @@ else: retval = WINDOWSUSER - + if WIN32ALL: try: @@ -918,7 +919,7 @@ retval = LookupAccountSid(fnhost, sido)[0] # On any error, just act like win32all isn't there - + except: retval = WINUSERNOT @@ -946,9 +947,9 @@ # Keep track of incremental renaming for use by debug RenSequence = [oldname] - + for renrequest in self.RenRequests: - + # Select portion of name targeted for renaming lname = "" @@ -960,17 +961,17 @@ # This couldn't be done at the time the target range was # saved intially, because the length of the name being processed # isn't known until here. - + if tstart == None: tstart = 0 if tend == None: tend = len(newname) - + if tstart or tend: bound = len(newname) - + # Normalize negative refs so we can use consistent # logic below @@ -1013,8 +1014,8 @@ lname, newname, rname = newname[:tstart], newname[tstart:tend], newname[tend:] else: - lname, newname, rname = newname, "", "" - + lname, newname, rname = newname, "", "" + # Handle conventional string replacement renaming requests # An empty newname here means that the -T argument processing @@ -1043,7 +1044,7 @@ # Find every instance of the 'old' string in the # current filename. 'Find' in this case can be either # a regular expression pattern match or a literal - # string match. + # string match. # # Either way, each 'hit' is recorded as a tuple: # @@ -1085,9 +1086,9 @@ else: searchtarget = newname - + # Collapse case if requested - + if not renrequest[CASESENSITIVE]: searchtarget = searchtarget.lower() @@ -1146,7 +1147,7 @@ # Handle case conversion renaming requests - + elif renrequest[CASECONV]: newname = CASETBL[renrequest[CASECONV]](newname) @@ -1158,7 +1159,7 @@ # Keep track of incremental renaming for use by debug RenSequence.append(newname) - + # Show the incremental renaming steps if debug is on if ProgramOptions[DEBUG]: @@ -1168,7 +1169,7 @@ if newname != oldname: self.__RenameIt(pathname, oldname, newname) - + # End of 'ProcessRenameRequests()' @@ -1181,7 +1182,7 @@ self.indentlevel += 1 indent = self.indentlevel * INDENT newlen = len(newname) - + # First make sure the new name meets length constraints if newlen < MINNAMELEN: @@ -1221,7 +1222,7 @@ else: InfoMsg(iFORCEDNOBKU % (fullold, fullnew)) - + else: InfoMsg(indent + iRENSKIPPED % (fullnew, fullold)) doit = False @@ -1241,7 +1242,7 @@ if not answer: answer = ASKNO.lower() - + if answer == ASKNO.lower(): doit = False @@ -1256,17 +1257,17 @@ sys.exit(1) if doit: - + # In test mode, track file names that would be produced. - + if ProgramOptions[TESTMODE]: self.NewFiles.append(fullnew) self.RenamedFiles.append(fullold) - + if fullold in self.NewFiles: self.NewFiles.remove(fullold) - + if fullnew in self.RenamedFiles: self.RenamedFiles.remove(fullnew) @@ -1276,14 +1277,15 @@ try: os.rename(fullold, fullnew) - except OSError, (errno, errstr): + except OSError as xxx_todo_changeme: + (errno, errstr) = xxx_todo_changeme.args ErrorMsg(eRENAMEFAIL % (fullold, fullnew, errstr)) self.indentlevel -= 1 # End of '__RenameIt()' - + ##### # Resolve Rename Tokens ##### @@ -1351,14 +1353,14 @@ for r in rentokens: fullrentoken = "%s%s%s" % (TOKDELIM, r[2], TOKDELIM) # Need this for error messages. - + ### # File Attribute Renaming Tokens ### if r[2] == TOKFILDEV: r[2] = str(self.RenNames[target][STATS][ST_DEV]) - + elif r[2] == TOKFILFNAME: r[2] = os.path.basename(target) @@ -1385,7 +1387,7 @@ elif r[2] == TOKFILUSER: r[2] = self.__GetFileUsername(target) - + ### # File Time Renaming Tokens @@ -1409,23 +1411,23 @@ elif parms[2] == "tm_wday": r[2] = DAYS[val] - + ### # System Renaming Tokens ### - + # Environment Variable replacement token - + elif r[2].startswith(TOKENV): r[2] = os.getenv(r[2][1:]) - + # Handle case for nonexistent environment variable - + if not r[2]: r[2] = "" - - + + # Command Run replacement token elif r[2].startswith(TOKCMDEXEC) and r[2].endswith(TOKCMDEXEC): @@ -1439,7 +1441,7 @@ # Handle Unix variants - else: + else: pipe = os.popen('{ ' + command + '; } 2>&1', 'r') output = pipe.read() @@ -1449,18 +1451,18 @@ status = 0 # Nonzero status means error attempting to execute the command - + if status: ErrorMsg(eEXECFAIL % (fullrentoken, command)) # Otherwise swap the command with its results, stripping newlines - + else: r[2] = output.replace("\n", "") # Random Number Replacement token - + elif r[2].startswith(TOKRAND): random.seed() @@ -1476,7 +1478,7 @@ if precision < 1: ErrorMsg(eTOKRANDIG % fullrentoken) - + fmt = '"%0' + str(precision) + 'd" % random.randint(0, pow(10, precision)-1)' r[2] = eval(fmt) @@ -1488,7 +1490,7 @@ ### # Sequence Renaming Tokens ### - + elif r[2] and (r[2][0] == TOKASCEND or r[2][0] == TOKDESCEND): # Parse the Sequence Renaming Token into the token itself @@ -1501,7 +1503,7 @@ token = r[2][1:] found = False - for seqtoken in self.SortViews.keys() + [ORDERBYADATE, ORDERBYCDATE, ORDERBYMDATE]: + for seqtoken in list(self.SortViews.keys()) + [ORDERBYADATE, ORDERBYCDATE, ORDERBYMDATE]: if token.split(ALPHADELIM)[0] == (seqtoken): @@ -1525,10 +1527,10 @@ ErrorMsg(eALPHABETMISSING % fullrentoken) # Empty alphabet string means default to decimal counting - + if not alphabet: alphabet = DECIMAL - + if alphabet not in ALPHABETS: ErrorMsg(eALPHABETEXIST % fullrentoken) @@ -1540,7 +1542,7 @@ # list). # One of the standard sorted views requested - + if token in self.SortViews: orderedlist = self.SortViews[token][:] @@ -1575,23 +1577,23 @@ ### # Unrecognized Renaming Token ### - + else: ErrorMsg(eTOKUNKNOWN % fullrentoken) - + ### # Successful Lookup, Do the actual replacement ### renstring = renstring[:r[0]] + r[2] + renstring[r[1]+1:] - + return renstring # End of '__ResolveRenameTokens()' # End of class 'RenameTargets' - + #----------------------------------------------------------# # Supporting Function Definitions # @@ -1659,7 +1661,7 @@ def ComputeSeqString(fmt, incr, alphabet): - """ + """ fmt = A literal "format field" string incr = A integer to be "added" to the field alphabet = The alphabet of characters to use, in ascending order @@ -1672,13 +1674,13 @@ rolling over the count. If 'fmt' is null on entry, the final result length is unlimited. """ - + base = len(alphabet) # Do position-wise "addition" via symbol substitution moving from # right-to-left adjusting for the fact that not all symbols in the # format string will be in the alphabet. - + # First convert the increment into a string in the base of the # alphabet @@ -1724,7 +1726,7 @@ sum += alphabet.index(incr[i]) # Do arithmetic modulo alphabet length - + carry, digit = sum/base, sum % base if not carry: @@ -1754,7 +1756,7 @@ # Result length constrained by format string if fmtlen: - + if len(newval) > fmtlen: InfoMsg(iSEQTOOLONG % (newval,fmt)) newval = newval[-fmtlen:] @@ -1773,7 +1775,7 @@ # Condition Line Length With Fancy Wrap And Formatting ##### -def ConditionLine(msg, +def ConditionLine(msg, PAD=PADCHAR, \ WIDTH=PADWIDTH, \ wrapindent=WRAPINDENT ): @@ -1797,7 +1799,7 @@ ##### def DebugMsg(msg): - + l = ConditionLine(msg) for msg in l: PrintStderr(PROGNAME + " " + dDEBUG + ": " + msg) @@ -1829,7 +1831,7 @@ DebugMsg(dCURSTATE) DebugMsg(SEPARATOR) - opts = ProgramOptions.keys() + opts = list(ProgramOptions.keys()) opts.sort() for o in opts: DebugMsg(ColumnPad([o, ProgramOptions[o]])) @@ -1866,7 +1868,7 @@ escaping = False - numseps = 0 + numseps = 0 sepindex = 0 oldnewsep = ProgramOptions[RENSEP] @@ -1880,7 +1882,7 @@ if (i > 0 and (arg[i-1] != ProgramOptions[ESCAPE])) or i == 0: sepindex = i numseps += 1 - + i += len(oldnewsep) else: @@ -1981,10 +1983,10 @@ field = OPTIONS[i] position = field.find(INCL) - + if field.startswith(OPTINTRO) and (position > -1): - + lhs = field[:position] rhs = field[position+1:] @@ -1998,14 +2000,14 @@ previousopt = True break - + # If the include symbol appears in the context of a # previous option, skip this field, otherwise process # it as an include. if not previousopt: - + FoundNewInclude = True if lhs == OPTINTRO: lhs = "" @@ -2067,9 +2069,10 @@ OPTIONS = OPTIONS[:i] + n + OPTIONS[i+1:] - except IOError, (errno, errstr): + except IOError as xxx_todo_changeme1: + (errno, errstr) = xxx_todo_changeme1.args ErrorMsg(eFILEOPEN % (inclfile, errstr)) - + i += 1 return OPTIONS @@ -2084,7 +2087,7 @@ def searchpath(filename, pathlist, pathdelim): # What we'll return if we find nothing - + retval = [] # Find all instances of filename in specified paths @@ -2122,7 +2125,7 @@ #----------------------------------------------------------# # Set up proper include path delimiter - + if WINDOWS: PATHDEL = PATHDELWIN @@ -2133,7 +2136,7 @@ ##### # Command Line Preprocessing -# +# # Some things have to be done *before* the command line # options can actually be processed. This includes: # @@ -2166,7 +2169,8 @@ try: opts, args = getopt.getopt(OPTIONS, OPTIONSLIST) -except getopt.GetoptError, (errmsg, badarg): +except getopt.GetoptError as xxx_todo_changeme3: + (errmsg, badarg) = xxx_todo_changeme3.args ErrorMsg(eBADARG % errmsg) # Create and populate an object with rename targets. This must be @@ -2231,7 +2235,7 @@ ProgramOptions[BACKUPS] = False # Select case-sensitivity for replacements (or not) - + if opt == "-C": ProgramOptions[CASESENSITIVE] = True @@ -2245,7 +2249,7 @@ DumpState() # Force case conversion - + if opt == "-e": # Make sure we support the requested case conversion @@ -2254,7 +2258,7 @@ ProgramOptions[CASECONV] = val # Construct a renaming request - + req = {} req[OLD], req[NEW] = None, None for opt in ProgramOptions: @@ -2265,7 +2269,7 @@ # Error out if we don't recognize it else: ErrorMsg(eBADCASECONV % (val, ", ".join(CASEOPS))) - + # Force renaming of existing targets @@ -2293,7 +2297,7 @@ ErrorMsg(eARGLENGTH % (NULLESC, 1)) # Set quiet mode - + if opt == "-q": ProgramOptions[QUIET] = True @@ -2388,4 +2392,3 @@ # Release the target container if we created one del targs - diff --git a/tren2.py b/tren2.py new file mode 100755 index 0000000..fb871e4 --- /dev/null +++ b/tren2.py @@ -0,0 +1,2391 @@ +#!/usr/bin/env python +# tren.py +# Copyright (c) 2010-2011 TundraWare Inc. +# For Updates See: http://www.tundraware.com/Software/tren + +# Program Information + +PROGNAME = "tren.py" +BASENAME = PROGNAME.split(".py")[0] +PROGENV = BASENAME.upper() +INCLENV = PROGENV + "INCL" +RCSID = "$Id: tren.py,v 1.242 2011/08/01 18:07:17 tundra Exp $" +VERSION = RCSID.split()[2] + +# Copyright Information + +CPRT = "(c)" +DATE = "2010-2011" +OWNER = "TundraWare Inc." +RIGHTS = "All Rights Reserved." +COPYRIGHT = "Copyright %s %s, %s %s" % (CPRT, DATE, OWNER, RIGHTS) + +PROGVER = PROGNAME + " " + VERSION + (" - %s" % COPYRIGHT) +HOMEPAGE = "http://www.tundraware.com/Software/%s\n" % BASENAME + + + +#----------------------------------------------------------# +# Variables User Might Change # +#----------------------------------------------------------# + + + +#------------------- Nothing Below Here Should Need Changing ------------------# + + +#----------------------------------------------------------# +# Imports # +#----------------------------------------------------------# + +import copy +import getopt +import glob +import os +import random +import re +import shlex +from stat import * +import sys +import time + +##### +# Imports conditional on OS +##### + +# Set OS type - this allows us to trigger OS-specific code +# where needed. + +OSNAME = os.name +POSIX = False +WINDOWS = False + +if OSNAME == 'nt': + WINDOWS = True + +elif OSNAME == 'posix': + POSIX = True + +# Set up Windows-specific stuff + +if WINDOWS: + + # Try to load win32all stuff if it's available + + try: + from win32api import GetFileAttributes, GetComputerName + import win32con + from win32file import GetDriveType + from win32wnet import WNetGetUniversalName + from win32security import * + WIN32HOST = GetComputerName() + WIN32ALL = True + + except: + WIN32ALL = False + +# Set up Unix-specific stuff + +elif POSIX: + + # Get Unix password and group features + + import grp + import pwd + + +# Uh oh, this is not an OS we know about + +else: + sys.stderr.write("Unsupported Operating System! Aborting ...\n") + sys.exit(1) + + +#----------------------------------------------------------# +# Aliases & Redefinitions # +#----------------------------------------------------------# + + + +#----------------------------------------------------------# +# Constants & Literals # +#----------------------------------------------------------# + + +##### +# General Program Constants +##### + +MAXINCLUDES = 1000 # Maximum number of includes allowed - used to catch circular references +MAXNAMELEN = 255 # Maximum file or directory name length +MINNAMELEN = 1 # Minimum file or directory name length + +##### +# Message Formatting Constants +##### + +# Make sure these make sense: ProgramOptions[MAXLINELEN] > PADWIDTH + WRAPINDENT +# because of the way line conditioning/wrap works. + +PADCHAR = " " # Padding character +PADWIDTH = 30 # Column width +LSTPAD = 13 # Padding to use when dumping lists +WRAPINDENT = 8 # Extra indent on wrapped lines +MINLEN = PADWIDTH + WRAPINDENT + 1 # Minimum line length + + +##### +# Command Line Option Strings +##### + +# List all legal command line options that will be processed by getopt() later. +# We exclude -I here because it is parsed manually before the getopt() call. + +OPTIONSLIST = "A:abCcde:fhi:P:qR:r:S:T:tvw:Xx" # All legal command line options in getopt() format + + +##### +# Literals +##### + +ARROW = "--->" # Used for formatting renaming messages +ASKDOREST = "!" # Do rest of renaming without asking +ASKNO = "N" # Do not rename current file +ASKQUIT = "q" # Quit renaming all further files +ASKYES = "y" # Rename current file +COMMENT = "#" # Comment character in include files +DEFINST = 0 # Default replacement instance +DEFLEN = 80 # Default output line length +DEFSEP = "=" # Default rename command separator: old=new +DEFSUFFIX = ".backup" # String used to rename existing targets +DEFESC = "\\" # Escape character +INCL = "I" # Include file command line option +INDENT = " " # Indent string for nested messages +NULLESC = "Escape string" # Cannot be null +NULLRENSEP = "Old/New separator string" # Cannot be null +NULLSUFFIX = "Forced renaming suffix string" # Cannot be null +OPTINTRO = "-" # Option introducer +PATHDELUNIX = ":" # Separates include path elements on Unix systems +PATHDELWIN = ";" # Separates include path elements on Windows systems +PATHSEP = os.path.sep # File path separator character +RANGESEP = ":" # Separator for instance ranges +SINGLEINST = "SINGLEINST" # Indicates a single, not range, in a slice +WINDOWSGROUP = "WindowsGroup" # Returned on Windows w/o win32all +WINDOWSUSER = "WindowsUser" # Reutrned on Windows w/o win32all +WINGROUPNOT = "GroupNotAvailable" # Returned when win32all can't get a group name +WINUSERNOT = "UserNotAvailable" # Returned when win32all can't get a user name + +##### +# Replacement Token Literals +##### + +# Sequence Alphabets + +BINARY = "Binary" +DECIMAL = "Decimal" +OCTAL = "Octal" +HEXLOWER = "HexLower" +HEXUPPER = "HexUpper" +LOWER = "Lower" +LOWERUPPER = "LowerUpper" +UPPER = "Upper" +UPPERLOWER = "UpperLower" + +# General Literals + +ALPHADELIM = ":" # Delimits alphabet name in a Sequence renaming token +TOKDELIM = "/" # Delimiter for all renaming tokens + +# Shared File Attribute And Sequence Renaming Tokens + +TOKFILADATE = "ADATE" +TOKFILATIME = "ATIME" +TOKFILCMD = "CMDLINE" +TOKFILCDATE = "CDATE" +TOKFILCTIME = "CTIME" +TOKFILDEV = "DEV" +TOKFILFNAME = "FNAME" +TOKFILGID = "GID" +TOKFILGROUP = "GROUP" +TOKFILINODE = "INODE" +TOKFILMODE = "MODE" +TOKFILMDATE = "MDATE" +TOKFILMTIME = "MTIME" +TOKFILNLINK = "NLINK" +TOKFILSIZE = "SIZE" +TOKFILUID = "UID" +TOKFILUSER = "USER" + +# File Time Renaming Tokens + +TOKADAY = "ADAY" # mm replacement token +TOKAHOUR = "AHOUR" # hh replacement token +TOKAMIN = "AMIN" # mm replacement token +TOKAMON = "AMON" # MM replacement token +TOKAMONTH = "AMONTH" # Mmm replacement token +TOKASEC = "ASEC" # ss replacement token +TOKAWDAY = "AWDAY" # Ddd replacement token +TOKAYEAR = "AYEAR" # yyyy replacement token + +TOKCDAY = "CDAY" # mm replacement token +TOKCHOUR = "CHOUR" # hh replacement token +TOKCMIN = "CMIN" # mm replacement token +TOKCMON = "CMON" # MM replacement token +TOKCMONTH = "CMONTH" # Mmm replacement token +TOKCSEC = "CSEC" # ss replacement token +TOKCWDAY = "CWDAY" # Ddd replacement token +TOKCYEAR = "CYEAR" # yyyy replacement token + +TOKMDAY = "MDAY" # mm replacement token +TOKMHOUR = "MHOUR" # hh replacement token +TOKMMIN = "MMIN" # mm replacement token +TOKMMON = "MMON" # MM replacement token +TOKMMONTH = "MMONTH" # Mmm replacement token +TOKMSEC = "MSEC" # ss replacement token +TOKMWDAY = "MWDAY" # Ddd replacement token +TOKMYEAR = "MYEAR" # yyyy replacement token + +# System Renaming Tokens + +TOKCMDEXEC = "`" # Delimiter for command execution renaming tokens +TOKENV = "$" # Introducer for environment variable replacement tokens +TOKRAND = "RAND" # Random replacement token +TOKNAMESOFAR = "NAMESOFAR" # New name so far + +# Sequence Renaming Tokens + +TOKASCEND = "+" # Ascending order flag +TOKDESCEND = "-" # Descending order flag + + +##### +# Internal program state literals +##### + +ASK = "ASK" +BACKUPS = "BACKUPS" +DEBUG = "DEBUG" +CASECONV = "CASECONV" +CASESENSITIVE = "CASESENSITIVE" +ESCAPE = "ESCAPE" +EXISTSUFFIX = "EXISTSUFFIX" +FORCERENAME = "FORCERENAME" +INSTANCESTART = "INSTANCESTART" +INSTANCEEND = "INSTANCEEND" +MAXLINELEN = "MAXLINELEN" +QUIET = "QUIET" +REGEX = "REGEX" +RENSEP = "RENSEP" +TARGETSTART = "TARGETSTART" +TARGETEND = "TARGETEND" +TESTMODE = "TESTMODE" + + +##### +# Renaming Literals +##### + +# Rename target keys + +BASE = "BASENAME" +PATHNAME = "PATHNAME" +STATS = "STATS" + +# These literals serve two purposes: +# +# 1) They are used as the type indicator in a Sequence Renaming Token +# 2) They are keys to the SortViews and DateViews dictionaries that stores the prestorted views + +ORDERBYADATE = TOKFILADATE +ORDERBYATIME = TOKFILATIME +ORDERBYCMDLINE = TOKFILCMD +ORDERBYCDATE = TOKFILCDATE +ORDERBYCTIME = TOKFILCTIME +ORDERBYDEV = TOKFILDEV +ORDERBYFNAME = TOKFILFNAME +ORDERBYGID = TOKFILGID +ORDERBYGROUP = TOKFILGROUP +ORDERBYINODE = TOKFILINODE +ORDERBYMODE = TOKFILMODE +ORDERBYMDATE = TOKFILMDATE +ORDERBYMTIME = TOKFILMTIME +ORDERBYNLINK = TOKFILNLINK +ORDERBYSIZE = TOKFILSIZE +ORDERBYUID = TOKFILUID +ORDERBYUSER = TOKFILUSER + +# Rename string keys + +NEW = "NEW" +OLD = "OLD" + + +#----------------------------------------------------------# +# Prompts, & Application Strings # +#----------------------------------------------------------# + + +##### +# Debug Messages +##### + +DEBUGFLAG = "-d" +dALPHABETS = "Alphabets" +dCMDLINE = "Command Line" +dCURSTATE = "Current State Of Program Options" +dDATEVIEW = "Date View:" +dDEBUG = "DEBUG" +dDUMPOBJ = "Dumping Object %s" +dINCLFILES = "Included Files:" +dPROGENV = "$" + PROGENV +dRENREQ = "Renaming Request:" +dRENSEQ = "Renaming Sequence: %s" +dRENTARGET = "Rename Target:" +dRESOLVEDOPTS = "Resolved Command Line" +dSEPCHAR = "-" # Used for debug separator lines +dSORTVIEW = "Sort View:" + + +##### +# Error Messages +##### + +eALPHABETEXIST = "Sequence renaming token '%s' specifies a non-existent alphabet!" +eALPHABETMISSING = "Sequence renaming token '%s' has a missing or incorrect alphabet specification!" +eALPHACMDBAD = "Alphabet specificaton '%s' malformed! Try \"Name:Alphabet\"" +eALPHACMDLEN = "Alphabet '%s' too short! Must contain at least 2 symbols." +eARGLENGTH = "%s must contain exactly %s character(s)!" +eBADARG = "Invalid command line: %s!" +eBADCASECONV = "Invalid case conversion argument: %s! Must be one of: %s" +eBADINCL = "option -%s requires argument" % INCL +eBADLEN = "Bad line length '%s'!" +eBADNEWOLD = "Bad -r argument '%s'! Requires exactly one new, old string separator (Default: " + DEFSEP + ")" +eBADREGEX = "Invalid Regular Expression: %s" +eBADSLICE = "%s invalid slice format! Must be integer values in the form: n, :n, n:, start:end, or :" +eERROR = "ERROR" +eEXECFAIL = "Renaming token: '%s', command '%s' Failed To Execute!" +eFILEOPEN = "Cannot open file '%s': %s!" +eLINELEN = "Specified line length too short! Must be at least %s" % MINLEN +eNAMELONG = "Renaming '%s' to new name '%s' too long! (Maximum length is %s.)" +eNAMESHORT = "Renaming '%s' to new name '%s' too short! (Minimum length is %s.)" +eNOROOTRENAME = "Cannot rename root of file tree!" +eNULLARG = "%s cannot be empty!" +eRENAMEFAIL = "Attempt to rename '%s' to '%s' failed : %s!" +eTOKBADSEQ = "Unknown sequence renaming token, '%s'!" +eTOKDELIM = "Renaming token '%s' missing delimiter!" +eTOKRANDIG = "Renaming token: '%s' has invalid random precision! Must be integer > 0." +eTOKUNKNOWN = "Unknown renaming token, '%s'!" +eTOOMANYINC = "Too many includes! (Max is %d) Possible circular reference?" % MAXINCLUDES + + +##### +# Informational Messages +##### + +iFORCEDNOBKU = "Forced renaming WITHOUT backups in effect!!! %s is overwriting %s." +iRENFORCED = "Target '%s' exists. Creating backup." +iRENSKIPPED = "Target '%s' exists. Renaming '%s' skipped." +iRENAMING = "Renaming '%s' " + ARROW + " '%s'" +iSEQTOOLONG = "Sequence number %s, longer than format string %s, Rolling over!" + + +##### +# Usage Prompts +##### + +uTable = [PROGVER, + HOMEPAGE, + "usage: " + PROGNAME + " [-abCcdfhqtvwXx] [-e type] [-I file] [-i instance] [-P escape] [ -R separator] [-r old=new] [-S suffix] [-T target] [-w width] ... file|dir file|dir ...", + " where,", + " -A alphabet Install \"alphabet\" for use by sequence renaming tokens", + " -a Ask interactively before renaming (Default: Off)", + " -b Turn off backups during forced renaming (Default: Do Backups)", + " -C Do case-sensitive renaming (Default)", + " -c Collapse case when doing string substitution (Default: False)", + " -d Dump debugging information (Default: False)", + " -e type Force case conversion (Default: None)", + " -f Force renaming even if target file or directory name already exists (Default: False)", + " -h Print help information (Default: False)", + " -I file Include command line arguments from file", + " -i num|range Specify which instance(s) to replace (Default: %s)" % DEFINST, + " -P char Use 'char' as the escape sequence (Default: %s)" % DEFESC, + " -q Quiet mode, do not show progress (Default: False)", + " -R char Separator character for -r rename arguments (Default: %s)" % DEFSEP, + " -r old=new Replace old with new in file or directory names", + " -S suffix Suffix to use when renaming existing filenames (Default: %s)" % DEFSUFFIX, + " -t Test mode, don't rename, just show what the program *would* do (Default: False)", + " -T num|range Specify which characters in file name are targeted for renaming (Default: Whole Name)", + " -v Print detailed program version information and continue (Default: False)", + " -w length Line length of diagnostic and error output (Default: %s)" % DEFLEN, + " -X Treat the renaming strings literally (Default)", + " -x Treat the old replacement string as a Python regular expression (Default: False)", + ] + + +#----------------------------------------------------------# +# Lookup Tables # +#----------------------------------------------------------# + + +# Case Conversion + + +# Notice use of *unbound* string function methods from the class definition + +CASETBL = {'c' : str.capitalize, + 'l' : str.lower, + 's' : str.swapcase, + 't' : str.title, + 'u' : str.upper + } + +CASEOPS = CASETBL.keys() +CASEOPS.sort() + + +# Day And Month Conversion Tables + + +DAYS = {0:"Mon", 1:"Tue", 2:"Wed", 3:"Thu", 4:"Fri", 5:"Sat", 6:"Sun"} + +MONTHS = {1:"Jan", 2:"Feb", 3:"Mar", 4:"Apr", 5:"May", 6:"Jun", + 7:"Jul", 8:"Aug", 9:"Sep", 10:"Oct", 11:"Nov", 12:"Dec"} + +# File Time Renaming Token Lookup Table + +FILETIMETOKS = { TOKADAY : ("%02d", "ST_ATIME", "tm_mday"), + TOKAHOUR : ("%02d", "ST_ATIME", "tm_hour"), + TOKAMIN : ("%02d", "ST_ATIME", "tm_min"), + TOKAMON : ("%02d", "ST_ATIME", "tm_mon"), + TOKAMONTH : ("", "ST_ATIME", "tm_mon"), + TOKASEC : ("%02d", "ST_ATIME", "tm_sec"), + TOKAWDAY : ("", "ST_ATIME", "tm_wday"), + TOKAYEAR : ("%04d", "ST_ATIME", "tm_year"), + TOKCDAY : ("%02d", "ST_CTIME", "tm_mday"), + TOKCHOUR : ("%02d", "ST_CTIME", "tm_hour"), + TOKCMIN : ("%02d", "ST_CTIME", "tm_min"), + TOKCMON : ("%02d", "ST_CTIME", "tm_mon"), + TOKCMONTH : ("", "ST_CTIME", "tm_mon"), + TOKCSEC : ("%02d", "ST_CTIME", "tm_sec"), + TOKCWDAY : ("", "ST_CTIME", "tm_wday"), + TOKCYEAR : ("%04d", "ST_CTIME", "tm_year"), + TOKMDAY : ("%02d", "ST_MTIME", "tm_mday"), + TOKMHOUR : ("%02d", "ST_MTIME", "tm_hour"), + TOKMMIN : ("%02d", "ST_MTIME", "tm_min"), + TOKMMON : ("%02d", "ST_MTIME", "tm_mon"), + TOKMMONTH : ("", "ST_MTIME", "tm_mon"), + TOKMSEC : ("%02d", "ST_MTIME", "tm_sec"), + TOKMWDAY : ("", "ST_MTIME", "tm_wday"), + TOKMYEAR : ("%04d", "ST_MTIME", "tm_year") + } + +# Alphabets - The user can add to these on the command line + +ALPHABETS = { + + BINARY : ["0", "1"], + + DECIMAL : ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], + + HEXLOWER : ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"], + + HEXUPPER : ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"], + + LOWER : ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", + "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" ], + + LOWERUPPER : ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", + "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", + "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", + "W", "X", "Y", "Z"], + + OCTAL : ["0", "1", "2", "3", "4", "5", "6", "7"], + + UPPER : ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", + "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ], + + UPPERLOWER : ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", + "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", + "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", + "w", "x", "y", "z"] + } + + + + +#----------------------------------------------------------# +# Global Variables & Data Structures # +#----------------------------------------------------------# + +# List of all the included files + +IncludedFiles = [] + + +# Program toggle and option defaults + +ProgramOptions = { + ASK : False, # Interactively ask user before renaming each file + BACKUPS : True, # Do backups during forced renaming + DEBUG : False, # Debugging off + CASECONV : None, # Forced case conversions + CASESENSITIVE : True, # Search is case-sensitive + ESCAPE : DEFESC, # Escape string + EXISTSUFFIX : DEFSUFFIX, # What to tack on when renaming existing targets + FORCERENAME : False, # Do not rename if target already exists + INSTANCESTART : DEFINST, # Replace first, leftmost instance by default + INSTANCEEND : SINGLEINST, + MAXLINELEN : DEFLEN, # Width of output messages + QUIET : False, # Display progress + REGEX : False, # Do not treat old string as a regex + RENSEP : DEFSEP, # Old, New string separator for -r + TARGETSTART : False, # Entire file name is renaming target by default + TARGETEND : False, + TESTMODE : False # Test mode off + } + + +# Used to track the sequence of name transformations as each renaming +# request is applied. The -1th entry is thus also the "name so far" +# used for the /NAMESOFAR/ renaming token. + +RenSequence = [] + +#--------------------------- Code Begins Here ---------------------------------# + + +#----------------------------------------------------------# +# Object Base Class Definitions # +#----------------------------------------------------------# + + +##### +# Container For Holding Rename Targets And Renaming Requests +##### + +class RenameTargets: + + """ + This class is used to keep track of all the files and/or + directories we're renaming. After the class is constructed + and the command line fully parsed, this will contain: + + self.RenNames = { fullname : {BASE : basename, PATHNAME : pathtofile, STATS : stats} + ... (repeated for each rename target) + } + + self.SortViews = { + ORDERBYATIME : [fullnames in atimes order], + ORDERBYCMDLINE : [fullnames in command line order], + ORDERBYCTIME : [fullnames in ctimes order], + ORDERBYDEV : [fullnames in devs order], + ORDERBYFNAME : [fullnames in alphabetic order], + ORDERBYGID : [fullnames in gids order], + ORDERBYGROUP ; [fullnames in group name order], + ORDERBYINODE : [fullnames in inode order], + ORDERBYMODE : [fullnames in mode order], + ORDERBYMTIME : [fullnames in mtimes order], + ORDERBYNLINK : [fullnames in nlinks order], + ORDERBYSIZE : [fullnames in size order], + ORDERBYUID : [fullnames in uids order], + ORDERBYUSER : [fullnames in user name order] + } + + self.DateViews = { + ORDERBYADATE-date... : [fullnames in order by atime within same 'date'] ... (repeated for each date), + ORDERBYCDATE-date... : [fullnames in order by ctime within same 'date'] ... (repeated for each date), + ORDERBYMDATE-date... : [fullnames in order by mtime within same 'date'] ... (repeated for each date) + } + + self.RenRequests = [ + { + ASK : interactive ask flag + BACKUPS : do backups during forced renaming flag, + OLD : old rename string, + NEW : new rename string, + DEBUG : debug flag, + CASECONV : type of case conversion, + CASESENSITIVE : case sensitivity flag, + FORCERENAME : force renaming flag, + INSTANCESTART : Replace first, leftmost instance by default, + INSTANCEEND : , + MAXLINELEN : max output line length, + QUIET : quiet output flag, + REGEX : regular expression enable flag, + RENSEP : old/new rename separator string, + TARGETSTART : Entire file name target for renaming by default, + TARGETEND : , + TESTMODE : testmode flag + } ... (repeated for each rename request) + ] + + """ + + ##### + # Constructor + ##### + + def __init__(self, targs): + + # Keep track of all the new filenames we write (or would have) + # so test mode can correctly report just what the the progam + # *would* do. Without this, backup generation is not properly + # reported in test mode. + + self.RenamedFiles = [] + self.NewFiles = [] + + # Dictionary of all rename targets and their stat info + + self.RenNames = {} + + # Dictionary of all possible sort views + # We can load the first two right away since they're based + # only on the target names provided on the command line + + i=0 + while i < len(targs): + targs[i] = os.path.abspath(targs[i]) + i += 1 + + alpha = targs[:] + alpha.sort() + self.SortViews = {ORDERBYCMDLINE : targs, ORDERBYFNAME : alpha} + del alpha + + # Dictionary to hold all possible date views - files sorted + # by time *within* a common date. + + self.DateViews = {} + + # Dictionary of all the renaming requests - will be filled in + # by -r command line parsing. + + self.RenRequests = [] + + + # This data structure is used to build various sort views + # A null first field means the view requires special handling, + # otherwise it's just a stat structure lookup. + + SeqTypes = [ + [ST_ATIME, {}, ORDERBYATIME], + [ST_CTIME, {}, ORDERBYCTIME], + [ST_DEV, {}, ORDERBYDEV], + [ST_GID, {}, ORDERBYGID], + ["", {}, ORDERBYGROUP], + [ST_INO, {}, ORDERBYINODE], + [ST_MODE, {}, ORDERBYMODE], + [ST_MTIME, {}, ORDERBYMTIME], + [ST_NLINK, {}, ORDERBYNLINK], + [ST_SIZE, {}, ORDERBYSIZE], + [ST_UID, {}, ORDERBYUID], + ["", {}, ORDERBYUSER], + ] + + # Populate the data structures with each targets' stat information + + for fullname in targs: + + try: + pathname, basename = os.path.split(fullname) + stats = os.stat(fullname) + except (IOError, OSError), (errno, errstr): + ErrorMsg(eFILEOPEN % (fullname, errstr)) + + # Some operating systems (Windows) terminate the path with + # a separator, some (Posix) do not. + + if pathname[-1] != os.sep: + pathname += os.sep + + # Store fullname, basename, and stat info for this file + + if basename: + self.RenNames[fullname] = {BASE : basename, PATHNAME : pathname, STATS : stats} + + # Catch the case where they're trying to rename the root of the directory tree + + else: + ErrorMsg(eNOROOTRENAME) + + # Incrementally build lists of keys that will later be + # used to create sequence renaming tokens + + for seqtype in SeqTypes: + + statflag, storage, order = seqtype + + # Handle os.stat() values + + if statflag: + sortkey = stats[statflag] + + # Handle group name values + + elif order == ORDERBYGROUP: + sortkey = self.__GetFileGroupname(fullname) + + # Handle user name values + + elif order == ORDERBYUSER: + sortkey = self.__GetFileUsername(fullname) + + # Save into storage + + if sortkey in storage: + storage[sortkey].append(fullname) + else: + storage[sortkey] = [fullname] + + + # Create the various sorted views we may need for sequence + # renaming tokens + + for seqtype in SeqTypes: + + statflag, storage, order = seqtype + + vieworder = storage.keys() + vieworder.sort() + + # Sort alphabetically when multiple filenames + # map to the same key, creating overall + # ordering as we go. + + t = [] + for i in vieworder: + storage[i].sort() + for j in storage[i]: + t.append(j) + + # Now store for future reference + + self.SortViews[order] = t + + # Release the working data structures + + del SeqTypes + + # Now build the special cases of ordering by time within date + # for each of the timestamp types. + + for dateorder, timeorder, year, mon, day in ((ORDERBYADATE, ORDERBYATIME, + FILETIMETOKS[TOKAYEAR], + FILETIMETOKS[TOKAMON], + FILETIMETOKS[TOKADAY]), + + (ORDERBYCDATE, ORDERBYCTIME, + FILETIMETOKS[TOKCYEAR], + FILETIMETOKS[TOKCMON], + FILETIMETOKS[TOKCDAY]), + + (ORDERBYMDATE, ORDERBYMTIME, + FILETIMETOKS[TOKMYEAR], + FILETIMETOKS[TOKMMON], + FILETIMETOKS[TOKMDAY])): + + + lastdate = "" + for fullname in self.SortViews[timeorder]: + + targettime = eval("time.localtime(self.RenNames[fullname][STATS][%s])" % year[1]) + + newdate = year[0] % eval("targettime.%s" % year[2]) + \ + mon[0] % eval("targettime.%s" % mon[2]) + \ + day[0] % eval("targettime.%s" % day[2]) + + key = dateorder+newdate + + # New file date encountered + + if newdate != lastdate: + + self.DateViews[key] = [fullname] + lastdate = newdate + + # Add file to existing list of others sharing that date + + else: + self.DateViews[key].append(fullname) + + # End of '__init__()' + + + ##### + # Debug Dump + ##### + + def DumpObj(self): + + SEPARATOR = dSEPCHAR * ProgramOptions[MAXLINELEN] + DebugMsg("\n") + DebugMsg(SEPARATOR) + DebugMsg(dDUMPOBJ % str(self)) + DebugMsg(SEPARATOR) + + + # Dump the RenNames and SortView dictionaries + + for i, msg in ((self.RenNames, dRENTARGET), (self.SortViews, dSORTVIEW), (self.DateViews, dDATEVIEW)): + + for j in i: + DumpList(msg, j, i[j]) + + for rr in self.RenRequests: + DumpList(dRENREQ, "", rr) + + DebugMsg(SEPARATOR + "\n\n") + + # End of 'DumpObj()' + + + ##### + # Determine File's Group Name + ##### + + def __GetFileGroupname(self, fullname): + + if POSIX: + return grp.getgrgid(self.RenNames[fullname][STATS][ST_GID])[0] + + else: + retval = WINDOWSGROUP + + if WIN32ALL: + + try: + # Get the internal Win32 security group information for this file. + + hg = GetFileSecurity(fullname, GROUP_SECURITY_INFORMATION) + sidg = hg.GetSecurityDescriptorGroup() + + # We have to know who is hosting the filesystem for this file + + drive = fullname[0:3] + if GetDriveType(drive) == win32con.DRIVE_REMOTE: + fnhost = WNetGetUniversalName(drive, 1).split('\\')[2] + + else: + fnhost = WIN32HOST + + # Now we can translate the sids into names + + retval = LookupAccountSid(fnhost, sidg)[0] + + # On any error, just act like win32all isn't there + + except: + retval = WINGROUPNOT + + return retval + + # End of 'GetFileGroupname()' + + + ##### + # Determine File's User Name + ##### + + def __GetFileUsername(self, fullname): + + if POSIX: + return pwd.getpwuid(self.RenNames[fullname][STATS][ST_UID])[0] + + else: + retval = WINDOWSUSER + + if WIN32ALL: + + try: + + # Get the internal Win32 security information for this file. + + ho = GetFileSecurity(fullname, OWNER_SECURITY_INFORMATION) + sido = ho.GetSecurityDescriptorOwner() + + # We have to know who is hosting the filesystem for this file + + drive = fullname[0:3] + if GetDriveType(drive) == win32con.DRIVE_REMOTE: + fnhost = WNetGetUniversalName(drive, 1).split('\\')[2] + + else: + fnhost = WIN32HOST + + # Now we can translate the sids into names + + retval = LookupAccountSid(fnhost, sido)[0] + + # On any error, just act like win32all isn't there + + except: + retval = WINUSERNOT + + return retval + + # End of 'GetFileUsername()' + + + ##### + # Go Do The Requested Renaming + ##### + + def ProcessRenameRequests(self): + + global RenSequence + self.indentlevel = -1 + + # Create a list of all renaming to be done. + # This includes the renaming of any existing targets. + + for target in self.SortViews[ORDERBYCMDLINE]: + + oldname, pathname = self.RenNames[target][BASE], self.RenNames[target][PATHNAME] + newname = oldname + + # Keep track of incremental renaming for use by debug + RenSequence = [oldname] + + for renrequest in self.RenRequests: + + # Select portion of name targeted for renaming + + lname = "" + rname = "" + tstart = renrequest[TARGETSTART] + tend = renrequest[TARGETEND] + + # Condition values so that range slicing works properly below. + # This couldn't be done at the time the target range was + # saved intially, because the length of the name being processed + # isn't known until here. + + if tstart == None: + tstart = 0 + + if tend == None: + tend = len(newname) + + if tstart or tend: + + bound = len(newname) + + # Normalize negative refs so we can use consistent + # logic below + + if tstart < 0: + tstart = bound + tstart + + if (tend != SINGLEINST and tend < 0): + tend = bound + tend + + # Condition and bounds check the target range as needed + + # Handle single position references + if (tend == SINGLEINST): + + # Select the desired position. Notice that + # out-of-bounds references are ignored and the + # name is left untouched. This is so the user + # can specify renaming operations on file + # names of varying lengths and have them apply + # only to those files long enough to + # accommodate the request without blowing up + # on the ones that are not long enough. + + if 0 <= tstart < bound: + lname, newname, rname = newname[:tstart], newname[tstart], newname[tstart+1:] + + # Reference is out of bounds - leave name untouched + + else: + lname, newname, rname = newname, "", "" + + # Handle slice range requests + + else: + + # Out-Of-Bounds or invalid slice ranges will + # cause renaming request to be ignored as above + + if newname[tstart:tend]: + lname, newname, rname = newname[:tstart], newname[tstart:tend], newname[tend:] + + else: + lname, newname, rname = newname, "", "" + + + # Handle conventional string replacement renaming requests + # An empty newname here means that the -T argument processing + # selected a new string and/or was out of bounds -> we ignore the request. + + if newname and (renrequest[OLD] or renrequest[NEW]): + + # Resolve any embedded renaming tokens + + old = self.__ResolveRenameTokens(target, renrequest[OLD]) + new = self.__ResolveRenameTokens(target, renrequest[NEW]) + + oldstrings = [] + + # Build a list of indexes to every occurence of the old string, + # taking case sensitivity into account + + # Handle the case when old = "". This means to + # *replace the entire* old name with new. More + # specifically, replace the entire old name *as + # modified so far by preceding rename commands*. + + if not old: + old = newname + + # Find every instance of the 'old' string in the + # current filename. 'Find' in this case can be either + # a regular expression pattern match or a literal + # string match. + # + # Either way, each 'hit' is recorded as a tuple: + # + # (index to beginning of hit, beginning of next non-hit text) + # + # This is so subsequent replacement logic knows: + # + # 1) Where the replacement begins + # 2) Where the replacement ends + # + # These two values are used during actual string + # replacement to properly replace the 'new' string + # into the requested locations. + + + # Handle regular expression pattern matching + + if renrequest[REGEX]: + + try: + # Do the match either case-insentitive or not + + if renrequest[CASESENSITIVE]: + rematches = re.finditer(old, newname) + + else: + rematches = re.finditer(old, newname, re.I) + + # And save off the results + + for match in rematches: + oldstrings.append(match.span()) + + except: + ErrorMsg(eBADREGEX % old) + + # Handle literal string replacement + + else: + + searchtarget = newname + + # Collapse case if requested + + if not renrequest[CASESENSITIVE]: + + searchtarget = searchtarget.lower() + old = old.lower() + + oldlen = len(old) + i = searchtarget.find(old) + while i >= 0: + + nextloc = i + oldlen + oldstrings.append((i, nextloc)) + i = searchtarget.find(old, nextloc) + + # If we found any matching strings, replace them + + if oldstrings: + + # But only process the instances the user asked for + + todo = [] + + # Handle single instance requests doing bounds checking as we go + + start = renrequest[INSTANCESTART] + end = renrequest[INSTANCEEND] + + if (end == SINGLEINST): + + # Compute bounds for positive and negative indicies. + # This is necessary because positive indicies are 0-based, + # but negative indicies start at -1. + + bound = len(oldstrings) + + if start < 0: + bound += 1 + + # Now go get that entry + + if abs(start) < bound: + todo.append(oldstrings[start]) + + # Handle instance range requests + + else: + todo = oldstrings[start:end] + + + # Replace selected substring(s). Substitute from + # R->L in original string so as not to mess up the + # replacement indicies. + + todo.reverse() + for i in todo: + newname = newname[:i[0]] + new + newname[i[1]:] + + + # Handle case conversion renaming requests + + elif renrequest[CASECONV]: + newname = CASETBL[renrequest[CASECONV]](newname) + + # Any subsequent replacements operate on the modified name + # which is reconstructed by combining what we've renamed + # with anything that was excluded from the rename operation. + + newname = lname + newname + rname + + # Keep track of incremental renaming for use by debug + RenSequence.append(newname) + + # Show the incremental renaming steps if debug is on + + if ProgramOptions[DEBUG]: + DebugMsg(dRENSEQ % ARROW.join(RenSequence)) + + # Nothing to do, if old- and new names are the same + + if newname != oldname: + self.__RenameIt(pathname, oldname, newname) + + # End of 'ProcessRenameRequests()' + + + ##### + # Actually Rename A File + ##### + + def __RenameIt(self, pathname, oldname, newname): + + self.indentlevel += 1 + indent = self.indentlevel * INDENT + newlen = len(newname) + + # First make sure the new name meets length constraints + + if newlen < MINNAMELEN: + ErrorMsg(indent + eNAMESHORT% (oldname, newname, MINNAMELEN)) + return + + if newlen > MAXNAMELEN: + ErrorMsg(indent + eNAMELONG % (oldname, newname, MAXNAMELEN)) + return + + # Get names into absolute path form + + fullold = pathname + oldname + fullnew = pathname + newname + + # See if our proposed renaming is about to stomp on an + # existing file, and create a backup if forced renaming + # requested. We do such backups with a recursive call to + # ourselves so that filename length limits are observed and + # backups-of-backups are preserved. + + doit = True + newexists = os.path.exists(fullnew) + + if (not ProgramOptions[TESTMODE] and newexists) or \ + (ProgramOptions[TESTMODE] and fullnew not in self.RenamedFiles and (newexists or fullnew in self.NewFiles)): + + if ProgramOptions[FORCERENAME]: + + # Create the backup unless we've been told not to + + if ProgramOptions[BACKUPS]: + + bkuname = newname + ProgramOptions[EXISTSUFFIX] + InfoMsg(indent + iRENFORCED % fullnew) + self.__RenameIt(pathname, newname, bkuname) + + else: + InfoMsg(iFORCEDNOBKU % (fullold, fullnew)) + + else: + InfoMsg(indent + iRENSKIPPED % (fullnew, fullold)) + doit = False + + if doit: + + if ProgramOptions[ASK]: + + answer = "" + while answer.lower() not in [ASKNO.lower(), ASKYES.lower(), ASKDOREST.lower(), ASKQUIT.lower()]: + + PrintStdout("Rename %s to %s? [%s]: " % (fullold, fullnew, ASKNO+ASKYES+ASKDOREST+ASKQUIT), TRAILING="") + + answer = sys.stdin.readline().lower().strip() + + # A blank line means take the default - do nothing. + + if not answer: + answer = ASKNO.lower() + + if answer == ASKNO.lower(): + doit = False + + if answer == ASKYES.lower(): + doit = True + + if answer == ASKDOREST.lower(): + doit = True + ProgramOptions[ASK] = False + + if answer == ASKQUIT.lower(): + sys.exit(1) + + if doit: + + # In test mode, track file names that would be produced. + + if ProgramOptions[TESTMODE]: + + self.NewFiles.append(fullnew) + self.RenamedFiles.append(fullold) + + if fullold in self.NewFiles: + self.NewFiles.remove(fullold) + + if fullnew in self.RenamedFiles: + self.RenamedFiles.remove(fullnew) + + InfoMsg(indent + iRENAMING % (fullold, fullnew)) + + if not ProgramOptions[TESTMODE]: + + try: + os.rename(fullold, fullnew) + except OSError, (errno, errstr): + ErrorMsg(eRENAMEFAIL % (fullold, fullnew, errstr)) + + self.indentlevel -= 1 + + # End of '__RenameIt()' + + + ##### + # Resolve Rename Tokens + ##### + + """ This replaces all renaming token references in 'renstring' + with the appropriate content and returns the resolved string. + 'target' is the name of the current file being renamed. We + need that as well because some renaming tokens refer to file + stat attributes or even the file name itself. + """ + + def __ResolveRenameTokens(self, target, renstring): + + # Find all token delimiters but ignore any that might appear + # inside a command execution replacement token string. + + rentokens = [] + odd = True + incmdexec = False + + i=0 + while i < len(renstring): + + if renstring[i] == TOKCMDEXEC: + incmdexec = not incmdexec + + elif renstring[i] == TOKDELIM: + + if incmdexec: + pass + + elif odd: + + rentokens.append([i]) + odd = not odd + + else: + + rentokens[-1].append(i) + odd = not odd + + + i += 1 + + # There must be an even number of token delimiters + # or the renaming token is malformed + + if rentokens and len(rentokens[-1]) != 2: + ErrorMsg(eTOKDELIM % renstring) + + # Now add the renaming token contents. This will be used to + # figure out what the replacement text should be. + + i = 0 + while i < len(rentokens): + + rentokens[i].append(renstring[rentokens[i][0]+1 : rentokens[i][1]]) + i += 1 + + # Process each token. Work left to right so as not to mess up + # the previously stored indexes. + + rentokens.reverse() + + for r in rentokens: + + fullrentoken = "%s%s%s" % (TOKDELIM, r[2], TOKDELIM) # Need this for error messages. + + ### + # File Attribute Renaming Tokens + ### + + if r[2] == TOKFILDEV: + r[2] = str(self.RenNames[target][STATS][ST_DEV]) + + elif r[2] == TOKFILFNAME: + r[2] = os.path.basename(target) + + elif r[2] == TOKFILGID: + r[2] = str(self.RenNames[target][STATS][ST_GID]) + + elif r[2] == TOKFILGROUP: + r[2] = self.__GetFileGroupname(target) + + elif r[2] == TOKFILINODE: + r[2] = str(self.RenNames[target][STATS][ST_INO]) + + elif r[2] == TOKFILMODE: + r[2] = str(self.RenNames[target][STATS][ST_MODE]) + + elif r[2] == TOKFILNLINK: + r[2] = str(self.RenNames[target][STATS][ST_NLINK]) + + elif r[2] == TOKFILSIZE: + r[2] = str(self.RenNames[target][STATS][ST_SIZE]) + + elif r[2] == TOKFILUID: + r[2] = str(self.RenNames[target][STATS][ST_UID]) + + elif r[2] == TOKFILUSER: + r[2] = self.__GetFileUsername(target) + + + ### + # File Time Renaming Tokens + ### + + elif r[2] in FILETIMETOKS: + + parms = FILETIMETOKS[r[2]] + val = eval("time.localtime(self.RenNames[target][STATS][%s]).%s" % (parms[1], parms[2])) + + # The first value of FILETIMETOKS table entry + # indicates the formatting string to use (if the entry + # is non null), or that we're doing a lookup for the + # name of a month (if the entry is null) + + if parms[0]: + r[2] = parms[0] % val + + elif parms[2] == "tm_mon": + r[2] = MONTHS[val] + + elif parms[2] == "tm_wday": + r[2] = DAYS[val] + + ### + # System Renaming Tokens + ### + + # Environment Variable replacement token + + elif r[2].startswith(TOKENV): + + r[2] = os.getenv(r[2][1:]) + + # Handle case for nonexistent environment variable + + if not r[2]: + r[2] = "" + + + # Command Run replacement token + + elif r[2].startswith(TOKCMDEXEC) and r[2].endswith(TOKCMDEXEC): + + command = r[2][1:-1] + + # Handle Windows variants - they act differently + + if not POSIX: + pipe = os.popen(command, 'r') + + # Handle Unix variants + + else: + pipe = os.popen('{ ' + command + '; } 2>&1', 'r') + + output = pipe.read() + status = pipe.close() + + if status == None: + status = 0 + + # Nonzero status means error attempting to execute the command + + if status: + ErrorMsg(eEXECFAIL % (fullrentoken, command)) + + # Otherwise swap the command with its results, stripping newlines + + else: + r[2] = output.replace("\n", "") + + + # Random Number Replacement token + + elif r[2].startswith(TOKRAND): + + random.seed() + + # Figure out how many digits of randomness the user want + + try: + precision = r[2].split(TOKRAND)[1] + precision = int(precision) + + except: + ErrorMsg(eTOKRANDIG % fullrentoken) + + if precision < 1: + ErrorMsg(eTOKRANDIG % fullrentoken) + + fmt = '"%0' + str(precision) + 'd" % random.randint(0, pow(10, precision)-1)' + r[2] = eval(fmt) + + # Name So Far Replacement Token + + elif r[2] == (TOKNAMESOFAR): + r[2] = RenSequence[-1] + + ### + # Sequence Renaming Tokens + ### + + elif r[2] and (r[2][0] == TOKASCEND or r[2][0] == TOKDESCEND): + + # Parse the Sequence Renaming Token into the token itself + # and its corresponding formatting field. + + # Note that the a legal Sequence Renaming Token will either + # be one of the keys of the SortViews dictionary or one + # of the "ORDERBYnDATE" orderings. + + token = r[2][1:] + + found = False + for seqtoken in self.SortViews.keys() + [ORDERBYADATE, ORDERBYCDATE, ORDERBYMDATE]: + + if token.split(ALPHADELIM)[0] == (seqtoken): + + token, field = token[:len(seqtoken)], token[len(seqtoken):] + found = True + break + + if not found: + ErrorMsg(eTOKBADSEQ % fullrentoken) + + # Now derive the name of the alphabet to use + + if not field.startswith(ALPHADELIM): + ErrorMsg(eALPHABETMISSING % fullrentoken) + + field = field[1:] + + alphabet, alphadelim, field = field.partition(ALPHADELIM) + + if not alphadelim: + ErrorMsg(eALPHABETMISSING % fullrentoken) + + # Empty alphabet string means default to decimal counting + + if not alphabet: + alphabet = DECIMAL + + if alphabet not in ALPHABETS: + ErrorMsg(eALPHABETEXIST % fullrentoken) + + + # Retrieve the ordered list of the requested type, + # adjust for descending order, and plug in the + # sequence number for the current renaming target + # (which is just the index of that filename in the + # list). + + # One of the standard sorted views requested + + if token in self.SortViews: + orderedlist = self.SortViews[token][:] + + # One of the views sorted within dates requested + + else: + + + if token == ORDERBYADATE: + year, mon, day = FILETIMETOKS[TOKAYEAR], FILETIMETOKS[TOKAMON], FILETIMETOKS[TOKADAY] + + elif token == ORDERBYCDATE: + year, mon, day = FILETIMETOKS[TOKCYEAR], FILETIMETOKS[TOKCMON], FILETIMETOKS[TOKCDAY] + + elif token == ORDERBYMDATE: + year, mon, day = FILETIMETOKS[TOKMYEAR], FILETIMETOKS[TOKMMON], FILETIMETOKS[TOKMDAY] + + targettime = eval("time.localtime(self.RenNames[target][STATS][%s])" % year[1]) + + key = token + \ + year[0] % eval("targettime.%s" % year[2]) + \ + mon[0] % eval("targettime.%s" % mon[2]) + \ + day[0] % eval("targettime.%s" % day[2]) + + orderedlist = self.DateViews[key][:] + + if r[2][0] == TOKDESCEND: + orderedlist.reverse() + + r[2] = ComputeSeqString(field, orderedlist.index(target), ALPHABETS[alphabet]) + + ### + # Unrecognized Renaming Token + ### + + else: + ErrorMsg(eTOKUNKNOWN % fullrentoken) + + ### + # Successful Lookup, Do the actual replacement + ### + + renstring = renstring[:r[0]] + r[2] + renstring[r[1]+1:] + + return renstring + + # End of '__ResolveRenameTokens()' + + +# End of class 'RenameTargets' + + +#----------------------------------------------------------# +# Supporting Function Definitions # +#----------------------------------------------------------# + + +##### +# Check For Correct Slice Syntax +##### + +def CheckSlice(val): + + try: + + # Process ranges + + if val.count(RANGESEP): + + lhs, rhs = val.split(RANGESEP) + + if not lhs: + lhs = None + + else: + lhs = int(lhs) + + if not rhs: + rhs = None + + else: + rhs = int(rhs) + + # Process single indexes + + else: + + lhs = int(val) + rhs = SINGLEINST + + # Something about the argument was bogus + + except: + ErrorMsg(eBADSLICE % val) + + return (lhs, rhs) + +# End Of 'CheckSlice()' + + +##### +# Turn A List Into Columns With Space Padding +##### + +def ColumnPad(list, PAD=PADCHAR, WIDTH=PADWIDTH): + + retval = "" + for l in list: + l = str(l) + retval += l + ((WIDTH - len(l)) * PAD) + + return retval + +# End of 'ColumnPad()' + + +def ComputeSeqString(fmt, incr, alphabet): + + """ + fmt = A literal "format field" string + incr = A integer to be "added" to the field + alphabet = The alphabet of characters to use, in ascending order + + Add 'incr' to 'fmt' in base(len(alphabet)). Characters in + 'fmt' that are not in 'alphabet' are ignored in this addition. + + The final result is limited to be no longer than 'fmt'. Any + result longer than fmt has MSD dropped, thereby effectively + rolling over the count. If 'fmt' is null on entry, the final + result length is unlimited. + """ + + base = len(alphabet) + + # Do position-wise "addition" via symbol substitution moving from + # right-to-left adjusting for the fact that not all symbols in the + # format string will be in the alphabet. + + + # First convert the increment into a string in the base of the + # alphabet + + idigits = [] + while incr > base-1: + + incr, digit = incr/base, incr % base + idigits.append(alphabet[digit]) + + idigits.append(alphabet[incr]) + idigits.reverse() + incr = "".join(idigits) + + # Now do right-to-left digit addition with the format + # field. + + # Do position-wise "addition" via symbol substitution moving from + # right-to-left. Take into account that the format pattern string + # may be a different length than the increment string and that not + # all characters in the format pattern are guaranteed to exist in + # the alphabet. + + newval = "" + carry = None + fmtlen = len(fmt) + incrlen = len(incr) + calcsize = max(fmtlen, incrlen) + + i = -1 + done = False + while abs(i) <= calcsize and not done: + + sum = 0 + + if carry: + sum += carry + + if fmt and (abs(i) <= fmtlen) and fmt[i] in alphabet: + sum += alphabet.index(fmt[i]) + + if abs(i) <= incrlen: + sum += alphabet.index(incr[i]) + + # Do arithmetic modulo alphabet length + + carry, digit = sum/base, sum % base + + if not carry: + carry = None + + # We're completely done if we're out of digits in incr and + # there's no carry to propagate. This prevents us from + # tacking on leading 0th characters which could overwrite + # out-of-alphabet characters in the format field. + + if abs(i-1) > incrlen: + done =True + + newval = alphabet[digit] + newval + + i -= 1 + + if carry: + newval = alphabet[carry] + newval + + # Constrain the results to the length of the original format + # string, rolling over and warning the user as necessary. The one + # exception to this is when a null format string is passed. This + # is understood to mean that sequences of any length are + # permitted. + + # Result length constrained by format string + + if fmtlen: + + if len(newval) > fmtlen: + InfoMsg(iSEQTOOLONG % (newval,fmt)) + newval = newval[-fmtlen:] + + return fmt[:-len(newval)] + newval + + # Any length result permitted + + else: + return newval + +# End of 'ComputeSeqString()' + + +##### +# Condition Line Length With Fancy Wrap And Formatting +##### + +def ConditionLine(msg, + PAD=PADCHAR, \ + WIDTH=PADWIDTH, \ + wrapindent=WRAPINDENT ): + + retval = [] + retval.append(msg[:ProgramOptions[MAXLINELEN]]) + msg = msg[ProgramOptions[MAXLINELEN]:] + + while msg: + msg = PAD * (WIDTH + wrapindent) + msg + retval.append(msg[:ProgramOptions[MAXLINELEN]]) + msg = msg[ProgramOptions[MAXLINELEN]:] + + return retval + +# End of 'ConditionLine()' + + +##### +# Print A Debug Message +##### + +def DebugMsg(msg): + + l = ConditionLine(msg) + for msg in l: + PrintStderr(PROGNAME + " " + dDEBUG + ": " + msg) + +# End of 'DebugMsg()' + + +##### +# Debug Dump Of A List +##### + +def DumpList(msg, listname, content): + + DebugMsg(msg) + itemarrow = ColumnPad([listname, " "], WIDTH=LSTPAD) + DebugMsg(ColumnPad([" ", " %s %s" % (itemarrow, content)])) + +# End of 'DumpList()' + + +##### +# Dump The State Of The Program +##### + +def DumpState(): + + SEPARATOR = dSEPCHAR * ProgramOptions[MAXLINELEN] + DebugMsg(SEPARATOR) + DebugMsg(dCURSTATE) + DebugMsg(SEPARATOR) + + opts = ProgramOptions.keys() + opts.sort() + for o in opts: + DebugMsg(ColumnPad([o, ProgramOptions[o]])) + + DumpList(dALPHABETS, "", ALPHABETS) + + DebugMsg(SEPARATOR) + + +# End of 'DumpState()' + + +##### +# Print An Error Message And Exit +##### + +def ErrorMsg(emsg): + + l = ConditionLine(emsg) + + for emsg in l: + PrintStderr(PROGNAME + " " + eERROR + ": " + emsg) + + sys.exit(1) + +# End of 'ErrorMsg()' + + +##### +# Split -r Argument Into Separate Old And New Strings +##### + +def GetOldNew(arg): + + + escaping = False + numseps = 0 + sepindex = 0 + oldnewsep = ProgramOptions[RENSEP] + + i = 0 + while i < len(arg): + + # Scan string ignoring escaped separators + + if arg[i:].startswith(oldnewsep): + + if (i > 0 and (arg[i-1] != ProgramOptions[ESCAPE])) or i == 0: + sepindex = i + numseps += 1 + + i += len(oldnewsep) + + else: + i += 1 + + + if numseps != 1: + ErrorMsg(eBADNEWOLD % arg) + + else: + old, new = arg[:sepindex], arg[sepindex + len(oldnewsep):] + old = old.replace(ProgramOptions[ESCAPE] + oldnewsep, oldnewsep) + new = new.replace(ProgramOptions[ESCAPE] + oldnewsep, oldnewsep) + return [old, new] + +# End of 'GetOldNew()' + + +##### +# Print An Informational Message +##### + +def InfoMsg(imsg): + + l = ConditionLine(imsg) + + msgtype = "" + if ProgramOptions[TESTMODE]: + msgtype = TESTMODE + + if not ProgramOptions[QUIET]: + for msg in l: + PrintStdout(PROGNAME + " " + msgtype + ": " + msg) + +# End of 'InfoMsg()' + + +##### +# 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 normal getopt() processing below will "see" + the included statements. + + This is a bit tricky because we have to handle every possible + legal command line syntax for option specification: + + -I filename + -Ifilename + -....I filename + -....Ifilename + """ + + # Build a list of all the options that take arguments. This is + # needed to determine whether the include symbol is an include + # option or part of an argument to a preceding option. + + OPTIONSWITHARGS = "" + for i in re.finditer(":", OPTIONSLIST): + OPTIONSWITHARGS += OPTIONSLIST[i.start() - 1] + + NUMINCLUDES = 0 + FoundNewInclude = True + + while FoundNewInclude: + + FoundNewInclude = False + i = 0 + while i < len(OPTIONS): + + # Detect all possible command line include constructs, + # isolating the requested filename and replaciing its + # contents at that position in the command line. + + field = OPTIONS[i] + position = field.find(INCL) + + if field.startswith(OPTINTRO) and (position > -1): + + + lhs = field[:position] + rhs = field[position+1:] + + # Make sure the include symbol isn't part of some + # previous option argument + + previousopt = False + for c in OPTIONSWITHARGS: + + if c in lhs: + + previousopt = True + break + + # If the include symbol appears in the context of a + # previous option, skip this field, otherwise process + # it as an include. + + + if not previousopt: + + FoundNewInclude = True + if lhs == OPTINTRO: + lhs = "" + + if rhs == "": + + if i < len(OPTIONS)-1: + + inclfile = OPTIONS[i+1] + OPTIONS = OPTIONS[:i+1] + OPTIONS[i+2:] + + # We have an include without a filename at the end + # of the command line which is bogus. + else: + ErrorMsg(eBADARG % eBADINCL) + + else: + inclfile = rhs + + # Before actually doing the include, make sure we've + # not exceeded the limit. This is here mostly to make + # sure we stop recursive/circular includes. + + NUMINCLUDES += 1 + if NUMINCLUDES > MAXINCLUDES: + ErrorMsg(eTOOMANYINC) + + # Read the included file, stripping out comments + + # Use include path if one was provided + + inclpath = os.getenv(INCLENV) + if inclpath: + + found = searchpath(inclfile, inclpath, PATHDEL) + if found: + inclfile = found[0] + + try: + n = [] + f = open(inclfile) + for line in f.readlines(): + line = line.split(COMMENT)[0] + n += shlex.split(line) + f.close() + + # Keep track of the filenames being included for debug output + + IncludedFiles.append(os.path.abspath(inclfile)) + + # Insert content of included file at current + # command line position + + # A non-null left hand side means that there were + # options before the include we need to preserve + + if lhs: + n = [lhs] + n + + OPTIONS = OPTIONS[:i] + n + OPTIONS[i+1:] + + except IOError, (errno, errstr): + ErrorMsg(eFILEOPEN % (inclfile, errstr)) + + i += 1 + + return OPTIONS + +# End of 'ProcessIncludes()' + + +##### +# Search Path Looking For Include File +##### + +def searchpath(filename, pathlist, pathdelim): + + # What we'll return if we find nothing + + retval = [] + + # Find all instances of filename in specified paths + + paths = pathlist.split(pathdelim) + + for path in paths: + + if path and path[-1] != PATHSEP: + path += PATHSEP + + path += filename + + if os.path.exists(path): + retval.append(os.path.realpath(path)) + + return retval + +# End of 'searchpath()' + + +##### +# Print Usage Information +##### + +def Usage(): + for line in uTable: + PrintStdout(line) + +# End of 'Usage()' + + +#----------------------------------------------------------# +# Program Entry Point # +#----------------------------------------------------------# + +# Set up proper include path delimiter + + +if WINDOWS: + PATHDEL = PATHDELWIN + +else: + PATHDEL = PATHDELUNIX + + +##### +# 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) 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 = shlex.split(envopt) + OPTIONS + +# Deal with include files + +OPTIONS = ProcessIncludes(OPTIONS) + +# And parse the command line + +try: + opts, args = getopt.getopt(OPTIONS, OPTIONSLIST) +except getopt.GetoptError, (errmsg, badarg): + ErrorMsg(eBADARG % errmsg) + +# Create and populate an object with rename targets. This must be +# done here because this object also stores the -r renaming requests +# we may find in the options processing below. Also, this object must +# be fully populated before any actual renaming can take place since +# many of the renaming tokens derive information about the file being +# renamed. + + +# Do wildcard expansion on the rename targets because they may +# have come from an include file (where they are not expanded) +# or from a Windows shell that doesn't know how to handle globbing +# properly. + +# If the glob expands to nothing, then supply the original string. +# That way an error will be thrown if either an explicitly named file +# does not exist, or if a wildcard expands to nothing. + +expandedlist = [] +for arg in args: + + wc = glob.glob(arg) + if wc: + expandedlist += wc + else: + expandedlist.append(arg) + +targs = RenameTargets(expandedlist) + +# Now process the options + +for opt, val in opts: + + # Install new alphabet + + if opt == "-A": + + alphaname, delim, alpha = val.partition(ALPHADELIM) + + if not delim: + ErrorMsg(eALPHACMDBAD % val) + + if not alphaname: + ErrorMsg(eALPHACMDBAD % val) + + if len(alpha) < 2: + ErrorMsg(eALPHACMDLEN % val) + + a = [] + for c in alpha: + a.append(c) + + ALPHABETS[alphaname] = a + + if opt == "-a": + ProgramOptions[ASK] = True + + # Turn off backups during forced renaming + + if opt == "-b": + ProgramOptions[BACKUPS] = False + + # Select case-sensitivity for replacements (or not) + + if opt == "-C": + ProgramOptions[CASESENSITIVE] = True + + if opt == "-c": + ProgramOptions[CASESENSITIVE] = False + + # Turn on debugging + + if opt == "-d": + ProgramOptions[DEBUG] = True + DumpState() + + # Force case conversion + + if opt == "-e": + + # Make sure we support the requested case conversion + if val in CASEOPS: + + ProgramOptions[CASECONV] = val + + # Construct a renaming request + + req = {} + req[OLD], req[NEW] = None, None + for opt in ProgramOptions: + req[opt] = ProgramOptions[opt] + + targs.RenRequests.append(req) + + # Error out if we don't recognize it + else: + ErrorMsg(eBADCASECONV % (val, ", ".join(CASEOPS))) + + + # Force renaming of existing targets + + if opt == "-f": + ProgramOptions[FORCERENAME] = True + + # Output usage information + + if opt == "-h": + Usage() + sys.exit(0) + + # Specify which instances to replace + + if opt == "-i": + + ProgramOptions[INSTANCESTART], ProgramOptions[INSTANCEEND] = CheckSlice(val) + + # Set the escape character + + if opt == "-P": + if len(val) == 1: + ProgramOptions[ESCAPE] = val + else: + ErrorMsg(eARGLENGTH % (NULLESC, 1)) + + # Set quiet mode + + if opt == "-q": + ProgramOptions[QUIET] = True + + # Set the separator character for replacement specifications + + if opt == '-R': + if len(val) == 1: + ProgramOptions[RENSEP] = val + else: + ErrorMsg(eARGLENGTH % (NULLRENSEP, 1)) + + # Specify a replacement command + + if opt == "-r": + req = {} + req[OLD], req[NEW] = GetOldNew(val) + ProgramOptions[CASECONV] = None + for opt in ProgramOptions: + req[opt] = ProgramOptions[opt] + targs.RenRequests.append(req) + + # Specify a renaming suffix + + if opt == "-S": + if val: + ProgramOptions[EXISTSUFFIX] = val + else: + ErrorMsg(eNULLARG % NULLSUFFIX) + + # Set substring targeted for renaming + + if opt == "-T": + ProgramOptions[TARGETSTART], ProgramOptions[TARGETEND] = CheckSlice(val) + + # Request test mode + + if opt == "-t": + ProgramOptions[TESTMODE] = True + + + # Output program version info + + if opt == "-v": + PrintStdout(RCSID) + + # Set output width + + if opt == "-w": + try: + l = int(val) + except: + ErrorMsg(eBADLEN % val) + if l < MINLEN: + ErrorMsg(eLINELEN) + ProgramOptions[MAXLINELEN] = l + + # Select whether 'old' replacement string is a regex or not + + if opt == "-X": + ProgramOptions[REGEX] = False + + if opt == "-x": + ProgramOptions[REGEX] = True + + +# At this point, the command line has been fully processed and the +# container fully populated. Provide debug info about both if +# requested. + +if ProgramOptions[DEBUG]: + + # Dump what we know about the command line + + DumpList(dCMDLINE, "", sys.argv) + DumpList(dPROGENV, "", envopt) + DumpList(dRESOLVEDOPTS, "", OPTIONS) + + # Dump what we know about included files + + DumpList(dINCLFILES, "", IncludedFiles) + + # Dump what we know about the container + + targs.DumpObj() + + +# Perform reqested renamings + +targs.ProcessRenameRequests() + + +# Release the target container if we created one + +del targs +