conf: Script to convert option descriptions to man page and config snippets added
[strongswan.git] / conf / format-options.py
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 """
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
53 import sys
54 import re
55 from textwrap import TextWrapper
56 from optparse import OptionParser
57
58 class ConfigOption:
59 """Representing a configuration option or described section in strongswan.conf"""
60 def __init__(self, name, default = None, section = False, commented = False):
61 self.name = name.split('.')[-1]
62 self.fullname = name
63 self.default = default
64 self.section = section
65 self.commented = commented
66 self.desc = []
67 self.options = []
68
69 def __cmp__(self, other):
70 if self.section == other.section:
71 return cmp(self.name, other.name)
72 return 1 if self.section else -1
73
74 def add_paragraph(self):
75 """Adds a new paragraph to the description"""
76 if len(self.desc) and len(self.desc[-1]):
77 self.desc.append("")
78
79 def add(self, line):
80 """Adds a line to the last paragraph"""
81 if not len(self.desc):
82 self.desc.append(line)
83 elif not len(self.desc[-1]):
84 self.desc[-1] = line
85 else:
86 self.desc[-1] += ' ' + line
87
88 def adopt(self, other):
89 """Adopts settings from other, which should be more recently parsed"""
90 self.default = other.default
91 self.commented = other.commented
92 self.desc = other.desc
93
94 class Parser:
95 """Parses one or more files of configuration options"""
96 def __init__(self):
97 self.options = []
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)
149 parent.options.sort()
150
151 def __get_option(self, parts, create = False):
152 """Searches/Creates the option (section) based on a list of section names"""
153 option = None
154 options = self.options
155 fullname = ""
156 for name in parts:
157 fullname += '.' + name if len(fullname) else name
158 option = next((x for x in options if x.name == name and x.section), None)
159 if not option:
160 if not create:
161 break
162 option = ConfigOption(fullname, section = True)
163 options.append(option)
164 options.sort()
165 options = option.options
166 return option
167
168 def get_option(self, name):
169 """Retrieves the option with the given name"""
170 return self.__get_option(name.split('.'))
171
172 class TagReplacer:
173 """Replaces formatting tags in text"""
174 def __init__(self):
175 self.__matcher_b = self.__create_matcher('**')
176 self.__matcher_i = self.__create_matcher('_')
177 self.__replacer = None
178
179 def __create_matcher(self, tag):
180 tag = re.escape(tag)
181 return re.compile(r'''
182 (^|\s|(?P<brack>[(\[])) # prefix with optional opening bracket
183 (?P<tag>''' + tag + r''') # start tag
184 (?P<text>\w|\S.*?\S) # text
185 ''' + tag + r''' # end tag
186 (?P<punct>([.,!:)\]]|\(\d+\))*) # punctuation
187 (?=$|\s) # suffix (don't consume it so that subsequent tags can match)
188 ''', flags = re.DOTALL | re.VERBOSE)
189
190 def _create_replacer(self):
191 def replacer(m):
192 punct = m.group('punct')
193 if not punct:
194 punct = ''
195 return '{0}{1}{2}'.format(m.group(1), m.group('text'), punct)
196 return replacer
197
198 def replace(self, text):
199 if not self.__replacer:
200 self.__replacer = self._create_replacer()
201 text = re.sub(self.__matcher_b, self.__replacer, text)
202 return re.sub(self.__matcher_i, self.__replacer, text)
203
204 class GroffTagReplacer(TagReplacer):
205 def _create_replacer(self):
206 def replacer(m):
207 nl = '\n' if m.group(1) else ''
208 format = 'I' if m.group('tag') == '_' else 'B'
209 brack = m.group('brack')
210 if not brack:
211 brack = ''
212 punct = m.group('punct')
213 if not punct:
214 punct = ''
215 text = re.sub(r'[\r\n\t]', ' ', m.group('text'))
216 return '{0}.R{1} "{2}" "{3}" "{4}"\n'.format(nl, format, brack, text, punct)
217 return replacer
218
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
227 def __print_description(self, opt, indent):
228 if len(opt.desc):
229 self.__wrapper.initial_indent = '{0}# '.format(self.__indent * indent)
230 self.__wrapper.subsequent_indent = self.__wrapper.initial_indent
231 print format(self.__wrapper.fill(self.__tags.replace(opt.desc[0])))
232
233 def __print_option(self, opt, indent, commented):
234 """Print a single option with description and default value"""
235 comment = "# " if commented or opt.commented else ""
236 self.__print_description(opt, indent)
237 if 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
243 def __print_section(self, section, indent, commented):
244 """Print a section with all options"""
245 comment = "# " if commented or section.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 section.options:
250 if o.section:
251 self.__print_section(o, indent + 1, section.commented)
252 else:
253 self.__print_option(o, indent + 1, section.commented)
254 print '{0}{1}}}'.format(self.__indent * indent, comment)
255 print
256
257 def format(self, options):
258 """Print a list of options"""
259 if not options:
260 return
261 for option in options:
262 if option.section:
263 self.__print_section(option, 0, False)
264 else:
265 self.__print_option(option, 0, False)
266
267 class ManFormatter:
268 """Formats a list of options into a groff snippet"""
269 def __init__(self):
270 self.__wrapper = TextWrapper(width = 80, replace_whitespace = False,
271 break_long_words = False, break_on_hyphens = False)
272 self.__tags = GroffTagReplacer()
273
274 def __groffize(self, text):
275 """Encode text as groff text"""
276 text = self.__tags.replace(text)
277 text = re.sub(r'(?<!\\)-', r'\\-', text)
278 # remove any leading whitespace
279 return re.sub(r'^\s+', '', text, flags = re.MULTILINE)
280
281 def __format_option(self, option):
282 """Print a single option"""
283 if option.section and not len(option.desc):
284 return
285 if option.section:
286 print '.TP\n.B {0}\n.br'.format(option.fullname)
287 else:
288 print '.TP'
289 default = option.default if option.default else ''
290 print '.BR {0} " [{1}]"'.format(option.fullname, default)
291 for para in option.desc if len(option.desc) < 2 else option.desc[1:]:
292 print self.__groffize(self.__wrapper.fill(para))
293 print ''
294
295 def format(self, options):
296 """Print a list of options"""
297 if not options:
298 return
299 for option in options:
300 if option.section:
301 self.__format_option(option)
302 self.format(option.options)
303 else:
304 self.__format_option(option)
305
306 options = OptionParser(usage = "Usage: %prog [options] file1 file2\n\n"
307 "If no filenames are provided the input is read from stdin.")
308 options.add_option("-f", "--format", dest="format", type="choice", choices=["conf", "man"],
309 help="output format: conf, man [default: %default]", default="conf")
310 options.add_option("-r", "--root", dest="root", metavar="NAME",
311 help="root section of which options are printed, "
312 "if not found everything is printed")
313 (opts, args) = options.parse_args()
314
315 parser = Parser()
316 if len(args):
317 for filename in args:
318 try:
319 with open(filename, 'r') as file:
320 parser.parse(file)
321 except IOError as e:
322 sys.stderr.write("Unable to open '{0}': {1}\n".format(filename, e.strerror))
323 else:
324 parser.parse(sys.stdin)
325
326 options = parser.options
327 if (opts.root):
328 root = parser.get_option(opts.root)
329 if root:
330 options = root.options
331
332 if opts.format == "conf":
333 formatter = ConfFormatter()
334 elif opts.format == "man":
335 formatter = ManFormatter()
336
337 formatter.format(options)