-#!/usr/bin/env python
+#!/usr/bin/env python3
#
-# Copyright (C) 2014 Tobias Brunner
-# Hochschule fuer Technik Rapperswil
+# Copyright (C) 2014-2019 Tobias Brunner
+#
+# Copyright (C) secunet Security Networks AG
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
If a # is added between the curly braces the section header will be commented
out in the configuration file snippet, which is useful for example sections.
+
+To add include statements to generated config files (ignored when generating
+man pages) the following format can be used:
+
+full.section.name.include files/to/include
+ Description of this include statement
+
+Dots in section/option names may be escaped with a backslash. For instance,
+with the following section description
+
+charon.filelog./var/log/daemon\.log {}
+ Section to define logging into /var/log/daemon.log
+
+/var/log/daemon.log will be the name of the last section.
"""
import sys
import re
from textwrap import TextWrapper
-from optparse import OptionParser
-from operator import attrgetter
+from argparse import ArgumentParser
+from functools import cmp_to_key, total_ordering
+@total_ordering
class ConfigOption:
"""Representing a configuration option or described section in strongswan.conf"""
- def __init__(self, name, default = None, section = False, commented = False):
- self.name = name.split('.')[-1]
- self.fullname = name
+ def __init__(self, path, default = None, section = False, commented = False, include = False):
+ self.path = path
+ self.name = path[-1]
+ self.fullname = '.'.join(path)
self.default = default
self.section = section
self.commented = commented
+ self.include = include
self.desc = []
self.options = []
- def __cmp__(self, other):
- return cmp(self.name, other.name)
+ def __eq__(self, other):
+ return self.name == other.name
+
+ def __lt__(self, other):
+ return self.name < other.name
def add_paragraph(self):
"""Adds a new paragraph to the description"""
self.commented = other.commented
self.desc = other.desc
+ @staticmethod
+ def cmp(a, b):
+ # order options before sections and includes last
+ if a.include or b.include:
+ return a.include - b.include
+ return a.section - b.section
+
class Parser:
"""Parses one or more files of configuration options"""
def __init__(self, sort = True):
if m:
if self.__current:
self.__add_option(self.__current)
- self.__current = ConfigOption(m.group('name'), m.group('default'),
+ path = self.__split_name(m.group('name'))
+ self.__current = ConfigOption(path, m.group('default'),
commented = not m.group('assign'))
return
# section definition
if m:
if self.__current:
self.__add_option(self.__current)
- self.__current = ConfigOption(m.group('name'), section = True,
+ path = self.__split_name(m.group('name'))
+ self.__current = ConfigOption(path, section = True,
commented = m.group('comment'))
return
+ # include definition
+ m = re.match(r'^(?P<name>\S+\.include|include)\s+(?P<pattern>\S+)\s*$', line)
+ if m:
+ if self.__current:
+ self.__add_option(self.__current)
+ path = self.__split_name(m.group('name'))
+ self.__current = ConfigOption(path, m.group('pattern'), include = True)
+ return
# paragraph separator
m = re.match(r'^\s*$', line)
if m and self.__current:
if m and self.__current:
self.__current.add(m.group('text'))
+ def __split_name(self, name):
+ """Split the given full name in a list of section/option names"""
+ return [x.replace('\.', '.') for x in re.split(r'(?<!\\)\.', name)]
+
def __add_option(self, option):
"""Adds the given option to the abstract storage"""
option.desc = [desc for desc in option.desc if len(desc)]
- parts = option.fullname.split('.')
- parent = self.__get_option(parts[:-1], True)
+ parent = self.__get_option(option.path[:-1], True)
if not parent:
parent = self
found = next((x for x in parent.options if x.name == option.name
if self.sort:
parent.options.sort()
- def __get_option(self, parts, create = False):
+ def __get_option(self, path, create = False):
"""Searches/Creates the option (section) based on a list of section names"""
option = None
options = self.options
- fullname = ""
- for name in parts:
- fullname += '.' + name if len(fullname) else name
+ for i, name in enumerate(path, 1):
option = next((x for x in options if x.name == name and x.section), None)
if not option:
if not create:
break
- option = ConfigOption(fullname, section = True)
+ option = ConfigOption(path[:i], section = True)
options.append(option)
if self.sort:
options.sort()
def get_option(self, name):
"""Retrieves the option with the given name"""
- return self.__get_option(name.split('.'))
+ return self.__get_option(self.__split_name(name))
class TagReplacer:
"""Replaces formatting tags in text"""
return re.compile(r'''
(^|\s|(?P<brack>[(\[])) # prefix with optional opening bracket
(?P<tag>''' + tag + r''') # start tag
- (?P<text>\w|\S.*?\S) # text
+ (?P<text>\S|\S.*?\S) # text
''' + tag + r''' # end tag
(?P<punct>([.,!:)\]]|\(\d+\))*) # punctuation
(?=$|\s) # suffix (don't consume it so that subsequent tags can match)
if len(opt.desc):
self.__wrapper.initial_indent = '{0}# '.format(self.__indent * indent)
self.__wrapper.subsequent_indent = self.__wrapper.initial_indent
- print format(self.__wrapper.fill(self.__tags.replace(opt.desc[0])))
+ print(self.__wrapper.fill(self.__tags.replace(opt.desc[0])))
def __print_option(self, opt, indent, commented):
"""Print a single option with description and default value"""
comment = "# " if commented or opt.commented else ""
self.__print_description(opt, indent)
- if opt.default:
- print '{0}{1}{2} = {3}'.format(self.__indent * indent, comment, opt.name, opt.default)
+ if opt.include:
+ print('{0}{1} {2}'.format(self.__indent * indent, opt.name, opt.default))
+ elif opt.default:
+ print('{0}{1}{2} = {3}'.format(self.__indent * indent, comment, opt.name, opt.default))
else:
- print '{0}{1}{2} ='.format(self.__indent * indent, comment, opt.name)
- print
+ print('{0}{1}{2} ='.format(self.__indent * indent, comment, opt.name))
+ print('')
def __print_section(self, section, indent, commented):
"""Print a section with all options"""
commented = commented or section.commented
comment = "# " if commented else ""
self.__print_description(section, indent)
- print '{0}{1}{2} {{'.format(self.__indent * indent, comment, section.name)
- print
- for o in sorted(section.options, key=attrgetter('section')):
+ print('{0}{1}{2} {{'.format(self.__indent * indent, comment, section.name))
+ print('')
+ for o in sorted(section.options, key=cmp_to_key(ConfigOption.cmp)):
if o.section:
self.__print_section(o, indent + 1, commented)
else:
self.__print_option(o, indent + 1, commented)
- print '{0}{1}}}'.format(self.__indent * indent, comment)
- print
+ print('{0}{1}}}'.format(self.__indent * indent, comment))
+ print('')
def format(self, options):
"""Print a list of options"""
if not options:
return
- for option in sorted(options, key=attrgetter('section')):
+ for option in sorted(options, key=cmp_to_key(ConfigOption.cmp)):
if option.section:
self.__print_section(option, 0, False)
else:
"""Print a single option"""
if option.section and not len(option.desc):
return
+ if option.include:
+ return
if option.section:
- print '.TP\n.B {0}\n.br'.format(option.fullname)
+ print('.TP\n.B {0}\n.br'.format(option.fullname))
else:
- print '.TP'
+ print('.TP')
default = option.default if option.default else ''
- print '.BR {0} " [{1}]"'.format(option.fullname, default)
+ print('.BR {0} " [{1}]"'.format(option.fullname, default))
for para in option.desc if len(option.desc) < 2 else option.desc[1:]:
- print self.__groffize(self.__wrapper.fill(para))
- print ''
+ print(self.__groffize(self.__wrapper.fill(para)))
+ print('')
def format(self, options):
"""Print a list of options"""
else:
self.__format_option(option)
-options = OptionParser(usage = "Usage: %prog [options] file1 file2\n\n"
- "If no filenames are provided the input is read from stdin.")
-options.add_option("-f", "--format", dest="format", type="choice", choices=["conf", "man"],
- help="output format: conf, man [default: %default]", default="conf")
-options.add_option("-r", "--root", dest="root", metavar="NAME",
- help="root section of which options are printed, "
- "if not found everything is printed")
-options.add_option("-n", "--nosort", action="store_false", dest="sort",
- default=True, help="do not sort sections alphabetically")
+args = ArgumentParser()
+args.add_argument('file', nargs='*',
+ help="files to process, omit to read input from stdin")
+args.add_argument("-f", "--format", dest="format", choices=["conf", "man"],
+ help="output format (default: %(default)s)", default="conf")
+args.add_argument("-r", "--root", dest="root", metavar="NAME",
+ help="root section of which options are printed; everything"
+ "is printed if not found")
+args.add_argument("-n", "--nosort", action="store_false", dest="sort",
+ default=True, help="do not sort sections alphabetically")
-(opts, args) = options.parse_args()
+opts = args.parse_args()
parser = Parser(opts.sort)
-if len(args):
- for filename in args:
+if len(opts.file):
+ for filename in opts.file:
try:
with open(filename, 'r') as file:
parser.parse(file)