1 #!/usr/bin/env @PYTHON@
3 # If the code below looks horrible and unpythonic, do not panic.
7 # This is a manual conversion from the original Perl script to
8 # Python. Improvements are welcome.
10 from __future__ import print_function, unicode_literals
22 VERSION_STR = '''glib-mkenums version @VERSION@
23 glib-genmarshal comes with ABSOLUTELY NO WARRANTY.
24 You may redistribute copies of glib-genmarshal under the terms of
25 the GNU General Public License which can be found in the
26 GLib source package. Sources, examples and contact
27 information are available at http://www.gtk.org'''
29 # Python 2 defaults to ASCII in case stdout is redirected.
30 # This should make it match Python 3, which uses the locale encoding.
31 if sys.stdout.encoding is None:
32 output_stream = codecs.getwriter(
33 locale.getpreferredencoding())(sys.stdout)
35 output_stream = sys.stdout
37 # pylint: disable=too-few-public-methods
39 '''ANSI Terminal colors'''
47 def print_color(msg, color=Color.END, prefix='MESSAGE'):
48 '''Print a string with a color prefix'''
49 if os.isatty(sys.stderr.fileno()):
50 real_prefix = '{start}{prefix}{end}'.format(start=color, prefix=prefix, end=Color.END)
53 print('{prefix}: {msg}'.format(prefix=real_prefix, msg=msg), file=sys.stderr)
57 '''Print an error, and terminate'''
58 print_color(msg, color=Color.RED, prefix='ERROR')
62 def print_warning(msg, fatal=False):
63 '''Print a warning, and optionally terminate'''
70 print_color(msg, color, prefix)
77 print_color(msg, color=Color.GREEN, prefix='INFO')
80 def write_output(output):
82 print(output, file=output_stream)
85 # Information about the current enumeration
86 flags = None # Is enumeration a bitmask?
87 option_underscore_name = '' # Overriden underscore variant of the enum name
88 # for example to fix the cases we don't get the
89 # mixed-case -> underscorized transform right.
90 option_lowercase_name = '' # DEPRECATED. A lower case name to use as part
91 # of the *_get_type() function, instead of the
92 # one that we guess. For instance, when an enum
93 # uses abnormal capitalization and we can not
94 # guess where to put the underscores.
95 seenbitshift = 0 # Have we seen bitshift operators?
96 enum_prefix = None # Prefix for this enumeration
97 enumname = '' # Name for this enumeration
98 enumshort = '' # $enumname without prefix
99 enumname_prefix = '' # prefix of $enumname
100 enumindex = 0 # Global enum counter
101 firstenum = 1 # Is this the first enumeration per file?
102 entries = [] # [ name, val ] for each entry
103 sandbox = None # sandbox for safe evaluation of expressions
105 output = '' # Filename to write result into
107 def parse_trigraph(opts):
110 for opt in re.split(r'\s*,\s*', opts):
111 opt = re.sub(r'^\s*', '', opt)
112 opt = re.sub(r'\s*$', '', opt)
113 m = re.search(r'(\w+)(?:=(.+))?', opt)
124 def parse_entries(file, file_name):
125 global entries, enumindex, enumname, seenbitshift, flags
126 looking_for_name = False
129 line = file.readline()
135 # read lines until we have no open comments
136 while re.search(r'/\*([^*]|\*(?!/))*$', line):
137 line += file.readline()
139 # strip comments w/o options
140 line = re.sub(r'''/\*(?!<)
142 \*/''', '', line, flags=re.X)
147 if len(line.strip()) == 0:
151 m = re.match(r'\s*(\w+)', line)
153 enumname = m.group(1)
156 # Handle include files
157 m = re.match(r'\#include\s*<([^>]*)>', line)
159 newfilename = os.path.join("..", m.group(1))
160 newfile = io.open(newfilename, encoding="utf-8")
162 if not parse_entries(newfile, newfilename):
167 m = re.match(r'\s*\}\s*(\w+)', line)
169 enumname = m.group(1)
173 m = re.match(r'\s*\}', line)
176 looking_for_name = True
182 \s*\w+\s*\(.*\)\s* # macro with multiple args
184 (?:[^,/]|/(?!\*))* # anything but a comma or comment
189 \s*$''', line, flags=re.X)
199 if flags is None and value is not None and '<<' in value:
202 if options is not None:
203 options = parse_trigraph(options)
204 if 'skip' not in options:
205 entries.append((name, value, options['nick']))
207 entries.append((name, value))
208 elif re.match(r's*\#', line):
211 print_warning('Failed to parse "{}" in {}'.format(line, file_name))
214 help_epilog = '''Production text substitutions:
215 \u0040EnumName\u0040 PrefixTheXEnum
216 \u0040enum_name\u0040 prefix_the_xenum
217 \u0040ENUMNAME\u0040 PREFIX_THE_XENUM
218 \u0040ENUMSHORT\u0040 THE_XENUM
219 \u0040ENUMPREFIX\u0040 PREFIX
220 \u0040VALUENAME\u0040 PREFIX_THE_XVALUE
221 \u0040valuenick\u0040 the-xvalue
222 \u0040valuenum\u0040 the integer value (limited support, Since: 2.26)
223 \u0040type\u0040 either enum or flags
224 \u0040Type\u0040 either Enum or Flags
225 \u0040TYPE\u0040 either ENUM or FLAGS
226 \u0040filename\u0040 name of current input file
227 \u0040basename\u0040 base name of the current input file (Since: 2.22)
231 # production variables:
232 idprefix = "" # "G", "Gtk", etc
233 symprefix = "" # "g", "gtk", etc, if not just lc($idprefix)
234 fhead = "" # output file header
235 fprod = "" # per input file production
236 ftail = "" # output file trailer
237 eprod = "" # per enum text (produced prior to value itarations)
238 vhead = "" # value header, produced before iterating over enum values
239 vprod = "" # value text, produced for each enum value
240 vtail = "" # value tail, produced after iterating over enum values
241 comment_tmpl = "" # comment template
243 def read_template_file(file):
244 global idprefix, symprefix, fhead, fprod, ftail, eprod, vhead, vprod, vtail, comment_tmpl
245 tmpl = {'file-header': fhead,
246 'file-production': fprod,
248 'enumeration-production': eprod,
249 'value-header': vhead,
250 'value-production': vprod,
252 'comment': comment_tmpl,
256 ifile = io.open(file, encoding="utf-8")
258 m = re.match(r'\/\*\*\*\s+(BEGIN|END)\s+([\w-]+)\s+\*\*\*\/', line)
260 if in_ == 'junk' and m.group(1) == 'BEGIN' and m.group(2) in tmpl:
263 elif in_ == m.group(2) and m.group(1) == 'END' and m.group(2) in tmpl:
267 sys.exit("Malformed template file " + file)
273 sys.exit("Malformed template file " + file)
275 fhead = tmpl['file-header']
276 fprod = tmpl['file-production']
277 ftail = tmpl['file-tail']
278 eprod = tmpl['enumeration-production']
279 vhead = tmpl['value-header']
280 vprod = tmpl['value-production']
281 vtail = tmpl['value-tail']
282 comment_tmpl = tmpl['comment']
284 # default to C-style comments
285 if comment_tmpl == "":
286 comment_tmpl = "/* \u0040comment\u0040 */"
288 parser = argparse.ArgumentParser(epilog=help_epilog,
289 formatter_class=argparse.RawDescriptionHelpFormatter)
291 parser.add_argument('--identifier-prefix', default='', dest='idprefix',
292 help='Identifier prefix')
293 parser.add_argument('--symbol-prefix', default='', dest='symprefix',
294 help='symbol-prefix')
295 parser.add_argument('--fhead', default=[], dest='fhead', action='append',
296 help='Output file header')
297 parser.add_argument('--ftail', default=[], dest='ftail', action='append',
298 help='Per input file production')
299 parser.add_argument('--fprod', default=[], dest='fprod', action='append',
300 help='Put out TEXT everytime a new input file is being processed.')
301 parser.add_argument('--eprod', default=[], dest='eprod', action='append',
302 help='Per enum text (produced prior to value iterations)')
303 parser.add_argument('--vhead', default=[], dest='vhead', action='append',
304 help='Value header, produced before iterating over enum values')
305 parser.add_argument('--vprod', default=[], dest='vprod', action='append',
306 help='Value text, produced for each enum value.')
307 parser.add_argument('--vtail', default=[], dest='vtail', action='append',
308 help='Value tail, produced after iterating over enum values')
309 parser.add_argument('--comments', default='', dest='comment_tmpl',
310 help='Comment structure')
311 parser.add_argument('--template', default='', dest='template',
312 help='Template file')
313 parser.add_argument('--output', default=None, dest='output')
314 parser.add_argument('--version', '-v', default=False, action='store_true', dest='version',
315 help='Print version informations')
316 parser.add_argument('args', nargs='*')
318 options = parser.parse_args()
324 def unescape_cmdline_args(arg):
325 arg = arg.replace('\\n', '\n')
326 arg = arg.replace('\\r', '\r')
327 return arg.replace('\\t', '\t')
329 if options.template != '':
330 read_template_file(options.template)
332 idprefix += options.idprefix
333 symprefix += options.symprefix
335 # This is a hack to maintain some semblance of backward compatibility with
336 # the old, Perl-based glib-mkenums. The old tool had an implicit ordering
337 # on the arguments and templates; each argument was parsed in order, and
338 # all the strings appended. This allowed developers to write:
342 # --template a-template-file.c.in \
345 # And have the fhead be prepended to the file-head stanza in the template,
346 # as well as the ftail be appended to the file-tail stanza in the template.
347 # Short of throwing away ArgumentParser and going over sys.argv[] element
348 # by element, we can simulate that behaviour by ensuring some ordering in
349 # how we build the template strings:
351 # - the head stanzas are always prepended to the template
352 # - the prod stanzas are always appended to the template
353 # - the tail stanzas are always appended to the template
355 # Within each instance of the command line argument, we append each value
356 # to the array in the order in which it appears on the command line.
357 fhead = ''.join([unescape_cmdline_args(x) for x in options.fhead]) + fhead
358 vhead = ''.join([unescape_cmdline_args(x) for x in options.vhead]) + vhead
360 fprod += ''.join([unescape_cmdline_args(x) for x in options.fprod])
361 eprod += ''.join([unescape_cmdline_args(x) for x in options.eprod])
362 vprod += ''.join([unescape_cmdline_args(x) for x in options.vprod])
364 ftail = ftail + ''.join([unescape_cmdline_args(x) for x in options.ftail])
365 vtail = vtail + ''.join([unescape_cmdline_args(x) for x in options.vtail])
367 if options.comment_tmpl != '':
368 comment_tmpl = unescape_cmdline_args(options.comment_tmpl)
370 output = options.output
372 if output is not None:
373 (out_dir, out_fn) = os.path.split(options.output)
374 out_suffix = '_' + os.path.splitext(out_fn)[1]
377 fd, filename = tempfile.mkstemp(dir=out_dir)
379 tmpfile = io.open(filename, "w", encoding="utf-8")
380 output_stream = tmpfile
384 # put auto-generation comment
385 comment = comment_tmpl.replace('\u0040comment\u0040', 'Generated data (by glib-mkenums)')
386 write_output("\n" + comment + '\n')
388 def replace_specials(prod):
389 prod = prod.replace(r'\\a', r'\a')
390 prod = prod.replace(r'\\b', r'\b')
391 prod = prod.replace(r'\\t', r'\t')
392 prod = prod.replace(r'\\n', r'\n')
393 prod = prod.replace(r'\\f', r'\f')
394 prod = prod.replace(r'\\r', r'\r')
400 base = os.path.basename(options.args[0])
402 prod = prod.replace('\u0040filename\u0040', options.args[0])
403 prod = prod.replace('\u0040basename\u0040', base)
404 prod = replace_specials(prod)
407 def process_file(curfilename):
408 global entries, flags, seenbitshift, enum_prefix
412 curfile = io.open(curfilename, encoding="utf-8")
414 if e.errno == errno.ENOENT:
415 print_warning('No file "{}" found.'.format(curfilename))
420 line = curfile.readline()
426 # read lines until we have no open comments
427 while re.search(r'/\*([^*]|\*(?!/))*$', line):
428 line += curfile.readline()
430 # strip comments w/o options
431 line = re.sub(r'''/\*(?!<)
435 # ignore forward declarations
436 if re.match(r'\s*typedef\s+enum.*;', line):
439 m = re.match(r'''\s*typedef\s+enum\s*
444 \s*({)?''', line, flags=re.X)
447 if len(groups) >= 2 and groups[1] is not None:
448 options = parse_trigraph(groups[1])
449 if 'skip' in options:
451 enum_prefix = options.get('prefix', None)
452 flags = options.get('flags', None)
453 if 'flags' in options:
458 option_lowercase_name = options.get('lowercase_name', None)
459 option_underscore_name = options.get('underscore_name', None)
463 option_lowercase_name = None
464 option_underscore_name = None
466 if option_lowercase_name is not None:
467 if option_underscore_name is not None:
468 print_warning("lowercase_name overriden with underscore_name")
469 option_lowercase_name = None
471 print_warning("lowercase_name is deprecated, use underscore_name")
473 # Didn't have trailing '{' look on next lines
474 if groups[0] is None and (len(groups) < 4 or groups[3] is None):
476 line = curfile.readline()
477 if re.match(r'\s*\{', line):
483 # Now parse the entries
484 parse_entries(curfile, curfilename)
486 # figure out if this was a flags or enums enumeration
490 # Autogenerate a prefix
491 if enum_prefix is None:
492 for entry in entries:
493 if len(entry) < 3 or entry[2] is None:
495 if enum_prefix is not None:
496 enum_prefix = os.path.commonprefix([name, enum_prefix])
499 if enum_prefix is None:
502 # Trim so that it ends in an underscore
503 enum_prefix = re.sub(r'_[^_]*$', '_', enum_prefix)
505 # canonicalize user defined prefixes
506 enum_prefix = enum_prefix.upper()
507 enum_prefix = enum_prefix.replace('-', '_')
508 enum_prefix = re.sub(r'(.*)([^_])$', r'\1\2_', enum_prefix)
514 if len(e) < 3 or e[2] is None:
515 nick = re.sub(r'^' + enum_prefix, '', name)
516 nick = nick.replace('_', '-').lower()
517 e = (name, num, nick)
518 fixed_entries.append(e)
519 entries = fixed_entries
521 # Spit out the output
522 if option_underscore_name is not None:
523 enumlong = option_underscore_name.upper()
524 enumsym = option_underscore_name.lower()
525 enumshort = re.sub(r'^[A-Z][A-Z0-9]*_', '', enumlong)
527 enumname_prefix = re.sub('_' + enumshort + '$', '', enumlong)
528 elif symprefix == '' and idprefix == '':
529 # enumname is e.g. GMatchType
530 enspace = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname)
532 enumshort = re.sub(r'^[A-Z][a-z]*', '', enumname)
533 enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
534 enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
535 enumshort = enumshort.upper()
537 enumname_prefix = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname).upper()
539 enumlong = enspace.upper() + "_" + enumshort
540 enumsym = enspace.lower() + "_" + enumshort.lower()
542 if option_lowercase_name is not None:
543 enumsym = option_lowercase_name
547 enumshort = re.sub(r'^' + idprefix, '', enumshort)
549 enumshort = re.sub(r'/^[A-Z][a-z]*', '', enumshort)
551 enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
552 enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
553 enumshort = enumshort.upper()
556 enumname_prefix = symprefix.upper()
558 enumname_prefix = idprefix.upper()
560 enumlong = enumname_prefix + "_" + enumshort
561 enumsym = enumlong.lower()
568 base = os.path.basename(curfilename)
570 prod = prod.replace('\u0040filename\u0040', curfilename)
571 prod = prod.replace('\u0040basename\u0040', base)
572 prod = replace_specials(prod)
579 prod = prod.replace('\u0040enum_name\u0040', enumsym)
580 prod = prod.replace('\u0040EnumName\u0040', enumname)
581 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
582 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
583 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
585 prod = prod.replace('\u0040type\u0040', 'flags')
587 prod = prod.replace('\u0040type\u0040', 'enum')
589 prod = prod.replace('\u0040Type\u0040', 'Flags')
591 prod = prod.replace('\u0040Type\u0040', 'Enum')
593 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
595 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
596 prod = replace_specials(prod)
601 prod = prod.replace('\u0040enum_name\u0040', enumsym)
602 prod = prod.replace('\u0040EnumName\u0040', enumname)
603 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
604 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
605 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
607 prod = prod.replace('\u0040type\u0040', 'flags')
609 prod = prod.replace('\u0040type\u0040', 'enum')
611 prod = prod.replace('\u0040Type\u0040', 'Flags')
613 prod = prod.replace('\u0040Type\u0040', 'Enum')
615 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
617 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
618 prod = replace_specials(prod)
625 prod = replace_specials(prod)
626 for name, num, nick in entries:
629 if '\u0040valuenum\u0040' in prod:
630 # only attempt to eval the value if it is requested
631 # this prevents us from throwing errors otherwise
633 # use sandboxed evaluation as a reasonable
634 # approximation to C constant folding
635 inum = eval(num, {}, {})
637 # make sure it parsed to an integer
638 if not isinstance(inum, int):
639 sys.exit("Unable to parse enum value '%s'" % num)
644 tmp_prod = tmp_prod.replace('\u0040valuenum\u0040', str(num))
645 next_num = int(num) + 1
647 tmp_prod = tmp_prod.replace('\u0040VALUENAME\u0040', name)
648 tmp_prod = tmp_prod.replace('\u0040valuenick\u0040', nick)
650 tmp_prod = tmp_prod.replace('\u0040type\u0040', 'flags')
652 tmp_prod = tmp_prod.replace('\u0040type\u0040', 'enum')
654 tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Flags')
656 tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Enum')
658 tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'FLAGS')
660 tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'ENUM')
661 tmp_prod = tmp_prod.rstrip()
663 write_output(tmp_prod)
667 prod = prod.replace('\u0040enum_name\u0040', enumsym)
668 prod = prod.replace('\u0040EnumName\u0040', enumname)
669 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
670 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
671 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
673 prod = prod.replace('\u0040type\u0040', 'flags')
675 prod = prod.replace('\u0040type\u0040', 'enum')
677 prod = prod.replace('\u0040Type\u0040', 'Flags')
679 prod = prod.replace('\u0040Type\u0040', 'Enum')
681 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
683 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
684 prod = replace_specials(prod)
687 for fname in options.args:
692 base = os.path.basename(options.args[-1]) # FIXME, wrong
694 prod = prod.replace('\u0040filename\u0040', 'ARGV') # wrong too
695 prod = prod.replace('\u0040basename\u0040', base)
696 prod = replace_specials(prod)
699 # put auto-generation comment
700 comment = comment_tmpl
701 comment = comment.replace('\u0040comment\u0040', 'Generated data ends here')
702 write_output("\n" + comment + "\n")
704 if tmpfile is not None:
705 tmpfilename = tmpfile.name
707 os.unlink(options.output)
708 os.rename(tmpfilename, options.output)