blob: 749af89c6901a2748725e101b814252130bc2147 [file] [log] [blame]
William Robertsc950a352016-03-04 18:12:29 -08001#!/usr/bin/env python
William Roberts11c29282016-04-09 10:32:30 -07002"""Generates config files for Android file system properties.
William Robertsc950a352016-03-04 18:12:29 -08003
William Roberts11c29282016-04-09 10:32:30 -07004This script is used for generating configuration files for configuring
5Android filesystem properties. Internally, its composed of a plug-able
6interface to support the understanding of new input and output parameters.
7
8Run the help for a list of supported plugins and their capabilities.
9
10Further documentation can be found in the README.
11"""
12
13import argparse
William Robertsc950a352016-03-04 18:12:29 -080014import ConfigParser
15import re
16import sys
William Roberts11c29282016-04-09 10:32:30 -070017import textwrap
William Robertsc950a352016-03-04 18:12:29 -080018
19
William Roberts11c29282016-04-09 10:32:30 -070020# Lowercase generator used to be inline with @staticmethod.
21class generator(object): # pylint: disable=invalid-name
22 """A decorator class to add commandlet plugins.
William Robertsc950a352016-03-04 18:12:29 -080023
William Roberts11c29282016-04-09 10:32:30 -070024 Used as a decorator to classes to add them to
25 the internal plugin interface. Plugins added
26 with @generator() are automatically added to
27 the command line.
William Robertsc950a352016-03-04 18:12:29 -080028
William Roberts11c29282016-04-09 10:32:30 -070029 For instance, to add a new generator
30 called foo and have it added just do this:
William Robertsc950a352016-03-04 18:12:29 -080031
William Roberts11c29282016-04-09 10:32:30 -070032 @generator("foo")
33 class FooGen(object):
34 ...
35 """
36 _generators = {}
William Robertsc950a352016-03-04 18:12:29 -080037
William Roberts11c29282016-04-09 10:32:30 -070038 def __init__(self, gen):
39 """
40 Args:
41 gen (str): The name of the generator to add.
William Robertsc950a352016-03-04 18:12:29 -080042
William Roberts11c29282016-04-09 10:32:30 -070043 Raises:
44 ValueError: If there is a similarly named generator already added.
William Robertsc950a352016-03-04 18:12:29 -080045
William Roberts11c29282016-04-09 10:32:30 -070046 """
47 self._gen = gen
William Robertsc950a352016-03-04 18:12:29 -080048
William Roberts11c29282016-04-09 10:32:30 -070049 if gen in generator._generators:
50 raise ValueError('Duplicate generator name: ' + gen)
William Robertsc950a352016-03-04 18:12:29 -080051
William Roberts11c29282016-04-09 10:32:30 -070052 generator._generators[gen] = None
William Robertsc950a352016-03-04 18:12:29 -080053
William Roberts11c29282016-04-09 10:32:30 -070054 def __call__(self, cls):
55
56 generator._generators[self._gen] = cls()
57 return cls
58
59 @staticmethod
60 def get():
61 """Gets the list of generators.
62
63 Returns:
64 The list of registered generators.
65 """
66 return generator._generators
William Robertsc950a352016-03-04 18:12:29 -080067
68
William Roberts11c29282016-04-09 10:32:30 -070069class AID(object):
70 """This class represents an Android ID or an AID.
William Robertsc950a352016-03-04 18:12:29 -080071
William Roberts11c29282016-04-09 10:32:30 -070072 Attributes:
73 identifier (str): The identifier name for a #define.
74 value (str) The User Id (uid) of the associate define.
75 found (str) The file it was found in, can be None.
76 normalized_value (str): Same as value, but base 10.
77 """
William Robertsc950a352016-03-04 18:12:29 -080078
William Roberts11c29282016-04-09 10:32:30 -070079 def __init__(self, identifier, value, found):
80 """
81 Args:
82 identifier: The identifier name for a #define <identifier>.
83 value: The value of the AID, aka the uid.
84 found (str): The file found in, not required to be specified.
William Robertsc950a352016-03-04 18:12:29 -080085
William Roberts11c29282016-04-09 10:32:30 -070086 Raises:
87 ValueError: if value is not a valid string number as processed by
88 int(x, 0)
89 """
90 self.identifier = identifier
91 self.value = value
92 self.found = found
93 self.normalized_value = str(int(value, 0))
William Robertsc950a352016-03-04 18:12:29 -080094
95
William Roberts11c29282016-04-09 10:32:30 -070096class FSConfig(object):
97 """Represents a filesystem config array entry.
William Robertsc950a352016-03-04 18:12:29 -080098
William Roberts11c29282016-04-09 10:32:30 -070099 Represents a file system configuration entry for specifying
100 file system capabilities.
William Robertsc950a352016-03-04 18:12:29 -0800101
William Roberts11c29282016-04-09 10:32:30 -0700102 Attributes:
103 mode (str): The mode of the file or directory.
104 user (str): The uid or #define identifier (AID_SYSTEM)
105 group (str): The gid or #define identifier (AID_SYSTEM)
106 caps (str): The capability set.
107 filename (str): The file it was found in.
108 """
William Robertsc950a352016-03-04 18:12:29 -0800109
William Roberts11c29282016-04-09 10:32:30 -0700110 def __init__(self, mode, user, group, caps, path, filename):
111 """
112 Args:
113 mode (str): The mode of the file or directory.
114 user (str): The uid or #define identifier (AID_SYSTEM)
115 group (str): The gid or #define identifier (AID_SYSTEM)
116 caps (str): The capability set as a list.
117 filename (str): The file it was found in.
118 """
119 self.mode = mode
120 self.user = user
121 self.group = group
122 self.caps = caps
123 self.path = path
124 self.filename = filename
125
126
127class FSConfigFileParser(object):
128 """Parses a config.fs ini format file.
129
130 This class is responsible for parsing the config.fs ini format files.
131 It collects and checks all the data in these files and makes it available
132 for consumption post processed.
133 """
134 # from system/core/include/private/android_filesystem_config.h
135 _AID_OEM_RESERVED_RANGES = [
136 (2900, 2999),
137 (5000, 5999),
138 ]
139
140 _AID_MATCH = re.compile('AID_[a-zA-Z]+')
141
142 def __init__(self, config_files):
143 """
144 Args:
145 config_files ([str]): The list of config.fs files to parse.
146 Note the filename is not important.
147 """
148
149 self._files = []
150 self._dirs = []
151 self._aids = []
152
153 self._seen_paths = {}
154 # (name to file, value to aid)
155 self._seen_aids = ({}, {})
156
157 self._config_files = config_files
158
159 for config_file in self._config_files:
160 self._parse(config_file)
161
162 def _parse(self, file_name):
163 """Parses and verifies config.fs files. Internal use only.
164
165 Args:
166 file_name (str): The config.fs (PythonConfigParser file format)
167 file to parse.
168
169 Raises:
170 Anything raised by ConfigParser.read()
171 """
172
173 # Separate config parsers for each file found. If you use
174 # read(filenames...) later files can override earlier files which is
175 # not what we want. Track state across files and enforce with
176 # _handle_dup(). Note, strict ConfigParser is set to true in
177 # Python >= 3.2, so in previous versions same file sections can
178 # override previous
179 # sections.
William Robertsc950a352016-03-04 18:12:29 -0800180
181 config = ConfigParser.ConfigParser()
182 config.read(file_name)
183
William Roberts11c29282016-04-09 10:32:30 -0700184 for section in config.sections():
William Robertsc950a352016-03-04 18:12:29 -0800185
William Roberts11c29282016-04-09 10:32:30 -0700186 if FSConfigFileParser._AID_MATCH.match(
187 section) and config.has_option(section, 'value'):
188 FSConfigFileParser._handle_dup('AID', file_name, section,
189 self._seen_aids[0])
190 self._seen_aids[0][section] = file_name
191 self._handle_aid(file_name, section, config)
William Robertsc950a352016-03-04 18:12:29 -0800192 else:
William Roberts11c29282016-04-09 10:32:30 -0700193 FSConfigFileParser._handle_dup('path', file_name, section,
194 self._seen_paths)
195 self._seen_paths[section] = file_name
196 self._handle_path(file_name, section, config)
William Robertsc950a352016-03-04 18:12:29 -0800197
William Roberts11c29282016-04-09 10:32:30 -0700198 # sort entries:
199 # * specified path before prefix match
200 # ** ie foo before f*
201 # * lexicographical less than before other
202 # ** ie boo before foo
203 # Given these paths:
204 # paths=['ac', 'a', 'acd', 'an', 'a*', 'aa', 'ac*']
205 # The sort order would be:
206 # paths=['a', 'aa', 'ac', 'acd', 'an', 'ac*', 'a*']
207 # Thus the fs_config tools will match on specified paths before
208 # attempting prefix, and match on the longest matching prefix.
209 self._files.sort(key=FSConfigFileParser._file_key)
William Robertsc950a352016-03-04 18:12:29 -0800210
William Roberts11c29282016-04-09 10:32:30 -0700211 # sort on value of (file_name, name, value, strvalue)
212 # This is only cosmetic so AIDS are arranged in ascending order
213 # within the generated file.
214 self._aids.sort(key=lambda item: item.normalized_value)
William Robertsc950a352016-03-04 18:12:29 -0800215
William Roberts11c29282016-04-09 10:32:30 -0700216 def _handle_aid(self, file_name, section_name, config):
217 """Verifies an AID entry and adds it to the aid list.
William Robertsc950a352016-03-04 18:12:29 -0800218
William Roberts11c29282016-04-09 10:32:30 -0700219 Calls sys.exit() with a descriptive message of the failure.
220
221 Args:
222 file_name (str): The filename of the config file being parsed.
223 section_name (str): The section name currently being parsed.
224 config (ConfigParser): The ConfigParser section being parsed that
225 the option values will come from.
226 """
227
228 def error_message(msg):
229 """Creates an error message with current parsing state."""
230 return '{} for: "{}" file: "{}"'.format(msg, section_name,
231 file_name)
232
233 value = config.get(section_name, 'value')
234
235 if not value:
236 sys.exit(error_message('Found specified but unset "value"'))
237
238 try:
239 aid = AID(section_name, value, file_name)
240 except ValueError:
241 sys.exit(
242 error_message('Invalid "value", not aid number, got: \"%s\"' %
243 value))
244
245 # Values must be within OEM range.
246 if not any(lower <= int(aid.value, 0) <= upper
247 for (lower, upper
248 ) in FSConfigFileParser._AID_OEM_RESERVED_RANGES):
249 emsg = '"value" not in valid range %s, got: %s'
250 emsg = emsg % (str(FSConfigFileParser._AID_OEM_RESERVED_RANGES),
251 value)
252 sys.exit(error_message(emsg))
253
254 # use the normalized int value in the dict and detect
255 # duplicate definitions of the same value
256 if aid.normalized_value in self._seen_aids[1]:
257 # map of value to aid name
258 aid = self._seen_aids[1][aid.normalized_value]
259
260 # aid name to file
261 file_name = self._seen_aids[0][aid]
262
263 emsg = 'Duplicate AID value "%s" found on AID: "%s".' % (
264 value, self._seen_aids[1][aid.normalized_value])
265 emsg += ' Previous found in file: "%s."' % file_name
266 sys.exit(error_message(emsg))
267
268 self._seen_aids[1][aid.normalized_value] = section_name
269
270 # Append aid tuple of (AID_*, base10(value), _path(value))
271 # We keep the _path version of value so we can print that out in the
272 # generated header so investigating parties can identify parts.
273 # We store the base10 value for sorting, so everything is ascending
274 # later.
275 self._aids.append(aid)
276
277 def _handle_path(self, file_name, section_name, config):
278 """Add a file capability entry to the internal list.
279
280 Handles a file capability entry, verifies it, and adds it to
281 to the internal dirs or files list based on path. If it ends
282 with a / its a dir. Internal use only.
283
284 Calls sys.exit() on any validation error with message set.
285
286 Args:
287 file_name (str): The current name of the file being parsed.
288 section_name (str): The name of the section to parse.
289 config (str): The config parser.
290 """
291
292 mode = config.get(section_name, 'mode')
293 user = config.get(section_name, 'user')
294 group = config.get(section_name, 'group')
295 caps = config.get(section_name, 'caps')
296
297 errmsg = ('Found specified but unset option: \"%s" in file: \"' +
298 file_name + '\"')
299
300 if not mode:
301 sys.exit(errmsg % 'mode')
302
303 if not user:
304 sys.exit(errmsg % 'user')
305
306 if not group:
307 sys.exit(errmsg % 'group')
308
309 if not caps:
310 sys.exit(errmsg % 'caps')
311
312 caps = caps.split()
313
314 tmp = []
315 for cap in caps:
316 try:
317 # test if string is int, if it is, use as is.
318 int(cap, 0)
319 tmp.append('(' + cap + ')')
320 except ValueError:
321 tmp.append('(1ULL << CAP_' + cap.upper() + ')')
322
323 caps = tmp
324
325 if len(mode) == 3:
326 mode = '0' + mode
327
328 try:
329 int(mode, 8)
330 except ValueError:
331 sys.exit('Mode must be octal characters, got: "%s"' % mode)
332
333 if len(mode) != 4:
334 sys.exit('Mode must be 3 or 4 characters, got: "%s"' % mode)
335
336 caps_str = '|'.join(caps)
337
338 entry = FSConfig(mode, user, group, caps_str, section_name, file_name)
339 if section_name[-1] == '/':
340 self._dirs.append(entry)
341 else:
342 self._files.append(entry)
343
344 @property
345 def files(self):
346 """Get the list of FSConfig file entries.
347
348 Returns:
349 a list of FSConfig() objects for file paths.
350 """
351 return self._files
352
353 @property
354 def dirs(self):
355 """Get the list of FSConfig dir entries.
356
357 Returns:
358 a list of FSConfig() objects for directory paths.
359 """
360 return self._dirs
361
362 @property
363 def aids(self):
364 """Get the list of AID entries.
365
366 Returns:
367 a list of AID() objects.
368 """
369 return self._aids
370
371 @staticmethod
372 def _file_key(fs_config):
373 """Used as the key paramter to sort.
374
375 This is used as a the function to the key parameter of a sort.
376 it wraps the string supplied in a class that implements the
377 appropriate __lt__ operator for the sort on path strings. See
378 StringWrapper class for more details.
379
380 Args:
381 fs_config (FSConfig): A FSConfig entry.
382
383 Returns:
384 A StringWrapper object
385 """
386
387 # Wrapper class for custom prefix matching strings
388 class StringWrapper(object):
389 """Wrapper class used for sorting prefix strings.
390
391 The algorithm is as follows:
392 - specified path before prefix match
393 - ie foo before f*
394 - lexicographical less than before other
395 - ie boo before foo
396
397 Given these paths:
398 paths=['ac', 'a', 'acd', 'an', 'a*', 'aa', 'ac*']
399 The sort order would be:
400 paths=['a', 'aa', 'ac', 'acd', 'an', 'ac*', 'a*']
401 Thus the fs_config tools will match on specified paths before
402 attempting prefix, and match on the longest matching prefix.
403 """
404
405 def __init__(self, path):
406 """
407 Args:
408 path (str): the path string to wrap.
409 """
410 self.is_prefix = path[-1] == '*'
411 if self.is_prefix:
412 self.path = path[:-1]
413 else:
414 self.path = path
415
416 def __lt__(self, other):
417
418 # if were both suffixed the smallest string
419 # is 'bigger'
420 if self.is_prefix and other.is_prefix:
421 result = len(self.path) > len(other.path)
422 # If I am an the suffix match, im bigger
423 elif self.is_prefix:
424 result = False
425 # If other is the suffix match, he's bigger
426 elif other.is_prefix:
427 result = True
428 # Alphabetical
429 else:
430 result = self.path < other.path
431 return result
432
433 return StringWrapper(fs_config.path)
434
435 @staticmethod
436 def _handle_dup(name, file_name, section_name, seen):
437 """Tracks and detects duplicates. Internal use only.
438
439 Calls sys.exit() on a duplicate.
440
441 Args:
442 name (str): The name to use in the error reporting. The pretty
443 name for the section.
444 file_name (str): The file currently being parsed.
445 section_name (str): The name of the section. This would be path
446 or identifier depending on what's being parsed.
447 seen (dict): The dictionary of seen things to check against.
448 """
449 if section_name in seen:
450 dups = '"' + seen[section_name] + '" and '
451 dups += file_name
452 sys.exit('Duplicate %s "%s" found in files: %s' %
453 (name, section_name, dups))
454
455 seen[section_name] = file_name
456
457
458class BaseGenerator(object):
459 """Interface for Generators.
460
461 Base class for generators, generators should implement
462 these method stubs.
463 """
464
465 def add_opts(self, opt_group):
466 """Used to add per-generator options to the command line.
467
468 Args:
469 opt_group (argument group object): The argument group to append to.
470 See the ArgParse docs for more details.
471 """
472
473 raise NotImplementedError("Not Implemented")
474
475 def __call__(self, args):
476 """This is called to do whatever magic the generator does.
477
478 Args:
479 args (dict): The arguments from ArgParse as a dictionary.
480 ie if you specified an argument of foo in add_opts, access
481 it via args['foo']
482 """
483
484 raise NotImplementedError("Not Implemented")
485
486
487@generator('fsconfig')
488class FSConfigGen(BaseGenerator):
489 """Generates the android_filesystem_config.h file.
490
491 Output is used in generating fs_config_files and fs_config_dirs.
492 """
493
494 _GENERATED = textwrap.dedent("""\
495 /*
496 * THIS IS AN AUTOGENERATED FILE! DO NOT MODIFY
497 */
498 """)
499
500 _INCLUDE = '#include <private/android_filesystem_config.h>'
501
502 _DEFINE_NO_DIRS = '#define NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS'
503 _DEFINE_NO_FILES = '#define NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_FILES'
504
505 _DEFAULT_WARNING = (
506 '#warning No device-supplied android_filesystem_config.h,'
507 ' using empty default.')
508
509 # Long names.
510 # pylint: disable=invalid-name
511 _NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS_ENTRY = (
512 '{ 00000, AID_ROOT, AID_ROOT, 0,'
513 '"system/etc/fs_config_dirs" },')
514
515 _NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_FILES_ENTRY = (
516 '{ 00000, AID_ROOT, AID_ROOT, 0,'
517 '"system/etc/fs_config_files" },')
518
519 _IFDEF_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS = (
520 '#ifdef NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS')
521 # pylint: enable=invalid-name
522
523 _ENDIF = '#endif'
524
525 _OPEN_FILE_STRUCT = (
526 'static const struct fs_path_config android_device_files[] = {')
527
528 _OPEN_DIR_STRUCT = (
529 'static const struct fs_path_config android_device_dirs[] = {')
530
531 _CLOSE_FILE_STRUCT = '};'
532
533 _GENERIC_DEFINE = "#define %s\t%s"
534
535 _FILE_COMMENT = '// Defined in file: \"%s\"'
536
537 def add_opts(self, opt_group):
538
539 opt_group.add_argument(
540 'fsconfig', nargs='+', help='The list of fsconfig files to parse')
541
542 def __call__(self, args):
543
544 parser = FSConfigFileParser(args['fsconfig'])
545 FSConfigGen._generate(parser.files, parser.dirs, parser.aids)
546
547 @staticmethod
548 def _to_fs_entry(fs_config):
549 """
550 Given an FSConfig entry, converts it to a proper
551 array entry for the array entry.
552
553 { mode, user, group, caps, "path" },
554
555 Args:
556 fs_config (FSConfig): The entry to convert to
557 a valid C array entry.
558 """
559
560 # Get some short names
561 mode = fs_config.mode
562 user = fs_config.user
563 group = fs_config.group
564 fname = fs_config.filename
565 caps = fs_config.caps
566 path = fs_config.path
567
568 fmt = '{ %s, %s, %s, %s, "%s" },'
569
570 expanded = fmt % (mode, user, group, caps, path)
571
572 print FSConfigGen._FILE_COMMENT % fname
573 print ' ' + expanded
574
575 @staticmethod
576 def _generate(files, dirs, aids):
577 """Generates an OEM android_filesystem_config.h header file to stdout.
578
579 Args:
580 files ([FSConfig]): A list of FSConfig objects for file entries.
581 dirs ([FSConfig]): A list of FSConfig objects for directory
582 entries.
583 aids ([AIDS]): A list of AID objects for Android Id entries.
584 """
585 print FSConfigGen._GENERATED
586 print FSConfigGen._INCLUDE
William Robertsc950a352016-03-04 18:12:29 -0800587 print
588
William Roberts11c29282016-04-09 10:32:30 -0700589 are_dirs = len(dirs) > 0
590 are_files = len(files) > 0
591 are_aids = len(aids) > 0
William Robertsc950a352016-03-04 18:12:29 -0800592
William Roberts11c29282016-04-09 10:32:30 -0700593 if are_aids:
594 for aid in aids:
595 # use the preserved _path value
596 print FSConfigGen._FILE_COMMENT % aid.found
597 print FSConfigGen._GENERIC_DEFINE % (aid.identifier, aid.value)
William Robertsc950a352016-03-04 18:12:29 -0800598
William Roberts11c29282016-04-09 10:32:30 -0700599 print
William Robertsc950a352016-03-04 18:12:29 -0800600
601 if not are_dirs:
William Roberts11c29282016-04-09 10:32:30 -0700602 print FSConfigGen._DEFINE_NO_DIRS + '\n'
William Robertsc950a352016-03-04 18:12:29 -0800603
William Roberts11c29282016-04-09 10:32:30 -0700604 if not are_files:
605 print FSConfigGen._DEFINE_NO_FILES + '\n'
William Robertsc950a352016-03-04 18:12:29 -0800606
William Roberts11c29282016-04-09 10:32:30 -0700607 if not are_files and not are_dirs and not are_aids:
608 print FSConfigGen._DEFAULT_WARNING
609 return
William Robertsc950a352016-03-04 18:12:29 -0800610
William Roberts11c29282016-04-09 10:32:30 -0700611 if are_files:
612 print FSConfigGen._OPEN_FILE_STRUCT
613 for fs_config in files:
614 FSConfigGen._to_fs_entry(fs_config)
William Robertsc950a352016-03-04 18:12:29 -0800615
William Roberts11c29282016-04-09 10:32:30 -0700616 if not are_dirs:
617 print FSConfigGen._IFDEF_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS
618 print(
619 ' ' +
620 FSConfigGen._NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS_ENTRY)
621 print FSConfigGen._ENDIF
622 print FSConfigGen._CLOSE_FILE_STRUCT
William Robertsc950a352016-03-04 18:12:29 -0800623
William Roberts11c29282016-04-09 10:32:30 -0700624 if are_dirs:
625 print FSConfigGen._OPEN_DIR_STRUCT
626 for dir_entry in dirs:
627 FSConfigGen._to_fs_entry(dir_entry)
William Robertsc950a352016-03-04 18:12:29 -0800628
William Roberts11c29282016-04-09 10:32:30 -0700629 print FSConfigGen._CLOSE_FILE_STRUCT
William Robertsc950a352016-03-04 18:12:29 -0800630
William Robertsc950a352016-03-04 18:12:29 -0800631
632def main():
William Roberts11c29282016-04-09 10:32:30 -0700633 """Main entry point for execution."""
William Robertsc950a352016-03-04 18:12:29 -0800634
William Roberts11c29282016-04-09 10:32:30 -0700635 opt_parser = argparse.ArgumentParser(
636 description='A tool for parsing fsconfig config files and producing' +
637 'digestable outputs.')
638 subparser = opt_parser.add_subparsers(help='generators')
William Robertsc950a352016-03-04 18:12:29 -0800639
William Roberts11c29282016-04-09 10:32:30 -0700640 gens = generator.get()
William Robertsc950a352016-03-04 18:12:29 -0800641
William Roberts11c29282016-04-09 10:32:30 -0700642 # for each gen, instantiate and add them as an option
643 for name, gen in gens.iteritems():
William Robertsc950a352016-03-04 18:12:29 -0800644
William Roberts11c29282016-04-09 10:32:30 -0700645 generator_option_parser = subparser.add_parser(name, help=gen.__doc__)
646 generator_option_parser.set_defaults(which=name)
William Roberts8cb6a182016-04-08 22:06:19 -0700647
William Roberts11c29282016-04-09 10:32:30 -0700648 opt_group = generator_option_parser.add_argument_group(name +
649 ' options')
650 gen.add_opts(opt_group)
William Roberts8cb6a182016-04-08 22:06:19 -0700651
William Roberts11c29282016-04-09 10:32:30 -0700652 args = opt_parser.parse_args()
653
654 args_as_dict = vars(args)
655 which = args_as_dict['which']
656 del args_as_dict['which']
657
658 gens[which](args_as_dict)
659
William Robertsc950a352016-03-04 18:12:29 -0800660
661if __name__ == '__main__':
662 main()