#!/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" BASENAME = PROGNAME.split(".py")[0] PROGENV = BASENAME.upper() RCSID = "$Id: tren.py,v 1.180 2010/03/10 00:50:14 tundra Exp $" VERSION = RCSID.split()[2] # Copyright Information CPRT = "(c)" DATE = "2010" 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 grp import pwd import os import re from stat import * import sys import time #----------------------------------------------------------# # 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 = "Ccdfhi:P:qR:r:S:tvw:Xx" # All legal command line options in getopt() format ##### # Literals ##### ARROW = "--->" # Used for formatting renaming messages 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 RANGESEP = ":" # Separator for instance ranges SINGLEINST = "SINGLEINST" # Indicates a single, not range, replacement instance ##### # Replacement Token Literals ##### TOKDELIM = "/" # Delimiter for all renaming tokens # Shared File Attribute And Sequence Renaming Tokens TOKFILALPHA = "ALPHA" TOKFILATIME = "ATIME" TOKFILCMD = "CMDLINE" TOKFILCTIME = "CTIME" TOKFILDEV = "DEV" TOKFILGID = "GID" TOKFILGRP = "GNAME" TOKFILINODE = "INODE" TOKFILMODE = "MODE" TOKFILMTIME = "MTIME" TOKFILNAM = "FNAME" TOKFILNLINK = "NLINK" TOKFILSIZE = "SIZE" TOKFILUID = "UID" TOKFILUNAM = "UNAME" # 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 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 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 TOKMYEAR = "MYEAR" # yyyy replacement token # System Renaming Tokens TOKCMDEXEC = "`" # Delimiter for command execution renaming tokens TOKENV = "$" # Introducer for environment variable replacement tokens # Sequence Renaming Tokens TOKASCEND = "+" # Ascending order flag TOKDESCEND = "-" # Descending order flag ##### # Internal program state literals ##### DEBUG = "DEBUG" CASESENSITIVE = "CASESENSITIVE" ESCAPE = "ESCAPE" EXISTSUFFIX = "EXISTSUFFIX" FORCERENAME = "FORCERENAME" INSTANCESTART = "INSTANCESTART" INSTANCEEND = "INSTANCEEND" MAXLINELEN = "MAXLINELEN" QUIET = "QUIET" REGEX = "REGEX" RENSEP = "RENSEP" 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 dictionary that stores the prestorted views ORDERBYALPHA = TOKFILALPHA ORDERBYATIME = TOKFILATIME ORDERBYCMDLINE = TOKFILCMD ORDERBYCTIME = TOKFILCTIME ORDERBYDEV = TOKFILDEV ORDERBYGID = TOKFILGID ORDERBYINODE = TOKFILINODE ORDERBYMODE = TOKFILMODE ORDERBYMTIME = TOKFILMTIME ORDERBYNLINK = TOKFILNLINK ORDERBYSIZE = TOKFILSIZE ORDERBYUID = TOKFILUID # Rename string keys NEW = "NEW" OLD = "OLD" #----------------------------------------------------------# # Prompts, & Application Strings # #----------------------------------------------------------# ##### # Debug Messages ##### DEBUGFLAG = "-d" dCMDLINE = "Command Line" dCURSTATE = "Current State Of Program Options" dDEBUG = "DEBUG" dDUMPOBJ = "Dumping Object %s" dINCLFILES = "Included Files:" dPROGENV = "$" + PROGENV dRENREQ = "Renaming Request:" dRENTARGET = "Rename Target:" dRESOLVEDOPTS = "Resolved Command Line" dSEPCHAR = "-" # Used for debug separator lines dSORTVIEW = "Sort View:" ##### # Error Messages ##### eARGLENGTH = "%s must contain exactly %s character(s)!" eBADARG = "Invalid command line: %s!" eBADINCL = "option -%s requires argument" % INCL eBADINSTANCE = "%s is an invalid replacement instance! Must be integer values in the form: n, n:n, :n, n:, or :" eBADLEN = "Bad line length '%s'!" eBADNEWOLD = "Bad -r argument '%s'! Requires exactly one new, old string separator (Default: " + DEFSEP + ")" eBADREGEX = "Invalid Regular Expression: %s" eERROR = "ERROR" eEXECFAIL = "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!" eTOKDELIM = "Renaming token '%s' missing delimiter!" eTOKUNKNOWN = "Renaming Token '%s' is unknown type!" eTOOMANYINC = "Too many includes! (Max is %d) Possible circular reference?" % MAXINCLUDES ##### # Informational Messages ##### iRENFORCED = "Target '%s' Exists. Creating Backup." iRENSKIPPED = "Target '%s' Exists. Renaming Of '%s' Skipped." iRENAMING = "Renaming '%s' " + ARROW + " '%s'." ##### # Usage Prompts ##### uTable = [PROGVER, HOMEPAGE, "usage: " + PROGNAME + " [[-CcdfhqtvwXx] [-I file] [-i instance] [-P escape] [ -R separator] [-S suffix] [-r old=new]] ... file|dir file|dir ...", " where,", " -C Do case-sensitive renaming (Default)", " -c Collapse case when doing string substitution (Default: False)", " -d Dump debugging information (Default: False)", " -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 Specify which instance 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)", " -v Print detailed program version information and continue (Default: False)", " -w width 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 # #----------------------------------------------------------# # Month Conversion Table 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"), 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"), 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"), TOKMYEAR : ("%04d", "ST_MTIME", "tm_year") } #----------------------------------------------------------# # Global Variables & Data Structures # #----------------------------------------------------------# # Program toggle and option defaults IncludedFiles = [] ProgramOptions = { DEBUG : False, # Debugging off 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 TESTMODE : False # Test mode off } #--------------------------- 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 = { ORDERBYCMDLINE : [fullnames in command line order], ORDERBYALPHA : [fullnames in alphabetic order], ORDERBYMODE : [fullnames in mode order], ORDERBYINODE : [fullnames in inode order], ORDERBYDEV : [fullnames in devs order], ORDERBYNLINK : [fullnames in nlinks order], ORDERBYUID : [fullnames in uids order], ORDERBYGID : [fullnames in gids order], ORDERBYATIME : [fullnames in atimes order], ORDERBYCTIME : [fullnames in ctimes order], ORDERBYMTIME : [fullnames in mtimes order], ORDERBYSIZE : [fullnames in size order] } self.RenRequests = [ { OLD : old rename string, NEW : new rename string, DEBUG : debug flag, CASESENSITIVE : case sensitivity flag, FORCERENAME : force renaming flag, INSTANCESTART : DReplace 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, TESTMODE : testmode flag } ... (repeated for each rename request) ] """ ##### # Constructor ##### def __init__(self, targs): # 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, ORDERBYALPHA : alpha} del alpha # Dictionary of all the renaming requests - will be filled in # by -r command line parsing. self.RenRequests = [] # This data structure is used while we build up sort # orders based on stat information. SeqTypes = [ [ST_MODE, {}, ORDERBYMODE], [ST_INO, {}, ORDERBYINODE], [ST_DEV, {}, ORDERBYDEV], [ST_NLINK, {}, ORDERBYNLINK], [ST_UID, {}, ORDERBYUID], [ST_GID, {}, ORDERBYGID], [ST_ATIME, {}, ORDERBYATIME], [ST_CTIME, {}, ORDERBYCTIME], [ST_MTIME, {}, ORDERBYMTIME], [ST_SIZE, {}, ORDERBYSIZE], ] # Populate the data structures with each targets' stat information for fullname in targs: try: basename = os.path.basename(fullname) stats = os.stat(fullname) except (IOError, OSError) as e: ErrorMsg(eFILEOPEN % (fullname, e.args[1])) # Store fullname, basename, and stat info for this file if basename: self.RenNames[fullname] = {BASE : basename, PATHNAME : fullname.split(basename)[0], 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 statval = stats[statflag] if statval in storage: storage[statval].append(fullname) else: storage[statval] = [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 # 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)): for j in i: DumpList(DebugMsg, msg, j, i[j]) for rr in self.RenRequests: DumpList(DebugMsg, dRENREQ, "", rr) DebugMsg(SEPARATOR + "\n\n") # End of 'DumpObj()' ##### # Go Do The Requested Renaming ##### def ProcessRenameRequests(self): 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 name = oldname for renrequest in self.RenRequests: # 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. if not old: old = oldname # 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, name) else: rematches = re.finditer(old, name, re.I) # And save off the results for match in rematches: oldstrings.append(match.span()) except: ErrorMsg(eBADREGEX % old) # Handle literal string replacement else: # Collapse case if requested if not renrequest[CASESENSITIVE]: name = name.lower() old = old.lower() oldlen = len(old) i = name.find(old) while i >= 0: nextloc = i + oldlen oldstrings.append((i, nextloc)) i = name.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]:] # Any subsequent replacements operate on the modified name name = newname # 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 if os.path.exists(fullnew): if ProgramOptions[FORCERENAME]: # Create the backup bkuname = newname + ProgramOptions[EXISTSUFFIX] InfoMsg(indent + iRENFORCED % fullnew) self.__RenameIt(pathname, newname, bkuname) else: InfoMsg(indent + iRENSKIPPED % (fullnew, fullold)) doit = False if doit: InfoMsg(indent + iRENAMING % (fullold, fullnew)) if not ProgramOptions[TESTMODE]: try: os.rename(fullold, fullnew) except OSError as e: ErrorMsg(eRENAMEFAIL % (fullold, fullnew, e.args[1])) 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: ### # File Attribute Renaming Tokens ### if r[2] == TOKFILDEV: r[2] = str(self.RenNames[target][STATS][ST_DEV]) elif r[2] == TOKFILGID: r[2] = str(self.RenNames[target][STATS][ST_GID]) elif r[2] == TOKFILGRP: r[2] = grp.getgrgid(self.RenNames[target][STATS][ST_GID])[0] 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] == TOKFILNAM: r[2] = os.path.basename(target) 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] == TOKFILUNAM: r[2] = pwd.getpwuid(self.RenNames[target][STATS][ST_UID])[0] ### # 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 else: r[2] = MONTHS[val] ### # System Renaming Tokens ### # Environment Variable replacement token elif r[2].startswith(TOKENV): r[2] = os.getenv(r[2][1:]) # 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 os.name != '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 % command) # Otherwise swap the command with its results, stripping newlines else: r[2] = output.replace("\n", "") ### # Sequence Renaming Tokens ### elif r[2] != "" and (r[2][0] == TOKASCEND or r[2][0] == TOKDESCEND): for token in self.SortViews: print token ### # Unrecognized Renaming Token ### else: ErrorMsg(eTOKUNKNOWN % (TOKDELIM + r[2] + TOKDELIM)) ### # 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 # #----------------------------------------------------------# ##### # 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()' ##### # 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(handler, msg, listname, content): handler(msg) itemarrow = ColumnPad([listname, " "], WIDTH=LSTPAD) handler(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]])) 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 try: n = [] f = open(inclfile) for line in f.readlines(): line = line.split(COMMENT)[0] n += line.split() 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 as e: ErrorMsg(eFILEOPEN % (inclfile, e.args[1])) i += 1 return OPTIONS # End of 'ProcessIncludes()' ##### # 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 # Deal with include files OPTIONS = ProcessIncludes(OPTIONS) # And parse the command line try: opts, args = getopt.getopt(OPTIONS, OPTIONSLIST) except getopt.GetoptError as e: ErrorMsg(eBADARG % e.args[0]) # 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. targs = RenameTargets(args) # Now process the options for opt, val in opts: # 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 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": # Parse the argument 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(eBADINSTANCE % val) ProgramOptions[INSTANCESTART] = lhs ProgramOptions[INSTANCEEND] = rhs # 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) 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) # 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(DebugMsg, dCMDLINE, "", sys.argv) DumpList(DebugMsg, dPROGENV, "", envopt) DumpList(DebugMsg, dRESOLVEDOPTS, "", OPTIONS) # Dump what we know about included files DumpList(DebugMsg, 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