]> git.ipfire.org Git - thirdparty/strongswan.git/blame - conf/format-options.py
github: Use tpm2-tss 3.2.3 for tests
[thirdparty/strongswan.git] / conf / format-options.py
CommitLineData
e166e691 1#!/usr/bin/env python3
e90b37b9 2#
eea58222 3# Copyright (C) 2014-2019 Tobias Brunner
19ef2aec
TB
4#
5# Copyright (C) secunet Security Networks AG
e90b37b9
TB
6#
7# This program is free software; you can redistribute it and/or modify it
8# under the terms of the GNU General Public License as published by the
9# Free Software Foundation; either version 2 of the License, or (at your
10# option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
11#
12# This program is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15# for more details.
16
17"""
18Parses strongswan.conf option descriptions and produces configuration file
19and man page snippets.
20
21The format for description files is as follows:
22
23full.option.name [[:]= default]
24 Short description intended as comment in config snippet
25
26 Long description for use in the man page, with
27 simple formatting: _italic_, **bold**
28
29 Second paragraph of the long description
30
31The descriptions must be indented by tabs or spaces but are both optional.
32If only a short description is given it is used for both intended usages.
33Line breaks within a paragraph of the long description or the short description
34are not preserved. But multiple paragraphs will be separated in the man page.
35Any formatting in the short description is removed when producing config
36snippets.
37
38Options for which a value is assigned with := are not commented out in the
39produced configuration file snippet. This allows to override a default value,
40that e.g. has to be preserved for legacy reasons, in the generated default
41config.
42
43To describe sections the following format can be used:
44
45full.section.name {[#]}
46 Short description of this section
47
48 Long description as above
49
50If a # is added between the curly braces the section header will be commented
51out in the configuration file snippet, which is useful for example sections.
84a3077e 52
fb8c9b3d
TB
53To add include statements to generated config files (ignored when generating
54man pages) the following format can be used:
55
56full.section.name.include files/to/include
57 Description of this include statement
58
84a3077e
TB
59Dots in section/option names may be escaped with a backslash. For instance,
60with the following section description
61
62charon.filelog./var/log/daemon\.log {}
63 Section to define logging into /var/log/daemon.log
64
65/var/log/daemon.log will be the name of the last section.
e90b37b9
TB
66"""
67
68import sys
69import re
70from textwrap import TextWrapper
eea58222 71from argparse import ArgumentParser
48017a27 72from functools import cmp_to_key, total_ordering
e90b37b9 73
48017a27 74@total_ordering
e90b37b9
TB
75class ConfigOption:
76 """Representing a configuration option or described section in strongswan.conf"""
fb8c9b3d 77 def __init__(self, path, default = None, section = False, commented = False, include = False):
84a3077e
TB
78 self.path = path
79 self.name = path[-1]
80 self.fullname = '.'.join(path)
e90b37b9
TB
81 self.default = default
82 self.section = section
83 self.commented = commented
fb8c9b3d 84 self.include = include
e90b37b9
TB
85 self.desc = []
86 self.options = []
87
48017a27
TB
88 def __eq__(self, other):
89 return self.name == other.name
90
35952dc1 91 def __lt__(self, other):
84a3077e 92 return self.name < other.name
e90b37b9
TB
93
94 def add_paragraph(self):
95 """Adds a new paragraph to the description"""
96 if len(self.desc) and len(self.desc[-1]):
97 self.desc.append("")
98
99 def add(self, line):
100 """Adds a line to the last paragraph"""
101 if not len(self.desc):
102 self.desc.append(line)
103 elif not len(self.desc[-1]):
104 self.desc[-1] = line
105 else:
106 self.desc[-1] += ' ' + line
107
108 def adopt(self, other):
109 """Adopts settings from other, which should be more recently parsed"""
110 self.default = other.default
111 self.commented = other.commented
112 self.desc = other.desc
113
fb8c9b3d
TB
114 @staticmethod
115 def cmp(a, b):
116 # order options before sections and includes last
117 if a.include or b.include:
118 return a.include - b.include
119 return a.section - b.section
120
e90b37b9
TB
121class Parser:
122 """Parses one or more files of configuration options"""
ae98a39e 123 def __init__(self, sort = True):
e90b37b9 124 self.options = []
ae98a39e 125 self.sort = sort
e90b37b9
TB
126
127 def parse(self, file):
128 """Parses the given file and adds all options to the internal store"""
129 self.__current = None
130 for line in file:
131 self.__parse_line(line)
132 if self.__current:
133 self.__add_option(self.__current)
134
135 def __parse_line(self, line):
136 """Parses a single line"""
137 if re.match(r'^\s*#', line):
138 return
139 # option definition
140 m = re.match(r'^(?P<name>\S+)\s*((?P<assign>:)?=\s*(?P<default>.+)?)?\s*$', line)
141 if m:
142 if self.__current:
143 self.__add_option(self.__current)
84a3077e
TB
144 path = self.__split_name(m.group('name'))
145 self.__current = ConfigOption(path, m.group('default'),
e90b37b9
TB
146 commented = not m.group('assign'))
147 return
148 # section definition
149 m = re.match(r'^(?P<name>\S+)\s*\{\s*(?P<comment>#)?\s*\}\s*$', line)
150 if m:
151 if self.__current:
152 self.__add_option(self.__current)
84a3077e
TB
153 path = self.__split_name(m.group('name'))
154 self.__current = ConfigOption(path, section = True,
e90b37b9
TB
155 commented = m.group('comment'))
156 return
fb8c9b3d
TB
157 # include definition
158 m = re.match(r'^(?P<name>\S+\.include|include)\s+(?P<pattern>\S+)\s*$', line)
159 if m:
160 if self.__current:
161 self.__add_option(self.__current)
162 path = self.__split_name(m.group('name'))
163 self.__current = ConfigOption(path, m.group('pattern'), include = True)
164 return
e90b37b9
TB
165 # paragraph separator
166 m = re.match(r'^\s*$', line)
167 if m and self.__current:
168 self.__current.add_paragraph()
169 # description line
170 m = re.match(r'^\s+(?P<text>.+?)\s*$', line)
171 if m and self.__current:
172 self.__current.add(m.group('text'))
173
84a3077e
TB
174 def __split_name(self, name):
175 """Split the given full name in a list of section/option names"""
176 return [x.replace('\.', '.') for x in re.split(r'(?<!\\)\.', name)]
177
e90b37b9
TB
178 def __add_option(self, option):
179 """Adds the given option to the abstract storage"""
180 option.desc = [desc for desc in option.desc if len(desc)]
84a3077e 181 parent = self.__get_option(option.path[:-1], True)
e90b37b9
TB
182 if not parent:
183 parent = self
184 found = next((x for x in parent.options if x.name == option.name
185 and x.section == option.section), None)
186 if found:
187 found.adopt(option)
188 else:
189 parent.options.append(option)
ae98a39e
MW
190 if self.sort:
191 parent.options.sort()
e90b37b9 192
84a3077e 193 def __get_option(self, path, create = False):
e90b37b9
TB
194 """Searches/Creates the option (section) based on a list of section names"""
195 option = None
196 options = self.options
84a3077e 197 for i, name in enumerate(path, 1):
e90b37b9
TB
198 option = next((x for x in options if x.name == name and x.section), None)
199 if not option:
200 if not create:
201 break
84a3077e 202 option = ConfigOption(path[:i], section = True)
e90b37b9 203 options.append(option)
ae98a39e
MW
204 if self.sort:
205 options.sort()
e90b37b9
TB
206 options = option.options
207 return option
208
209 def get_option(self, name):
210 """Retrieves the option with the given name"""
84a3077e 211 return self.__get_option(self.__split_name(name))
e90b37b9
TB
212
213class TagReplacer:
214 """Replaces formatting tags in text"""
215 def __init__(self):
216 self.__matcher_b = self.__create_matcher('**')
217 self.__matcher_i = self.__create_matcher('_')
218 self.__replacer = None
219
220 def __create_matcher(self, tag):
221 tag = re.escape(tag)
222 return re.compile(r'''
223 (^|\s|(?P<brack>[(\[])) # prefix with optional opening bracket
224 (?P<tag>''' + tag + r''') # start tag
00498d78 225 (?P<text>\S|\S.*?\S) # text
e90b37b9
TB
226 ''' + tag + r''' # end tag
227 (?P<punct>([.,!:)\]]|\(\d+\))*) # punctuation
228 (?=$|\s) # suffix (don't consume it so that subsequent tags can match)
229 ''', flags = re.DOTALL | re.VERBOSE)
230
231 def _create_replacer(self):
232 def replacer(m):
233 punct = m.group('punct')
234 if not punct:
235 punct = ''
236 return '{0}{1}{2}'.format(m.group(1), m.group('text'), punct)
237 return replacer
238
239 def replace(self, text):
240 if not self.__replacer:
241 self.__replacer = self._create_replacer()
242 text = re.sub(self.__matcher_b, self.__replacer, text)
243 return re.sub(self.__matcher_i, self.__replacer, text)
244
245class GroffTagReplacer(TagReplacer):
246 def _create_replacer(self):
247 def replacer(m):
248 nl = '\n' if m.group(1) else ''
249 format = 'I' if m.group('tag') == '_' else 'B'
250 brack = m.group('brack')
251 if not brack:
252 brack = ''
253 punct = m.group('punct')
254 if not punct:
255 punct = ''
256 text = re.sub(r'[\r\n\t]', ' ', m.group('text'))
257 return '{0}.R{1} "{2}" "{3}" "{4}"\n'.format(nl, format, brack, text, punct)
258 return replacer
259
260class ConfFormatter:
261 """Formats options to a strongswan.conf snippet"""
262 def __init__(self):
263 self.__indent = ' '
264 self.__wrapper = TextWrapper(width = 80, replace_whitespace = True,
265 break_long_words = False, break_on_hyphens = False)
266 self.__tags = TagReplacer()
267
268 def __print_description(self, opt, indent):
269 if len(opt.desc):
270 self.__wrapper.initial_indent = '{0}# '.format(self.__indent * indent)
271 self.__wrapper.subsequent_indent = self.__wrapper.initial_indent
5ee4984d 272 print(self.__wrapper.fill(self.__tags.replace(opt.desc[0])))
e90b37b9
TB
273
274 def __print_option(self, opt, indent, commented):
275 """Print a single option with description and default value"""
276 comment = "# " if commented or opt.commented else ""
277 self.__print_description(opt, indent)
fb8c9b3d
TB
278 if opt.include:
279 print('{0}{1} {2}'.format(self.__indent * indent, opt.name, opt.default))
280 elif opt.default:
5ee4984d 281 print('{0}{1}{2} = {3}'.format(self.__indent * indent, comment, opt.name, opt.default))
e90b37b9 282 else:
5ee4984d
TB
283 print('{0}{1}{2} ='.format(self.__indent * indent, comment, opt.name))
284 print('')
e90b37b9
TB
285
286 def __print_section(self, section, indent, commented):
287 """Print a section with all options"""
e20deeca
TB
288 commented = commented or section.commented
289 comment = "# " if commented else ""
e90b37b9 290 self.__print_description(section, indent)
5ee4984d
TB
291 print('{0}{1}{2} {{'.format(self.__indent * indent, comment, section.name))
292 print('')
fb8c9b3d 293 for o in sorted(section.options, key=cmp_to_key(ConfigOption.cmp)):
e90b37b9 294 if o.section:
e20deeca 295 self.__print_section(o, indent + 1, commented)
e90b37b9 296 else:
e20deeca 297 self.__print_option(o, indent + 1, commented)
5ee4984d
TB
298 print('{0}{1}}}'.format(self.__indent * indent, comment))
299 print('')
e90b37b9
TB
300
301 def format(self, options):
302 """Print a list of options"""
303 if not options:
304 return
fb8c9b3d 305 for option in sorted(options, key=cmp_to_key(ConfigOption.cmp)):
e90b37b9
TB
306 if option.section:
307 self.__print_section(option, 0, False)
308 else:
309 self.__print_option(option, 0, False)
310
311class ManFormatter:
312 """Formats a list of options into a groff snippet"""
313 def __init__(self):
314 self.__wrapper = TextWrapper(width = 80, replace_whitespace = False,
315 break_long_words = False, break_on_hyphens = False)
316 self.__tags = GroffTagReplacer()
317
318 def __groffize(self, text):
319 """Encode text as groff text"""
320 text = self.__tags.replace(text)
321 text = re.sub(r'(?<!\\)-', r'\\-', text)
322 # remove any leading whitespace
323 return re.sub(r'^\s+', '', text, flags = re.MULTILINE)
324
325 def __format_option(self, option):
326 """Print a single option"""
327 if option.section and not len(option.desc):
328 return
fb8c9b3d
TB
329 if option.include:
330 return
e90b37b9 331 if option.section:
5ee4984d 332 print('.TP\n.B {0}\n.br'.format(option.fullname))
e90b37b9 333 else:
5ee4984d 334 print('.TP')
e90b37b9 335 default = option.default if option.default else ''
5ee4984d 336 print('.BR {0} " [{1}]"'.format(option.fullname, default))
e90b37b9 337 for para in option.desc if len(option.desc) < 2 else option.desc[1:]:
5ee4984d
TB
338 print(self.__groffize(self.__wrapper.fill(para)))
339 print('')
e90b37b9
TB
340
341 def format(self, options):
342 """Print a list of options"""
343 if not options:
344 return
345 for option in options:
346 if option.section:
347 self.__format_option(option)
348 self.format(option.options)
349 else:
350 self.__format_option(option)
351
eea58222
TB
352args = ArgumentParser()
353args.add_argument('file', nargs='*',
354 help="files to process, omit to read input from stdin")
355args.add_argument("-f", "--format", dest="format", choices=["conf", "man"],
356 help="output format (default: %(default)s)", default="conf")
357args.add_argument("-r", "--root", dest="root", metavar="NAME",
358 help="root section of which options are printed; everything"
359 "is printed if not found")
360args.add_argument("-n", "--nosort", action="store_false", dest="sort",
361 default=True, help="do not sort sections alphabetically")
ae98a39e 362
eea58222 363opts = args.parse_args()
e90b37b9 364
ae98a39e 365parser = Parser(opts.sort)
eea58222
TB
366if len(opts.file):
367 for filename in opts.file:
e90b37b9
TB
368 try:
369 with open(filename, 'r') as file:
370 parser.parse(file)
371 except IOError as e:
372 sys.stderr.write("Unable to open '{0}': {1}\n".format(filename, e.strerror))
373else:
374 parser.parse(sys.stdin)
375
376options = parser.options
377if (opts.root):
378 root = parser.get_option(opts.root)
379 if root:
380 options = root.options
381
382if opts.format == "conf":
383 formatter = ConfFormatter()
384elif opts.format == "man":
385 formatter = ManFormatter()
386
387formatter.format(options)