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