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 output_stream = sys.stdout
20 def write_output(output):
22 print(output, file=output_stream)
24 version = '@GLIB_VERSION@'
27 # Information about the current enumeration
28 flags = False # Is enumeration a bitmask?
29 option_underscore_name = '' # Overriden underscore variant of the enum name
30 # for example to fix the cases we don't get the
31 # mixed-case -> underscorized transform right.
32 option_lowercase_name = '' # DEPRECATED. A lower case name to use as part
33 # of the *_get_type() function, instead of the
34 # one that we guess. For instance, when an enum
35 # uses abnormal capitalization and we can not
36 # guess where to put the underscores.
37 seenbitshift = 0 # Have we seen bitshift operators?
38 enum_prefix = None # Prefix for this enumeration
39 enumname = '' # Name for this enumeration
40 enumshort = '' # $enumname without prefix
41 enumname_prefix = '' # prefix of $enumname
42 enumindex = 0 # Global enum counter
43 firstenum = 1 # Is this the first enumeration per file?
44 entries = [] # [ name, val ] for each entry
45 sandbox = None # sandbox for safe evaluation of expressions
47 output = '' # Filename to write result into
49 def parse_trigraph(opts):
52 for opt in re.split(r'\s*,\s*', opts):
53 opt = re.sub(r'^\s*', '', opt)
54 opt = re.sub(r'\s*$', '', opt)
55 m = re.search(r'(\w+)(?:=(.+))?', opt)
66 def parse_entries(file, file_name):
67 global entries, enumindex, enumname, seenbitshift, flags
68 looking_for_name = False
71 # read lines until we have no open comments
72 while re.search(r'/\*([^*]|\*(?!/))*$', line):
73 line += file.readline()
75 # strip comments w/o options
76 line = re.sub(r'''/\*(?!<)
78 \*/''', '', line, flags=re.X)
83 if len(line.strip()) == 0:
87 m = re.match(r'\s*(\w+)', line)
92 # Handle include files
93 m = re.match(r'\#include\s*<([^>]*)>', line)
95 newfilename = os.path.join("..", m.group(1))
96 newfile = open(newfilename)
98 if not parse_entries(newfile, newfilename):
103 m = re.match(r'\s*\}\s*(\w+)', line)
105 enumname = m.group(1)
109 m = re.match(r'\s*\}', line)
112 looking_for_name = True
118 \s*\w+\s*\(.*\)\s* # macro with multiple args
120 (?:[^,/]|/(?!\*))* # anything but a comma or comment
125 \s*$''', line, flags=re.X)
135 if not flags and value is not None and '<<' in value:
138 if options is not None:
139 options = parse_trigraph(options)
140 if 'skip' not in options:
141 entries.append((name, value, options['nick']))
143 entries.append((name, value))
144 elif re.match(r's*\#', line):
147 sys.exit("Failed to parse %s." % file_name)
151 print("glib-mkenums version glib-" + version)
152 print("glib-mkenums comes with ABSOLUTELY NO WARRANTY.")
153 print("You may redistribute copies of glib-mkenums under the terms of")
154 print("the GNU General Public License which can be found in the")
155 print("GLib source package. Sources, examples and contact")
156 print("information are available at http://www.gtk.org")
159 help_epilog = '''Production text substitutions:
160 \u0040EnumName\u0040 PrefixTheXEnum
161 \u0040enum_name\u0040 prefix_the_xenum
162 \u0040ENUMNAME\u0040 PREFIX_THE_XENUM
163 \u0040ENUMSHORT\u0040 THE_XENUM
164 \u0040ENUMPREFIX\u0040 PREFIX
165 \u0040VALUENAME\u0040 PREFIX_THE_XVALUE
166 \u0040valuenick\u0040 the-xvalue
167 \u0040valuenum\u0040 the integer value (limited support, Since: 2.26)
168 \u0040type\u0040 either enum or flags
169 \u0040Type\u0040 either Enum or Flags
170 \u0040TYPE\u0040 either ENUM or FLAGS
171 \u0040filename\u0040 name of current input file
172 \u0040basename\u0040 base name of the current input file (Since: 2.22)
176 # production variables:
177 idprefix = "" # "G", "Gtk", etc
178 symprefix = "" # "g", "gtk", etc, if not just lc($idprefix)
179 fhead = "" # output file header
180 fprod = "" # per input file production
181 ftail = "" # output file trailer
182 eprod = "" # per enum text (produced prior to value itarations)
183 vhead = "" # value header, produced before iterating over enum values
184 vprod = "" # value text, produced for each enum value
185 vtail = "" # value tail, produced after iterating over enum values
186 comment_tmpl = "" # comment template
188 def read_template_file(file):
189 global idprefix, symprefix, fhead, fprod, ftail, eprod, vhead, vprod, vtail, comment_tmpl
190 tmpl = {'file-header': fhead,
191 'file-production': fprod,
193 'enumeration-production': eprod,
194 'value-header': vhead,
195 'value-production': vprod,
197 'comment': comment_tmpl,
203 m = re.match(r'\/\*\*\*\s+(BEGIN|END)\s+([\w-]+)\s+\*\*\*\/', line)
205 if in_ == 'junk' and m.group(1) == 'BEGIN' and m.group(2) in tmpl:
208 elif in_ == m.group(2) and m.group(1) == 'END' and m.group(2) in tmpl:
212 sys.exit("Malformed template file " + file)
218 sys.exit("Malformed template file " + file)
220 fhead = tmpl['file-header']
221 fprod = tmpl['file-production']
222 ftail = tmpl['file-tail']
223 eprod = tmpl['enumeration-production']
224 vhead = tmpl['value-header']
225 vprod = tmpl['value-production']
226 vtail = tmpl['value-tail']
227 comment_tmpl = tmpl['comment']
229 # default to C-style comments
230 if comment_tmpl == "":
231 comment_tmpl = "/* \u0040comment\u0040 */"
233 parser = argparse.ArgumentParser(epilog=help_epilog,
234 formatter_class=argparse.RawDescriptionHelpFormatter)
236 parser.add_argument('--identifier-prefix', default='', dest='idprefix',
237 help='Identifier prefix')
238 parser.add_argument('--symbol-prefix', default='', dest='symprefix',
239 help='symbol-prefix')
240 parser.add_argument('--fhead', default=[], dest='fhead', action='append',
241 help='Output file header')
242 parser.add_argument('--ftail', default=[], dest='ftail', action='append',
243 help='Per input file production')
244 parser.add_argument('--fprod', default=[], dest='fprod', action='append',
245 help='Put out TEXT everytime a new input file is being processed.')
246 parser.add_argument('--eprod', default=[], dest='eprod', action='append',
247 help='Per enum text (produced prior to value iterations)')
248 parser.add_argument('--vhead', default=[], dest='vhead', action='append',
249 help='Value header, produced before iterating over enum values')
250 parser.add_argument('--vprod', default=[], dest='vprod', action='append',
251 help='Value text, produced for each enum value.')
252 parser.add_argument('--vtail', default=[], dest='vtail', action='append',
253 help='Value tail, produced after iterating over enum values')
254 parser.add_argument('--comments', default='', dest='comment_tmpl',
255 help='Comment structure')
256 parser.add_argument('--template', default='', dest='template',
257 help='Template file')
258 parser.add_argument('--output', default=None, dest='output')
259 parser.add_argument('--version', '-v', default=False, action='store_true', dest='version',
260 help='Print version informations')
261 parser.add_argument('args', nargs='*')
263 options = parser.parse_args()
268 def unescape_cmdline_args(arg):
269 arg = arg.replace('\\n', '\n')
270 arg = arg.replace('\\r', '\r')
271 return arg.replace('\\t', '\t')
273 if options.template != '':
274 read_template_file(options.template)
276 idprefix += options.idprefix
277 symprefix += options.symprefix
279 # This is a hack to maintain some semblance of backward compatibility with
280 # the old, Perl-based glib-mkenums. The old tool had an implicit ordering
281 # on the arguments and templates; each argument was parsed in order, and
282 # all the strings appended. This allowed developers to write:
286 # --template a-template-file.c.in \
289 # And have the fhead be prepended to the file-head stanza in the template,
290 # as well as the ftail be appended to the file-tail stanza in the template.
291 # Short of throwing away ArgumentParser and going over sys.argv[] element
292 # by element, we can simulate that behaviour by ensuring some ordering in
293 # how we build the template strings:
295 # - the head stanzas are always prepended to the template
296 # - the prod stanzas are always appended to the template
297 # - the tail stanzas are always appended to the template
299 # Within each instance of the command line argument, we append each value
300 # to the array in the order in which it appears on the command line.
301 fhead = ''.join([unescape_cmdline_args(x) for x in options.fhead]) + fhead
302 vhead = ''.join([unescape_cmdline_args(x) for x in options.vhead]) + vhead
304 eprod += ''.join([unescape_cmdline_args(x) for x in options.eprod])
305 vprod += ''.join([unescape_cmdline_args(x) for x in options.vprod])
307 ftail = ftail + ''.join([unescape_cmdline_args(x) for x in options.ftail])
308 vtail = vtail + ''.join([unescape_cmdline_args(x) for x in options.vtail])
310 if options.comment_tmpl != '':
311 comment_tmpl = unescape_cmdline_args(options.comment_tmpl)
313 output = options.output
315 if output is not None:
316 (out_dir, out_fn) = os.path.split(options.output)
317 out_suffix = '_' + os.path.splitext(out_fn)[1]
320 tmpfile = tempfile.NamedTemporaryFile(dir=out_dir, delete=False)
321 output_stream = tmpfile
325 # put auto-generation comment
326 comment = comment_tmpl.replace('\u0040comment\u0040', 'Generated data (by glib-mkenums)')
327 write_output("\n" + comment + '\n')
329 def replace_specials(prod):
330 prod = prod.replace(r'\\a', r'\a')
331 prod = prod.replace(r'\\b', r'\b')
332 prod = prod.replace(r'\\t', r'\t')
333 prod = prod.replace(r'\\n', r'\n')
334 prod = prod.replace(r'\\f', r'\f')
335 prod = prod.replace(r'\\r', r'\r')
341 base = os.path.basename(options.args[0])
343 prod = prod.replace('\u0040filename\u0040', options.args[0])
344 prod = prod.replace('\u0040basename\u0040', base)
345 prod = replace_specials(prod)
348 def process_file(curfilename):
349 global entries, flags, seenbitshift, enum_prefix
353 curfile = open(curfilename)
354 except FileNotFoundError:
355 sys.stderr.write('WARNING: No file "{}" found.'.format(curfilename))
359 # read lines until we have no open comments
360 while re.search(r'/\*([^*]|\*(?!/))*$', line):
361 line += curfile.readline()
363 # strip comments w/o options
364 line = re.sub(r'''/\*(?!<)
368 # ignore forward declarations
369 if re.match(r'\s*typedef\s+enum.*;', line):
372 m = re.match(r'''\s*typedef\s+enum\s*
377 \s*({)?''', line, flags=re.X)
380 if len(groups) >= 2 and groups[1] is not None:
381 options = parse_trigraph(groups[1])
382 if 'skip' in options:
384 enum_prefix = options.get('prefix', None)
385 flags = 'flags' in options
386 option_lowercase_name = options.get('lowercase_name', None)
387 option_underscore_name = options.get('underscore_name', None)
391 option_lowercase_name = None
392 option_underscore_name = None
394 if option_lowercase_name is not None:
395 if option_underscore_name is not None:
396 print("$0: $ARGV:$.: lowercase_name overriden with underscore_name", file=sys.stderr)
397 option_lowercase_name = None
399 print("$0: $ARGV:$.: lowercase_name is deprecated, use underscore_name", file=sys.stderr)
401 # Didn't have trailing '{' look on next lines
402 if groups[0] is None and (len(groups) < 4 or groups[3] is None):
404 line = curfile.readline()
405 if re.match(r'\s*\{', line):
411 # Now parse the entries
412 parse_entries(curfile, curfilename)
414 # figure out if this was a flags or enums enumeration
418 # Autogenerate a prefix
419 if enum_prefix is None:
420 for entry in entries:
421 if len(entry) < 3 or entry[2] is None:
423 if enum_prefix is not None:
424 enum_prefix = os.path.commonprefix([name, enum_prefix])
427 if enum_prefix is None:
430 # Trim so that it ends in an underscore
431 enum_prefix = re.sub(r'_[^_]*$', '_', enum_prefix)
433 # canonicalize user defined prefixes
434 enum_prefix = enum_prefix.upper()
435 enum_prefix = enum_prefix.replace('-', '_')
436 enum_prefix = re.sub(r'(.*)([^_])$', r'\1\2_', enum_prefix)
442 if len(e) < 3 or e[2] is None:
443 nick = re.sub(r'^' + enum_prefix, '', name)
444 nick = nick.replace('_', '-').lower()
445 e = (name, num, nick)
446 fixed_entries.append(e)
447 entries = fixed_entries
449 # Spit out the output
450 if option_underscore_name is not None:
451 enumlong = option_underscore_name.upper()
452 enumsym = option_underscore_name.lower()
453 enumshort = re.sub(r'^[A-Z][A-Z0-9]*_', '', enumlong)
455 enumname_prefix = re.sub('_' + enumshort + '$', '', enumlong)
456 elif symprefix == '' and idprefix == '':
457 # enumname is e.g. GMatchType
458 enspace = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname)
460 enumshort = re.sub(r'^[A-Z][a-z]*', '', enumname)
461 enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
462 enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
463 enumshort = enumshort.upper()
465 enumname_prefix = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname).upper()
467 enumlong = enspace.upper() + "_" + enumshort
468 enumsym = enspace.lower() + "_" + enumshort.lower()
470 if option_lowercase_name is not None:
471 enumsym = option_lowercase_name
475 enumshort = re.sub(r'^' + idprefix, '', enumshort)
477 enumshort = re.sub(r'/^[A-Z][a-z]*', '', enumshort)
479 enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort)
480 enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort)
481 enumshort = enumshort.upper()
484 enumname_prefix = symprefix.upper()
486 enumname_prefix = idprefix.upper()
488 enumlong = enumname_prefix + "_" + enumshort
489 enumsym = enumlong.lower()
496 base = os.path.basename(curfilename)
498 prod = prod.replace('\u0040filename\u0040', curfilename)
499 prod = prod.replace('\u0040basename\u0040', base)
500 prod = replace_specials(prod)
507 prod = prod.replace('\u0040enum_name\u0040', enumsym)
508 prod = prod.replace('\u0040EnumName\u0040', enumname)
509 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
510 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
511 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
513 prod = prod.replace('\u0040type\u0040', 'flags')
515 prod = prod.replace('\u0040type\u0040', 'enum')
517 prod = prod.replace('\u0040Type\u0040', 'Flags')
519 prod = prod.replace('\u0040Type\u0040', 'Enum')
521 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
523 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
524 prod = replace_specials(prod)
529 prod = prod.replace('\u0040enum_name\u0040', enumsym)
530 prod = prod.replace('\u0040EnumName\u0040', enumname)
531 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
532 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
533 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
535 prod = prod.replace('\u0040type\u0040', 'flags')
537 prod = prod.replace('\u0040type\u0040', 'enum')
539 prod = prod.replace('\u0040Type\u0040', 'Flags')
541 prod = prod.replace('\u0040Type\u0040', 'Enum')
543 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
545 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
546 prod = replace_specials(prod)
553 prod = replace_specials(prod)
554 for name, num, nick in entries:
557 if '\u0040valuenum\u0040' in prod:
558 # only attempt to eval the value if it is requested
559 # this prevents us from throwing errors otherwise
561 # use sandboxed evaluation as a reasonable
562 # approximation to C constant folding
563 inum = eval(num, {}, {})
565 # make sure it parsed to an integer
566 if not isinstance(inum, int):
567 sys.exit("Unable to parse enum value '%s'" % num)
572 tmp_prod = tmp_prod.replace('\u0040valuenum\u0040', str(num))
573 next_num = int(num) + 1
575 tmp_prod = tmp_prod.replace('\u0040VALUENAME\u0040', name)
576 tmp_prod = tmp_prod.replace('\u0040valuenick\u0040', nick)
578 tmp_prod = tmp_prod.replace('\u0040type\u0040', 'flags')
580 tmp_prod = tmp_prod.replace('\u0040type\u0040', 'enum')
582 tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Flags')
584 tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Enum')
586 tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'FLAGS')
588 tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'ENUM')
589 tmp_prod = tmp_prod.rstrip()
591 write_output(tmp_prod)
595 prod = prod.replace('\u0040enum_name\u0040', enumsym)
596 prod = prod.replace('\u0040EnumName\u0040', enumname)
597 prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort)
598 prod = prod.replace('\u0040ENUMNAME\u0040', enumlong)
599 prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix)
601 prod = prod.replace('\u0040type\u0040', 'flags')
603 prod = prod.replace('\u0040type\u0040', 'enum')
605 prod = prod.replace('\u0040Type\u0040', 'Flags')
607 prod = prod.replace('\u0040Type\u0040', 'Enum')
609 prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
611 prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
612 prod = replace_specials(prod)
615 for fname in options.args:
620 base = os.path.basename(options.args[-1]) # FIXME, wrong
622 prod = prod.replace('\u0040filename\u0040', 'ARGV') # wrong too
623 prod = prod.replace('\u0040basename\u0040', base)
624 prod = replace_specials(prod)
627 # put auto-generation comment
628 comment = comment_tmpl
629 comment = comment.replace('\u0040comment\u0040', 'Generated data ends here')
630 write_output("\n" + comment + "\n")
632 if tmpfile is not None:
633 tmpfilename = tmpfile.name
635 os.unlink(options.output)
636 os.rename(tmpfilename, options.output)