#!/usr/bin/python2 # -*- coding: utf-8 -*- from __future__ import print_function import re import argparse from abc import abstractmethod import os from shutil import copyfile import subprocess import jwutils from jwutils.log import * _exts_h = set([ '.h', '.H', '.hxx', '.HXX']) _exts_cpp = set([ '.cpp', '.CPP', '.c', '.C', '.cxx', '.CXX' ]) _exts_h_cpp = _exts_h | _exts_cpp class Cmd(jwutils.Cmd): def __init__(self, name, help): self.replacements = None super(Cmd, self).__init__(name, help=help) @staticmethod def _replace_pattern(line, src, target, context=None): return line.replace(src, target) @staticmethod def _indent_pattern(data, src, target, context=None): indent = 30 pattern = '=' right_align_match = 0 skip_lhs_pattern = None require_lhs_pattern = None skip_short = None min_assignments = None if context is not None: if 'indent' in context: indent = context['indent'] if 'pattern' in context: pattern = context['pattern'] if 'right-align-match' in context: right_align_match = context['right-align-match'] if 'skip-lhs-pattern' in context: skip_lhs_pattern = context['skip-lhs-pattern'] if 'require-lhs-pattern' in context: require_lhs_pattern = context['require-lhs-pattern'] if 'skip-short' in context: skip_short = context['skip-short'] if 'min-assignments' in context: min_assignments = context['min-assignments'] r = '' assignments=0 lines = data.splitlines() if skip_short is not None and len(lines) < skip_short: return data for line in iter(lines): #slog(NOTICE, "indenting pattern", pattern, "of", line) parts = re.split(pattern, line) if ( len(parts) < 2 or (skip_lhs_pattern is not None and re.match(skip_lhs_pattern, parts[0])) or (require_lhs_pattern is not None and not re.match(require_lhs_pattern, parts[0])) ): r += line + '\n' continue #slog(NOTICE, "split into", parts) if right_align_match > 0: parts[1] = parts[1].rjust(right_align_match) if len(parts) > 2: p2_stripped = parts[2].strip() if len(p2_stripped) or len(parts) > 3: parts[2] = ' ' + p2_stripped r += parts[0].rstrip().ljust(indent) + ''.join(parts[1:]) + '\n' assignments += 1 if min_assignments is not None and assignments < min_assignments: return data return r @staticmethod def _cleanup_spaces(data, src, target, context=None): lines = data.splitlines() last = len(lines) - 1 r = '' while last >= 0 and len(lines[last].strip()) == 0: last -= 1 i = 0 while i <= last: r += lines[i].rstrip() + '\n' i += 1 return r @staticmethod def _replace_cpp_symbol(data, src, target, context=None): stopc = "^a-zA-Z0-9_" stopa = "(^|[" + stopc + "])" stope = "([" + stopc + "]|$)" f = stopa + src + stope t = "\\1" + target + "\\2" done = False try: while True: #slog(WARNING, "replacing", f, "by", t) r = re.sub(f, t, data, flags=re.MULTILINE) if r == data: break data = r except: slog(ERR, "failed to replace", f, "by", t, "in", data) return data #if r != data: # slog(NOTICE, " replaced ", f, "->", t) # slog(NOTICE, " resulted in ", data, "->", r) return r def _replace_in_file(self, path, replacements, func=None, backup='rep', context=None): if func is None: func = self._replace_pattern tmp_ext = backup if tmp_ext is None: tmp_ext = tmp tmp = path + '.' + tmp_ext changed = False with open(path) as infile, open(tmp, 'w') as outfile: data = infile.read() #slog(NOTICE, "-- opened", path) for src, target in replacements.iteritems(): #slog(NOTICE, "replacing", src, "to", target) odata = data #data = data.replace(src, target) data = func(data, src, target, context) if data != odata: changed = True outfile.write(data) if not changed: os.unlink(tmp) return False if backup is None: os.rename(tmp, path) else: copyfile(tmp, path) return True def _replace_in_string(self, string, replacements, func=None): r = "" if func is None: func = self._replace_pattern for line in iter(string.splitlines()): for src, target in replacements.iteritems(): line = func(line, src, target) r = r + line return r def _add_namespace_to_header(self, data, ns_new): lines = data.splitlines() old = None ns_cur = [] for line in iter(lines): match = re.sub('^ *namespace[ \t]*([^ ]+)[ \t]*{.*', '\\1', line) if match != line: #ns_cur = match(blah, bl raise Exception("Name space addition is not yet implemented") classname = re.sub('^ *#[ \t]*ifndef[\t ]+([^ ]+)($|[ \t/;])', '\\1', line) def _fix_multiple_inclusion_preventer(self, prefix, path): dir, name = os.path.split(path) if len(name) == 0: return False trunc, ext = os.path.splitext(name) if ext not in _exts_h: return False tok = re.sub('([A-Z])', '_\\1', name) tok = re.sub('\.', '_', tok) mip = prefix + '_' + tok mip = re.sub('__', '_', mip) mip = mip.upper() # find first old mip with open(path, 'r') as f: data = f.read() lines = data.splitlines() old = None for line in iter(lines): old = re.sub('^ *#[ \t]*ifndef[\t ]+([^ ]+)($|[ \t/;])', '\\1', line) if old == line: continue #slog(NOTICE, "found MIP", old, "in", line) break if old is None: # TODO: add anyway at beginning and end raise Exception('No multiple inclusion preventer found in', path, ', not implemented') data = '' level = 0 for line in iter(lines): if re.match('^ *#[ \t]*if.?def[\t ]', line): level += 1 if re.match('^ *#[ \t]*ifndef[\t ]+' + old + '($|[ \t/;])', line): data += '#ifndef ' + mip + '\n' continue elif re.match('^ *#[ \t]*define[\t ]+' + old + '($|[ \t/;])', line): data += '#define ' + mip + '\n' continue elif re.match('^ *#[ \t]*endif($|[ \t/;])', line): level -= 1 if level == 0: data += '#endif /* ' + mip + ' */' + '\n' continue data += line + '\n' tmp = path + '.mip' with open(tmp, 'w') as f: f.write(data) os.rename(tmp, path) def add_parser(self, parsers): p = super(Cmd, self).add_parser(parsers) p.add_argument('-r', "--root", help="Point in file system from which to start search", default='.') p.add_argument("--name-regex", help="Regular expression to select input file names", default=None) p.add_argument("--replace-patterns-from", help="File with patterns to replace, side by side, divided by '->'", default=None) p.add_argument("--backup", help="Backup extension", default='rep') p.add_argument('-g', '--git', help="Use git mv for renaming files", action='store_true', default=False) return p def _init(self, args): if args.replace_patterns_from is not None: self.replacements = dict() with open(args.replace_patterns_from) as infile: for line in infile: s = re.split('->', line) self.replacements[s[0]] = s[1].rstrip('\n') #slog(NOTICE, "replacements =", self.replacements) # overriding def run(self, args): self._init(args) files = [] if args.name_regex is not None: for root, dirs, names in os.walk(args.root): for name in names: if re.search(args.name_regex, name): files.append((root, name)) self.process(args, files) @abstractmethod def process(self, args, files): pass class CmdReplacePatterns(Cmd): def __init__(self): super(CmdReplacePatterns, self).__init__("replace-patterns", "Replace patterns in files") def process(self, args, files): for dir, name in files: if self.replacements is not None: path = dir + '/' + name self._replace_in_file(path, self.replacements, func=self._replace_pattern) class CmdReplaceCppSymbols(Cmd): def __init__(self): super(CmdReplaceCppSymbols, self).__init__("replace-cpp-symbols", "Replace C++ symbols in files") def add_parser(self, parsers): p = super(CmdReplaceCppSymbols, self).add_parser(parsers) p.add_argument('-F', '--rename-files', help="Rename source files, too", action='store_true', default=False) p.add_argument('-P', '--mip-prefix', help="Prefix to multiple-inclusion preventer", default='') return p # overriding def run(self, args): if args.name_regex is not None: return super(CmdReplaceCppSymbols, self).run(args) self._init(args) files = [] exts = _exts_h_cpp | set([ '.sh', '.py' ]) for root, dirs, names in os.walk(args.root): for name in names: trunc, ext = os.path.splitext(name) if ext in exts: files.append((root, name)) self.process(args, files) # overriding def _init(self, args): r = super(CmdReplaceCppSymbols, self)._init(args) self.file_truncs = set() if self.replacements is not None: for patt in self.replacements: self.file_truncs.add(patt.lower()) return r def process(self, args, files): for dir, name in files: path = dir + '/' + name if self.replacements is not None: self._replace_in_file(path, self.replacements, func=self._replace_cpp_symbol) if args.rename_files: for dir, name in files: trunc, ext = os.path.splitext(name) if not ext in _exts_h_cpp: continue if not trunc.lower() in self.file_truncs: continue for patt, repl in self.replacements.iteritems(): if patt == trunc: path = dir + '/' + name new_path = dir + '/' + repl + ext assert(new_path != path) slog(NOTICE, "renaming", path, "->", new_path) if args.git: subprocess.call(['git', 'mv', path, new_path]) else: os.rename(path, new_path) self._fix_multiple_inclusion_preventer(args.mip_prefix, new_path) class CmdIndentMakefileEquals(Cmd): def __init__(self): super(CmdIndentMakefileEquals, self).__init__("indent-makefiles", "Indent and beautify makefiles") def add_parser(self, parsers): p = super(CmdIndentMakefileEquals, self).add_parser(parsers) p.add_argument('-e', "--equal-pos", help="Columns number of equal sign", type=int, default=40) p.add_argument("--skip-short", help="Don't change makefiles with less lines of code", type=int, default=6) p.add_argument("--min-assignments", help="Don't change makefiles with less assignment statements", type=int, default=4) return p def process(self, args, files): slog(NOTICE, "Beautifying", len(files), "makefiles:") context = dict() right_align_match = 2 context["indent"] = args.equal_pos - right_align_match context["pattern"] = "([?+:]*=|::=)" context["right-align-match"] = right_align_match context["skip-lhs-pattern"] = "[^A-Za-z0-9_# ]" context["require-lhs-pattern"] = "^[ #]*[A-Za-z0-9_]" context["skip-short"] = args.skip_short context["min-assignments"] = args.min_assignments replacements = {"blah": "blub"} # just a dummy to use _replace_in_file, TODO: obviate the need for dir, name in files: path = dir + '/' + name if self._replace_in_file(path, replacements, func=self._cleanup_spaces): slog(NOTICE, "+ purged spaces :", path) if self._replace_in_file(path, replacements, func=self._indent_pattern, context=context): slog(NOTICE, "+ aligned equals :", path) class CmdCleanupSpaces(Cmd): def __init__(self): super(CmdCleanupSpaces, self).__init__("cleanup-spaces", "Remove trailing empty lines") def add_parser(self, parsers): p = super(CmdCleanupSpaces, self).add_parser(parsers) return p def process(self, args, files): slog(NOTICE, "Cleaning up unnecessary space in", len(files), "files:") context = dict() replacements = {"blah": "blub"} # just a dummy to use _replace_in_file, TODO: obviate the need for dir, name in files: path = dir + '/' + name if self._replace_in_file(path, replacements, func=self._cleanup_spaces, context=context): slog(NOTICE, "+ purged spaces :", path) class CmdAddCppNamespace(Cmd): def __init__(self): super(CmdAddCppNamespace, self).__init__("add-cpp-namespace", "Enclose C++ classes in namespace") def add_parser(self, parsers): p = super(CmdAddCppNamespace, self).add_parser(parsers) p.add_argument('-n', '--namespace', help="Namespace", default=None) p.add_argument('-p', '--package', help="Package", default=None) return p # overriding def run(self, args): if args.name_regex is not None: return super(CmdAddCppNamespace, self).run(args) self._init(args) files = [] exts = _exts_h_cpp | set([ '.sh', '.py' ]) for root, dirs, names in os.walk(args.root): for name in names: trunc, ext = os.path.splitext(name) if ext in exts: files.append((root, name)) self.process(args, files) # overriding def _init(self, args): r = super(CmdAddCppNamespace, self)._init(args) self.file_truncs = set() if self.replacements is not None: for patt in self.replacements: self.file_truncs.add(patt.lower()) return r def process(self, args, files): if args.namespace: for dir, name in files: path = dir + '/' + name with open(path) as infile: data = odata = infile.read() trunc, ext = os.path.splitext(name) if ext in _exts_h: data = self._add_namespace_to_header(data, namespace) elif ext in _exts_cpp: data = self._add_using_namespace(data, namespace) #elif: Not sure what this was meant to do # continue if data == odata: continue tmp = path + '.' + ('rep' if args.backup is None else args.backup) with open(tmp, 'w') as outfile: outfile.write(data) jwutils.run_sub_commands('process text files')