Annotation of embedaddon/strongswan/conf/format-options.py, revision 1.1.1.1

1.1       misho       1: #!/usr/bin/env python
                      2: #
                      3: # Copyright (C) 2014-2019 Tobias Brunner
                      4: # HSR Hochschule fuer Technik Rapperswil
                      5: #
                      6: # This program is free software; you can redistribute it and/or modify it
                      7: # under the terms of the GNU General Public License as published by the
                      8: # Free Software Foundation; either version 2 of the License, or (at your
                      9: # option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
                     10: #
                     11: # This program is distributed in the hope that it will be useful, but
                     12: # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
                     13: # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
                     14: # for more details.
                     15: 
                     16: """
                     17: Parses strongswan.conf option descriptions and produces configuration file
                     18: and man page snippets.
                     19: 
                     20: The format for description files is as follows:
                     21: 
                     22: full.option.name [[:]= default]
                     23:        Short description intended as comment in config snippet
                     24: 
                     25:        Long description for use in the man page, with
                     26:        simple formatting: _italic_, **bold**
                     27: 
                     28:        Second paragraph of the long description
                     29: 
                     30: The descriptions must be indented by tabs or spaces but are both optional.
                     31: If only a short description is given it is used for both intended usages.
                     32: Line breaks within a paragraph of the long description or the short description
                     33: are not preserved.  But multiple paragraphs will be separated in the man page.
                     34: Any formatting in the short description is removed when producing config
                     35: snippets.
                     36: 
                     37: Options for which a value is assigned with := are not commented out in the
                     38: produced configuration file snippet.  This allows to override a default value,
                     39: that e.g. has to be preserved for legacy reasons, in the generated default
                     40: config.
                     41: 
                     42: To describe sections the following format can be used:
                     43: 
                     44: full.section.name {[#]}
                     45:        Short description of this section
                     46: 
                     47:        Long description as above
                     48: 
                     49: If a # is added between the curly braces the section header will be commented
                     50: out in the configuration file snippet, which is useful for example sections.
                     51: 
                     52: To add include statements to generated config files (ignored when generating
                     53: man pages) the following format can be used:
                     54: 
                     55: full.section.name.include files/to/include
                     56:        Description of this include statement
                     57: 
                     58: Dots in section/option names may be escaped with a backslash.  For instance,
                     59: with the following section description
                     60: 
                     61: charon.filelog./var/log/daemon\.log {}
                     62:        Section to define logging into /var/log/daemon.log
                     63: 
                     64: /var/log/daemon.log will be the name of the last section.
                     65: """
                     66: 
                     67: import sys
                     68: import re
                     69: from textwrap import TextWrapper
                     70: from argparse import ArgumentParser
                     71: from functools import cmp_to_key, total_ordering
                     72: 
                     73: @total_ordering
                     74: class ConfigOption:
                     75:        """Representing a configuration option or described section in strongswan.conf"""
                     76:        def __init__(self, path, default = None, section = False, commented = False, include = False):
                     77:                self.path = path
                     78:                self.name = path[-1]
                     79:                self.fullname = '.'.join(path)
                     80:                self.default = default
                     81:                self.section = section
                     82:                self.commented = commented
                     83:                self.include = include
                     84:                self.desc = []
                     85:                self.options = []
                     86: 
                     87:        def __eq__(self, other):
                     88:                return self.name == other.name
                     89: 
                     90:        def __lt__(self, other):
                     91:                return self.name < other.name
                     92: 
                     93:        def add_paragraph(self):
                     94:                """Adds a new paragraph to the description"""
                     95:                if len(self.desc) and len(self.desc[-1]):
                     96:                        self.desc.append("")
                     97: 
                     98:        def add(self, line):
                     99:                """Adds a line to the last paragraph"""
                    100:                if not len(self.desc):
                    101:                        self.desc.append(line)
                    102:                elif not len(self.desc[-1]):
                    103:                        self.desc[-1] = line
                    104:                else:
                    105:                        self.desc[-1] += ' ' + line
                    106: 
                    107:        def adopt(self, other):
                    108:                """Adopts settings from other, which should be more recently parsed"""
                    109:                self.default = other.default
                    110:                self.commented = other.commented
                    111:                self.desc = other.desc
                    112: 
                    113:        @staticmethod
                    114:        def cmp(a, b):
                    115:                # order options before sections and includes last
                    116:                if a.include or b.include:
                    117:                        return a.include - b.include
                    118:                return a.section - b.section
                    119: 
                    120: class Parser:
                    121:        """Parses one or more files of configuration options"""
                    122:        def __init__(self, sort = True):
                    123:                self.options = []
                    124:                self.sort = sort
                    125: 
                    126:        def parse(self, file):
                    127:                """Parses the given file and adds all options to the internal store"""
                    128:                self.__current = None
                    129:                for line in file:
                    130:                        self.__parse_line(line)
                    131:                if self.__current:
                    132:                        self.__add_option(self.__current)
                    133: 
                    134:        def __parse_line(self, line):
                    135:                """Parses a single line"""
                    136:                if re.match(r'^\s*#', line):
                    137:                        return
                    138:                # option definition
                    139:                m = re.match(r'^(?P<name>\S+)\s*((?P<assign>:)?=\s*(?P<default>.+)?)?\s*$', line)
                    140:                if m:
                    141:                        if self.__current:
                    142:                                self.__add_option(self.__current)
                    143:                        path = self.__split_name(m.group('name'))
                    144:                        self.__current = ConfigOption(path, m.group('default'),
                    145:                                                                                  commented = not m.group('assign'))
                    146:                        return
                    147:                # section definition
                    148:                m = re.match(r'^(?P<name>\S+)\s*\{\s*(?P<comment>#)?\s*\}\s*$', line)
                    149:                if m:
                    150:                        if self.__current:
                    151:                                self.__add_option(self.__current)
                    152:                        path = self.__split_name(m.group('name'))
                    153:                        self.__current = ConfigOption(path, section = True,
                    154:                                                                                  commented = m.group('comment'))
                    155:                        return
                    156:                # include definition
                    157:                m = re.match(r'^(?P<name>\S+\.include|include)\s+(?P<pattern>\S+)\s*$', line)
                    158:                if m:
                    159:                        if self.__current:
                    160:                                self.__add_option(self.__current)
                    161:                        path = self.__split_name(m.group('name'))
                    162:                        self.__current = ConfigOption(path, m.group('pattern'), include = True)
                    163:                        return
                    164:                # paragraph separator
                    165:                m = re.match(r'^\s*$', line)
                    166:                if m and self.__current:
                    167:                        self.__current.add_paragraph()
                    168:                # description line
                    169:                m = re.match(r'^\s+(?P<text>.+?)\s*$', line)
                    170:                if m and self.__current:
                    171:                        self.__current.add(m.group('text'))
                    172: 
                    173:        def __split_name(self, name):
                    174:                """Split the given full name in a list of section/option names"""
                    175:                return [x.replace('\.', '.') for x in re.split(r'(?<!\\)\.', name)]
                    176: 
                    177:        def __add_option(self, option):
                    178:                """Adds the given option to the abstract storage"""
                    179:                option.desc = [desc for desc in option.desc if len(desc)]
                    180:                parent = self.__get_option(option.path[:-1], True)
                    181:                if not parent:
                    182:                        parent = self
                    183:                found = next((x for x in parent.options if x.name == option.name
                    184:                                                                                and x.section == option.section), None)
                    185:                if found:
                    186:                        found.adopt(option)
                    187:                else:
                    188:                        parent.options.append(option)
                    189:                        if self.sort:
                    190:                                parent.options.sort()
                    191: 
                    192:        def __get_option(self, path, create = False):
                    193:                """Searches/Creates the option (section) based on a list of section names"""
                    194:                option = None
                    195:                options = self.options
                    196:                for i, name in enumerate(path, 1):
                    197:                        option = next((x for x in options if x.name == name and x.section), None)
                    198:                        if not option:
                    199:                                if not create:
                    200:                                        break
                    201:                                option = ConfigOption(path[:i], section = True)
                    202:                                options.append(option)
                    203:                                if self.sort:
                    204:                                        options.sort()
                    205:                        options = option.options
                    206:                return option
                    207: 
                    208:        def get_option(self, name):
                    209:                """Retrieves the option with the given name"""
                    210:                return self.__get_option(self.__split_name(name))
                    211: 
                    212: class TagReplacer:
                    213:        """Replaces formatting tags in text"""
                    214:        def __init__(self):
                    215:                self.__matcher_b = self.__create_matcher('**')
                    216:                self.__matcher_i = self.__create_matcher('_')
                    217:                self.__replacer = None
                    218: 
                    219:        def __create_matcher(self, tag):
                    220:                tag = re.escape(tag)
                    221:                return re.compile(r'''
                    222:                        (^|\s|(?P<brack>[(\[])) # prefix with optional opening bracket
                    223:                        (?P<tag>''' + tag + r''') # start tag
                    224:                        (?P<text>\S|\S.*?\S) # text
                    225:                        ''' + tag + r''' # end tag
                    226:                        (?P<punct>([.,!:)\]]|\(\d+\))*) # punctuation
                    227:                        (?=$|\s) # suffix (don't consume it so that subsequent tags can match)
                    228:                        ''', flags = re.DOTALL | re.VERBOSE)
                    229: 
                    230:        def _create_replacer(self):
                    231:                def replacer(m):
                    232:                        punct = m.group('punct')
                    233:                        if not punct:
                    234:                                punct = ''
                    235:                        return '{0}{1}{2}'.format(m.group(1), m.group('text'), punct)
                    236:                return replacer
                    237: 
                    238:        def replace(self, text):
                    239:                if not self.__replacer:
                    240:                        self.__replacer = self._create_replacer()
                    241:                text = re.sub(self.__matcher_b, self.__replacer, text)
                    242:                return re.sub(self.__matcher_i, self.__replacer, text)
                    243: 
                    244: class GroffTagReplacer(TagReplacer):
                    245:        def _create_replacer(self):
                    246:                def replacer(m):
                    247:                        nl = '\n' if m.group(1) else ''
                    248:                        format = 'I' if m.group('tag') == '_' else 'B'
                    249:                        brack = m.group('brack')
                    250:                        if not brack:
                    251:                                brack = ''
                    252:                        punct = m.group('punct')
                    253:                        if not punct:
                    254:                                punct = ''
                    255:                        text = re.sub(r'[\r\n\t]', ' ', m.group('text'))
                    256:                        return '{0}.R{1} "{2}" "{3}" "{4}"\n'.format(nl, format, brack, text, punct)
                    257:                return replacer
                    258: 
                    259: class ConfFormatter:
                    260:        """Formats options to a strongswan.conf snippet"""
                    261:        def __init__(self):
                    262:                self.__indent = '    '
                    263:                self.__wrapper = TextWrapper(width = 80, replace_whitespace = True,
                    264:                                                                         break_long_words = False, break_on_hyphens = False)
                    265:                self.__tags = TagReplacer()
                    266: 
                    267:        def __print_description(self, opt, indent):
                    268:                if len(opt.desc):
                    269:                        self.__wrapper.initial_indent = '{0}# '.format(self.__indent * indent)
                    270:                        self.__wrapper.subsequent_indent = self.__wrapper.initial_indent
                    271:                        print(self.__wrapper.fill(self.__tags.replace(opt.desc[0])))
                    272: 
                    273:        def __print_option(self, opt, indent, commented):
                    274:                """Print a single option with description and default value"""
                    275:                comment = "# " if commented or opt.commented else ""
                    276:                self.__print_description(opt, indent)
                    277:                if opt.include:
                    278:                        print('{0}{1} {2}'.format(self.__indent * indent, opt.name, opt.default))
                    279:                elif opt.default:
                    280:                        print('{0}{1}{2} = {3}'.format(self.__indent * indent, comment, opt.name, opt.default))
                    281:                else:
                    282:                        print('{0}{1}{2} ='.format(self.__indent * indent, comment, opt.name))
                    283:                print('')
                    284: 
                    285:        def __print_section(self, section, indent, commented):
                    286:                """Print a section with all options"""
                    287:                commented = commented or section.commented
                    288:                comment = "# " if commented else ""
                    289:                self.__print_description(section, indent)
                    290:                print('{0}{1}{2} {{'.format(self.__indent * indent, comment, section.name))
                    291:                print('')
                    292:                for o in sorted(section.options, key=cmp_to_key(ConfigOption.cmp)):
                    293:                        if o.section:
                    294:                                self.__print_section(o, indent + 1, commented)
                    295:                        else:
                    296:                                self.__print_option(o, indent + 1, commented)
                    297:                print('{0}{1}}}'.format(self.__indent * indent, comment))
                    298:                print('')
                    299: 
                    300:        def format(self, options):
                    301:                """Print a list of options"""
                    302:                if not options:
                    303:                        return
                    304:                for option in sorted(options, key=cmp_to_key(ConfigOption.cmp)):
                    305:                        if option.section:
                    306:                                self.__print_section(option, 0, False)
                    307:                        else:
                    308:                                self.__print_option(option, 0, False)
                    309: 
                    310: class ManFormatter:
                    311:        """Formats a list of options into a groff snippet"""
                    312:        def __init__(self):
                    313:                self.__wrapper = TextWrapper(width = 80, replace_whitespace = False,
                    314:                                                                         break_long_words = False, break_on_hyphens = False)
                    315:                self.__tags = GroffTagReplacer()
                    316: 
                    317:        def __groffize(self, text):
                    318:                """Encode text as groff text"""
                    319:                text = self.__tags.replace(text)
                    320:                text = re.sub(r'(?<!\\)-', r'\\-', text)
                    321:                # remove any leading whitespace
                    322:                return re.sub(r'^\s+', '', text, flags = re.MULTILINE)
                    323: 
                    324:        def __format_option(self, option):
                    325:                """Print a single option"""
                    326:                if option.section and not len(option.desc):
                    327:                        return
                    328:                if option.include:
                    329:                        return
                    330:                if option.section:
                    331:                        print('.TP\n.B {0}\n.br'.format(option.fullname))
                    332:                else:
                    333:                        print('.TP')
                    334:                        default = option.default if option.default else ''
                    335:                        print('.BR {0} " [{1}]"'.format(option.fullname, default))
                    336:                for para in option.desc if len(option.desc) < 2 else option.desc[1:]:
                    337:                        print(self.__groffize(self.__wrapper.fill(para)))
                    338:                        print('')
                    339: 
                    340:        def format(self, options):
                    341:                """Print a list of options"""
                    342:                if not options:
                    343:                        return
                    344:                for option in options:
                    345:                        if option.section:
                    346:                                self.__format_option(option)
                    347:                                self.format(option.options)
                    348:                        else:
                    349:                                self.__format_option(option)
                    350: 
                    351: args = ArgumentParser()
                    352: args.add_argument('file', nargs='*',
                    353:                                  help="files to process, omit to read input from stdin")
                    354: args.add_argument("-f", "--format", dest="format", choices=["conf", "man"],
                    355:                                  help="output format (default: %(default)s)", default="conf")
                    356: args.add_argument("-r", "--root", dest="root", metavar="NAME",
                    357:                                  help="root section of which options are printed; everything"
                    358:                                  "is printed if not found")
                    359: args.add_argument("-n", "--nosort", action="store_false", dest="sort",
                    360:                                  default=True, help="do not sort sections alphabetically")
                    361: 
                    362: opts = args.parse_args()
                    363: 
                    364: parser = Parser(opts.sort)
                    365: if len(opts.file):
                    366:        for filename in opts.file:
                    367:                try:
                    368:                        with open(filename, 'r') as file:
                    369:                                parser.parse(file)
                    370:                except IOError as e:
                    371:                        sys.stderr.write("Unable to open '{0}': {1}\n".format(filename, e.strerror))
                    372: else:
                    373:        parser.parse(sys.stdin)
                    374: 
                    375: options = parser.options
                    376: if (opts.root):
                    377:        root = parser.get_option(opts.root)
                    378:        if root:
                    379:                options = root.options
                    380: 
                    381: if opts.format == "conf":
                    382:        formatter = ConfFormatter()
                    383: elif opts.format == "man":
                    384:        formatter = ManFormatter()
                    385: 
                    386: formatter.format(options)

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>