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='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*[_A-Za-z]*[_A-Za-z0-9]*\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 overridden 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()
499 print_error("Syntax error when looking for opening { in enum")
500 if re.match(r'\s*\{', line):
506 # Now parse the entries
507 parse_entries(curfile, curfilename)
509 # figure out if this was a flags or enums enumeration
513 # Autogenerate a prefix
514 if enum_prefix is None:
515 for entry in entries:
516 if len(entry) < 3 or entry[2] is None:
518 if enum_prefix is not None:
519 enum_prefix = os.path.commonprefix([name, enum_prefix])
522 if enum_prefix is None:
525 # Trim so that it ends in an underscore
526 enum_prefix = re.sub(r'_[^_]*$', '_', enum_prefix)
528 # canonicalize user defined prefixes
529 enum_prefix = enum_prefix.upper()
530 enum_prefix = enum_prefix.replace('-', '_')
531 enum_prefix = re.sub(r'(.*)([^_])$', r'\1\2_', enum_prefix)
537 if len(e) < 3 or e[2] is None:
538 nick = re.sub(r'^' + enum_prefix, '', name)
539 nick = nick.replace('_', '-').lower()
540 e = (name, num, nick)
541 fixed_entries.append(e)
542 entries = fixed_entries
544 # Spit out the output
545 if option_underscore_name is not None:
546 enumlong = option_underscore_name.upper()
547 enumsym = option_underscore_name.lower()
548 enumshort = re.sub(r'^[A-Z][A-Z0-9]*_', '', enumlong)
550 enumname_prefix = re.sub('_' + enumshort + '$', '', enumlong)
551 elif symprefix == '' and idprefix == '':
552 # enumname is e.g. GMatchType
553 enspace = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname)
555 enumshort = re.sub(r'^[A-Z][a-z]*', '', enumname)
556 enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
557 enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
558 enumshort = enumshort.upper()
560 enumname_prefix = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname).upper()
562 enumlong = enspace.upper() + "_" + enumshort
563 enumsym = enspace.lower() + "_" + enumshort.lower()
565 if option_lowercase_name is not None:
566 enumsym = option_lowercase_name
570 enumshort = re.sub(r'^' + idprefix, '', enumshort)
572 enumshort = re.sub(r'/^[A-Z][a-z]*', '', enumshort)
574 enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
575 enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
576 enumshort = enumshort.upper()
579 enumname_prefix = symprefix.upper()
581 enumname_prefix = idprefix.upper()
583 enumlong = enumname_prefix + "_" + enumshort
584 enumsym = enumlong.lower()
591 base = os.path.basename(curfilename)
593 prod = prod.replace('\u0040filename\u0040', curfilename)
594 prod = prod.replace('\u0040basename\u0040', base)
595 prod = replace_specials(prod)
602 prod = prod.replace('\u0040enum_name\u0040', enumsym)
603 prod = prod.replace('\u0040EnumName\u0040', enumname)
604 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
605 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
606 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
608 prod = prod.replace('\u0040type\u0040', 'flags')
610 prod = prod.replace('\u0040type\u0040', 'enum')
612 prod = prod.replace('\u0040Type\u0040', 'Flags')
614 prod = prod.replace('\u0040Type\u0040', 'Enum')
616 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
618 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
619 prod = replace_specials(prod)
624 prod = prod.replace('\u0040enum_name\u0040', enumsym)
625 prod = prod.replace('\u0040EnumName\u0040', enumname)
626 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
627 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
628 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
630 prod = prod.replace('\u0040type\u0040', 'flags')
632 prod = prod.replace('\u0040type\u0040', 'enum')
634 prod = prod.replace('\u0040Type\u0040', 'Flags')
636 prod = prod.replace('\u0040Type\u0040', 'Enum')
638 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
640 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
641 prod = replace_specials(prod)
648 prod = replace_specials(prod)
649 for name, num, nick in entries:
652 if '\u0040valuenum\u0040' in prod:
653 # only attempt to eval the value if it is requested
654 # this prevents us from throwing errors otherwise
656 # use sandboxed evaluation as a reasonable
657 # approximation to C constant folding
658 inum = eval(num, {}, {})
660 # make sure it parsed to an integer
661 if not isinstance(inum, int):
662 sys.exit("Unable to parse enum value '%s'" % num)
667 tmp_prod = tmp_prod.replace('\u0040valuenum\u0040', str(num))
668 next_num = int(num) + 1
670 tmp_prod = tmp_prod.replace('\u0040VALUENAME\u0040', name)
671 tmp_prod = tmp_prod.replace('\u0040valuenick\u0040', nick)
673 tmp_prod = tmp_prod.replace('\u0040type\u0040', 'flags')
675 tmp_prod = tmp_prod.replace('\u0040type\u0040', 'enum')
677 tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Flags')
679 tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Enum')
681 tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'FLAGS')
683 tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'ENUM')
684 tmp_prod = tmp_prod.rstrip()
686 write_output(tmp_prod)
690 prod = prod.replace('\u0040enum_name\u0040', enumsym)
691 prod = prod.replace('\u0040EnumName\u0040', enumname)
692 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
693 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
694 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
696 prod = prod.replace('\u0040type\u0040', 'flags')
698 prod = prod.replace('\u0040type\u0040', 'enum')
700 prod = prod.replace('\u0040Type\u0040', 'Flags')
702 prod = prod.replace('\u0040Type\u0040', 'Enum')
704 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
706 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
707 prod = replace_specials(prod)
710 for fname in sorted(options.args):
715 base = os.path.basename(options.args[-1]) # FIXME, wrong
717 prod = prod.replace('\u0040filename\u0040', 'ARGV') # wrong too
718 prod = prod.replace('\u0040basename\u0040', base)
719 prod = replace_specials(prod)
722 # put auto-generation comment
723 comment = comment_tmpl
724 comment = comment.replace('\u0040comment\u0040', 'Generated data ends here')
725 write_output("\n" + comment + "\n")
727 if tmpfile is not None:
728 tmpfilename = tmpfile.name
732 os.unlink(options.output)
733 except OSError as error:
734 if error.errno != errno.ENOENT:
737 os.rename(tmpfilename, options.output)