conf: Script to convert option descriptions to man page and config snippets added
parent
dee50a6046
commit
e90b37b9c3
|
@ -0,0 +1,337 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2014 Tobias Brunner
|
||||
# Hochschule fuer Technik Rapperswil
|
||||
#
|
||||
# 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
|
||||
# Free Software Foundation; either version 2 of the License, or (at your
|
||||
# option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
# for more details.
|
||||
|
||||
"""
|
||||
Parses strongswan.conf option descriptions and produces configuration file
|
||||
and man page snippets.
|
||||
|
||||
The format for description files is as follows:
|
||||
|
||||
full.option.name [[:]= default]
|
||||
Short description intended as comment in config snippet
|
||||
|
||||
Long description for use in the man page, with
|
||||
simple formatting: _italic_, **bold**
|
||||
|
||||
Second paragraph of the long description
|
||||
|
||||
The descriptions must be indented by tabs or spaces but are both optional.
|
||||
If only a short description is given it is used for both intended usages.
|
||||
Line breaks within a paragraph of the long description or the short description
|
||||
are not preserved. But multiple paragraphs will be separated in the man page.
|
||||
Any formatting in the short description is removed when producing config
|
||||
snippets.
|
||||
|
||||
Options for which a value is assigned with := are not commented out in the
|
||||
produced configuration file snippet. This allows to override a default value,
|
||||
that e.g. has to be preserved for legacy reasons, in the generated default
|
||||
config.
|
||||
|
||||
To describe sections the following format can be used:
|
||||
|
||||
full.section.name {[#]}
|
||||
Short description of this section
|
||||
|
||||
Long description as above
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
from textwrap import TextWrapper
|
||||
from optparse import OptionParser
|
||||
|
||||
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
|
||||
self.default = default
|
||||
self.section = section
|
||||
self.commented = commented
|
||||
self.desc = []
|
||||
self.options = []
|
||||
|
||||
def __cmp__(self, other):
|
||||
if self.section == other.section:
|
||||
return cmp(self.name, other.name)
|
||||
return 1 if self.section else -1
|
||||
|
||||
def add_paragraph(self):
|
||||
"""Adds a new paragraph to the description"""
|
||||
if len(self.desc) and len(self.desc[-1]):
|
||||
self.desc.append("")
|
||||
|
||||
def add(self, line):
|
||||
"""Adds a line to the last paragraph"""
|
||||
if not len(self.desc):
|
||||
self.desc.append(line)
|
||||
elif not len(self.desc[-1]):
|
||||
self.desc[-1] = line
|
||||
else:
|
||||
self.desc[-1] += ' ' + line
|
||||
|
||||
def adopt(self, other):
|
||||
"""Adopts settings from other, which should be more recently parsed"""
|
||||
self.default = other.default
|
||||
self.commented = other.commented
|
||||
self.desc = other.desc
|
||||
|
||||
class Parser:
|
||||
"""Parses one or more files of configuration options"""
|
||||
def __init__(self):
|
||||
self.options = []
|
||||
|
||||
def parse(self, file):
|
||||
"""Parses the given file and adds all options to the internal store"""
|
||||
self.__current = None
|
||||
for line in file:
|
||||
self.__parse_line(line)
|
||||
if self.__current:
|
||||
self.__add_option(self.__current)
|
||||
|
||||
def __parse_line(self, line):
|
||||
"""Parses a single line"""
|
||||
if re.match(r'^\s*#', line):
|
||||
return
|
||||
# option definition
|
||||
m = re.match(r'^(?P<name>\S+)\s*((?P<assign>:)?=\s*(?P<default>.+)?)?\s*$', line)
|
||||
if m:
|
||||
if self.__current:
|
||||
self.__add_option(self.__current)
|
||||
self.__current = ConfigOption(m.group('name'), m.group('default'),
|
||||
commented = not m.group('assign'))
|
||||
return
|
||||
# section definition
|
||||
m = re.match(r'^(?P<name>\S+)\s*\{\s*(?P<comment>#)?\s*\}\s*$', line)
|
||||
if m:
|
||||
if self.__current:
|
||||
self.__add_option(self.__current)
|
||||
self.__current = ConfigOption(m.group('name'), section = True,
|
||||
commented = m.group('comment'))
|
||||
return
|
||||
# paragraph separator
|
||||
m = re.match(r'^\s*$', line)
|
||||
if m and self.__current:
|
||||
self.__current.add_paragraph()
|
||||
# description line
|
||||
m = re.match(r'^\s+(?P<text>.+?)\s*$', line)
|
||||
if m and self.__current:
|
||||
self.__current.add(m.group('text'))
|
||||
|
||||
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)
|
||||
if not parent:
|
||||
parent = self
|
||||
found = next((x for x in parent.options if x.name == option.name
|
||||
and x.section == option.section), None)
|
||||
if found:
|
||||
found.adopt(option)
|
||||
else:
|
||||
parent.options.append(option)
|
||||
parent.options.sort()
|
||||
|
||||
def __get_option(self, parts, 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
|
||||
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)
|
||||
options.append(option)
|
||||
options.sort()
|
||||
options = option.options
|
||||
return option
|
||||
|
||||
def get_option(self, name):
|
||||
"""Retrieves the option with the given name"""
|
||||
return self.__get_option(name.split('.'))
|
||||
|
||||
class TagReplacer:
|
||||
"""Replaces formatting tags in text"""
|
||||
def __init__(self):
|
||||
self.__matcher_b = self.__create_matcher('**')
|
||||
self.__matcher_i = self.__create_matcher('_')
|
||||
self.__replacer = None
|
||||
|
||||
def __create_matcher(self, tag):
|
||||
tag = re.escape(tag)
|
||||
return re.compile(r'''
|
||||
(^|\s|(?P<brack>[(\[])) # prefix with optional opening bracket
|
||||
(?P<tag>''' + tag + r''') # start tag
|
||||
(?P<text>\w|\S.*?\S) # text
|
||||
''' + tag + r''' # end tag
|
||||
(?P<punct>([.,!:)\]]|\(\d+\))*) # punctuation
|
||||
(?=$|\s) # suffix (don't consume it so that subsequent tags can match)
|
||||
''', flags = re.DOTALL | re.VERBOSE)
|
||||
|
||||
def _create_replacer(self):
|
||||
def replacer(m):
|
||||
punct = m.group('punct')
|
||||
if not punct:
|
||||
punct = ''
|
||||
return '{0}{1}{2}'.format(m.group(1), m.group('text'), punct)
|
||||
return replacer
|
||||
|
||||
def replace(self, text):
|
||||
if not self.__replacer:
|
||||
self.__replacer = self._create_replacer()
|
||||
text = re.sub(self.__matcher_b, self.__replacer, text)
|
||||
return re.sub(self.__matcher_i, self.__replacer, text)
|
||||
|
||||
class GroffTagReplacer(TagReplacer):
|
||||
def _create_replacer(self):
|
||||
def replacer(m):
|
||||
nl = '\n' if m.group(1) else ''
|
||||
format = 'I' if m.group('tag') == '_' else 'B'
|
||||
brack = m.group('brack')
|
||||
if not brack:
|
||||
brack = ''
|
||||
punct = m.group('punct')
|
||||
if not punct:
|
||||
punct = ''
|
||||
text = re.sub(r'[\r\n\t]', ' ', m.group('text'))
|
||||
return '{0}.R{1} "{2}" "{3}" "{4}"\n'.format(nl, format, brack, text, punct)
|
||||
return replacer
|
||||
|
||||
class ConfFormatter:
|
||||
"""Formats options to a strongswan.conf snippet"""
|
||||
def __init__(self):
|
||||
self.__indent = ' '
|
||||
self.__wrapper = TextWrapper(width = 80, replace_whitespace = True,
|
||||
break_long_words = False, break_on_hyphens = False)
|
||||
self.__tags = TagReplacer()
|
||||
|
||||
def __print_description(self, opt, indent):
|
||||
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])))
|
||||
|
||||
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)
|
||||
else:
|
||||
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"""
|
||||
comment = "# " if commented or section.commented else ""
|
||||
self.__print_description(section, indent)
|
||||
print '{0}{1}{2} {{'.format(self.__indent * indent, comment, section.name)
|
||||
print
|
||||
for o in section.options:
|
||||
if o.section:
|
||||
self.__print_section(o, indent + 1, section.commented)
|
||||
else:
|
||||
self.__print_option(o, indent + 1, section.commented)
|
||||
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 options:
|
||||
if option.section:
|
||||
self.__print_section(option, 0, False)
|
||||
else:
|
||||
self.__print_option(option, 0, False)
|
||||
|
||||
class ManFormatter:
|
||||
"""Formats a list of options into a groff snippet"""
|
||||
def __init__(self):
|
||||
self.__wrapper = TextWrapper(width = 80, replace_whitespace = False,
|
||||
break_long_words = False, break_on_hyphens = False)
|
||||
self.__tags = GroffTagReplacer()
|
||||
|
||||
def __groffize(self, text):
|
||||
"""Encode text as groff text"""
|
||||
text = self.__tags.replace(text)
|
||||
text = re.sub(r'(?<!\\)-', r'\\-', text)
|
||||
# remove any leading whitespace
|
||||
return re.sub(r'^\s+', '', text, flags = re.MULTILINE)
|
||||
|
||||
def __format_option(self, option):
|
||||
"""Print a single option"""
|
||||
if option.section and not len(option.desc):
|
||||
return
|
||||
if option.section:
|
||||
print '.TP\n.B {0}\n.br'.format(option.fullname)
|
||||
else:
|
||||
print '.TP'
|
||||
default = option.default if option.default else ''
|
||||
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 ''
|
||||
|
||||
def format(self, options):
|
||||
"""Print a list of options"""
|
||||
if not options:
|
||||
return
|
||||
for option in options:
|
||||
if option.section:
|
||||
self.__format_option(option)
|
||||
self.format(option.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")
|
||||
(opts, args) = options.parse_args()
|
||||
|
||||
parser = Parser()
|
||||
if len(args):
|
||||
for filename in args:
|
||||
try:
|
||||
with open(filename, 'r') as file:
|
||||
parser.parse(file)
|
||||
except IOError as e:
|
||||
sys.stderr.write("Unable to open '{0}': {1}\n".format(filename, e.strerror))
|
||||
else:
|
||||
parser.parse(sys.stdin)
|
||||
|
||||
options = parser.options
|
||||
if (opts.root):
|
||||
root = parser.get_option(opts.root)
|
||||
if root:
|
||||
options = root.options
|
||||
|
||||
if opts.format == "conf":
|
||||
formatter = ConfFormatter()
|
||||
elif opts.format == "man":
|
||||
formatter = ManFormatter()
|
||||
|
||||
formatter.format(options)
|
Loading…
Reference in New Issue