|
@@ -0,0 +1,317 @@
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+r"""
|
|
|
+This script iterates over a list of C files. For each file, it looks at the
|
|
|
+
|
|
|
+match.
|
|
|
+
|
|
|
+For example, it replaces this kind of input...
|
|
|
+
|
|
|
+>>> INPUT = '''
|
|
|
+...
|
|
|
+... C code here
|
|
|
+...
|
|
|
+... block 1
|
|
|
+... block 1
|
|
|
+... block 1
|
|
|
+... block 1
|
|
|
+...
|
|
|
+... block 2
|
|
|
+... block 2
|
|
|
+... block 2
|
|
|
+... block 2
|
|
|
+...
|
|
|
+...
|
|
|
+... '''
|
|
|
+
|
|
|
+With this kind of output:
|
|
|
+>>> EXPECTED_OUTPUT = '''
|
|
|
+...
|
|
|
+... C code here
|
|
|
+...
|
|
|
+... block 1
|
|
|
+... block 1
|
|
|
+... block 1
|
|
|
+... block 1
|
|
|
+...
|
|
|
+... block 2
|
|
|
+... block 2
|
|
|
+... block 2
|
|
|
+... block 2
|
|
|
+...
|
|
|
+...
|
|
|
+... '''
|
|
|
+
|
|
|
+Here's how to use it:
|
|
|
+>>> import sys
|
|
|
+>>> if sys.version_info.major < 3: from cStringIO import StringIO
|
|
|
+>>> if sys.version_info.major >= 3: from io import StringIO
|
|
|
+
|
|
|
+>>> OUTPUT = StringIO()
|
|
|
+>>> translate(StringIO(INPUT), OUTPUT)
|
|
|
+>>> assert OUTPUT.getvalue() == EXPECTED_OUTPUT
|
|
|
+
|
|
|
+Note that only
|
|
|
+on those lines are removed.
|
|
|
+"""
|
|
|
+
|
|
|
+import re
|
|
|
+
|
|
|
+
|
|
|
+LINE_OBVIOUSNESS_LIMIT = 4
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+LINE_WIDTH=80
|
|
|
+
|
|
|
+class Problem(Exception):
|
|
|
+ pass
|
|
|
+
|
|
|
+def close_parens_needed(expr):
|
|
|
+ """Return the number of left-parentheses needed to make 'expr'
|
|
|
+ balanced.
|
|
|
+
|
|
|
+ >>> close_parens_needed("1+2")
|
|
|
+ 0
|
|
|
+ >>> close_parens_needed("(1 + 2)")
|
|
|
+ 0
|
|
|
+ >>> close_parens_needed("(1 + 2")
|
|
|
+ 1
|
|
|
+ >>> close_parens_needed("(1 + (2 *")
|
|
|
+ 2
|
|
|
+ >>> close_parens_needed("(1 + (2 * 3) + (4")
|
|
|
+ 2
|
|
|
+ """
|
|
|
+ return expr.count("(") - expr.count(")")
|
|
|
+
|
|
|
+def truncate_expression(expr, new_width):
|
|
|
+ """Given a parenthesized C expression in 'expr', try to return a new
|
|
|
+ expression that is similar to 'expr', but no more than 'new_width'
|
|
|
+ characters long.
|
|
|
+
|
|
|
+ Try to return an expression with balanced parentheses.
|
|
|
+
|
|
|
+ >>> truncate_expression("1+2+3", 8)
|
|
|
+ '1+2+3'
|
|
|
+ >>> truncate_expression("1+2+3+4+5", 8)
|
|
|
+ '1+2+3...'
|
|
|
+ >>> truncate_expression("(1+2+3+4)", 8)
|
|
|
+ '(1+2...)'
|
|
|
+ >>> truncate_expression("(1+(2+3+4))", 8)
|
|
|
+ '(1+...)'
|
|
|
+ >>> truncate_expression("(((((((((", 8)
|
|
|
+ '((...))'
|
|
|
+ """
|
|
|
+ if len(expr) <= new_width:
|
|
|
+
|
|
|
+ return expr
|
|
|
+
|
|
|
+ ellipsis = "..."
|
|
|
+
|
|
|
+
|
|
|
+ n_to_remove = len(expr) + len(ellipsis) - new_width
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ while n_to_remove < len(expr):
|
|
|
+ truncated = expr[:-n_to_remove] + ellipsis
|
|
|
+ truncated += ")" * close_parens_needed(truncated)
|
|
|
+ if len(truncated) <= new_width:
|
|
|
+ return truncated
|
|
|
+ n_to_remove += 1
|
|
|
+
|
|
|
+ return ellipsis
|
|
|
+
|
|
|
+def commented_line(fmt, argument, maxwidth=LINE_WIDTH):
|
|
|
+
|
|
|
+ r"""
|
|
|
+ Return fmt%argument, for use as a commented line. If the line would
|
|
|
+ be longer than maxwidth, truncate argument but try to keep its
|
|
|
+ parentheses balanced.
|
|
|
+
|
|
|
+ Requires that fmt%"..." will fit into maxwidth characters.
|
|
|
+
|
|
|
+ Requires that fmt ends with a newline.
|
|
|
+
|
|
|
+ >>> commented_line("/* %s */\n", "hello world", 32)
|
|
|
+ '/* hello world */\n'
|
|
|
+ >>> commented_line("/* %s */\n", "hello world", 15)
|
|
|
+ '/* hello... */\n'
|
|
|
+ >>> commented_line("#endif /* %s */\n", "((1+2) && defined(FOO))", 32)
|
|
|
+ '#endif /* ((1+2) && defi...) */\n'
|
|
|
+
|
|
|
+
|
|
|
+ The default line limit is 80 characters including the newline:
|
|
|
+
|
|
|
+ >>> long_argument = "long " * 100
|
|
|
+ >>> long_line = commented_line("#endif /* %s */\n", long_argument)
|
|
|
+ >>> len(long_line)
|
|
|
+ 80
|
|
|
+
|
|
|
+ >>> long_line[:40]
|
|
|
+ '#endif /* long long long long long long '
|
|
|
+ >>> long_line[40:]
|
|
|
+ 'long long long long long long lon... */\n'
|
|
|
+
|
|
|
+ If a line works out to being 80 characters naturally, it isn't truncated,
|
|
|
+ and no ellipsis is added.
|
|
|
+
|
|
|
+ >>> medium_argument = "a"*66
|
|
|
+ >>> medium_line = commented_line("#endif /* %s */\n", medium_argument)
|
|
|
+ >>> len(medium_line)
|
|
|
+ 80
|
|
|
+ >>> "..." in medium_line
|
|
|
+ False
|
|
|
+ >>> medium_line[:40]
|
|
|
+ '#endif /* aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
|
|
+ >>> medium_line[40:]
|
|
|
+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa */\n'
|
|
|
+
|
|
|
+
|
|
|
+ """
|
|
|
+ assert fmt.endswith("\n")
|
|
|
+ result = fmt % argument
|
|
|
+ if len(result) <= maxwidth:
|
|
|
+ return result
|
|
|
+ else:
|
|
|
+
|
|
|
+
|
|
|
+ max_arg_width = maxwidth - len(fmt % "")
|
|
|
+ result = fmt % truncate_expression(argument, max_arg_width)
|
|
|
+ assert len(result) <= maxwidth
|
|
|
+ return result
|
|
|
+
|
|
|
+def negate(expr):
|
|
|
+ """Return a negated version of expr; try to avoid double-negation.
|
|
|
+
|
|
|
+ We usually wrap expressions in parentheses and add a "!".
|
|
|
+ >>> negate("A && B")
|
|
|
+ '!(A && B)'
|
|
|
+
|
|
|
+ But if we recognize the expression as negated, we can restore it.
|
|
|
+ >>> negate(negate("A && B"))
|
|
|
+ 'A && B'
|
|
|
+
|
|
|
+ The same applies for defined(FOO).
|
|
|
+ >>> negate("defined(FOO)")
|
|
|
+ '!defined(FOO)'
|
|
|
+ >>> negate(negate("defined(FOO)"))
|
|
|
+ 'defined(FOO)'
|
|
|
+
|
|
|
+ Internal parentheses don't confuse us:
|
|
|
+ >>> negate("!(FOO) && !(BAR)")
|
|
|
+ '!(!(FOO) && !(BAR))'
|
|
|
+
|
|
|
+ """
|
|
|
+ expr = expr.strip()
|
|
|
+
|
|
|
+ m = re.match(r'^!\s*\(([^\)]*)\)$', expr)
|
|
|
+ if m:
|
|
|
+ return m.group(1)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ m = re.match(r'^(!?)\s*(defined\([^\)]*\))$', expr)
|
|
|
+ if m:
|
|
|
+ if m.group(1) == "!":
|
|
|
+ prefix = ""
|
|
|
+ else:
|
|
|
+ prefix = "!"
|
|
|
+ return prefix + m.group(2)
|
|
|
+
|
|
|
+ return "!(%s)" % expr
|
|
|
+
|
|
|
+def uncomment(s):
|
|
|
+ """
|
|
|
+ Remove existing trailing comments from an
|
|
|
+ """
|
|
|
+ s = re.sub(r'//.*','',s)
|
|
|
+ s = re.sub(r'/\*.*','',s)
|
|
|
+ return s.strip()
|
|
|
+
|
|
|
+def translate(f_in, f_out):
|
|
|
+ """
|
|
|
+ Read a file from f_in, and write its annotated version to f_out.
|
|
|
+ """
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ stack = []
|
|
|
+
|
|
|
+ whole_file = []
|
|
|
+ cur_level = whole_file
|
|
|
+ lineno = 0
|
|
|
+ for line in f_in:
|
|
|
+ lineno += 1
|
|
|
+ m = re.match(r'\s*#\s*(if|ifdef|ifndef|else|endif|elif)\b\s*(.*)',
|
|
|
+ line)
|
|
|
+ if not m:
|
|
|
+
|
|
|
+ f_out.write(line)
|
|
|
+ continue
|
|
|
+ command,rest = m.groups()
|
|
|
+ if command in ("if", "ifdef", "ifndef"):
|
|
|
+
|
|
|
+ if command == 'ifdef':
|
|
|
+ rest = "defined(%s)"%uncomment(rest)
|
|
|
+ elif command == 'ifndef':
|
|
|
+ rest = "!defined(%s)"%uncomment(rest)
|
|
|
+ elif rest.endswith("\\"):
|
|
|
+ rest = rest[:-1]+"..."
|
|
|
+
|
|
|
+ rest = uncomment(rest)
|
|
|
+
|
|
|
+ new_level = [ (command, rest, lineno) ]
|
|
|
+ stack.append(cur_level)
|
|
|
+ cur_level = new_level
|
|
|
+ f_out.write(line)
|
|
|
+ elif command in ("else", "elif"):
|
|
|
+
|
|
|
+
|
|
|
+ if len(cur_level) == 0 or cur_level[-1][0] == 'else':
|
|
|
+ raise Problem("Unexpected #%s on %d"% (command,lineno))
|
|
|
+ if (len(cur_level) == 1 and command == 'else' and
|
|
|
+ lineno > cur_level[0][2] + LINE_OBVIOUSNESS_LIMIT):
|
|
|
+ f_out.write(commented_line("#else /* %s */\n",
|
|
|
+ negate(cur_level[0][1])))
|
|
|
+ else:
|
|
|
+ f_out.write(line)
|
|
|
+ cur_level.append((command, rest, lineno))
|
|
|
+ else:
|
|
|
+
|
|
|
+ assert command == 'endif'
|
|
|
+ if len(stack) == 0:
|
|
|
+ raise Problem("Unmatched #%s on %s"% (command,lineno))
|
|
|
+ if lineno <= cur_level[0][2] + LINE_OBVIOUSNESS_LIMIT:
|
|
|
+ f_out.write(line)
|
|
|
+ elif len(cur_level) == 1 or (
|
|
|
+ len(cur_level) == 2 and cur_level[1][0] == 'else'):
|
|
|
+ f_out.write(commented_line("#endif /* %s */\n",
|
|
|
+ cur_level[0][1]))
|
|
|
+ else:
|
|
|
+ f_out.write(commented_line("#endif /* %s || ... */\n",
|
|
|
+ cur_level[0][1]))
|
|
|
+ cur_level = stack.pop()
|
|
|
+ if len(stack) or cur_level != whole_file:
|
|
|
+ raise Problem("Missing #endif")
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+
|
|
|
+ import sys,os
|
|
|
+
|
|
|
+ if sys.argv[1] == "--self-test":
|
|
|
+ import doctest
|
|
|
+ doctest.testmod()
|
|
|
+ sys.exit(0)
|
|
|
+
|
|
|
+ for fn in sys.argv[1:]:
|
|
|
+ with open(fn+"_OUT", 'w') as output_file:
|
|
|
+ translate(open(fn, 'r'), output_file)
|
|
|
+ os.rename(fn+"_OUT", fn)
|