#!/usr/bin/env python3 # If the code below looks horrible and unpythonic, do not panic. # # It is. # # This is a manual conversion from the original Perl script to # Python. Improvements are welcome. import os, sys, re, argparse, tempfile output_stream = sys.stdout def write_output(output): global output_stream print(output, file=output_stream) version = '@GLIB_VERSION@' # glib-mkenums.py # Information about the current enumeration flags = None # Is enumeration a bitmask? option_underscore_name = '' # Overriden underscore variant of the enum name # for example to fix the cases we don't get the # mixed-case -> underscorized transform right. option_lowercase_name = '' # DEPRECATED. A lower case name to use as part # of the *_get_type() function, instead of the # one that we guess. For instance, when an enum # uses abnormal capitalization and we can not # guess where to put the underscores. seenbitshift = 0 # Have we seen bitshift operators? enum_prefix = None # Prefix for this enumeration enumname = '' # Name for this enumeration enumshort = '' # $enumname without prefix enumname_prefix = '' # prefix of $enumname enumindex = 0 # Global enum counter firstenum = 1 # Is this the first enumeration per file? entries = [] # [ name, val ] for each entry sandbox = None # sandbox for safe evaluation of expressions output = '' # Filename to write result into def parse_trigraph(opts): result = {} for opt in re.split(r'\s*,\s*', opts): opt = re.sub(r'^\s*', '', opt) opt = re.sub(r'\s*$', '', opt) m = re.search(r'(\w+)(?:=(.+))?', opt) assert(m is not None) groups = m.groups() key = groups[0] if len(groups) > 1: val = groups[1] else: val = 1 result[key] = val return result def parse_entries(file, file_name): global entries, enumindex, enumname, seenbitshift, flags looking_for_name = False for line in file: # read lines until we have no open comments while re.search(r'/\*([^*]|\*(?!/))*$', line): line = file.readline() # strip comments w/o options line = re.sub(r'''/\*(?!<) ([^*]+|\*(?!/))* \*/''', '', line, flags=re.X) line = line.rstrip() # skip empty lines if len(line.strip()) == 0: continue if looking_for_name: m = re.match('\s*(\w+)', line) if m: enumname = m.group(1); return True # Handle include files m = re.match(r'\#include\s*<([^>]*)>', line) if m: newfilename = os.path.join("..", m.group(1)) newfile = open(newfilename) if not parse_entries(newfile, newfilename): return False else: continue m = re.match(r'\s*\}\s*(\w+)', line) if m: enumname = m.group(1) enumindex+=1; return 1; m = re.match(r'\s*\}', line) if m: enumindex+=1; looking_for_name = True continue m = re.match(r'''\s* (\w+)\s* # name (?:=( # value \s*\w+\s*\(.*\)\s* # macro with multiple args | # OR (?:[^,/]|/(?!\*))* # anything but a comma or comment ))?,?\s* (?:/\*< # options (([^*]|\*(?!/))*) >\s*\*/)?,? \s*$''', line, flags=re.X) if m: groups = m.groups() name = groups[0] value = None options = None if len(groups) > 1: value = groups[1] if len(groups) > 2: options = groups[2] if flags is None and value is not None and '<<' in value: seenbitshift = 1 if options is not None: options = parse_trigraph(options) if 'skip' in options: entries.append((name, value, options['nick'])); else: entries.append((name, value)) elif re.match('s*\#', line): pass else: sys.exit("Failed to parse %s." % file_name) return False def print_version(): print("glib-mkenums version glib-" + version) print("glib-mkenums comes with ABSOLUTELY NO WARRANTY.") print("You may redistribute copies of glib-mkenums under the terms of") print("the GNU General Public License which can be found in the") print("GLib source package. Sources, examples and contact") print("information are available at http://www.gtk.org") sys.exit(0) help_epilog = '''Production text substitutions: \u0040EnumName\u0040 PrefixTheXEnum \u0040enum_name\u0040 prefix_the_xenum \u0040ENUMNAME\u0040 PREFIX_THE_XENUM \u0040ENUMSHORT\u0040 THE_XENUM \u0040ENUMPREFIX\u0040 PREFIX \u0040VALUENAME\u0040 PREFIX_THE_XVALUE \u0040valuenick\u0040 the-xvalue \u0040valuenum\u0040 the integer value (limited support, Since: 2.26) \u0040type\u0040 either enum or flags \u0040Type\u0040 either Enum or Flags \u0040TYPE\u0040 either ENUM or FLAGS \u0040filename\u0040 name of current input file \u0040basename\u0040 base name of the current input file (Since: 2.22) ''' # production variables: idprefix = "" # "G", "Gtk", etc symprefix = "" # "g", "gtk", etc, if not just lc($idprefix) fhead = "" # output file header fprod = "" # per input file production ftail = "" # output file trailer eprod = "" # per enum text (produced prior to value itarations) vhead = "" # value header, produced before iterating over enum values vprod = "" # value text, produced for each enum value vtail = "" # value tail, produced after iterating over enum values comment_tmpl = "" # comment template def read_template_file(file): global idprefix, symprefix, fhead, fprod, ftail, eprod, vhead, vprod, vtail, comment_tmpl tmpl = {'file-header': fhead, 'file-production': fprod, 'file-tail': ftail, 'enumeration-production': eprod, 'value-header': vhead, 'value-production': vprod, 'value-tail': vtail, 'comment': comment_tmpl, } in_ = 'junk' ifile = open(file) for line in ifile: m = re.match(r'\/\*\*\*\s+(BEGIN|END)\s+([\w-]+)\s+\*\*\*\/', line) if m: if in_ == 'junk' and m.group(1) == 'BEGIN' and m.group(2) in tmpl: in_ = m.group(2) continue elif in_ == m.group(2) and m.group(1) == 'END' and m.group(2) in tmpl: in_ = 'junk' continue else: sys.exit("Malformed template file " + file); if in_ != 'junk': tmpl[in_] += line if in_ != 'junk': sys.exit("Malformed template file " + file) fhead = tmpl['file-header'] fprod = tmpl['file-production'] ftail = tmpl['file-tail'] eprod = tmpl['enumeration-production'] vhead = tmpl['value-header'] vprod = tmpl['value-production'] vtail = tmpl['value-tail'] comment_tmpl = tmpl['comment'] # default to C-style comments if comment_tmpl == "": comment_tmpl = "/* \u0040comment\u0040 */" if len(sys.argv) == 1: usage() parser = argparse.ArgumentParser(epilog=help_epilog, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('--identifier-prefix', default='', dest='idprefix', help='Identifier prefix') parser.add_argument('--symbol-prefix', default='', dest='symprefix', help='symbol-prefix') parser.add_argument('--fhead', default='', dest='fhead', help='Output file header') parser.add_argument('--ftail', default='', dest='ftail', help='Per input file production') parser.add_argument('--eprod', default='', dest='eprod', help='Per enum text (produced prior to value iterations)') parser.add_argument('--vhead', default='', dest='vhead', help='Value header, produced before iterating over enum values') parser.add_argument('--vprod', default='', dest='vprod', help='Value text, produced for each enum value.') parser.add_argument('--vtail', default='', dest='vtail', help='Value tail, produced after iterating over enum values') parser.add_argument('--comments', default='', dest='comment_tmpl', help='Comment structure') parser.add_argument('--template', default='', dest='template', help='Template file') parser.add_argument('--output', default=None, dest='output') parser.add_argument('--version', '-v', default=False, action='store_true', dest='version', help='Print version informations') parser.add_argument('args', nargs='*') options = parser.parse_args() if options.version: print_version() if options.template != '': read_template_file(options.template) idprefix += options.idprefix symprefix += options.symprefix fhead += options.fhead ftail += options.ftail eprod += options.eprod vhead += options.vhead vprod += options.vprod vtail += options.vtail if options.comment_tmpl != '': comment_tmpl = options.comment_tmpl output = options.output if output is not None: (out_dir, out_fn) = os.path.split(options.output) out_suffix = '_' + os.path.splitext(out_fn)[1] if out_dir == '': out_dir = '.' tmpfile = tempfile.NamedTemporaryFile(dir=out_dir, delete=False) output_stream = tmpfile else: tmpfile = None # put auto-generation comment comment = comment_tmpl.replace('\u0040comment\u0040', 'Generated data (by glib-mkenums)') write_output("\n" + comment + '\n') def replace_specials(prod): prod = prod.replace(r'\\a', r'\a') prod = prod.replace(r'\\b', r'\b') prod = prod.replace(r'\\t', r'\t') prod = prod.replace(r'\\n', r'\n') prod = prod.replace(r'\\f', r'\f') prod = prod.replace(r'\\r', r'\r') prod = prod.rstrip() return prod if len(fhead) > 0: prod = fhead; base = os.path.basename(options.args[0]) prod = prod.replace('\u0040filename\u0040', options.args[0]) prod = prod.replace('\u0040basename\u0040', base) prod = replace_specials(prod) write_output(prod) def process_file(curfilename): global entries, flags, seenbitshift, enum_prefix firstenum = True curfile = open(curfilename) for line in curfile: # read lines until we have no open comments while re.search(r'/\*([^*]|\*(?!/))*$', line): line = curfile.readline() # strip comments w/o options line = re.sub(r'''/\*(?!<) ([^*]+|\*(?!/))* \*/''', '', line) # ignore forward declarations if re.match(r'\s*typedef\s+enum.*;', line): continue m = re.match(r'''\s*typedef\s+enum\s* ({)?\s* (?:/\*< (([^*]|\*(?!/))*) >\s*\*/)? \s*({)?''', line, flags=re.X) if m: groups = m.groups() if len(groups) >= 2 and groups[1] is not None: options = parse_trigraph(groups[1]); if 'skip' in options: continue enum_prefix = options.get('prefix', None) flags = options.get('flags', None) option_lowercase_name = options.get('lowercase_name', None) option_underscore_name = options.get('underscore_name', None) else: enum_prefix = None flags = None option_lowercase_name = None option_underscore_name = None if option_lowercase_name is not None: if option_underscore_name is not None: print("$0: $ARGV:$.: lowercase_name overriden with underscore_name", file=sys.stderr) option_lowercase_name = None else: print("$0: $ARGV:$.: lowercase_name is deprecated, use underscore_name", file=sys.stderr) # Didn't have trailing '{' look on next lines if groups[0] is None and (len(groups) < 4 or groups[3] is None): while True: line = curfile.readline() if re.match(r'\s*\{', line): break seenbitshift = 0; entries = []; # Now parse the entries parse_entries(curfile, curfilename); # figure out if this was a flags or enums enumeration if flags is None: flags = seenbitshift # Autogenerate a prefix if enum_prefix is None: for entry in entries: if len(entry) < 3 or entry[2] is None: name = entry[0]; if enum_prefix is not None: enum_prefix = os.path.commonprefix([name, enum_prefix]) else: enum_prefix = name; if enum_prefix is None: enum_prefix = "" else: # Trim so that it ends in an underscore enum_prefix = re.sub(r'_[^_]*$', '_', enum_prefix) else: # canonicalize user defined prefixes enum_prefix = enum_prefix.upper() enum_prefix = enum_prefix.replace('-', '_') enum_prefix = re.sub(r'(.*)([^_])$', r'\1\2_') fixed_entries = [] for e in entries: name = e[0] num = e[1] if len(e) < 3 or e[2] is None: nick = re.sub(r'^' + enum_prefix, '', name) nick = nick.replace('_', '-').lower() e = (name, num, nick) fixed_entries.append(e) entries = fixed_entries # Spit out the output if option_underscore_name is not None: enumlong = option_underscore_name.upper() enumsym = option_underscore_name.lower() enumshort = re.sub(r'^[A-Z][A-Z0-9]*_', '', enumlong) enumname_prefix = re.sub('_' + enumshort + '$', '', enumlong) elif symprefix == '' and idprefix == '': # enumname is e.g. GMatchType enspace = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname) enumshort = re.sub(r'^[A-Z][a-z]*', '', enumname) enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort) enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort) enumshort = enumshort.upper() enumname_prefix = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname).upper() enumlong = enspace.upper() + "_" + enumshort enumsym = enspace.lower() + "_" + enumshort.lower() if option_lowercase_name is not None: enumsym = option_lowercase_name; else: enumshort = enumname; if idprefix: enumshort = re.sub(r'^' + idprefix, '', enumshort) else: enumshort = re.sub(r'/^[A-Z][a-z]*', '', enumshort) enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort) enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort) enumshort = enumshort.upper(); if symprefix is not None: enumname_prefix = symprefix.upper() else: enumname_prefix = idprefix.upper() enumlong = enumname_prefix + "_" + enumshort enumsym = enumlong.lower() if firstenum: firstenum = False if len(fprod) > 0: prod = fprod; base = os.path.basename(curfilename); prod = prod.replace('\u0040filename\u0040', curfilename) prod = prod.replace('\u0040basename\u0040', base) prod = replace_specials(prod) write_output(prod) if len(eprod) > 0: prod = eprod; prod = prod.replace('\u0040enum_name\u0040', enumsym) prod = prod.replace('\u0040EnumName\u0040', enumname) prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort) prod = prod.replace('\u0040ENUMNAME\u0040', enumlong) prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix) if flags: prod =~ prod.replace('\u0040type\u0040', 'flags') else: prod = prod.replace('\u0040type\u0040', 'enum') if flags: prod = prod.replace('\u0040Type\u0040', 'Flags') else: prod = prod.replace('\u0040Type\u0040', 'Enum') if flags: prod = prod.replace('\u0040TYPE\u0040', 'FLAGS') else: prod = prod.replace('\u0040TYPE\u0040', 'ENUM') prod = replace_specials(prod) write_output(prod) if len(vhead) > 0: prod = vhead; prod = prod.replace('\u0040enum_name\u0040', enumsym) prod = prod.replace('\u0040EnumName\u0040', enumname) prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort) prod = prod.replace('\u0040ENUMNAME\u0040', enumlong) prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix) if flags: prod = prod.replace('\u0040type\u0040', 'flags') else: prod = prod.replace('\u0040type\u0040', 'enum') if flags: prod = prod.replace('\u0040Type\u0040', 'Flags') else: prod = prod.replace('\u0040Type\u0040', 'Enum') if flags: prod = prod.replace('\u0040TYPE\u0040', 'FLAGS') else: prod = prod.replace('\u0040TYPE\u0040', 'ENUM') prod = replace_specials(prod) write_output(prod) if len(vprod) > 0: prod = vprod; next_num = 0 prod = replace_specials(prod) for name, num, nick in entries: tmp_prod = prod if '\u0040valuenum\u0040' in prod: # only attempt to eval the value if it is requested # this prevents us from throwing errors otherwise if num is not None: # use sandboxed evaluation as a reasonable # approximation to C constant folding inum = eval(num, {}, {}); # make sure it parsed to an integer if not isinstance(inum, int): sys.exit("Unable to parse enum value '%s'" % num) num = inum else: num = next_num tmp_prod = tmp_prod.replace('\u0040valuenum\u0040', str(num)) next_num = int(num) + 1 tmp_prod = tmp_prod.replace('\u0040VALUENAME\u0040', name) tmp_prod = tmp_prod.replace('\u0040valuenick\u0040', nick) if flags: tmp_prod = tmp_prod.replace('\u0040type\u0040', 'flags') else: tmp_prod = tmp_prod.replace('\u0040type\u0040', 'enum') if flags: tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Flags') else: tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Enum') if flags: tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'FLAGS') else: tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'ENUM') tmp_prod = tmp_prod.rstrip() write_output(tmp_prod) if len(vtail) > 0: prod = vtail prod = prod.replace('\u0040enum_name\u0040', enumsym) prod = prod.replace('\u0040EnumName\u0040', enumname) prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort) prod = prod.replace('\u0040ENUMNAME\u0040', enumlong) prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix) if flags: prod = prod.replace('\u0040type\u0040', 'flags') else: prod = prod.replace('\u0040type\u0040', 'enum') if flags: prod = prod.replace('\u0040Type\u0040', 'Flags') else: prod = prod.replace('\u0040Type\u0040', 'Enum') if flags: prod = prod.replace('\u0040TYPE\u0040', 'FLAGS') else: prod = prod.replace('\u0040TYPE\u0040', 'ENUM') prod = replace_specials(prod) write_output(prod) for fname in options.args: process_file(fname) if len(ftail) > 0: prod = ftail base = os.path.basename(options.args[-1]) # FIXME, wrong prod = prod.replace('\u0040filename\u0040', 'ARGV') # wrong too prod = prod.replace('\u0040basename\u0040', base) prod = replace_specials(prod) write_output(prod) # put auto-generation comment comment = comment_tmpl; comment = comment.replace('\u0040comment\u0040', 'Generated data ends here') write_output("\n" + comment + "\n") if tmpfile is not None: tmpfilename = tmpfile.name tmpfile.close() os.unlink(options.output) os.rename(tmpfilename, options.output)