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