]>
Commit | Line | Data |
---|---|---|
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 | """ | |
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. | |
84a3077e | 51 | |
fb8c9b3d TB |
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 | ||
84a3077e TB |
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. | |
e90b37b9 TB |
65 | """ |
66 | ||
67 | import sys | |
68 | import re | |
69 | from textwrap import TextWrapper | |
eea58222 | 70 | from argparse import ArgumentParser |
48017a27 | 71 | from functools import cmp_to_key, total_ordering |
e90b37b9 | 72 | |
48017a27 | 73 | @total_ordering |
e90b37b9 TB |
74 | class 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 |
120 | class 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 | |
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 | |
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 | ||
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 | |
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 | ||
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 | |
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 |
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") | |
ae98a39e | 361 | |
eea58222 | 362 | opts = args.parse_args() |
e90b37b9 | 363 | |
ae98a39e | 364 | parser = Parser(opts.sort) |
eea58222 TB |
365 | if 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)) | |
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) |