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-mkenums comes with ABSOLUTELY NO WARRANTY.
24 You may redistribute copies of glib-mkenums 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 # pylint: disable=too-few-public-methods
31 '''ANSI Terminal colors'''
39 def print_color(msg, color=Color.END, prefix='MESSAGE'):
40 '''Print a string with a color prefix'''
41 if os.isatty(sys.stderr.fileno()):
42 real_prefix = '{start}{prefix}{end}'.format(start=color, prefix=prefix, end=Color.END)
45 print('{prefix}: {msg}'.format(prefix=real_prefix, msg=msg), file=sys.stderr)
49 '''Print an error, and terminate'''
50 print_color(msg, color=Color.RED, prefix='ERROR')
54 def print_warning(msg, fatal=False):
55 '''Print a warning, and optionally terminate'''
62 print_color(msg, color, prefix)
69 print_color(msg, color=Color.GREEN, prefix='INFO')
72 def write_output(output):
74 print(output, file=output_stream)
77 # Python 2 defaults to ASCII in case stdout is redirected.
78 # This should make it match Python 3, which uses the locale encoding.
79 if sys.stdout.encoding is None:
80 output_stream = codecs.getwriter(
81 locale.getpreferredencoding())(sys.stdout)
83 output_stream = sys.stdout
86 # Some source files aren't UTF-8 and the old perl version didn't care.
87 # Replace invalid data with a replacement character to keep things working.
88 # https://bugzilla.gnome.org/show_bug.cgi?id=785113#c20
89 def replace_and_warn(err):
90 # 7 characters of context either side of the offending character
91 print_warning('UnicodeWarning: {} at {} ({})'.format(
92 err.reason, err.start,
93 err.object[err.start - 7:err.end + 7]))
96 codecs.register_error('replace_and_warn', replace_and_warn)
100 # Information about the current enumeration
101 flags = None # Is enumeration a bitmask?
102 option_underscore_name = '' # Overridden underscore variant of the enum name
103 # for example to fix the cases we don't get the
104 # mixed-case -> underscorized transform right.
105 option_lowercase_name = '' # DEPRECATED. A lower case name to use as part
106 # of the *_get_type() function, instead of the
107 # one that we guess. For instance, when an enum
108 # uses abnormal capitalization and we can not
109 # guess where to put the underscores.
110 seenbitshift = 0 # Have we seen bitshift operators?
111 enum_prefix = None # Prefix for this enumeration
112 enumname = '' # Name for this enumeration
113 enumshort = '' # $enumname without prefix
114 enumname_prefix = '' # prefix of $enumname
115 enumindex = 0 # Global enum counter
116 firstenum = 1 # Is this the first enumeration per file?
117 entries = [] # [ name, val ] for each entry
118 sandbox = None # sandbox for safe evaluation of expressions
120 output = '' # Filename to write result into
122 def parse_trigraph(opts):
125 for opt in re.split(r'\s*,\s*', opts):
126 opt = re.sub(r'^\s*', '', opt)
127 opt = re.sub(r'\s*$', '', opt)
128 m = re.search(r'(\w+)(?:=(.+))?', opt)
139 def parse_entries(file, file_name):
140 global entries, enumindex, enumname, seenbitshift, flags
141 looking_for_name = False
144 line = file.readline()
150 # read lines until we have no open comments
151 while re.search(r'/\*([^*]|\*(?!/))*$', line):
152 line += file.readline()
154 # strip comments w/o options
155 line = re.sub(r'''/\*(?!<)
157 \*/''', '', line, flags=re.X)
162 if len(line.strip()) == 0:
166 m = re.match(r'\s*(\w+)', line)
168 enumname = m.group(1)
171 # Handle include files
172 m = re.match(r'\#include\s*<([^>]*)>', line)
174 newfilename = os.path.join("..", m.group(1))
175 newfile = io.open(newfilename, encoding="utf-8",
176 errors="replace_and_warn")
178 if not parse_entries(newfile, newfilename):
183 m = re.match(r'\s*\}\s*(\w+)', line)
185 enumname = m.group(1)
189 m = re.match(r'\s*\}', line)
192 looking_for_name = True
198 \s*\w+\s*\(.*\)\s* # macro with multiple args
200 (?:[^,/]|/(?!\*))* # anything but a comma or comment
205 \s*$''', line, flags=re.X)
215 if flags is None and value is not None and '<<' in value:
218 if options is not None:
219 options = parse_trigraph(options)
220 if 'skip' not in options:
221 entries.append((name, value, options.get('nick')))
223 entries.append((name, value))
224 elif re.match(r's*\#', line):
227 print_warning('Failed to parse "{}" in {}'.format(line, file_name))
230 help_epilog = '''Production text substitutions:
231 \u0040EnumName\u0040 PrefixTheXEnum
232 \u0040enum_name\u0040 prefix_the_xenum
233 \u0040ENUMNAME\u0040 PREFIX_THE_XENUM
234 \u0040ENUMSHORT\u0040 THE_XENUM
235 \u0040ENUMPREFIX\u0040 PREFIX
236 \u0040VALUENAME\u0040 PREFIX_THE_XVALUE
237 \u0040valuenick\u0040 the-xvalue
238 \u0040valuenum\u0040 the integer value (limited support, Since: 2.26)
239 \u0040type\u0040 either enum or flags
240 \u0040Type\u0040 either Enum or Flags
241 \u0040TYPE\u0040 either ENUM or FLAGS
242 \u0040filename\u0040 name of current input file
243 \u0040basename\u0040 base name of the current input file (Since: 2.22)
247 # production variables:
248 idprefix = "" # "G", "Gtk", etc
249 symprefix = "" # "g", "gtk", etc, if not just lc($idprefix)
250 fhead = "" # output file header
251 fprod = "" # per input file production
252 ftail = "" # output file trailer
253 eprod = "" # per enum text (produced prior to value itarations)
254 vhead = "" # value header, produced before iterating over enum values
255 vprod = "" # value text, produced for each enum value
256 vtail = "" # value tail, produced after iterating over enum values
257 comment_tmpl = "" # comment template
259 def read_template_file(file):
260 global idprefix, symprefix, fhead, fprod, ftail, eprod, vhead, vprod, vtail, comment_tmpl
261 tmpl = {'file-header': fhead,
262 'file-production': fprod,
264 'enumeration-production': eprod,
265 'value-header': vhead,
266 'value-production': vprod,
268 'comment': comment_tmpl,
272 ifile = io.open(file, encoding="utf-8", errors="replace_and_warn")
274 m = re.match(r'\/\*\*\*\s+(BEGIN|END)\s+([\w-]+)\s+\*\*\*\/', line)
276 if in_ == 'junk' and m.group(1) == 'BEGIN' and m.group(2) in tmpl:
279 elif in_ == m.group(2) and m.group(1) == 'END' and m.group(2) in tmpl:
283 sys.exit("Malformed template file " + file)
289 sys.exit("Malformed template file " + file)
291 fhead = tmpl['file-header']
292 fprod = tmpl['file-production']
293 ftail = tmpl['file-tail']
294 eprod = tmpl['enumeration-production']
295 vhead = tmpl['value-header']
296 vprod = tmpl['value-production']
297 vtail = tmpl['value-tail']
298 comment_tmpl = tmpl['comment']
300 parser = argparse.ArgumentParser(epilog=help_epilog,
301 formatter_class=argparse.RawDescriptionHelpFormatter)
303 parser.add_argument('--identifier-prefix', default='', dest='idprefix',
304 help='Identifier prefix')
305 parser.add_argument('--symbol-prefix', default='', dest='symprefix',
306 help='Symbol prefix')
307 parser.add_argument('--fhead', default=[], dest='fhead', action='append',
308 help='Output file header')
309 parser.add_argument('--ftail', default=[], dest='ftail', action='append',
310 help='Output file footer')
311 parser.add_argument('--fprod', default=[], dest='fprod', action='append',
312 help='Put out TEXT every time a new input file is being processed.')
313 parser.add_argument('--eprod', default=[], dest='eprod', action='append',
314 help='Per enum text, produced prior to value iterations')
315 parser.add_argument('--vhead', default=[], dest='vhead', action='append',
316 help='Value header, produced before iterating over enum values')
317 parser.add_argument('--vprod', default=[], dest='vprod', action='append',
318 help='Value text, produced for each enum value.')
319 parser.add_argument('--vtail', default=[], dest='vtail', action='append',
320 help='Value tail, produced after iterating over enum values')
321 parser.add_argument('--comments', default='', dest='comment_tmpl',
322 help='Comment structure')
323 parser.add_argument('--template', default='', dest='template',
324 help='Template file')
325 parser.add_argument('--output', default=None, dest='output')
326 parser.add_argument('--version', '-v', default=False, action='store_true', dest='version',
327 help='Print version information')
328 parser.add_argument('args', nargs='*',
331 options = parser.parse_args()
337 def unescape_cmdline_args(arg):
338 arg = arg.replace('\\n', '\n')
339 arg = arg.replace('\\r', '\r')
340 return arg.replace('\\t', '\t')
342 if options.template != '':
343 read_template_file(options.template)
345 idprefix += options.idprefix
346 symprefix += options.symprefix
348 # This is a hack to maintain some semblance of backward compatibility with
349 # the old, Perl-based glib-mkenums. The old tool had an implicit ordering
350 # on the arguments and templates; each argument was parsed in order, and
351 # all the strings appended. This allowed developers to write:
355 # --template a-template-file.c.in \
358 # And have the fhead be prepended to the file-head stanza in the template,
359 # as well as the ftail be appended to the file-tail stanza in the template.
360 # Short of throwing away ArgumentParser and going over sys.argv[] element
361 # by element, we can simulate that behaviour by ensuring some ordering in
362 # how we build the template strings:
364 # - the head stanzas are always prepended to the template
365 # - the prod stanzas are always appended to the template
366 # - the tail stanzas are always appended to the template
368 # Within each instance of the command line argument, we append each value
369 # to the array in the order in which it appears on the command line.
370 fhead = ''.join([unescape_cmdline_args(x) for x in options.fhead]) + fhead
371 vhead = ''.join([unescape_cmdline_args(x) for x in options.vhead]) + vhead
373 fprod += ''.join([unescape_cmdline_args(x) for x in options.fprod])
374 eprod += ''.join([unescape_cmdline_args(x) for x in options.eprod])
375 vprod += ''.join([unescape_cmdline_args(x) for x in options.vprod])
377 ftail = ftail + ''.join([unescape_cmdline_args(x) for x in options.ftail])
378 vtail = vtail + ''.join([unescape_cmdline_args(x) for x in options.vtail])
380 if options.comment_tmpl != '':
381 comment_tmpl = unescape_cmdline_args(options.comment_tmpl)
382 elif comment_tmpl == "":
383 # default to C-style comments
384 comment_tmpl = "/* \u0040comment\u0040 */"
386 output = options.output
388 if output is not None:
389 (out_dir, out_fn) = os.path.split(options.output)
390 out_suffix = '_' + os.path.splitext(out_fn)[1]
393 fd, filename = tempfile.mkstemp(dir=out_dir)
395 tmpfile = io.open(filename, "w", encoding="utf-8")
396 output_stream = tmpfile
400 # put auto-generation comment
401 comment = comment_tmpl.replace('\u0040comment\u0040',
402 'This file is generated by glib-mkenums, do '
403 'not modify it. This code is licensed under '
404 'the same license as the containing project. '
405 'Note that it links to GLib, so must comply '
406 'with the LGPL linking clauses.')
407 write_output("\n" + comment + '\n')
409 def replace_specials(prod):
410 prod = prod.replace(r'\\a', r'\a')
411 prod = prod.replace(r'\\b', r'\b')
412 prod = prod.replace(r'\\t', r'\t')
413 prod = prod.replace(r'\\n', r'\n')
414 prod = prod.replace(r'\\f', r'\f')
415 prod = prod.replace(r'\\r', r'\r')
420 def warn_if_filename_basename_used(section, prod):
421 for substitution in ('\u0040filename\u0040',
422 '\u0040basename\u0040'):
423 if substitution in prod:
424 print_warning('{} used in {} section.'.format(substitution,
429 warn_if_filename_basename_used('file-header', prod)
430 prod = replace_specials(prod)
433 def process_file(curfilename):
434 global entries, flags, seenbitshift, enum_prefix
438 curfile = io.open(curfilename, encoding="utf-8",
439 errors="replace_and_warn")
441 if e.errno == errno.ENOENT:
442 print_warning('No file "{}" found.'.format(curfilename))
447 line = curfile.readline()
453 # read lines until we have no open comments
454 while re.search(r'/\*([^*]|\*(?!/))*$', line):
455 line += curfile.readline()
457 # strip comments w/o options
458 line = re.sub(r'''/\*(?!<)
462 # ignore forward declarations
463 if re.match(r'\s*typedef\s+enum.*;', line):
466 m = re.match(r'''\s*typedef\s+enum\s*[_A-Za-z]*[_A-Za-z0-9]*\s*
471 \s*({)?''', line, flags=re.X)
474 if len(groups) >= 2 and groups[1] is not None:
475 options = parse_trigraph(groups[1])
476 if 'skip' in options:
478 enum_prefix = options.get('prefix', None)
479 flags = options.get('flags', None)
480 if 'flags' in options:
485 option_lowercase_name = options.get('lowercase_name', None)
486 option_underscore_name = options.get('underscore_name', None)
490 option_lowercase_name = None
491 option_underscore_name = None
493 if option_lowercase_name is not None:
494 if option_underscore_name is not None:
495 print_warning("lowercase_name overridden with underscore_name")
496 option_lowercase_name = None
498 print_warning("lowercase_name is deprecated, use underscore_name")
500 # Didn't have trailing '{' look on next lines
501 if groups[0] is None and (len(groups) < 4 or groups[3] is None):
503 line = curfile.readline()
505 print_error("Syntax error when looking for opening { in enum")
506 if re.match(r'\s*\{', line):
512 # Now parse the entries
513 parse_entries(curfile, curfilename)
515 # figure out if this was a flags or enums enumeration
519 # Autogenerate a prefix
520 if enum_prefix is None:
521 for entry in entries:
522 if len(entry) < 3 or entry[2] is None:
524 if enum_prefix is not None:
525 enum_prefix = os.path.commonprefix([name, enum_prefix])
528 if enum_prefix is None:
531 # Trim so that it ends in an underscore
532 enum_prefix = re.sub(r'_[^_]*$', '_', enum_prefix)
534 # canonicalize user defined prefixes
535 enum_prefix = enum_prefix.upper()
536 enum_prefix = enum_prefix.replace('-', '_')
537 enum_prefix = re.sub(r'(.*)([^_])$', r'\1\2_', enum_prefix)
543 if len(e) < 3 or e[2] is None:
544 nick = re.sub(r'^' + enum_prefix, '', name)
545 nick = nick.replace('_', '-').lower()
546 e = (name, num, nick)
547 fixed_entries.append(e)
548 entries = fixed_entries
550 # Spit out the output
551 if option_underscore_name is not None:
552 enumlong = option_underscore_name.upper()
553 enumsym = option_underscore_name.lower()
554 enumshort = re.sub(r'^[A-Z][A-Z0-9]*_', '', enumlong)
556 enumname_prefix = re.sub('_' + enumshort + '$', '', enumlong)
557 elif symprefix == '' and idprefix == '':
558 # enumname is e.g. GMatchType
559 enspace = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname)
561 enumshort = re.sub(r'^[A-Z][a-z]*', '', enumname)
562 enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
563 enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
564 enumshort = enumshort.upper()
566 enumname_prefix = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname).upper()
568 enumlong = enspace.upper() + "_" + enumshort
569 enumsym = enspace.lower() + "_" + enumshort.lower()
571 if option_lowercase_name is not None:
572 enumsym = option_lowercase_name
576 enumshort = re.sub(r'^' + idprefix, '', enumshort)
578 enumshort = re.sub(r'/^[A-Z][a-z]*', '', enumshort)
580 enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
581 enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
582 enumshort = enumshort.upper()
585 enumname_prefix = symprefix.upper()
587 enumname_prefix = idprefix.upper()
589 enumlong = enumname_prefix + "_" + enumshort
590 enumsym = enumlong.lower()
597 base = os.path.basename(curfilename)
599 prod = prod.replace('\u0040filename\u0040', curfilename)
600 prod = prod.replace('\u0040basename\u0040', base)
601 prod = replace_specials(prod)
608 prod = prod.replace('\u0040enum_name\u0040', enumsym)
609 prod = prod.replace('\u0040EnumName\u0040', enumname)
610 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
611 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
612 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
614 prod = prod.replace('\u0040type\u0040', 'flags')
616 prod = prod.replace('\u0040type\u0040', 'enum')
618 prod = prod.replace('\u0040Type\u0040', 'Flags')
620 prod = prod.replace('\u0040Type\u0040', 'Enum')
622 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
624 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
625 prod = replace_specials(prod)
630 prod = prod.replace('\u0040enum_name\u0040', enumsym)
631 prod = prod.replace('\u0040EnumName\u0040', enumname)
632 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
633 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
634 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
636 prod = prod.replace('\u0040type\u0040', 'flags')
638 prod = prod.replace('\u0040type\u0040', 'enum')
640 prod = prod.replace('\u0040Type\u0040', 'Flags')
642 prod = prod.replace('\u0040Type\u0040', 'Enum')
644 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
646 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
647 prod = replace_specials(prod)
654 prod = replace_specials(prod)
655 for name, num, nick in entries:
658 if '\u0040valuenum\u0040' in prod:
659 # only attempt to eval the value if it is requested
660 # this prevents us from throwing errors otherwise
662 # use sandboxed evaluation as a reasonable
663 # approximation to C constant folding
664 inum = eval(num, {}, {})
666 # make sure it parsed to an integer
667 if not isinstance(inum, int):
668 sys.exit("Unable to parse enum value '%s'" % num)
673 tmp_prod = tmp_prod.replace('\u0040valuenum\u0040', str(num))
674 next_num = int(num) + 1
676 tmp_prod = tmp_prod.replace('\u0040VALUENAME\u0040', name)
677 tmp_prod = tmp_prod.replace('\u0040valuenick\u0040', nick)
679 tmp_prod = tmp_prod.replace('\u0040type\u0040', 'flags')
681 tmp_prod = tmp_prod.replace('\u0040type\u0040', 'enum')
683 tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Flags')
685 tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Enum')
687 tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'FLAGS')
689 tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'ENUM')
690 tmp_prod = tmp_prod.rstrip()
692 write_output(tmp_prod)
696 prod = prod.replace('\u0040enum_name\u0040', enumsym)
697 prod = prod.replace('\u0040EnumName\u0040', enumname)
698 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
699 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
700 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
702 prod = prod.replace('\u0040type\u0040', 'flags')
704 prod = prod.replace('\u0040type\u0040', 'enum')
706 prod = prod.replace('\u0040Type\u0040', 'Flags')
708 prod = prod.replace('\u0040Type\u0040', 'Enum')
710 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
712 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
713 prod = replace_specials(prod)
716 for fname in sorted(options.args):
721 warn_if_filename_basename_used('file-tail', prod)
722 prod = replace_specials(prod)
725 # put auto-generation comment
726 comment = comment_tmpl
727 comment = comment.replace('\u0040comment\u0040', 'Generated data ends here')
728 write_output("\n" + comment + "\n")
730 if tmpfile is not None:
731 tmpfilename = tmpfile.name
735 os.unlink(options.output)
736 except OSError as error:
737 if error.errno != errno.ENOENT:
740 os.rename(tmpfilename, options.output)