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