PageRenderTime 61ms CodeModel.GetById 32ms RepoModel.GetById 1ms app.codeStats 0ms

/js/src/config/Preprocessor.py

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