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