]> git.ipfire.org Git - people/ms/strongswan.git/blob - conf/format-options.py
Merge branch 'utils-split'
[people/ms/strongswan.git] / conf / format-options.py
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
57 from operator import attrgetter
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 __lt__(self, other):
71 return self.name < other.name
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"""
95 def __init__(self, sort = True):
96 self.options = []
97 self.sort = sort
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)
149 if self.sort:
150 parent.options.sort()
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)
165 if self.sort:
166 options.sort()
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(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 print('')
244
245 def __print_section(self, section, indent, commented):
246 """Print a section with all options"""
247 commented = commented or section.commented
248 comment = "# " if commented else ""
249 self.__print_description(section, indent)
250 print('{0}{1}{2} {{'.format(self.__indent * indent, comment, section.name))
251 print('')
252 for o in sorted(section.options, key=attrgetter('section')):
253 if o.section:
254 self.__print_section(o, indent + 1, commented)
255 else:
256 self.__print_option(o, indent + 1, commented)
257 print('{0}{1}}}'.format(self.__indent * indent, comment))
258 print('')
259
260 def format(self, options):
261 """Print a list of options"""
262 if not options:
263 return
264 for option in sorted(options, key=attrgetter('section')):
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")
316 options.add_option("-n", "--nosort", action="store_false", dest="sort",
317 default=True, help="do not sort sections alphabetically")
318
319 (opts, args) = options.parse_args()
320
321 parser = Parser(opts.sort)
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)