| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 | 
							- #!/usr/bin/env python
 
- #
 
- # Copyright (C) 2014-2017 Tobias Brunner
 
- # HSR 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.
 
- 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 functools import cmp_to_key
 
- class ConfigOption:
 
- 	"""Representing a configuration option or described section in strongswan.conf"""
 
- 	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 __lt__(self, other):
 
- 		return self.name < other.name
 
- 	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
 
- 	@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):
 
- 		self.options = []
 
- 		self.sort = sort
 
- 	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)
 
- 			path = self.__split_name(m.group('name'))
 
- 			self.__current = ConfigOption(path, 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)
 
- 			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:
 
- 			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 __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)]
 
- 		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
 
- 										and x.section == option.section), None)
 
- 		if found:
 
- 			found.adopt(option)
 
- 		else:
 
- 			parent.options.append(option)
 
- 			if self.sort:
 
- 				parent.options.sort()
 
- 	def __get_option(self, path, create = False):
 
- 		"""Searches/Creates the option (section) based on a list of section names"""
 
- 		option = None
 
- 		options = self.options
 
- 		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(path[:i], section = True)
 
- 				options.append(option)
 
- 				if self.sort:
 
- 					options.sort()
 
- 			options = option.options
 
- 		return option
 
- 	def get_option(self, name):
 
- 		"""Retrieves the option with the given name"""
 
- 		return self.__get_option(self.__split_name(name))
 
- 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>\S|\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(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.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('')
 
- 	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=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('')
 
- 	def format(self, options):
 
- 		"""Print a list of options"""
 
- 		if not options:
 
- 			return
 
- 		for option in sorted(options, key=cmp_to_key(ConfigOption.cmp)):
 
- 			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.include:
 
- 			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")
 
- options.add_option("-n", "--nosort", action="store_false", dest="sort",
 
- 				   default=True, help="do not sort sections alphabetically")
 
- (opts, args) = options.parse_args()
 
- parser = Parser(opts.sort)
 
- 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)
 
 
  |