ikev1: Always enable charon.reuse_ikesa
[strongswan.git] / conf / format-options.py
1 #!/usr/bin/env python
2 #
3 # Copyright (C) 2014-2015 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 Dots in section/option names may be escaped with a backslash. For instance,
53 with the following section description
54
55 charon.filelog./var/log/daemon\.log {}
56 Section to define logging into /var/log/daemon.log
57
58 /var/log/daemon.log will be the name of the last section.
59 """
60
61 import sys
62 import re
63 from textwrap import TextWrapper
64 from optparse import OptionParser
65 from operator import attrgetter
66
67 class ConfigOption:
68 """Representing a configuration option or described section in strongswan.conf"""
69 def __init__(self, path, default = None, section = False, commented = False):
70 self.path = path
71 self.name = path[-1]
72 self.fullname = '.'.join(path)
73 self.default = default
74 self.section = section
75 self.commented = commented
76 self.desc = []
77 self.options = []
78
79 def __lt__(self, other):
80 return self.name < other.name
81
82 def add_paragraph(self):
83 """Adds a new paragraph to the description"""
84 if len(self.desc) and len(self.desc[-1]):
85 self.desc.append("")
86
87 def add(self, line):
88 """Adds a line to the last paragraph"""
89 if not len(self.desc):
90 self.desc.append(line)
91 elif not len(self.desc[-1]):
92 self.desc[-1] = line
93 else:
94 self.desc[-1] += ' ' + line
95
96 def adopt(self, other):
97 """Adopts settings from other, which should be more recently parsed"""
98 self.default = other.default
99 self.commented = other.commented
100 self.desc = other.desc
101
102 class Parser:
103 """Parses one or more files of configuration options"""
104 def __init__(self, sort = True):
105 self.options = []
106 self.sort = sort
107
108 def parse(self, file):
109 """Parses the given file and adds all options to the internal store"""
110 self.__current = None
111 for line in file:
112 self.__parse_line(line)
113 if self.__current:
114 self.__add_option(self.__current)
115
116 def __parse_line(self, line):
117 """Parses a single line"""
118 if re.match(r'^\s*#', line):
119 return
120 # option definition
121 m = re.match(r'^(?P<name>\S+)\s*((?P<assign>:)?=\s*(?P<default>.+)?)?\s*$', line)
122 if m:
123 if self.__current:
124 self.__add_option(self.__current)
125 path = self.__split_name(m.group('name'))
126 self.__current = ConfigOption(path, m.group('default'),
127 commented = not m.group('assign'))
128 return
129 # section definition
130 m = re.match(r'^(?P<name>\S+)\s*\{\s*(?P<comment>#)?\s*\}\s*$', line)
131 if m:
132 if self.__current:
133 self.__add_option(self.__current)
134 path = self.__split_name(m.group('name'))
135 self.__current = ConfigOption(path, section = True,
136 commented = m.group('comment'))
137 return
138 # paragraph separator
139 m = re.match(r'^\s*$', line)
140 if m and self.__current:
141 self.__current.add_paragraph()
142 # description line
143 m = re.match(r'^\s+(?P<text>.+?)\s*$', line)
144 if m and self.__current:
145 self.__current.add(m.group('text'))
146
147 def __split_name(self, name):
148 """Split the given full name in a list of section/option names"""
149 return [x.replace('\.', '.') for x in re.split(r'(?<!\\)\.', name)]
150
151 def __add_option(self, option):
152 """Adds the given option to the abstract storage"""
153 option.desc = [desc for desc in option.desc if len(desc)]
154 parent = self.__get_option(option.path[:-1], True)
155 if not parent:
156 parent = self
157 found = next((x for x in parent.options if x.name == option.name
158 and x.section == option.section), None)
159 if found:
160 found.adopt(option)
161 else:
162 parent.options.append(option)
163 if self.sort:
164 parent.options.sort()
165
166 def __get_option(self, path, create = False):
167 """Searches/Creates the option (section) based on a list of section names"""
168 option = None
169 options = self.options
170 for i, name in enumerate(path, 1):
171 option = next((x for x in options if x.name == name and x.section), None)
172 if not option:
173 if not create:
174 break
175 option = ConfigOption(path[:i], section = True)
176 options.append(option)
177 if self.sort:
178 options.sort()
179 options = option.options
180 return option
181
182 def get_option(self, name):
183 """Retrieves the option with the given name"""
184 return self.__get_option(self.__split_name(name))
185
186 class TagReplacer:
187 """Replaces formatting tags in text"""
188 def __init__(self):
189 self.__matcher_b = self.__create_matcher('**')
190 self.__matcher_i = self.__create_matcher('_')
191 self.__replacer = None
192
193 def __create_matcher(self, tag):
194 tag = re.escape(tag)
195 return re.compile(r'''
196 (^|\s|(?P<brack>[(\[])) # prefix with optional opening bracket
197 (?P<tag>''' + tag + r''') # start tag
198 (?P<text>\w|\S.*?\S) # text
199 ''' + tag + r''' # end tag
200 (?P<punct>([.,!:)\]]|\(\d+\))*) # punctuation
201 (?=$|\s) # suffix (don't consume it so that subsequent tags can match)
202 ''', flags = re.DOTALL | re.VERBOSE)
203
204 def _create_replacer(self):
205 def replacer(m):
206 punct = m.group('punct')
207 if not punct:
208 punct = ''
209 return '{0}{1}{2}'.format(m.group(1), m.group('text'), punct)
210 return replacer
211
212 def replace(self, text):
213 if not self.__replacer:
214 self.__replacer = self._create_replacer()
215 text = re.sub(self.__matcher_b, self.__replacer, text)
216 return re.sub(self.__matcher_i, self.__replacer, text)
217
218 class GroffTagReplacer(TagReplacer):
219 def _create_replacer(self):
220 def replacer(m):
221 nl = '\n' if m.group(1) else ''
222 format = 'I' if m.group('tag') == '_' else 'B'
223 brack = m.group('brack')
224 if not brack:
225 brack = ''
226 punct = m.group('punct')
227 if not punct:
228 punct = ''
229 text = re.sub(r'[\r\n\t]', ' ', m.group('text'))
230 return '{0}.R{1} "{2}" "{3}" "{4}"\n'.format(nl, format, brack, text, punct)
231 return replacer
232
233 class ConfFormatter:
234 """Formats options to a strongswan.conf snippet"""
235 def __init__(self):
236 self.__indent = ' '
237 self.__wrapper = TextWrapper(width = 80, replace_whitespace = True,
238 break_long_words = False, break_on_hyphens = False)
239 self.__tags = TagReplacer()
240
241 def __print_description(self, opt, indent):
242 if len(opt.desc):
243 self.__wrapper.initial_indent = '{0}# '.format(self.__indent * indent)
244 self.__wrapper.subsequent_indent = self.__wrapper.initial_indent
245 print(self.__wrapper.fill(self.__tags.replace(opt.desc[0])))
246
247 def __print_option(self, opt, indent, commented):
248 """Print a single option with description and default value"""
249 comment = "# " if commented or opt.commented else ""
250 self.__print_description(opt, indent)
251 if opt.default:
252 print('{0}{1}{2} = {3}'.format(self.__indent * indent, comment, opt.name, opt.default))
253 else:
254 print('{0}{1}{2} ='.format(self.__indent * indent, comment, opt.name))
255 print('')
256
257 def __print_section(self, section, indent, commented):
258 """Print a section with all options"""
259 commented = commented or section.commented
260 comment = "# " if commented else ""
261 self.__print_description(section, indent)
262 print('{0}{1}{2} {{'.format(self.__indent * indent, comment, section.name))
263 print('')
264 for o in sorted(section.options, key=attrgetter('section')):
265 if o.section:
266 self.__print_section(o, indent + 1, commented)
267 else:
268 self.__print_option(o, indent + 1, commented)
269 print('{0}{1}}}'.format(self.__indent * indent, comment))
270 print('')
271
272 def format(self, options):
273 """Print a list of options"""
274 if not options:
275 return
276 for option in sorted(options, key=attrgetter('section')):
277 if option.section:
278 self.__print_section(option, 0, False)
279 else:
280 self.__print_option(option, 0, False)
281
282 class ManFormatter:
283 """Formats a list of options into a groff snippet"""
284 def __init__(self):
285 self.__wrapper = TextWrapper(width = 80, replace_whitespace = False,
286 break_long_words = False, break_on_hyphens = False)
287 self.__tags = GroffTagReplacer()
288
289 def __groffize(self, text):
290 """Encode text as groff text"""
291 text = self.__tags.replace(text)
292 text = re.sub(r'(?<!\\)-', r'\\-', text)
293 # remove any leading whitespace
294 return re.sub(r'^\s+', '', text, flags = re.MULTILINE)
295
296 def __format_option(self, option):
297 """Print a single option"""
298 if option.section and not len(option.desc):
299 return
300 if option.section:
301 print('.TP\n.B {0}\n.br'.format(option.fullname))
302 else:
303 print('.TP')
304 default = option.default if option.default else ''
305 print('.BR {0} " [{1}]"'.format(option.fullname, default))
306 for para in option.desc if len(option.desc) < 2 else option.desc[1:]:
307 print(self.__groffize(self.__wrapper.fill(para)))
308 print('')
309
310 def format(self, options):
311 """Print a list of options"""
312 if not options:
313 return
314 for option in options:
315 if option.section:
316 self.__format_option(option)
317 self.format(option.options)
318 else:
319 self.__format_option(option)
320
321 options = OptionParser(usage = "Usage: %prog [options] file1 file2\n\n"
322 "If no filenames are provided the input is read from stdin.")
323 options.add_option("-f", "--format", dest="format", type="choice", choices=["conf", "man"],
324 help="output format: conf, man [default: %default]", default="conf")
325 options.add_option("-r", "--root", dest="root", metavar="NAME",
326 help="root section of which options are printed, "
327 "if not found everything is printed")
328 options.add_option("-n", "--nosort", action="store_false", dest="sort",
329 default=True, help="do not sort sections alphabetically")
330
331 (opts, args) = options.parse_args()
332
333 parser = Parser(opts.sort)
334 if len(args):
335 for filename in args:
336 try:
337 with open(filename, 'r') as file:
338 parser.parse(file)
339 except IOError as e:
340 sys.stderr.write("Unable to open '{0}': {1}\n".format(filename, e.strerror))
341 else:
342 parser.parse(sys.stdin)
343
344 options = parser.options
345 if (opts.root):
346 root = parser.get_option(opts.root)
347 if root:
348 options = root.options
349
350 if opts.format == "conf":
351 formatter = ConfFormatter()
352 elif opts.format == "man":
353 formatter = ManFormatter()
354
355 formatter.format(options)