]> git.ipfire.org Git - thirdparty/strongswan.git/blame - conf/format-options.py
travis: Add sleep in after_failure
[thirdparty/strongswan.git] / conf / format-options.py
CommitLineData
e90b37b9
TB
1#!/usr/bin/env python
2#
fb8c9b3d
TB
3# Copyright (C) 2014-2017 Tobias Brunner
4# HSR Hochschule fuer Technik Rapperswil
e90b37b9
TB
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.
84a3077e 51
fb8c9b3d
TB
52To add include statements to generated config files (ignored when generating
53man pages) the following format can be used:
54
55full.section.name.include files/to/include
56 Description of this include statement
57
84a3077e
TB
58Dots in section/option names may be escaped with a backslash. For instance,
59with the following section description
60
61charon.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.
e90b37b9
TB
65"""
66
67import sys
68import re
69from textwrap import TextWrapper
70from optparse import OptionParser
fb8c9b3d 71from functools import cmp_to_key
e90b37b9
TB
72
73class ConfigOption:
74 """Representing a configuration option or described section in strongswan.conf"""
fb8c9b3d 75 def __init__(self, path, default = None, section = False, commented = False, include = False):
84a3077e
TB
76 self.path = path
77 self.name = path[-1]
78 self.fullname = '.'.join(path)
e90b37b9
TB
79 self.default = default
80 self.section = section
81 self.commented = commented
fb8c9b3d 82 self.include = include
e90b37b9
TB
83 self.desc = []
84 self.options = []
85
35952dc1 86 def __lt__(self, other):
84a3077e 87 return self.name < other.name
e90b37b9
TB
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
fb8c9b3d
TB
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
e90b37b9
TB
116class Parser:
117 """Parses one or more files of configuration options"""
ae98a39e 118 def __init__(self, sort = True):
e90b37b9 119 self.options = []
ae98a39e 120 self.sort = sort
e90b37b9
TB
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)
84a3077e
TB
139 path = self.__split_name(m.group('name'))
140 self.__current = ConfigOption(path, m.group('default'),
e90b37b9
TB
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)
84a3077e
TB
148 path = self.__split_name(m.group('name'))
149 self.__current = ConfigOption(path, section = True,
e90b37b9
TB
150 commented = m.group('comment'))
151 return
fb8c9b3d
TB
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
e90b37b9
TB
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
84a3077e
TB
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
e90b37b9
TB
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)]
84a3077e 176 parent = self.__get_option(option.path[:-1], True)
e90b37b9
TB
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)
ae98a39e
MW
185 if self.sort:
186 parent.options.sort()
e90b37b9 187
84a3077e 188 def __get_option(self, path, create = False):
e90b37b9
TB
189 """Searches/Creates the option (section) based on a list of section names"""
190 option = None
191 options = self.options
84a3077e 192 for i, name in enumerate(path, 1):
e90b37b9
TB
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
84a3077e 197 option = ConfigOption(path[:i], section = True)
e90b37b9 198 options.append(option)
ae98a39e
MW
199 if self.sort:
200 options.sort()
e90b37b9
TB
201 options = option.options
202 return option
203
204 def get_option(self, name):
205 """Retrieves the option with the given name"""
84a3077e 206 return self.__get_option(self.__split_name(name))
e90b37b9
TB
207
208class 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
00498d78 220 (?P<text>\S|\S.*?\S) # text
e90b37b9
TB
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
240class 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
255class 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
5ee4984d 267 print(self.__wrapper.fill(self.__tags.replace(opt.desc[0])))
e90b37b9
TB
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)
fb8c9b3d
TB
273 if opt.include:
274 print('{0}{1} {2}'.format(self.__indent * indent, opt.name, opt.default))
275 elif opt.default:
5ee4984d 276 print('{0}{1}{2} = {3}'.format(self.__indent * indent, comment, opt.name, opt.default))
e90b37b9 277 else:
5ee4984d
TB
278 print('{0}{1}{2} ='.format(self.__indent * indent, comment, opt.name))
279 print('')
e90b37b9
TB
280
281 def __print_section(self, section, indent, commented):
282 """Print a section with all options"""
e20deeca
TB
283 commented = commented or section.commented
284 comment = "# " if commented else ""
e90b37b9 285 self.__print_description(section, indent)
5ee4984d
TB
286 print('{0}{1}{2} {{'.format(self.__indent * indent, comment, section.name))
287 print('')
fb8c9b3d 288 for o in sorted(section.options, key=cmp_to_key(ConfigOption.cmp)):
e90b37b9 289 if o.section:
e20deeca 290 self.__print_section(o, indent + 1, commented)
e90b37b9 291 else:
e20deeca 292 self.__print_option(o, indent + 1, commented)
5ee4984d
TB
293 print('{0}{1}}}'.format(self.__indent * indent, comment))
294 print('')
e90b37b9
TB
295
296 def format(self, options):
297 """Print a list of options"""
298 if not options:
299 return
fb8c9b3d 300 for option in sorted(options, key=cmp_to_key(ConfigOption.cmp)):
e90b37b9
TB
301 if option.section:
302 self.__print_section(option, 0, False)
303 else:
304 self.__print_option(option, 0, False)
305
306class 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
fb8c9b3d
TB
324 if option.include:
325 return
e90b37b9 326 if option.section:
5ee4984d 327 print('.TP\n.B {0}\n.br'.format(option.fullname))
e90b37b9 328 else:
5ee4984d 329 print('.TP')
e90b37b9 330 default = option.default if option.default else ''
5ee4984d 331 print('.BR {0} " [{1}]"'.format(option.fullname, default))
e90b37b9 332 for para in option.desc if len(option.desc) < 2 else option.desc[1:]:
5ee4984d
TB
333 print(self.__groffize(self.__wrapper.fill(para)))
334 print('')
e90b37b9
TB
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
347options = OptionParser(usage = "Usage: %prog [options] file1 file2\n\n"
348 "If no filenames are provided the input is read from stdin.")
349options.add_option("-f", "--format", dest="format", type="choice", choices=["conf", "man"],
350 help="output format: conf, man [default: %default]", default="conf")
351options.add_option("-r", "--root", dest="root", metavar="NAME",
352 help="root section of which options are printed, "
353 "if not found everything is printed")
ae98a39e
MW
354options.add_option("-n", "--nosort", action="store_false", dest="sort",
355 default=True, help="do not sort sections alphabetically")
356
e90b37b9
TB
357(opts, args) = options.parse_args()
358
ae98a39e 359parser = Parser(opts.sort)
e90b37b9
TB
360if 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))
367else:
368 parser.parse(sys.stdin)
369
370options = parser.options
371if (opts.root):
372 root = parser.get_option(opts.root)
373 if root:
374 options = root.options
375
376if opts.format == "conf":
377 formatter = ConfFormatter()
378elif opts.format == "man":
379 formatter = ManFormatter()
380
381formatter.format(options)