format-options.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. #!/usr/bin/env python
  2. #
  3. # Copyright (C) 2014-2017 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. Parses strongswan.conf option descriptions and produces configuration file
  17. and man page snippets.
  18. The format for description files is as follows:
  19. full.option.name [[:]= default]
  20. Short description intended as comment in config snippet
  21. Long description for use in the man page, with
  22. simple formatting: _italic_, **bold**
  23. Second paragraph of the long description
  24. The descriptions must be indented by tabs or spaces but are both optional.
  25. If only a short description is given it is used for both intended usages.
  26. Line breaks within a paragraph of the long description or the short description
  27. are not preserved. But multiple paragraphs will be separated in the man page.
  28. Any formatting in the short description is removed when producing config
  29. snippets.
  30. Options for which a value is assigned with := are not commented out in the
  31. produced configuration file snippet. This allows to override a default value,
  32. that e.g. has to be preserved for legacy reasons, in the generated default
  33. config.
  34. To describe sections the following format can be used:
  35. full.section.name {[#]}
  36. Short description of this section
  37. Long description as above
  38. If a # is added between the curly braces the section header will be commented
  39. out in the configuration file snippet, which is useful for example sections.
  40. To add include statements to generated config files (ignored when generating
  41. man pages) the following format can be used:
  42. full.section.name.include files/to/include
  43. Description of this include statement
  44. Dots in section/option names may be escaped with a backslash. For instance,
  45. with the following section description
  46. charon.filelog./var/log/daemon\.log {}
  47. Section to define logging into /var/log/daemon.log
  48. /var/log/daemon.log will be the name of the last section.
  49. """
  50. import sys
  51. import re
  52. from textwrap import TextWrapper
  53. from optparse import OptionParser
  54. from functools import cmp_to_key
  55. class ConfigOption:
  56. """Representing a configuration option or described section in strongswan.conf"""
  57. def __init__(self, path, default = None, section = False, commented = False, include = False):
  58. self.path = path
  59. self.name = path[-1]
  60. self.fullname = '.'.join(path)
  61. self.default = default
  62. self.section = section
  63. self.commented = commented
  64. self.include = include
  65. self.desc = []
  66. self.options = []
  67. def __lt__(self, other):
  68. return self.name < other.name
  69. def add_paragraph(self):
  70. """Adds a new paragraph to the description"""
  71. if len(self.desc) and len(self.desc[-1]):
  72. self.desc.append("")
  73. def add(self, line):
  74. """Adds a line to the last paragraph"""
  75. if not len(self.desc):
  76. self.desc.append(line)
  77. elif not len(self.desc[-1]):
  78. self.desc[-1] = line
  79. else:
  80. self.desc[-1] += ' ' + line
  81. def adopt(self, other):
  82. """Adopts settings from other, which should be more recently parsed"""
  83. self.default = other.default
  84. self.commented = other.commented
  85. self.desc = other.desc
  86. @staticmethod
  87. def cmp(a, b):
  88. # order options before sections and includes last
  89. if a.include or b.include:
  90. return a.include - b.include
  91. return a.section - b.section
  92. class Parser:
  93. """Parses one or more files of configuration options"""
  94. def __init__(self, sort = True):
  95. self.options = []
  96. self.sort = sort
  97. def parse(self, file):
  98. """Parses the given file and adds all options to the internal store"""
  99. self.__current = None
  100. for line in file:
  101. self.__parse_line(line)
  102. if self.__current:
  103. self.__add_option(self.__current)
  104. def __parse_line(self, line):
  105. """Parses a single line"""
  106. if re.match(r'^\s*#', line):
  107. return
  108. # option definition
  109. m = re.match(r'^(?P<name>\S+)\s*((?P<assign>:)?=\s*(?P<default>.+)?)?\s*$', line)
  110. if m:
  111. if self.__current:
  112. self.__add_option(self.__current)
  113. path = self.__split_name(m.group('name'))
  114. self.__current = ConfigOption(path, m.group('default'),
  115. commented = not m.group('assign'))
  116. return
  117. # section definition
  118. m = re.match(r'^(?P<name>\S+)\s*\{\s*(?P<comment>#)?\s*\}\s*$', line)
  119. if m:
  120. if self.__current:
  121. self.__add_option(self.__current)
  122. path = self.__split_name(m.group('name'))
  123. self.__current = ConfigOption(path, section = True,
  124. commented = m.group('comment'))
  125. return
  126. # include definition
  127. m = re.match(r'^(?P<name>\S+\.include|include)\s+(?P<pattern>\S+)\s*$', line)
  128. if m:
  129. if self.__current:
  130. self.__add_option(self.__current)
  131. path = self.__split_name(m.group('name'))
  132. self.__current = ConfigOption(path, m.group('pattern'), include = True)
  133. return
  134. # paragraph separator
  135. m = re.match(r'^\s*$', line)
  136. if m and self.__current:
  137. self.__current.add_paragraph()
  138. # description line
  139. m = re.match(r'^\s+(?P<text>.+?)\s*$', line)
  140. if m and self.__current:
  141. self.__current.add(m.group('text'))
  142. def __split_name(self, name):
  143. """Split the given full name in a list of section/option names"""
  144. return [x.replace('\.', '.') for x in re.split(r'(?<!\\)\.', name)]
  145. def __add_option(self, option):
  146. """Adds the given option to the abstract storage"""
  147. option.desc = [desc for desc in option.desc if len(desc)]
  148. parent = self.__get_option(option.path[:-1], True)
  149. if not parent:
  150. parent = self
  151. found = next((x for x in parent.options if x.name == option.name
  152. and x.section == option.section), None)
  153. if found:
  154. found.adopt(option)
  155. else:
  156. parent.options.append(option)
  157. if self.sort:
  158. parent.options.sort()
  159. def __get_option(self, path, create = False):
  160. """Searches/Creates the option (section) based on a list of section names"""
  161. option = None
  162. options = self.options
  163. for i, name in enumerate(path, 1):
  164. option = next((x for x in options if x.name == name and x.section), None)
  165. if not option:
  166. if not create:
  167. break
  168. option = ConfigOption(path[:i], section = True)
  169. options.append(option)
  170. if self.sort:
  171. options.sort()
  172. options = option.options
  173. return option
  174. def get_option(self, name):
  175. """Retrieves the option with the given name"""
  176. return self.__get_option(self.__split_name(name))
  177. class TagReplacer:
  178. """Replaces formatting tags in text"""
  179. def __init__(self):
  180. self.__matcher_b = self.__create_matcher('**')
  181. self.__matcher_i = self.__create_matcher('_')
  182. self.__replacer = None
  183. def __create_matcher(self, tag):
  184. tag = re.escape(tag)
  185. return re.compile(r'''
  186. (^|\s|(?P<brack>[(\[])) # prefix with optional opening bracket
  187. (?P<tag>''' + tag + r''') # start tag
  188. (?P<text>\S|\S.*?\S) # text
  189. ''' + tag + r''' # end tag
  190. (?P<punct>([.,!:)\]]|\(\d+\))*) # punctuation
  191. (?=$|\s) # suffix (don't consume it so that subsequent tags can match)
  192. ''', flags = re.DOTALL | re.VERBOSE)
  193. def _create_replacer(self):
  194. def replacer(m):
  195. punct = m.group('punct')
  196. if not punct:
  197. punct = ''
  198. return '{0}{1}{2}'.format(m.group(1), m.group('text'), punct)
  199. return replacer
  200. def replace(self, text):
  201. if not self.__replacer:
  202. self.__replacer = self._create_replacer()
  203. text = re.sub(self.__matcher_b, self.__replacer, text)
  204. return re.sub(self.__matcher_i, self.__replacer, text)
  205. class GroffTagReplacer(TagReplacer):
  206. def _create_replacer(self):
  207. def replacer(m):
  208. nl = '\n' if m.group(1) else ''
  209. format = 'I' if m.group('tag') == '_' else 'B'
  210. brack = m.group('brack')
  211. if not brack:
  212. brack = ''
  213. punct = m.group('punct')
  214. if not punct:
  215. punct = ''
  216. text = re.sub(r'[\r\n\t]', ' ', m.group('text'))
  217. return '{0}.R{1} "{2}" "{3}" "{4}"\n'.format(nl, format, brack, text, punct)
  218. return replacer
  219. class ConfFormatter:
  220. """Formats options to a strongswan.conf snippet"""
  221. def __init__(self):
  222. self.__indent = ' '
  223. self.__wrapper = TextWrapper(width = 80, replace_whitespace = True,
  224. break_long_words = False, break_on_hyphens = False)
  225. self.__tags = TagReplacer()
  226. def __print_description(self, opt, indent):
  227. if len(opt.desc):
  228. self.__wrapper.initial_indent = '{0}# '.format(self.__indent * indent)
  229. self.__wrapper.subsequent_indent = self.__wrapper.initial_indent
  230. print(self.__wrapper.fill(self.__tags.replace(opt.desc[0])))
  231. def __print_option(self, opt, indent, commented):
  232. """Print a single option with description and default value"""
  233. comment = "# " if commented or opt.commented else ""
  234. self.__print_description(opt, indent)
  235. if opt.include:
  236. print('{0}{1} {2}'.format(self.__indent * indent, opt.name, opt.default))
  237. elif opt.default:
  238. print('{0}{1}{2} = {3}'.format(self.__indent * indent, comment, opt.name, opt.default))
  239. else:
  240. print('{0}{1}{2} ='.format(self.__indent * indent, comment, opt.name))
  241. print('')
  242. def __print_section(self, section, indent, commented):
  243. """Print a section with all options"""
  244. commented = commented or section.commented
  245. comment = "# " if commented else ""
  246. self.__print_description(section, indent)
  247. print('{0}{1}{2} {{'.format(self.__indent * indent, comment, section.name))
  248. print('')
  249. for o in sorted(section.options, key=cmp_to_key(ConfigOption.cmp)):
  250. if o.section:
  251. self.__print_section(o, indent + 1, commented)
  252. else:
  253. self.__print_option(o, indent + 1, commented)
  254. print('{0}{1}}}'.format(self.__indent * indent, comment))
  255. print('')
  256. def format(self, options):
  257. """Print a list of options"""
  258. if not options:
  259. return
  260. for option in sorted(options, key=cmp_to_key(ConfigOption.cmp)):
  261. if option.section:
  262. self.__print_section(option, 0, False)
  263. else:
  264. self.__print_option(option, 0, False)
  265. class ManFormatter:
  266. """Formats a list of options into a groff snippet"""
  267. def __init__(self):
  268. self.__wrapper = TextWrapper(width = 80, replace_whitespace = False,
  269. break_long_words = False, break_on_hyphens = False)
  270. self.__tags = GroffTagReplacer()
  271. def __groffize(self, text):
  272. """Encode text as groff text"""
  273. text = self.__tags.replace(text)
  274. text = re.sub(r'(?<!\\)-', r'\\-', text)
  275. # remove any leading whitespace
  276. return re.sub(r'^\s+', '', text, flags = re.MULTILINE)
  277. def __format_option(self, option):
  278. """Print a single option"""
  279. if option.section and not len(option.desc):
  280. return
  281. if option.include:
  282. return
  283. if option.section:
  284. print('.TP\n.B {0}\n.br'.format(option.fullname))
  285. else:
  286. print('.TP')
  287. default = option.default if option.default else ''
  288. print('.BR {0} " [{1}]"'.format(option.fullname, default))
  289. for para in option.desc if len(option.desc) < 2 else option.desc[1:]:
  290. print(self.__groffize(self.__wrapper.fill(para)))
  291. print('')
  292. def format(self, options):
  293. """Print a list of options"""
  294. if not options:
  295. return
  296. for option in options:
  297. if option.section:
  298. self.__format_option(option)
  299. self.format(option.options)
  300. else:
  301. self.__format_option(option)
  302. options = OptionParser(usage = "Usage: %prog [options] file1 file2\n\n"
  303. "If no filenames are provided the input is read from stdin.")
  304. options.add_option("-f", "--format", dest="format", type="choice", choices=["conf", "man"],
  305. help="output format: conf, man [default: %default]", default="conf")
  306. options.add_option("-r", "--root", dest="root", metavar="NAME",
  307. help="root section of which options are printed, "
  308. "if not found everything is printed")
  309. options.add_option("-n", "--nosort", action="store_false", dest="sort",
  310. default=True, help="do not sort sections alphabetically")
  311. (opts, args) = options.parse_args()
  312. parser = Parser(opts.sort)
  313. if len(args):
  314. for filename in args:
  315. try:
  316. with open(filename, 'r') as file:
  317. parser.parse(file)
  318. except IOError as e:
  319. sys.stderr.write("Unable to open '{0}': {1}\n".format(filename, e.strerror))
  320. else:
  321. parser.parse(sys.stdin)
  322. options = parser.options
  323. if (opts.root):
  324. root = parser.get_option(opts.root)
  325. if root:
  326. options = root.options
  327. if opts.format == "conf":
  328. formatter = ConfFormatter()
  329. elif opts.format == "man":
  330. formatter = ManFormatter()
  331. formatter.format(options)