conf: Properly propagate whether a section is commented or not
[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 from operator import attrgetter
58
59 class 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
70 def __cmp__(self, other):
71 return cmp(self.name, other.name)
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
93 class Parser:
94 """Parses one or more files of configuration options"""
95 def __init__(self):
96 self.options = []
97
98 def parse(self, file):
99 """Parses the given file and adds all options to the internal store"""
100 self.__current = None
101 for line in file:
102 self.__parse_line(line)
103 if self.__current:
104 self.__add_option(self.__current)
105
106 def __parse_line(self, line):
107 """Parses a single line"""
108 if re.match(r'^\s*#', line):
109 return
110 # option definition
111 m = re.match(r'^(?P<name>\S+)\s*((?P<assign>:)?=\s*(?P<default>.+)?)?\s*$', line)
112 if m:
113 if self.__current:
114 self.__add_option(self.__current)
115 self.__current = ConfigOption(m.group('name'), m.group('default'),
116 commented = not m.group('assign'))
117 return
118 # section definition
119 m = re.match(r'^(?P<name>\S+)\s*\{\s*(?P<comment>#)?\s*\}\s*$', line)
120 if m:
121 if self.__current:
122 self.__add_option(self.__current)
123 self.__current = ConfigOption(m.group('name'), section = True,
124 commented = m.group('comment'))
125 return
126 # paragraph separator
127 m = re.match(r'^\s*$', line)
128 if m and self.__current:
129 self.__current.add_paragraph()
130 # description line
131 m = re.match(r'^\s+(?P<text>.+?)\s*$', line)
132 if m and self.__current:
133 self.__current.add(m.group('text'))
134
135 def __add_option(self, option):
136 """Adds the given option to the abstract storage"""
137 option.desc = [desc for desc in option.desc if len(desc)]
138 parts = option.fullname.split('.')
139 parent = self.__get_option(parts[:-1], True)
140 if not parent:
141 parent = self
142 found = next((x for x in parent.options if x.name == option.name
143 and x.section == option.section), None)
144 if found:
145 found.adopt(option)
146 else:
147 parent.options.append(option)
148 parent.options.sort()
149
150 def __get_option(self, parts, create = False):
151 """Searches/Creates the option (section) based on a list of section names"""
152 option = None
153 options = self.options
154 fullname = ""
155 for name in parts:
156 fullname += '.' + name if len(fullname) else name
157 option = next((x for x in options if x.name == name and x.section), None)
158 if not option:
159 if not create:
160 break
161 option = ConfigOption(fullname, section = True)
162 options.append(option)
163 options.sort()
164 options = option.options
165 return option
166
167 def get_option(self, name):
168 """Retrieves the option with the given name"""
169 return self.__get_option(name.split('.'))
170
171 class TagReplacer:
172 """Replaces formatting tags in text"""
173 def __init__(self):
174 self.__matcher_b = self.__create_matcher('**')
175 self.__matcher_i = self.__create_matcher('_')
176 self.__replacer = None
177
178 def __create_matcher(self, tag):
179 tag = re.escape(tag)
180 return re.compile(r'''
181 (^|\s|(?P<brack>[(\[])) # prefix with optional opening bracket
182 (?P<tag>''' + tag + r''') # start tag
183 (?P<text>\w|\S.*?\S) # text
184 ''' + tag + r''' # end tag
185 (?P<punct>([.,!:)\]]|\(\d+\))*) # punctuation
186 (?=$|\s) # suffix (don't consume it so that subsequent tags can match)
187 ''', flags = re.DOTALL | re.VERBOSE)
188
189 def _create_replacer(self):
190 def replacer(m):
191 punct = m.group('punct')
192 if not punct:
193 punct = ''
194 return '{0}{1}{2}'.format(m.group(1), m.group('text'), punct)
195 return replacer
196
197 def replace(self, text):
198 if not self.__replacer:
199 self.__replacer = self._create_replacer()
200 text = re.sub(self.__matcher_b, self.__replacer, text)
201 return re.sub(self.__matcher_i, self.__replacer, text)
202
203 class GroffTagReplacer(TagReplacer):
204 def _create_replacer(self):
205 def replacer(m):
206 nl = '\n' if m.group(1) else ''
207 format = 'I' if m.group('tag') == '_' else 'B'
208 brack = m.group('brack')
209 if not brack:
210 brack = ''
211 punct = m.group('punct')
212 if not punct:
213 punct = ''
214 text = re.sub(r'[\r\n\t]', ' ', m.group('text'))
215 return '{0}.R{1} "{2}" "{3}" "{4}"\n'.format(nl, format, brack, text, punct)
216 return replacer
217
218 class ConfFormatter:
219 """Formats options to a strongswan.conf snippet"""
220 def __init__(self):
221 self.__indent = ' '
222 self.__wrapper = TextWrapper(width = 80, replace_whitespace = True,
223 break_long_words = False, break_on_hyphens = False)
224 self.__tags = TagReplacer()
225
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 format(self.__wrapper.fill(self.__tags.replace(opt.desc[0])))
231
232 def __print_option(self, opt, indent, commented):
233 """Print a single option with description and default value"""
234 comment = "# " if commented or opt.commented else ""
235 self.__print_description(opt, indent)
236 if opt.default:
237 print '{0}{1}{2} = {3}'.format(self.__indent * indent, comment, opt.name, opt.default)
238 else:
239 print '{0}{1}{2} ='.format(self.__indent * indent, comment, opt.name)
240 print
241
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=attrgetter('section')):
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
257 def format(self, options):
258 """Print a list of options"""
259 if not options:
260 return
261 for option in sorted(options, key=attrgetter('section')):
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)