File:  [ELWIX - Embedded LightWeight unIX -] / embedaddon / strongswan / conf / format-options.py
Revision 1.1.1.1 (vendor branch): download - view: text, annotated - select for diffs - revision graph
Wed Jun 3 09:46:43 2020 UTC (4 years, 1 month ago) by misho
Branches: strongswan, MAIN
CVS tags: v5_9_2p0, v5_8_4p7, HEAD
Strongswan

    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>