Annotation of embedaddon/strongswan/conf/format-options.py, revision 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>