]>
Commit | Line | Data |
---|---|---|
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 | """ | |
18 | Parses strongswan.conf option descriptions and produces configuration file | |
19 | and man page snippets. | |
20 | ||
21 | The format for description files is as follows: | |
22 | ||
23 | full.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 | ||
31 | The descriptions must be indented by tabs or spaces but are both optional. | |
32 | If only a short description is given it is used for both intended usages. | |
33 | Line breaks within a paragraph of the long description or the short description | |
34 | are not preserved. But multiple paragraphs will be separated in the man page. | |
35 | Any formatting in the short description is removed when producing config | |
36 | snippets. | |
37 | ||
38 | Options for which a value is assigned with := are not commented out in the | |
39 | produced configuration file snippet. This allows to override a default value, | |
40 | that e.g. has to be preserved for legacy reasons, in the generated default | |
41 | config. | |
42 | ||
43 | To describe sections the following format can be used: | |
44 | ||
45 | full.section.name {[#]} | |
46 | Short description of this section | |
47 | ||
48 | Long description as above | |
49 | ||
50 | If a # is added between the curly braces the section header will be commented | |
51 | out in the configuration file snippet, which is useful for example sections. | |
84a3077e | 52 | |
fb8c9b3d TB |
53 | To add include statements to generated config files (ignored when generating |
54 | man pages) the following format can be used: | |
55 | ||
56 | full.section.name.include files/to/include | |
57 | Description of this include statement | |
58 | ||
84a3077e TB |
59 | Dots in section/option names may be escaped with a backslash. For instance, |
60 | with the following section description | |
61 | ||
62 | charon.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 | ||
68 | import sys | |
69 | import re | |
70 | from textwrap import TextWrapper | |
eea58222 | 71 | from argparse import ArgumentParser |
48017a27 | 72 | from functools import cmp_to_key, total_ordering |
e90b37b9 | 73 | |
48017a27 | 74 | @total_ordering |
e90b37b9 TB |
75 | class 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 |
121 | class 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 | |
213 | class 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 | ||
245 | class 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 | ||
260 | class 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 | ||
311 | class 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 |
352 | args = ArgumentParser() |
353 | args.add_argument('file', nargs='*', | |
354 | help="files to process, omit to read input from stdin") | |
355 | args.add_argument("-f", "--format", dest="format", choices=["conf", "man"], | |
356 | help="output format (default: %(default)s)", default="conf") | |
357 | args.add_argument("-r", "--root", dest="root", metavar="NAME", | |
358 | help="root section of which options are printed; everything" | |
359 | "is printed if not found") | |
360 | args.add_argument("-n", "--nosort", action="store_false", dest="sort", | |
361 | default=True, help="do not sort sections alphabetically") | |
ae98a39e | 362 | |
eea58222 | 363 | opts = args.parse_args() |
e90b37b9 | 364 | |
ae98a39e | 365 | parser = Parser(opts.sort) |
eea58222 TB |
366 | if 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)) | |
373 | else: | |
374 | parser.parse(sys.stdin) | |
375 | ||
376 | options = parser.options | |
377 | if (opts.root): | |
378 | root = parser.get_option(opts.root) | |
379 | if root: | |
380 | options = root.options | |
381 | ||
382 | if opts.format == "conf": | |
383 | formatter = ConfFormatter() | |
384 | elif opts.format == "man": | |
385 | formatter = ManFormatter() | |
386 | ||
387 | formatter.format(options) |