PageRenderTime 43ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/config/Preprocessor.py

https://bitbucket.org/bgirard/mozilla-central
Python | 482 lines | 474 code | 0 blank | 8 comment | 0 complexity | 114bd5ba69fca7cc43d34ed824f4e04f MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception, BSD-3-Clause, GPL-2.0, Apache-2.0, MIT, JSON, 0BSD, BSD-2-Clause, LGPL-3.0, AGPL-1.0
  1. """
  2. This is a very primitive line based preprocessor, for times when using
  3. a C preprocessor isn't an option.
  4. """
  5. # ***** BEGIN LICENSE BLOCK *****
  6. # Version: MPL 1.1/GPL 2.0/LGPL 2.1
  7. #
  8. # The contents of this file are subject to the Mozilla Public License Version
  9. # 1.1 (the "License"); you may not use this file except in compliance with
  10. # the License. You may obtain a copy of the License at
  11. # http://www.mozilla.org/MPL/
  12. #
  13. # Software distributed under the License is distributed on an "AS IS" basis,
  14. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  15. # for the specific language governing rights and limitations under the
  16. # License.
  17. #
  18. # The Original Code is Mozilla build system.
  19. #
  20. # The Initial Developer of the Original Code is
  21. # Mozilla Foundation.
  22. # Portions created by the Initial Developer are Copyright (C) 2007
  23. # the Initial Developer. All Rights Reserved.
  24. #
  25. # Contributor(s):
  26. # Axel Hecht <axel@pike.org>
  27. #
  28. # Alternatively, the contents of this file may be used under the terms of
  29. # either the GNU General Public License Version 2 or later (the "GPL"), or
  30. # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  31. # in which case the provisions of the GPL or the LGPL are applicable instead
  32. # of those above. If you wish to allow use of your version of this file only
  33. # under the terms of either the GPL or the LGPL, and not to allow others to
  34. # use your version of this file under the terms of the MPL, indicate your
  35. # decision by deleting the provisions above and replace them with the notice
  36. # and other provisions required by the GPL or the LGPL. If you do not delete
  37. # the provisions above, a recipient may use your version of this file under
  38. # the terms of any one of the MPL, the GPL or the LGPL.
  39. #
  40. # ***** END LICENSE BLOCK *****
  41. import sys
  42. import os
  43. import os.path
  44. import re
  45. from optparse import OptionParser
  46. # hack around win32 mangling our line endings
  47. # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65443
  48. if sys.platform == "win32":
  49. import msvcrt
  50. msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
  51. os.linesep = '\n'
  52. import Expression
  53. __all__ = ['Preprocessor', 'preprocess']
  54. class Preprocessor:
  55. """
  56. Class for preprocessing text files.
  57. """
  58. class Error(RuntimeError):
  59. def __init__(self, cpp, MSG, context):
  60. self.file = cpp.context['FILE']
  61. self.line = cpp.context['LINE']
  62. self.key = MSG
  63. RuntimeError.__init__(self, (self.file, self.line, self.key, context))
  64. def __init__(self):
  65. self.context = Expression.Context()
  66. for k,v in {'FILE': '',
  67. 'LINE': 0,
  68. 'DIRECTORY': os.path.abspath('.')}.iteritems():
  69. self.context[k] = v
  70. self.disableLevel = 0
  71. # ifStates can be
  72. # 0: hadTrue
  73. # 1: wantsTrue
  74. # 2: #else found
  75. self.ifStates = []
  76. self.checkLineNumbers = False
  77. self.writtenLines = 0
  78. self.filters = []
  79. self.cmds = {}
  80. for cmd, level in {'define': 0,
  81. 'undef': 0,
  82. 'if': sys.maxint,
  83. 'ifdef': sys.maxint,
  84. 'ifndef': sys.maxint,
  85. 'else': 1,
  86. 'elif': 1,
  87. 'elifdef': 1,
  88. 'elifndef': 1,
  89. 'endif': sys.maxint,
  90. 'expand': 0,
  91. 'literal': 0,
  92. 'filter': 0,
  93. 'unfilter': 0,
  94. 'include': 0,
  95. 'includesubst': 0,
  96. 'error': 0}.iteritems():
  97. self.cmds[cmd] = (level, getattr(self, 'do_' + cmd))
  98. self.out = sys.stdout
  99. self.setMarker('#')
  100. self.LE = '\n'
  101. self.varsubst = re.compile('@(?P<VAR>\w+)@', re.U)
  102. def setLineEndings(self, aLE):
  103. """
  104. Set the line endings to be used for output.
  105. """
  106. self.LE = {'cr': '\x0D', 'lf': '\x0A', 'crlf': '\x0D\x0A'}[aLE]
  107. def setMarker(self, aMarker):
  108. """
  109. Set the marker to be used for processing directives.
  110. Used for handling CSS files, with pp.setMarker('%'), for example.
  111. """
  112. self.marker = aMarker
  113. self.instruction = re.compile('%s(?P<cmd>[a-z]+)(?:\s(?P<args>.*))?$'%aMarker, re.U)
  114. self.comment = re.compile(aMarker, re.U)
  115. def clone(self):
  116. """
  117. Create a clone of the current processor, including line ending
  118. settings, marker, variable definitions, output stream.
  119. """
  120. rv = Preprocessor()
  121. rv.context.update(self.context)
  122. rv.setMarker(self.marker)
  123. rv.LE = self.LE
  124. rv.out = self.out
  125. return rv
  126. def applyFilters(self, aLine):
  127. for f in self.filters:
  128. aLine = f[1](aLine)
  129. return aLine
  130. def write(self, aLine):
  131. """
  132. Internal method for handling output.
  133. """
  134. if self.checkLineNumbers:
  135. self.writtenLines += 1
  136. ln = self.context['LINE']
  137. if self.writtenLines != ln:
  138. self.out.write('//@line %(line)d "%(file)s"%(le)s'%{'line': ln,
  139. 'file': self.context['FILE'],
  140. 'le': self.LE})
  141. self.writtenLines = ln
  142. aLine = self.applyFilters(aLine)
  143. # ensure our line ending. Only need to handle \n, as we're reading
  144. # with universal line ending support, at least for files.
  145. aLine = re.sub('\n', self.LE, aLine)
  146. self.out.write(aLine)
  147. def handleCommandLine(self, args, defaultToStdin = False):
  148. """
  149. Parse a commandline into this parser.
  150. Uses OptionParser internally, no args mean sys.argv[1:].
  151. """
  152. p = self.getCommandLineParser()
  153. (options, args) = p.parse_args(args=args)
  154. includes = options.I
  155. if defaultToStdin and len(args) == 0:
  156. args = [sys.stdin]
  157. includes.extend(args)
  158. for f in includes:
  159. self.do_include(f)
  160. pass
  161. def getCommandLineParser(self, unescapeDefines = False):
  162. escapedValue = re.compile('".*"$')
  163. numberValue = re.compile('\d+$')
  164. def handleE(option, opt, value, parser):
  165. for k,v in os.environ.iteritems():
  166. self.context[k] = v
  167. def handleD(option, opt, value, parser):
  168. vals = value.split('=', 1)
  169. if len(vals) == 1:
  170. vals.append(1)
  171. elif unescapeDefines and escapedValue.match(vals[1]):
  172. # strip escaped string values
  173. vals[1] = vals[1][1:-1]
  174. elif numberValue.match(vals[1]):
  175. vals[1] = int(vals[1])
  176. self.context[vals[0]] = vals[1]
  177. def handleU(option, opt, value, parser):
  178. del self.context[value]
  179. def handleF(option, opt, value, parser):
  180. self.do_filter(value)
  181. def handleLE(option, opt, value, parser):
  182. self.setLineEndings(value)
  183. def handleMarker(option, opt, value, parser):
  184. self.setMarker(value)
  185. p = OptionParser()
  186. p.add_option('-I', action='append', type="string", default = [],
  187. metavar="FILENAME", help='Include file')
  188. p.add_option('-E', action='callback', callback=handleE,
  189. help='Import the environment into the defined variables')
  190. p.add_option('-D', action='callback', callback=handleD, type="string",
  191. metavar="VAR[=VAL]", help='Define a variable')
  192. p.add_option('-U', action='callback', callback=handleU, type="string",
  193. metavar="VAR", help='Undefine a variable')
  194. p.add_option('-F', action='callback', callback=handleF, type="string",
  195. metavar="FILTER", help='Enable the specified filter')
  196. p.add_option('--line-endings', action='callback', callback=handleLE,
  197. type="string", metavar="[cr|lr|crlf]",
  198. help='Use the specified line endings [Default: OS dependent]')
  199. p.add_option('--marker', action='callback', callback=handleMarker,
  200. type="string",
  201. help='Use the specified marker instead of #')
  202. return p
  203. def handleLine(self, aLine):
  204. """
  205. Handle a single line of input (internal).
  206. """
  207. m = self.instruction.match(aLine)
  208. if m:
  209. args = None
  210. cmd = m.group('cmd')
  211. try:
  212. args = m.group('args')
  213. except IndexError:
  214. pass
  215. if cmd not in self.cmds:
  216. raise Preprocessor.Error(self, 'INVALID_CMD', aLine)
  217. level, cmd = self.cmds[cmd]
  218. if (level >= self.disableLevel):
  219. cmd(args)
  220. elif self.disableLevel == 0 and not self.comment.match(aLine):
  221. self.write(aLine)
  222. pass
  223. # Instruction handlers
  224. # These are named do_'instruction name' and take one argument
  225. # Variables
  226. def do_define(self, args):
  227. m = re.match('(?P<name>\w+)(?:\s(?P<value>.*))?', args, re.U)
  228. if not m:
  229. raise Preprocessor.Error(self, 'SYNTAX_DEF', args)
  230. val = 1
  231. if m.group('value'):
  232. val = self.applyFilters(m.group('value'))
  233. try:
  234. val = int(val)
  235. except:
  236. pass
  237. self.context[m.group('name')] = val
  238. def do_undef(self, args):
  239. m = re.match('(?P<name>\w+)$', args, re.U)
  240. if not m:
  241. raise Preprocessor.Error(self, 'SYNTAX_DEF', args)
  242. if args in self.context:
  243. del self.context[args]
  244. # Logic
  245. def ensure_not_else(self):
  246. if len(self.ifStates) == 0 or self.ifStates[-1] == 2:
  247. sys.stderr.write('WARNING: bad nesting of #else\n')
  248. def do_if(self, args, replace=False):
  249. if self.disableLevel and not replace:
  250. self.disableLevel += 1
  251. return
  252. val = None
  253. try:
  254. e = Expression.Expression(args)
  255. val = e.evaluate(self.context)
  256. except Exception:
  257. # XXX do real error reporting
  258. raise Preprocessor.Error(self, 'SYNTAX_ERR', args)
  259. if type(val) == str:
  260. # we're looking for a number value, strings are false
  261. val = False
  262. if not val:
  263. self.disableLevel = 1
  264. if replace:
  265. if val:
  266. self.disableLevel = 0
  267. self.ifStates[-1] = self.disableLevel
  268. else:
  269. self.ifStates.append(self.disableLevel)
  270. pass
  271. def do_ifdef(self, args, replace=False):
  272. if self.disableLevel and not replace:
  273. self.disableLevel += 1
  274. return
  275. if re.match('\W', args, re.U):
  276. raise Preprocessor.Error(self, 'INVALID_VAR', args)
  277. if args not in self.context:
  278. self.disableLevel = 1
  279. if replace:
  280. if args in self.context:
  281. self.disableLevel = 0
  282. self.ifStates[-1] = self.disableLevel
  283. else:
  284. self.ifStates.append(self.disableLevel)
  285. pass
  286. def do_ifndef(self, args, replace=False):
  287. if self.disableLevel and not replace:
  288. self.disableLevel += 1
  289. return
  290. if re.match('\W', args, re.U):
  291. raise Preprocessor.Error(self, 'INVALID_VAR', args)
  292. if args in self.context:
  293. self.disableLevel = 1
  294. if replace:
  295. if args not in self.context:
  296. self.disableLevel = 0
  297. self.ifStates[-1] = self.disableLevel
  298. else:
  299. self.ifStates.append(self.disableLevel)
  300. pass
  301. def do_else(self, args, ifState = 2):
  302. self.ensure_not_else()
  303. hadTrue = self.ifStates[-1] == 0
  304. self.ifStates[-1] = ifState # in-else
  305. if hadTrue:
  306. self.disableLevel = 1
  307. return
  308. self.disableLevel = 0
  309. def do_elif(self, args):
  310. if self.disableLevel == 1:
  311. if self.ifStates[-1] == 1:
  312. self.do_if(args, replace=True)
  313. else:
  314. self.do_else(None, self.ifStates[-1])
  315. def do_elifdef(self, args):
  316. if self.disableLevel == 1:
  317. if self.ifStates[-1] == 1:
  318. self.do_ifdef(args, replace=True)
  319. else:
  320. self.do_else(None, self.ifStates[-1])
  321. def do_elifndef(self, args):
  322. if self.disableLevel == 1:
  323. if self.ifStates[-1] == 1:
  324. self.do_ifndef(args, replace=True)
  325. else:
  326. self.do_else(None, self.ifStates[-1])
  327. def do_endif(self, args):
  328. if self.disableLevel > 0:
  329. self.disableLevel -= 1
  330. if self.disableLevel == 0:
  331. self.ifStates.pop()
  332. # output processing
  333. def do_expand(self, args):
  334. lst = re.split('__(\w+)__', args, re.U)
  335. do_replace = False
  336. def vsubst(v):
  337. if v in self.context:
  338. return str(self.context[v])
  339. return ''
  340. for i in range(1, len(lst), 2):
  341. lst[i] = vsubst(lst[i])
  342. lst.append('\n') # add back the newline
  343. self.write(reduce(lambda x, y: x+y, lst, ''))
  344. def do_literal(self, args):
  345. self.write(args + self.LE)
  346. def do_filter(self, args):
  347. filters = [f for f in args.split(' ') if hasattr(self, 'filter_' + f)]
  348. if len(filters) == 0:
  349. return
  350. current = dict(self.filters)
  351. for f in filters:
  352. current[f] = getattr(self, 'filter_' + f)
  353. filterNames = current.keys()
  354. filterNames.sort()
  355. self.filters = [(fn, current[fn]) for fn in filterNames]
  356. return
  357. def do_unfilter(self, args):
  358. filters = args.split(' ')
  359. current = dict(self.filters)
  360. for f in filters:
  361. if f in current:
  362. del current[f]
  363. filterNames = current.keys()
  364. filterNames.sort()
  365. self.filters = [(fn, current[fn]) for fn in filterNames]
  366. return
  367. # Filters
  368. #
  369. # emptyLines
  370. # Strips blank lines from the output.
  371. def filter_emptyLines(self, aLine):
  372. if aLine == '\n':
  373. return ''
  374. return aLine
  375. # slashslash
  376. # Strips everything after //
  377. def filter_slashslash(self, aLine):
  378. [aLine, rest] = aLine.split('//', 1)
  379. if rest:
  380. aLine += '\n'
  381. return aLine
  382. # spaces
  383. # Collapses sequences of spaces into a single space
  384. def filter_spaces(self, aLine):
  385. return re.sub(' +', ' ', aLine).strip(' ')
  386. # substition
  387. # helper to be used by both substition and attemptSubstitution
  388. def filter_substitution(self, aLine, fatal=True):
  389. def repl(matchobj):
  390. varname = matchobj.group('VAR')
  391. if varname in self.context:
  392. return str(self.context[varname])
  393. if fatal:
  394. raise Preprocessor.Error(self, 'UNDEFINED_VAR', varname)
  395. return ''
  396. return self.varsubst.sub(repl, aLine)
  397. def filter_attemptSubstitution(self, aLine):
  398. return self.filter_substitution(aLine, fatal=False)
  399. # File ops
  400. def do_include(self, args):
  401. """
  402. Preprocess a given file.
  403. args can either be a file name, or a file-like object.
  404. Files should be opened, and will be closed after processing.
  405. """
  406. isName = type(args) == str or type(args) == unicode
  407. oldWrittenLines = self.writtenLines
  408. oldCheckLineNumbers = self.checkLineNumbers
  409. self.checkLineNumbers = False
  410. if isName:
  411. try:
  412. args = str(args)
  413. args = self.applyFilters(args)
  414. if not os.path.isabs(args):
  415. args = os.path.join(self.context['DIRECTORY'], args)
  416. args = open(args, 'rU')
  417. except:
  418. raise Preprocessor.Error(self, 'FILE_NOT_FOUND', str(args))
  419. self.checkLineNumbers = bool(re.search('\.(js|java)(?:\.in)?$', args.name))
  420. oldFile = self.context['FILE']
  421. oldLine = self.context['LINE']
  422. oldDir = self.context['DIRECTORY']
  423. if args.isatty():
  424. # we're stdin, use '-' and '' for file and dir
  425. self.context['FILE'] = '-'
  426. self.context['DIRECTORY'] = ''
  427. else:
  428. abspath = os.path.abspath(args.name)
  429. self.context['FILE'] = abspath
  430. self.context['DIRECTORY'] = os.path.dirname(abspath)
  431. self.context['LINE'] = 0
  432. self.writtenLines = 0
  433. for l in args:
  434. self.context['LINE'] += 1
  435. self.handleLine(l)
  436. args.close()
  437. self.context['FILE'] = oldFile
  438. self.checkLineNumbers = oldCheckLineNumbers
  439. self.writtenLines = oldWrittenLines
  440. self.context['LINE'] = oldLine
  441. self.context['DIRECTORY'] = oldDir
  442. def do_includesubst(self, args):
  443. args = self.filter_substitution(args)
  444. self.do_include(args)
  445. def do_error(self, args):
  446. raise Preprocessor.Error(self, 'Error: ', str(args))
  447. def main():
  448. pp = Preprocessor()
  449. pp.handleCommandLine(None, True)
  450. return
  451. def preprocess(includes=[sys.stdin], defines={},
  452. output = sys.stdout,
  453. line_endings='\n', marker='#'):
  454. pp = Preprocessor()
  455. pp.context.update(defines)
  456. pp.setLineEndings(line_endings)
  457. pp.setMarker(marker)
  458. pp.out = output
  459. for f in includes:
  460. pp.do_include(f)
  461. if __name__ == "__main__":
  462. main()