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