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