]>
Commit | Line | Data |
---|---|---|
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 | """ | |
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 TB |
51 | |
52 | Dots in section/option names may be escaped with a backslash. For instance, | |
53 | with the following section description | |
54 | ||
55 | charon.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 | ||
61 | import sys | |
62 | import re | |
63 | from textwrap import TextWrapper | |
64 | from optparse import OptionParser | |
9fa7b037 | 65 | from operator import attrgetter |
e90b37b9 TB |
66 | |
67 | class 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 | ||
102 | class 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 | |
186 | class 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 | ||
218 | class 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 | ||
233 | class 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 | ||
282 | class 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 | ||
321 | options = OptionParser(usage = "Usage: %prog [options] file1 file2\n\n" | |
322 | "If no filenames are provided the input is read from stdin.") | |
323 | options.add_option("-f", "--format", dest="format", type="choice", choices=["conf", "man"], | |
324 | help="output format: conf, man [default: %default]", default="conf") | |
325 | options.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 |
328 | options.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 | 333 | parser = Parser(opts.sort) |
e90b37b9 TB |
334 | if 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)) | |
341 | else: | |
342 | parser.parse(sys.stdin) | |
343 | ||
344 | options = parser.options | |
345 | if (opts.root): | |
346 | root = parser.get_option(opts.root) | |
347 | if root: | |
348 | options = root.options | |
349 | ||
350 | if opts.format == "conf": | |
351 | formatter = ConfFormatter() | |
352 | elif opts.format == "man": | |
353 | formatter = ManFormatter() | |
354 | ||
355 | formatter.format(options) |