PageRenderTime 118ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/js/src/config/Preprocessor.py

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