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 = '' # Overriden 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['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='Per input file production')
311 parser.add_argument('--fprod', default=[], dest='fprod', action='append',
312 help='Put out TEXT everytime 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 informations')
328 parser.add_argument('args', nargs='*')
330 options = parser.parse_args()
336 def unescape_cmdline_args(arg):
337 arg = arg.replace('\\n', '\n')
338 arg = arg.replace('\\r', '\r')
339 return arg.replace('\\t', '\t')
341 if options.template != '':
342 read_template_file(options.template)
344 idprefix += options.idprefix
345 symprefix += options.symprefix
347 # This is a hack to maintain some semblance of backward compatibility with
348 # the old, Perl-based glib-mkenums. The old tool had an implicit ordering
349 # on the arguments and templates; each argument was parsed in order, and
350 # all the strings appended. This allowed developers to write:
354 # --template a-template-file.c.in \
357 # And have the fhead be prepended to the file-head stanza in the template,
358 # as well as the ftail be appended to the file-tail stanza in the template.
359 # Short of throwing away ArgumentParser and going over sys.argv[] element
360 # by element, we can simulate that behaviour by ensuring some ordering in
361 # how we build the template strings:
363 # - the head stanzas are always prepended to the template
364 # - the prod stanzas are always appended to the template
365 # - the tail stanzas are always appended to the template
367 # Within each instance of the command line argument, we append each value
368 # to the array in the order in which it appears on the command line.
369 fhead = ''.join([unescape_cmdline_args(x) for x in options.fhead]) + fhead
370 vhead = ''.join([unescape_cmdline_args(x) for x in options.vhead]) + vhead
372 fprod += ''.join([unescape_cmdline_args(x) for x in options.fprod])
373 eprod += ''.join([unescape_cmdline_args(x) for x in options.eprod])
374 vprod += ''.join([unescape_cmdline_args(x) for x in options.vprod])
376 ftail = ftail + ''.join([unescape_cmdline_args(x) for x in options.ftail])
377 vtail = vtail + ''.join([unescape_cmdline_args(x) for x in options.vtail])
379 if options.comment_tmpl != '':
380 comment_tmpl = unescape_cmdline_args(options.comment_tmpl)
381 elif comment_tmpl == "":
382 # default to C-style comments
383 comment_tmpl = "/* \u0040comment\u0040 */"
385 output = options.output
387 if output is not None:
388 (out_dir, out_fn) = os.path.split(options.output)
389 out_suffix = '_' + os.path.splitext(out_fn)[1]
392 fd, filename = tempfile.mkstemp(dir=out_dir)
394 tmpfile = io.open(filename, "w", encoding="utf-8")
395 output_stream = tmpfile
399 # put auto-generation comment
400 comment = comment_tmpl.replace('\u0040comment\u0040',
401 'This file is generated by glib-mkenums, do '
402 'not modify it. This code is licensed under '
403 'the same license as the containing project. '
404 'Note that it links to GLib, so must comply '
405 'with the LGPL linking clauses.')
406 write_output("\n" + comment + '\n')
408 def replace_specials(prod):
409 prod = prod.replace(r'\\a', r'\a')
410 prod = prod.replace(r'\\b', r'\b')
411 prod = prod.replace(r'\\t', r'\t')
412 prod = prod.replace(r'\\n', r'\n')
413 prod = prod.replace(r'\\f', r'\f')
414 prod = prod.replace(r'\\r', r'\r')
420 base = os.path.basename(options.args[0])
422 prod = prod.replace('\u0040filename\u0040', options.args[0])
423 prod = prod.replace('\u0040basename\u0040', base)
424 prod = replace_specials(prod)
427 def process_file(curfilename):
428 global entries, flags, seenbitshift, enum_prefix
432 curfile = io.open(curfilename, encoding="utf-8",
433 errors="replace_and_warn")
435 if e.errno == errno.ENOENT:
436 print_warning('No file "{}" found.'.format(curfilename))
441 line = curfile.readline()
447 # read lines until we have no open comments
448 while re.search(r'/\*([^*]|\*(?!/))*$', line):
449 line += curfile.readline()
451 # strip comments w/o options
452 line = re.sub(r'''/\*(?!<)
456 # ignore forward declarations
457 if re.match(r'\s*typedef\s+enum.*;', line):
460 m = re.match(r'''\s*typedef\s+enum\s*
465 \s*({)?''', line, flags=re.X)
468 if len(groups) >= 2 and groups[1] is not None:
469 options = parse_trigraph(groups[1])
470 if 'skip' in options:
472 enum_prefix = options.get('prefix', None)
473 flags = options.get('flags', None)
474 if 'flags' in options:
479 option_lowercase_name = options.get('lowercase_name', None)
480 option_underscore_name = options.get('underscore_name', None)
484 option_lowercase_name = None
485 option_underscore_name = None
487 if option_lowercase_name is not None:
488 if option_underscore_name is not None:
489 print_warning("lowercase_name overriden with underscore_name")
490 option_lowercase_name = None
492 print_warning("lowercase_name is deprecated, use underscore_name")
494 # Didn't have trailing '{' look on next lines
495 if groups[0] is None and (len(groups) < 4 or groups[3] is None):
497 line = curfile.readline()
498 if re.match(r'\s*\{', line):
504 # Now parse the entries
505 parse_entries(curfile, curfilename)
507 # figure out if this was a flags or enums enumeration
511 # Autogenerate a prefix
512 if enum_prefix is None:
513 for entry in entries:
514 if len(entry) < 3 or entry[2] is None:
516 if enum_prefix is not None:
517 enum_prefix = os.path.commonprefix([name, enum_prefix])
520 if enum_prefix is None:
523 # Trim so that it ends in an underscore
524 enum_prefix = re.sub(r'_[^_]*$', '_', enum_prefix)
526 # canonicalize user defined prefixes
527 enum_prefix = enum_prefix.upper()
528 enum_prefix = enum_prefix.replace('-', '_')
529 enum_prefix = re.sub(r'(.*)([^_])$', r'\1\2_', enum_prefix)
535 if len(e) < 3 or e[2] is None:
536 nick = re.sub(r'^' + enum_prefix, '', name)
537 nick = nick.replace('_', '-').lower()
538 e = (name, num, nick)
539 fixed_entries.append(e)
540 entries = fixed_entries
542 # Spit out the output
543 if option_underscore_name is not None:
544 enumlong = option_underscore_name.upper()
545 enumsym = option_underscore_name.lower()
546 enumshort = re.sub(r'^[A-Z][A-Z0-9]*_', '', enumlong)
548 enumname_prefix = re.sub('_' + enumshort + '$', '', enumlong)
549 elif symprefix == '' and idprefix == '':
550 # enumname is e.g. GMatchType
551 enspace = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname)
553 enumshort = re.sub(r'^[A-Z][a-z]*', '', enumname)
554 enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
555 enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
556 enumshort = enumshort.upper()
558 enumname_prefix = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname).upper()
560 enumlong = enspace.upper() + "_" + enumshort
561 enumsym = enspace.lower() + "_" + enumshort.lower()
563 if option_lowercase_name is not None:
564 enumsym = option_lowercase_name
568 enumshort = re.sub(r'^' + idprefix, '', enumshort)
570 enumshort = re.sub(r'/^[A-Z][a-z]*', '', enumshort)
572 enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
573 enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
574 enumshort = enumshort.upper()
577 enumname_prefix = symprefix.upper()
579 enumname_prefix = idprefix.upper()
581 enumlong = enumname_prefix + "_" + enumshort
582 enumsym = enumlong.lower()
589 base = os.path.basename(curfilename)
591 prod = prod.replace('\u0040filename\u0040', curfilename)
592 prod = prod.replace('\u0040basename\u0040', base)
593 prod = replace_specials(prod)
600 prod = prod.replace('\u0040enum_name\u0040', enumsym)
601 prod = prod.replace('\u0040EnumName\u0040', enumname)
602 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
603 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
604 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
606 prod = prod.replace('\u0040type\u0040', 'flags')
608 prod = prod.replace('\u0040type\u0040', 'enum')
610 prod = prod.replace('\u0040Type\u0040', 'Flags')
612 prod = prod.replace('\u0040Type\u0040', 'Enum')
614 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
616 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
617 prod = replace_specials(prod)
622 prod = prod.replace('\u0040enum_name\u0040', enumsym)
623 prod = prod.replace('\u0040EnumName\u0040', enumname)
624 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
625 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
626 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
628 prod = prod.replace('\u0040type\u0040', 'flags')
630 prod = prod.replace('\u0040type\u0040', 'enum')
632 prod = prod.replace('\u0040Type\u0040', 'Flags')
634 prod = prod.replace('\u0040Type\u0040', 'Enum')
636 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
638 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
639 prod = replace_specials(prod)
646 prod = replace_specials(prod)
647 for name, num, nick in entries:
650 if '\u0040valuenum\u0040' in prod:
651 # only attempt to eval the value if it is requested
652 # this prevents us from throwing errors otherwise
654 # use sandboxed evaluation as a reasonable
655 # approximation to C constant folding
656 inum = eval(num, {}, {})
658 # make sure it parsed to an integer
659 if not isinstance(inum, int):
660 sys.exit("Unable to parse enum value '%s'" % num)
665 tmp_prod = tmp_prod.replace('\u0040valuenum\u0040', str(num))
666 next_num = int(num) + 1
668 tmp_prod = tmp_prod.replace('\u0040VALUENAME\u0040', name)
669 tmp_prod = tmp_prod.replace('\u0040valuenick\u0040', nick)
671 tmp_prod = tmp_prod.replace('\u0040type\u0040', 'flags')
673 tmp_prod = tmp_prod.replace('\u0040type\u0040', 'enum')
675 tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Flags')
677 tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Enum')
679 tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'FLAGS')
681 tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'ENUM')
682 tmp_prod = tmp_prod.rstrip()
684 write_output(tmp_prod)
688 prod = prod.replace('\u0040enum_name\u0040', enumsym)
689 prod = prod.replace('\u0040EnumName\u0040', enumname)
690 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
691 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
692 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
694 prod = prod.replace('\u0040type\u0040', 'flags')
696 prod = prod.replace('\u0040type\u0040', 'enum')
698 prod = prod.replace('\u0040Type\u0040', 'Flags')
700 prod = prod.replace('\u0040Type\u0040', 'Enum')
702 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
704 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
705 prod = replace_specials(prod)
708 for fname in sorted(options.args):
713 base = os.path.basename(options.args[-1]) # FIXME, wrong
715 prod = prod.replace('\u0040filename\u0040', 'ARGV') # wrong too
716 prod = prod.replace('\u0040basename\u0040', base)
717 prod = replace_specials(prod)
720 # put auto-generation comment
721 comment = comment_tmpl
722 comment = comment.replace('\u0040comment\u0040', 'Generated data ends here')
723 write_output("\n" + comment + "\n")
725 if tmpfile is not None:
726 tmpfilename = tmpfile.name
730 os.unlink(options.output)
731 except OSError as error:
732 if error.errno != errno.ENOENT:
735 os.rename(tmpfilename, options.output)