mkenums: Skip files not found
[glib.git] / gobject / glib-mkenums.in
blobc59d17cee5e53e38a17d24cef47b40823df99eba
1 #!/usr/bin/env @PYTHON@
3 # If the code below looks horrible and unpythonic, do not panic.
5 # It is.
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
12 import argparse
13 import os
14 import re
15 import sys
16 import tempfile
18 output_stream = sys.stdout
20 def write_output(output):
21     global output_stream
22     print(output, file=output_stream)
24 version = '@GLIB_VERSION@'
26 # glib-mkenums.py
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):
50     result = {}
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)
56         assert m is not None
57         groups = m.groups()
58         key = groups[0]
59         if len(groups) > 1:
60             val = groups[1]
61         else:
62             val = 1
63         result[key] = val
64     return result
66 def parse_entries(file, file_name):
67     global entries, enumindex, enumname, seenbitshift, flags
68     looking_for_name = False
70     for line in file:
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'''/\*(?!<)
77             ([^*]+|\*(?!/))*
78            \*/''', '', line, flags=re.X)
80         line = line.rstrip()
82         # skip empty lines
83         if len(line.strip()) == 0:
84             continue
86         if looking_for_name:
87             m = re.match(r'\s*(\w+)', line)
88             if m:
89                 enumname = m.group(1)
90                 return True
92         # Handle include files
93         m = re.match(r'\#include\s*<([^>]*)>', line)
94         if m:
95             newfilename = os.path.join("..", m.group(1))
96             newfile = open(newfilename)
98             if not parse_entries(newfile, newfilename):
99                 return False
100             else:
101                 continue
103         m = re.match(r'\s*\}\s*(\w+)', line)
104         if m:
105             enumname = m.group(1)
106             enumindex += 1
107             return 1
109         m = re.match(r'\s*\}', line)
110         if m:
111             enumindex += 1
112             looking_for_name = True
113             continue
115         m = re.match(r'''\s*
116               (\w+)\s*                   # name
117               (?:=(                      # value
118                    \s*\w+\s*\(.*\)\s*       # macro with multiple args
119                    |                        # OR
120                    (?:[^,/]|/(?!\*))*       # anything but a comma or comment
121                   ))?,?\s*
122               (?:/\*<                    # options
123                 (([^*]|\*(?!/))*)
124                >\s*\*/)?,?
125               \s*$''', line, flags=re.X)
126         if m:
127             groups = m.groups()
128             name = groups[0]
129             value = None
130             options = None
131             if len(groups) > 1:
132                 value = groups[1]
133             if len(groups) > 2:
134                 options = groups[2]
135             if not flags and value is not None and '<<' in value:
136                 seenbitshift = 1
138             if options is not None:
139                 options = parse_trigraph(options)
140                 if 'skip' not in options:
141                     entries.append((name, value, options['nick']))
142             else:
143                 entries.append((name, value))
144         elif re.match(r's*\#', line):
145             pass
146         else:
147             sys.exit("Failed to parse %s." % file_name)
148     return False
150 def print_version():
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")
157     sys.exit(0)
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,
192             'file-tail': ftail,
193             'enumeration-production': eprod,
194             'value-header': vhead,
195             'value-production': vprod,
196             'value-tail': vtail,
197             'comment': comment_tmpl,
198            }
199     in_ = 'junk'
201     ifile = open(file)
202     for line in ifile:
203         m = re.match(r'\/\*\*\*\s+(BEGIN|END)\s+([\w-]+)\s+\*\*\*\/', line)
204         if m:
205             if in_ == 'junk' and m.group(1) == 'BEGIN' and m.group(2) in tmpl:
206                 in_ = m.group(2)
207                 continue
208             elif in_ == m.group(2) and m.group(1) == 'END' and m.group(2) in tmpl:
209                 in_ = 'junk'
210                 continue
211             else:
212                 sys.exit("Malformed template file " + file)
214         if in_ != 'junk':
215             tmpl[in_] += line
217     if in_ != 'junk':
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()
265 if options.version:
266     print_version()
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:
284 #   glib-mkenums \
285 #     --fhead ... \
286 #     --template a-template-file.c.in \
287 #     --ftail ...
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]
318     if out_dir == '':
319         out_dir = '.'
320     tmpfile = tempfile.NamedTemporaryFile(dir=out_dir, delete=False)
321     output_stream = tmpfile
322 else:
323     tmpfile = None
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')
336     prod = prod.rstrip()
337     return prod
339 if len(fhead) > 0:
340     prod = fhead
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)
346     write_output(prod)
348 def process_file(curfilename):
349     global entries, flags, seenbitshift, enum_prefix
350     firstenum = True
352     try:
353         curfile = open(curfilename)
354     except FileNotFoundError:
355         sys.stderr.write('WARNING: No file "{}" found.'.format(curfilename))
356         return
358     for line in curfile:
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'''/\*(?!<)
365            ([^*]+|\*(?!/))*
366            \*/''', '', line)
368         # ignore forward declarations
369         if re.match(r'\s*typedef\s+enum.*;', line):
370             continue
372         m = re.match(r'''\s*typedef\s+enum\s*
373                ({)?\s*
374                (?:/\*<
375                  (([^*]|\*(?!/))*)
376                 >\s*\*/)?
377                \s*({)?''', line, flags=re.X)
378         if m:
379             groups = m.groups()
380             if len(groups) >= 2 and groups[1] is not None:
381                 options = parse_trigraph(groups[1])
382                 if 'skip' in options:
383                     continue
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)
388             else:
389                 enum_prefix = None
390                 flags = False
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
398                 else:
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):
403                 while True:
404                     line = curfile.readline()
405                     if re.match(r'\s*\{', line):
406                         break
408             seenbitshift = 0
409             entries = []
411             # Now parse the entries
412             parse_entries(curfile, curfilename)
414             # figure out if this was a flags or enums enumeration
415             if not flags:
416                 flags = seenbitshift
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:
422                         name = entry[0]
423                         if enum_prefix is not None:
424                             enum_prefix = os.path.commonprefix([name, enum_prefix])
425                         else:
426                             enum_prefix = name
427                 if enum_prefix is None:
428                     enum_prefix = ""
429                 else:
430                     # Trim so that it ends in an underscore
431                     enum_prefix = re.sub(r'_[^_]*$', '_', enum_prefix)
432             else:
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)
438             fixed_entries = []
439             for e in entries:
440                 name = e[0]
441                 num = e[1]
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
472             else:
473                 enumshort = enumname
474                 if idprefix:
475                     enumshort = re.sub(r'^' + idprefix, '', enumshort)
476                 else:
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()
483                 if symprefix:
484                     enumname_prefix = symprefix.upper()
485                 else:
486                     enumname_prefix = idprefix.upper()
488                 enumlong = enumname_prefix + "_" + enumshort
489                 enumsym = enumlong.lower()
491             if firstenum:
492                 firstenum = False
494                 if len(fprod) > 0:
495                     prod = fprod
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)
502                     write_output(prod)
504             if len(eprod) > 0:
505                 prod = eprod
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)
512                 if flags:
513                     prod = prod.replace('\u0040type\u0040', 'flags')
514                 else:
515                     prod = prod.replace('\u0040type\u0040', 'enum')
516                 if flags:
517                     prod = prod.replace('\u0040Type\u0040', 'Flags')
518                 else:
519                     prod = prod.replace('\u0040Type\u0040', 'Enum')
520                 if flags:
521                     prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
522                 else:
523                     prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
524                 prod = replace_specials(prod)
525                 write_output(prod)
527             if len(vhead) > 0:
528                 prod = vhead
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)
534                 if flags:
535                     prod = prod.replace('\u0040type\u0040', 'flags')
536                 else:
537                     prod = prod.replace('\u0040type\u0040', 'enum')
538                 if flags:
539                     prod = prod.replace('\u0040Type\u0040', 'Flags')
540                 else:
541                     prod = prod.replace('\u0040Type\u0040', 'Enum')
542                 if flags:
543                     prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
544                 else:
545                     prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
546                 prod = replace_specials(prod)
547                 write_output(prod)
549             if len(vprod) > 0:
550                 prod = vprod
551                 next_num = 0
553                 prod = replace_specials(prod)
554                 for name, num, nick in entries:
555                     tmp_prod = prod
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
560                         if num is not None:
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)
568                             num = inum
569                         else:
570                             num = next_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)
577                     if flags:
578                         tmp_prod = tmp_prod.replace('\u0040type\u0040', 'flags')
579                     else:
580                         tmp_prod = tmp_prod.replace('\u0040type\u0040', 'enum')
581                     if flags:
582                         tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Flags')
583                     else:
584                         tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Enum')
585                     if flags:
586                         tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'FLAGS')
587                     else:
588                         tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'ENUM')
589                     tmp_prod = tmp_prod.rstrip()
591                     write_output(tmp_prod)
593             if len(vtail) > 0:
594                 prod = vtail
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)
600                 if flags:
601                     prod = prod.replace('\u0040type\u0040', 'flags')
602                 else:
603                     prod = prod.replace('\u0040type\u0040', 'enum')
604                 if flags:
605                     prod = prod.replace('\u0040Type\u0040', 'Flags')
606                 else:
607                     prod = prod.replace('\u0040Type\u0040', 'Enum')
608                 if flags:
609                     prod = prod.replace('\u0040TYPE\u0040', 'FLAGS')
610                 else:
611                     prod = prod.replace('\u0040TYPE\u0040', 'ENUM')
612                 prod = replace_specials(prod)
613                 write_output(prod)
615 for fname in options.args:
616     process_file(fname)
618 if len(ftail) > 0:
619     prod = ftail
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)
625     write_output(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
634     tmpfile.close()
635     os.unlink(options.output)
636     os.rename(tmpfilename, options.output)