]> git.ipfire.org Git - thirdparty/strongswan.git/blame - conf/format-options.py
charon-nm: Correctly set remote auth class for PSK authentication
[thirdparty/strongswan.git] / conf / format-options.py
CommitLineData
e90b37b9
TB
1#!/usr/bin/env python
2#
eea58222 3# Copyright (C) 2014-2019 Tobias Brunner
fb8c9b3d 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
eea58222 70from argparse import ArgumentParser
48017a27 71from functools import cmp_to_key, total_ordering
e90b37b9 72
48017a27 73@total_ordering
e90b37b9
TB
74class ConfigOption:
75 """Representing a configuration option or described section in strongswan.conf"""
fb8c9b3d 76 def __init__(self, path, default = None, section = False, commented = False, include = False):
84a3077e
TB
77 self.path = path
78 self.name = path[-1]
79 self.fullname = '.'.join(path)
e90b37b9
TB
80 self.default = default
81 self.section = section
82 self.commented = commented
fb8c9b3d 83 self.include = include
e90b37b9
TB
84 self.desc = []
85 self.options = []
86
48017a27
TB
87 def __eq__(self, other):
88 return self.name == other.name
89
35952dc1 90 def __lt__(self, other):
84a3077e 91 return self.name < other.name
e90b37b9
TB
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
fb8c9b3d
TB
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
e90b37b9
TB
120class Parser:
121 """Parses one or more files of configuration options"""
ae98a39e 122 def __init__(self, sort = True):
e90b37b9 123 self.options = []
ae98a39e 124 self.sort = sort
e90b37b9
TB
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)
84a3077e
TB
143 path = self.__split_name(m.group('name'))
144 self.__current = ConfigOption(path, m.group('default'),
e90b37b9
TB
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)
84a3077e
TB
152 path = self.__split_name(m.group('name'))
153 self.__current = ConfigOption(path, section = True,
e90b37b9
TB
154 commented = m.group('comment'))
155 return
fb8c9b3d
TB
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
e90b37b9
TB
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
84a3077e
TB
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
e90b37b9
TB
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)]
84a3077e 180 parent = self.__get_option(option.path[:-1], True)
e90b37b9
TB
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)
ae98a39e
MW
189 if self.sort:
190 parent.options.sort()
e90b37b9 191
84a3077e 192 def __get_option(self, path, create = False):
e90b37b9
TB
193 """Searches/Creates the option (section) based on a list of section names"""
194 option = None
195 options = self.options
84a3077e 196 for i, name in enumerate(path, 1):
e90b37b9
TB
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
84a3077e 201 option = ConfigOption(path[:i], section = True)
e90b37b9 202 options.append(option)
ae98a39e
MW
203 if self.sort:
204 options.sort()
e90b37b9
TB
205 options = option.options
206 return option
207
208 def get_option(self, name):
209 """Retrieves the option with the given name"""
84a3077e 210 return self.__get_option(self.__split_name(name))
e90b37b9
TB
211
212class 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
00498d78 224 (?P<text>\S|\S.*?\S) # text
e90b37b9
TB
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
244class 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
259class 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
5ee4984d 271 print(self.__wrapper.fill(self.__tags.replace(opt.desc[0])))
e90b37b9
TB
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)
fb8c9b3d
TB
277 if opt.include:
278 print('{0}{1} {2}'.format(self.__indent * indent, opt.name, opt.default))
279 elif opt.default:
5ee4984d 280 print('{0}{1}{2} = {3}'.format(self.__indent * indent, comment, opt.name, opt.default))
e90b37b9 281 else:
5ee4984d
TB
282 print('{0}{1}{2} ='.format(self.__indent * indent, comment, opt.name))
283 print('')
e90b37b9
TB
284
285 def __print_section(self, section, indent, commented):
286 """Print a section with all options"""
e20deeca
TB
287 commented = commented or section.commented
288 comment = "# " if commented else ""
e90b37b9 289 self.__print_description(section, indent)
5ee4984d
TB
290 print('{0}{1}{2} {{'.format(self.__indent * indent, comment, section.name))
291 print('')
fb8c9b3d 292 for o in sorted(section.options, key=cmp_to_key(ConfigOption.cmp)):
e90b37b9 293 if o.section:
e20deeca 294 self.__print_section(o, indent + 1, commented)
e90b37b9 295 else:
e20deeca 296 self.__print_option(o, indent + 1, commented)
5ee4984d
TB
297 print('{0}{1}}}'.format(self.__indent * indent, comment))
298 print('')
e90b37b9
TB
299
300 def format(self, options):
301 """Print a list of options"""
302 if not options:
303 return
fb8c9b3d 304 for option in sorted(options, key=cmp_to_key(ConfigOption.cmp)):
e90b37b9
TB
305 if option.section:
306 self.__print_section(option, 0, False)
307 else:
308 self.__print_option(option, 0, False)
309
310class 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
fb8c9b3d
TB
328 if option.include:
329 return
e90b37b9 330 if option.section:
5ee4984d 331 print('.TP\n.B {0}\n.br'.format(option.fullname))
e90b37b9 332 else:
5ee4984d 333 print('.TP')
e90b37b9 334 default = option.default if option.default else ''
5ee4984d 335 print('.BR {0} " [{1}]"'.format(option.fullname, default))
e90b37b9 336 for para in option.desc if len(option.desc) < 2 else option.desc[1:]:
5ee4984d
TB
337 print(self.__groffize(self.__wrapper.fill(para)))
338 print('')
e90b37b9
TB
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
eea58222
TB
351args = ArgumentParser()
352args.add_argument('file', nargs='*',
353 help="files to process, omit to read input from stdin")
354args.add_argument("-f", "--format", dest="format", choices=["conf", "man"],
355 help="output format (default: %(default)s)", default="conf")
356args.add_argument("-r", "--root", dest="root", metavar="NAME",
357 help="root section of which options are printed; everything"
358 "is printed if not found")
359args.add_argument("-n", "--nosort", action="store_false", dest="sort",
360 default=True, help="do not sort sections alphabetically")
ae98a39e 361
eea58222 362opts = args.parse_args()
e90b37b9 363
ae98a39e 364parser = Parser(opts.sort)
eea58222
TB
365if len(opts.file):
366 for filename in opts.file:
e90b37b9
TB
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))
372else:
373 parser.parse(sys.stdin)
374
375options = parser.options
376if (opts.root):
377 root = parser.get_option(opts.root)
378 if root:
379 options = root.options
380
381if opts.format == "conf":
382 formatter = ConfFormatter()
383elif opts.format == "man":
384 formatter = ManFormatter()
385
386formatter.format(options)