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>