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