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
18 VERSION_STR = '''glib-mkenums version @VERSION@
19 glib-genmarshal comes with ABSOLUTELY NO WARRANTY.
20 You may redistribute copies of glib-genmarshal under the terms of
21 the GNU General Public License which can be found in the
22 GLib source package. Sources, examples and contact
23 information are available at http://www.gtk.org'''
25 output_stream = sys.stdout
27 # pylint: disable=too-few-public-methods
29 '''ANSI Terminal colors'''
37 def print_color(msg, color=Color.END, prefix='MESSAGE'):
38 '''Print a string with a color prefix'''
39 if os.isatty(sys.stderr.fileno()):
40 real_prefix = '{start}{prefix}{end}'.format(start=color, prefix=prefix, end=Color.END)
43 print('{prefix}: {msg}'.format(prefix=real_prefix, msg=msg), file=sys.stderr)
47 '''Print an error, and terminate'''
48 print_color(msg, color=Color.RED, prefix='ERROR')
52 def print_warning(msg, fatal=False):
53 '''Print a warning, and optionally terminate'''
60 print_color(msg, color, prefix)
67 print_color(msg, color=Color.GREEN, prefix='INFO')
70 def write_output(output):
72 print(output, file=output_stream)
75 # Information about the current enumeration
76 flags = False # Is enumeration a bitmask?
77 option_underscore_name = '' # Overriden underscore variant of the enum name
78 # for example to fix the cases we don't get the
79 # mixed-case -> underscorized transform right.
80 option_lowercase_name = '' # DEPRECATED. A lower case name to use as part
81 # of the *_get_type() function, instead of the
82 # one that we guess. For instance, when an enum
83 # uses abnormal capitalization and we can not
84 # guess where to put the underscores.
85 seenbitshift = 0 # Have we seen bitshift operators?
86 enum_prefix = None # Prefix for this enumeration
87 enumname = '' # Name for this enumeration
88 enumshort = '' # $enumname without prefix
89 enumname_prefix = '' # prefix of $enumname
90 enumindex = 0 # Global enum counter
91 firstenum = 1 # Is this the first enumeration per file?
92 entries = [] # [ name, val ] for each entry
93 sandbox = None # sandbox for safe evaluation of expressions
95 output = '' # Filename to write result into
97 def parse_trigraph(opts):
100 for opt in re.split(r'\s*,\s*', opts):
101 opt = re.sub(r'^\s*', '', opt)
102 opt = re.sub(r'\s*$', '', opt)
103 m = re.search(r'(\w+)(?:=(.+))?', opt)
114 def parse_entries(file, file_name):
115 global entries, enumindex, enumname, seenbitshift, flags
116 looking_for_name = False
119 # read lines until we have no open comments
120 while re.search(r'/\*([^*]|\*(?!/))*$', line):
121 line += file.readline()
123 # strip comments w/o options
124 line = re.sub(r'''/\*(?!<)
126 \*/''', '', line, flags=re.X)
131 if len(line.strip()) == 0:
135 m = re.match(r'\s*(\w+)', line)
137 enumname = m.group(1)
140 # Handle include files
141 m = re.match(r'\#include\s*<([^>]*)>', line)
143 newfilename = os.path.join("..", m.group(1))
144 newfile = open(newfilename)
146 if not parse_entries(newfile, newfilename):
151 m = re.match(r'\s*\}\s*(\w+)', line)
153 enumname = m.group(1)
157 m = re.match(r'\s*\}', line)
160 looking_for_name = True
166 \s*\w+\s*\(.*\)\s* # macro with multiple args
168 (?:[^,/]|/(?!\*))* # anything but a comma or comment
173 \s*$''', line, flags=re.X)
183 if not flags and value is not None and '<<' in value:
186 if options is not None:
187 options = parse_trigraph(options)
188 if 'skip' not in options:
189 entries.append((name, value, options['nick']))
191 entries.append((name, value))
192 elif re.match(r's*\#', line):
195 sys.exit("Failed to parse %s." % file_name)
198 help_epilog = '''Production text substitutions:
199 \u0040EnumName\u0040 PrefixTheXEnum
200 \u0040enum_name\u0040 prefix_the_xenum
201 \u0040ENUMNAME\u0040 PREFIX_THE_XENUM
202 \u0040ENUMSHORT\u0040 THE_XENUM
203 \u0040ENUMPREFIX\u0040 PREFIX
204 \u0040VALUENAME\u0040 PREFIX_THE_XVALUE
205 \u0040valuenick\u0040 the-xvalue
206 \u0040valuenum\u0040 the integer value (limited support, Since: 2.26)
207 \u0040type\u0040 either enum or flags
208 \u0040Type\u0040 either Enum or Flags
209 \u0040TYPE\u0040 either ENUM or FLAGS
210 \u0040filename\u0040 name of current input file
211 \u0040basename\u0040 base name of the current input file (Since: 2.22)
215 # production variables:
216 idprefix = "" # "G", "Gtk", etc
217 symprefix = "" # "g", "gtk", etc, if not just lc($idprefix)
218 fhead = "" # output file header
219 fprod = "" # per input file production
220 ftail = "" # output file trailer
221 eprod = "" # per enum text (produced prior to value itarations)
222 vhead = "" # value header, produced before iterating over enum values
223 vprod = "" # value text, produced for each enum value
224 vtail = "" # value tail, produced after iterating over enum values
225 comment_tmpl = "" # comment template
227 def read_template_file(file):
228 global idprefix, symprefix, fhead, fprod, ftail, eprod, vhead, vprod, vtail, comment_tmpl
229 tmpl = {'file-header': fhead,
230 'file-production': fprod,
232 'enumeration-production': eprod,
233 'value-header': vhead,
234 'value-production': vprod,
236 'comment': comment_tmpl,
242 m = re.match(r'\/\*\*\*\s+(BEGIN|END)\s+([\w-]+)\s+\*\*\*\/', line)
244 if in_ == 'junk' and m.group(1) == 'BEGIN' and m.group(2) in tmpl:
247 elif in_ == m.group(2) and m.group(1) == 'END' and m.group(2) in tmpl:
251 sys.exit("Malformed template file " + file)
257 sys.exit("Malformed template file " + file)
259 fhead = tmpl['file-header']
260 fprod = tmpl['file-production']
261 ftail = tmpl['file-tail']
262 eprod = tmpl['enumeration-production']
263 vhead = tmpl['value-header']
264 vprod = tmpl['value-production']
265 vtail = tmpl['value-tail']
266 comment_tmpl = tmpl['comment']
268 # default to C-style comments
269 if comment_tmpl == "":
270 comment_tmpl = "/* \u0040comment\u0040 */"
272 parser = argparse.ArgumentParser(epilog=help_epilog,
273 formatter_class=argparse.RawDescriptionHelpFormatter)
275 parser.add_argument('--identifier-prefix', default='', dest='idprefix',
276 help='Identifier prefix')
277 parser.add_argument('--symbol-prefix', default='', dest='symprefix',
278 help='symbol-prefix')
279 parser.add_argument('--fhead', default=[], dest='fhead', action='append',
280 help='Output file header')
281 parser.add_argument('--ftail', default=[], dest='ftail', action='append',
282 help='Per input file production')
283 parser.add_argument('--fprod', default=[], dest='fprod', action='append',
284 help='Put out TEXT everytime a new input file is being processed.')
285 parser.add_argument('--eprod', default=[], dest='eprod', action='append',
286 help='Per enum text (produced prior to value iterations)')
287 parser.add_argument('--vhead', default=[], dest='vhead', action='append',
288 help='Value header, produced before iterating over enum values')
289 parser.add_argument('--vprod', default=[], dest='vprod', action='append',
290 help='Value text, produced for each enum value.')
291 parser.add_argument('--vtail', default=[], dest='vtail', action='append',
292 help='Value tail, produced after iterating over enum values')
293 parser.add_argument('--comments', default='', dest='comment_tmpl',
294 help='Comment structure')
295 parser.add_argument('--template', default='', dest='template',
296 help='Template file')
297 parser.add_argument('--output', default=None, dest='output')
298 parser.add_argument('--version', '-v', default=False, action='store_true', dest='version',
299 help='Print version informations')
300 parser.add_argument('args', nargs='*')
302 options = parser.parse_args()
308 def unescape_cmdline_args(arg):
309 arg = arg.replace('\\n', '\n')
310 arg = arg.replace('\\r', '\r')
311 return arg.replace('\\t', '\t')
313 if options.template != '':
314 read_template_file(options.template)
316 idprefix += options.idprefix
317 symprefix += options.symprefix
319 # This is a hack to maintain some semblance of backward compatibility with
320 # the old, Perl-based glib-mkenums. The old tool had an implicit ordering
321 # on the arguments and templates; each argument was parsed in order, and
322 # all the strings appended. This allowed developers to write:
326 # --template a-template-file.c.in \
329 # And have the fhead be prepended to the file-head stanza in the template,
330 # as well as the ftail be appended to the file-tail stanza in the template.
331 # Short of throwing away ArgumentParser and going over sys.argv[] element
332 # by element, we can simulate that behaviour by ensuring some ordering in
333 # how we build the template strings:
335 # - the head stanzas are always prepended to the template
336 # - the prod stanzas are always appended to the template
337 # - the tail stanzas are always appended to the template
339 # Within each instance of the command line argument, we append each value
340 # to the array in the order in which it appears on the command line.
341 fhead = ''.join([unescape_cmdline_args(x) for x in options.fhead]) + fhead
342 vhead = ''.join([unescape_cmdline_args(x) for x in options.vhead]) + vhead
344 eprod += ''.join([unescape_cmdline_args(x) for x in options.eprod])
345 vprod += ''.join([unescape_cmdline_args(x) for x in options.vprod])
347 ftail = ftail + ''.join([unescape_cmdline_args(x) for x in options.ftail])
348 vtail = vtail + ''.join([unescape_cmdline_args(x) for x in options.vtail])
350 if options.comment_tmpl != '':
351 comment_tmpl = unescape_cmdline_args(options.comment_tmpl)
353 output = options.output
355 if output is not None:
356 (out_dir, out_fn) = os.path.split(options.output)
357 out_suffix = '_' + os.path.splitext(out_fn)[1]
360 tmpfile = tempfile.NamedTemporaryFile(dir=out_dir, delete=False)
361 output_stream = tmpfile
365 # put auto-generation comment
366 comment = comment_tmpl.replace('\u0040comment\u0040', 'Generated data (by glib-mkenums)')
367 write_output("\n" + comment + '\n')
369 def replace_specials(prod):
370 prod = prod.replace(r'\\a', r'\a')
371 prod = prod.replace(r'\\b', r'\b')
372 prod = prod.replace(r'\\t', r'\t')
373 prod = prod.replace(r'\\n', r'\n')
374 prod = prod.replace(r'\\f', r'\f')
375 prod = prod.replace(r'\\r', r'\r')
381 base = os.path.basename(options.args[0])
383 prod = prod.replace('\u0040filename\u0040', options.args[0])
384 prod = prod.replace('\u0040basename\u0040', base)
385 prod = replace_specials(prod)
388 def process_file(curfilename):
389 global entries, flags, seenbitshift, enum_prefix
393 curfile = open(curfilename)
394 except FileNotFoundError:
395 print_warning('No file "{}" found.'.format(curfilename))
399 # read lines until we have no open comments
400 while re.search(r'/\*([^*]|\*(?!/))*$', line):
401 line += curfile.readline()
403 # strip comments w/o options
404 line = re.sub(r'''/\*(?!<)
408 # ignore forward declarations
409 if re.match(r'\s*typedef\s+enum.*;', line):
412 m = re.match(r'''\s*typedef\s+enum\s*
417 \s*({)?''', line, flags=re.X)
420 if len(groups) >= 2 and groups[1] is not None:
421 options = parse_trigraph(groups[1])
422 if 'skip' in options:
424 enum_prefix = options.get('prefix', None)
425 flags = 'flags' in options
426 option_lowercase_name = options.get('lowercase_name', None)
427 option_underscore_name = options.get('underscore_name', None)
431 option_lowercase_name = None
432 option_underscore_name = None
434 if option_lowercase_name is not None:
435 if option_underscore_name is not None:
436 print_warning("lowercase_name overriden with underscore_name")
437 option_lowercase_name = None
439 print_warning("lowercase_name is deprecated, use underscore_name")
441 # Didn't have trailing '{' look on next lines
442 if groups[0] is None and (len(groups) < 4 or groups[3] is None):
444 line = curfile.readline()
445 if re.match(r'\s*\{', line):
451 # Now parse the entries
452 parse_entries(curfile, curfilename)
454 # figure out if this was a flags or enums enumeration
458 # Autogenerate a prefix
459 if enum_prefix is None:
460 for entry in entries:
461 if len(entry) < 3 or entry[2] is None:
463 if enum_prefix is not None:
464 enum_prefix = os.path.commonprefix([name, enum_prefix])
467 if enum_prefix is None:
470 # Trim so that it ends in an underscore
471 enum_prefix = re.sub(r'_[^_]*$', '_', enum_prefix)
473 # canonicalize user defined prefixes
474 enum_prefix = enum_prefix.upper()
475 enum_prefix = enum_prefix.replace('-', '_')
476 enum_prefix = re.sub(r'(.*)([^_])$', r'\1\2_', enum_prefix)
482 if len(e) < 3 or e[2] is None:
483 nick = re.sub(r'^' + enum_prefix, '', name)
484 nick = nick.replace('_', '-').lower()
485 e = (name, num, nick)
486 fixed_entries.append(e)
487 entries = fixed_entries
489 # Spit out the output
490 if option_underscore_name is not None:
491 enumlong = option_underscore_name.upper()
492 enumsym = option_underscore_name.lower()
493 enumshort = re.sub(r'^[A-Z][A-Z0-9]*_', '', enumlong)
495 enumname_prefix = re.sub('_' + enumshort + '$', '', enumlong)
496 elif symprefix == '' and idprefix == '':
497 # enumname is e.g. GMatchType
498 enspace = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname)
500 enumshort = re.sub(r'^[A-Z][a-z]*', '', enumname)
501 enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
502 enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
503 enumshort = enumshort.upper()
505 enumname_prefix = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname).upper()
507 enumlong = enspace.upper() + "_" + enumshort
508 enumsym = enspace.lower() + "_" + enumshort.lower()
510 if option_lowercase_name is not None:
511 enumsym = option_lowercase_name
515 enumshort = re.sub(r'^' + idprefix, '', enumshort)
517 enumshort = re.sub(r'/^[A-Z][a-z]*', '', enumshort)
519 enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
520 enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
521 enumshort = enumshort.upper()
524 enumname_prefix = symprefix.upper()
526 enumname_prefix = idprefix.upper()
528 enumlong = enumname_prefix + "_" + enumshort
529 enumsym = enumlong.lower()
536 base = os.path.basename(curfilename)
538 prod = prod.replace('\u0040filename\u0040', curfilename)
539 prod = prod.replace('\u0040basename\u0040', base)
540 prod = replace_specials(prod)
547 prod = prod.replace('\u0040enum_name\u0040', enumsym)
548 prod = prod.replace('\u0040EnumName\u0040', enumname)
549 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
550 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
551 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
553 prod = prod.replace('\u0040type\u0040', 'flags')
555 prod = prod.replace('\u0040type\u0040', 'enum')
557 prod = prod.replace('\u0040Type\u0040', 'Flags')
559 prod = prod.replace('\u0040Type\u0040', 'Enum')
561 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
563 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
564 prod = replace_specials(prod)
569 prod = prod.replace('\u0040enum_name\u0040', enumsym)
570 prod = prod.replace('\u0040EnumName\u0040', enumname)
571 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
572 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
573 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
575 prod = prod.replace('\u0040type\u0040', 'flags')
577 prod = prod.replace('\u0040type\u0040', 'enum')
579 prod = prod.replace('\u0040Type\u0040', 'Flags')
581 prod = prod.replace('\u0040Type\u0040', 'Enum')
583 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
585 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
586 prod = replace_specials(prod)
593 prod = replace_specials(prod)
594 for name, num, nick in entries:
597 if '\u0040valuenum\u0040' in prod:
598 # only attempt to eval the value if it is requested
599 # this prevents us from throwing errors otherwise
601 # use sandboxed evaluation as a reasonable
602 # approximation to C constant folding
603 inum = eval(num, {}, {})
605 # make sure it parsed to an integer
606 if not isinstance(inum, int):
607 sys.exit("Unable to parse enum value '%s'" % num)
612 tmp_prod = tmp_prod.replace('\u0040valuenum\u0040', str(num))
613 next_num = int(num) + 1
615 tmp_prod = tmp_prod.replace('\u0040VALUENAME\u0040', name)
616 tmp_prod = tmp_prod.replace('\u0040valuenick\u0040', nick)
618 tmp_prod = tmp_prod.replace('\u0040type\u0040', 'flags')
620 tmp_prod = tmp_prod.replace('\u0040type\u0040', 'enum')
622 tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Flags')
624 tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Enum')
626 tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'FLAGS')
628 tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'ENUM')
629 tmp_prod = tmp_prod.rstrip()
631 write_output(tmp_prod)
635 prod = prod.replace('\u0040enum_name\u0040', enumsym)
636 prod = prod.replace('\u0040EnumName\u0040', enumname)
637 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
638 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
639 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
641 prod = prod.replace('\u0040type\u0040', 'flags')
643 prod = prod.replace('\u0040type\u0040', 'enum')
645 prod = prod.replace('\u0040Type\u0040', 'Flags')
647 prod = prod.replace('\u0040Type\u0040', 'Enum')
649 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
651 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
652 prod = replace_specials(prod)
655 for fname in options.args:
660 base = os.path.basename(options.args[-1]) # FIXME, wrong
662 prod = prod.replace('\u0040filename\u0040', 'ARGV') # wrong too
663 prod = prod.replace('\u0040basename\u0040', base)
664 prod = replace_specials(prod)
667 # put auto-generation comment
668 comment = comment_tmpl
669 comment = comment.replace('\u0040comment\u0040', 'Generated data ends here')
670 write_output("\n" + comment + "\n")
672 if tmpfile is not None:
673 tmpfilename = tmpfile.name
675 os.unlink(options.output)
676 os.rename(tmpfilename, options.output)