diff --git a/bin/project_rename.py b/bin/project_rename.py new file mode 100644 index 0000000000..6723e3fd0c --- /dev/null +++ b/bin/project_rename.py @@ -0,0 +1,316 @@ +""" +Project rename utility + +Created for the Player->Account renaming + +Griatch 2017, released under the BSD license. + +""" +from __future__ import print_function + +import re +import sys +import os +import fnmatch + +ANSI_HILITE = "\033[1m" +ANSI_RED = "\033[31m" +ANSI_GREEN = "\033[32m" +ANSI_YELLOW = "\033[33m" +ANSI_NORMAL = "\033[0m" + +USE_COLOR = True +FAKE_MODE = False + +# if these words are longer than output word, retain given case +CASE_WORD_EXCEPTIONS = ('an', ) + + +# Helper functions + +def _green(string): + if USE_COLOR: + return "%s%s%s" % (ANSI_GREEN, string, ANSI_NORMAL) + return string + + +def _yellow(string): + if USE_COLOR: + return "%s%s%s" % (ANSI_YELLOW, string, ANSI_NORMAL) + return string + + +def _red(string): + if USE_COLOR: + return "%s%s%s" % (ANSI_HILITE + ANSI_RED, string, ANSI_NORMAL) + return string + + +def _case_sensitive_replace(string, old, new): + """ + Replace text, retaining exact case. + + Args: + string (str): String in which to perform replacement. + old (str): Word or substring to replace. + new (str): What to replace `old` with. + + Returns: + repl_string (str): Version of string where instances of + `old` has been replaced with `new`, retaining case. + + """ + def repl(match): + current = match.group() + # treat multi-word sentences word-by-word + old_words = current.split(" ") + new_words = new.split(" ") + out = [] + for old_word, new_word in zip(old_words, new_words): + result = [] + all_upper = True + for ind, chr in enumerate(old_word): + if ind >= len(new): + break + if chr.isupper(): + result.append(new_word[ind].upper()) + else: + result.append(new_word[ind].lower()) + all_upper = False + # special cases - keep remaing case) + if new_word.lower() in CASE_WORD_EXCEPTIONS: + result.append(new_word[ind+1:]) + # append any remaining characters from new + elif all_upper: + result.append(new_word[ind+1:].upper()) + else: + result.append(new_word[ind+1:].lower()) + out.append("".join(result)) + # if we have more new words than old ones, just add them verbatim + out.extend([new_word for ind, new_word in enumerate(new_words) if ind >= len(old_words)]) + return " ".join(out) + + regex = re.compile(re.escape(old), re.I) + return regex.sub(repl, string) + + +def rename_in_tree(path, in_list, out_list, excl_list, fileend_list, is_interactive): + """ + Rename across a recursive directory structure. + + Args: + path (str): Root directory to traverse. All subdirectories + will be visited. + in_list (list): List of src words to replace. + out_list (list): Matching list of words to replace with. + excl_list (list): List of paths to exclude. + fileend_list (list): List of file endings to accept. If + not given, accept all file endings. + is_interactive (bool): If we should stop to ask about the + replacements in each file. + + """ + repl_mapping = zip(in_list, out_list) + + for root, dirs, files in os.walk(path): + + print("\ndir: %s\n" % root) + + if any(fnmatch.fnmatch(root, excl) for excl in excl_list): + print("%s skipped (excluded)." % root) + continue + + for file in files: + + full_path = os.path.join(root, file) + if any(fnmatch.fnmatch(full_path, excl) for excl in excl_list): + print("%s skipped (excluded)." % full_path) + continue + + if not fileend_list or any(file.endswith(ending) for ending in fileend_list): + rename_in_file(full_path, in_list, out_list, is_interactive) + + # rename file - always ask + new_file = file + for src, dst in repl_mapping: + new_file = _case_sensitive_replace(new_file, src, dst) + if new_file != file: + inp = raw_input(_green("Rename %s\n -> %s\n Y/[N]? > " % (file, new_file))) + if inp.upper() == 'Y': + new_full_path = os.path.join(root, new_file) + try: + os.rename(full_path, new_full_path) + except OSError as err: + raw_input(_red("Could not rename - %s (return to skip)" % err)) + else: + print("... Renamed.") + else: + print("... Skipped.") + # rename the dir + new_root = root + for src, dst in repl_mapping: + new_root = _case_sensitive_replace(new_root, src, dst) + if new_root != root: + inp = raw_input(_green("Dir Rename %s\n -> %s\n Y/[N]? > " % (root, new_root))) + if inp.upper() == 'Y': + new_full_path = os.path.join(root, new_file) + try: + os.rename(root, new_root) + except OSError as err: + raw_input(_red("Could not rename - %s (return to skip)" % err)) + else: + print("... Renamed.") + else: + print("... Skipped.") + + +def rename_in_file(path, in_list, out_list, is_interactive): + """ + Args: + path (str): Path to file in which to perform renaming. + in_list (list): List of src words to replace. + out_list (list): Matching list of words to replace with. + is_interactive (bool): If we should stop to ask about the + replacements in each file. + + """ + print("-- %s" % path) + + org_text = "" + new_text = None + if os.path.isdir(path): + print("%s is a directory. You should use the --recursive option." % path) + sys.exit() + + with open(path, 'r') as fil: + org_text = fil.read() + + repl_mapping = zip(in_list, out_list) + + if not is_interactive: + # just replace everything immediately + new_text = org_text + for src, dst in repl_mapping: + new_text = _case_sensitive_replace(new_text, src, dst) + if new_text != org_text: + if FAKE_MODE: + print(" ... Saved changes to %s. (faked)" % path) + else: + with open(path, 'w') as fil: + fil.write(new_text) + print(" ... Saved changes to %s." % path) + else: + # interactive mode + while True: + renamed = {} + + org_lines = org_text.split("\n") + + for iline, old_line in enumerate(org_lines): + new_line = old_line + for src, dst in repl_mapping: + new_line = _case_sensitive_replace(new_line, src, dst) + if new_line != old_line: + renamed[iline] = new_line + + if not renamed: + # no changes + print(" ... no changes to %s." % path) + return + + while True: + + for iline, renamed_line in sorted(renamed.items(), key=lambda tup: tup[0]): + print("%3i orig: %s" % (iline + 1, org_lines[iline])) + print(" new : %s" % (_yellow(renamed_line))) + print(_green("%s (%i lines changed)" % (path, len(renamed)))) + + ret = raw_input(_green("Choose: " + "[q]uit, " + "[s]kip file, " + "[i]gnore lines, " + "[c]lear ignores, " + "[a]ccept/save file: ")) + + if ret == "s": + # skip file entirely + print(" ... Skipping file %s." % path) + return + elif ret == "c": + # clear ignores - rerun rename + break + elif ret == "a": + # save result + for iline, renamed_line in renamed.items(): + org_lines[iline] = renamed_line + + if FAKE_MODE: + print(" ... Saved file %s (faked)" % path) + return + with open(path, 'w') as fil: + fil.writelines("\n".join(org_lines)) + print(" ... Saved file %s" % path) + return + elif ret == "q": + print("Quit renaming.") + sys.exit() + elif ret.startswith("i"): + # ignore one or more lines + ignores = [int(ind)-1 for ind in ret[1:].split(',') if ind.strip().isdigit()] + if not ignores: + raw_input("Ignore example: i 2,7,34,133\n (return to continue)") + continue + for ign in ignores: + renamed.pop(ign, None) + continue + + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Rename text in a source tree, or a single file") + + parser.add_argument('-i', '--input', action='append', + help="Source word to rename (quote around multiple words)") + parser.add_argument('-o', '--output', action='append', + help="Word to rename a matching src-word to") + parser.add_argument('-x', '--exc', action='append', + help="File path patterns to exclude") + parser.add_argument('-a', '--auto', action='store_true', + help="Automatic mode, don't ask to rename") + parser.add_argument('-r', '--recursive', action='store_true', + help="Recurse subdirs") + parser.add_argument('-f', '--fileending', action='append', + help="Change which file endings to allow (default .py)") + parser.add_argument('--nocolor', action='store_true', + help="Turn off in-program color") + parser.add_argument('--fake', action='store_true', + help="Simulate run but don't actually save") + parser.add_argument('path', + help="File or directory in which to rename text") + + args = parser.parse_args() + + in_list, out_list, exc_list, fileend_list = args.input, args.output, args.exc, args.fileending + + if not (in_list and out_list): + print('At least one source- and destination word must be given.') + sys.exit() + if len(in_list) != len(out_list): + print('Number of sources must be identical to the number of destination arguments.') + sys.exit() + + exc_list = exc_list or [] + fileend_list = fileend_list or [".py"] + is_interactive = not args.auto + is_recursive = args.recursive + + USE_COLOR = not args.nocolor + FAKE_MODE = args.fake + + if is_recursive: + rename_in_tree(args.path, in_list, out_list, exc_list, fileend_list, is_interactive) + else: + rename_in_file(args.path, in_list, out_list, is_interactive)