PageRenderTime 236ms CodeModel.GetById 29ms RepoModel.GetById 3ms app.codeStats 0ms

/js/src/config/Preprocessor.py

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