mirror of
ssh://git.janware.com/srv/git/janware/proj/jw-python
synced 2026-01-15 09:53:32 +01:00
The command removes traling empty spaces from lines and trailing empty lines from files. Signed-off-by: Jan Lindemann <jan@janware.com>
418 lines
16 KiB
Python
418 lines
16 KiB
Python
#!/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:
|
|
parts[2] = ' ' + parts[2].strip()
|
|
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')
|