3 # Copyright (C) 2014 Tobias Brunner
4 # Hochschule fuer Technik Rapperswil
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>.
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
17 Parses strongswan.conf option descriptions and produces configuration file
18 and man page snippets.
20 The format for description files is as follows:
22 full.option.name [[:]= default]
23 Short description intended as comment in config snippet
25 Long description for use in the man page, with
26 simple formatting: _italic_, **bold**
28 Second paragraph of the long description
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
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
42 To describe sections the following format can be used:
44 full.section.name {[#]}
45 Short description of this section
47 Long description as above
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.
55 from textwrap
import TextWrapper
56 from optparse
import OptionParser
57 from operator
import attrgetter
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]
64 self
.default
= default
65 self
.section
= section
66 self
.commented
= commented
70 def __cmp__(self
, other
):
71 return cmp(self
.name
, other
.name
)
73 def add_paragraph(self
):
74 """Adds a new paragraph to the description"""
75 if len(self
.desc
) and len(self
.desc
[-1]):
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]):
85 self
.desc
[-1] += ' ' + line
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
94 """Parses one or more files of configuration options"""
98 def parse(self
, file):
99 """Parses the given file and adds all options to the internal store"""
100 self
.__current
= None
102 self
.__parse_line(line
)
104 self
.__add_option(self
.__current
)
106 def __parse_line(self
, line
):
107 """Parses a single line"""
108 if re
.match(r
'^\s*#', line
):
111 m
= re
.match(r
'^(?P<name>\S+)\s*((?P<assign>:)?=\s*(?P<default>.+)?)?\s*$', line
)
114 self
.__add_option(self
.__current
)
115 self
.__current
= ConfigOption(m
.group('name'), m
.group('default'),
116 commented
= not m
.group('assign'))
119 m
= re
.match(r
'^(?P<name>\S+)\s*\{\s*(?P<comment>#)?\s*\}\s*$', line
)
122 self
.__add_option(self
.__current
)
123 self
.__current
= ConfigOption(m
.group('name'), section
= True,
124 commented
= m
.group('comment'))
126 # paragraph separator
127 m
= re
.match(r
'^\s*$', line
)
128 if m
and self
.__current
:
129 self
.__current
.add_paragraph()
131 m
= re
.match(r
'^\s+(?P<text>.+?)\s*$', line
)
132 if m
and self
.__current
:
133 self
.__current
.add(m
.group('text'))
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)
142 found
= next((x
for x
in parent
.options
if x
.name
== option
.name
143 and x
.section
== option
.section
), None)
147 parent
.options
.append(option
)
148 parent
.options
.sort()
150 def __get_option(self
, parts
, create
= False):
151 """Searches/Creates the option (section) based on a list of section names"""
153 options
= self
.options
156 fullname
+= '.' + name
if len(fullname
) else name
157 option
= next((x
for x
in options
if x
.name
== name
and x
.section
), None)
161 option
= ConfigOption(fullname
, section
= True)
162 options
.append(option
)
164 options
= option
.options
167 def get_option(self
, name
):
168 """Retrieves the option with the given name"""
169 return self
.__get_option(name
.split('.'))
172 """Replaces formatting tags in text"""
174 self
.__matcher_b
= self
.__create_matcher('**')
175 self
.__matcher_i
= self
.__create_matcher('_')
176 self
.__replacer
= None
178 def __create_matcher(self
, 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
)
189 def _create_replacer(self
):
191 punct
= m
.group('punct')
194 return '{0}{1}{2}'.format(m
.group(1), m
.group('text'), punct
)
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
)
203 class GroffTagReplacer(TagReplacer
):
204 def _create_replacer(self
):
206 nl
= '\n' if m
.group(1) else ''
207 format
= 'I' if m
.group('tag') == '_' else 'B'
208 brack
= m
.group('brack')
211 punct
= m
.group('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
)
219 """Formats options to a strongswan.conf snippet"""
222 self
.__wrapper
= TextWrapper(width
= 80, replace_whitespace
= True,
223 break_long_words
= False, break_on_hyphens
= False)
224 self
.__tags
= TagReplacer()
226 def __print_description(self
, opt
, indent
):
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])))
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
)
237 print '{0}{1}{2} = {3}'.format(self
.__indent
* indent
, comment
, opt
.name
, opt
.default
)
239 print '{0}{1}{2} ='.format(self
.__indent
* indent
, comment
, opt
.name
)
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
)
248 for o
in sorted(section
.options
, key
=attrgetter('section')):
250 self
.__print_section(o
, indent
+ 1, section
.commented
)
252 self
.__print_option(o
, indent
+ 1, section
.commented
)
253 print '{0}{1}}}'.format(self
.__indent
* indent
, comment
)
256 def format(self
, options
):
257 """Print a list of options"""
260 for option
in sorted(options
, key
=attrgetter('section')):
262 self
.__print_section(option
, 0, False)
264 self
.__print_option(option
, 0, False)
267 """Formats a list of options into a groff snippet"""
269 self
.__wrapper
= TextWrapper(width
= 80, replace_whitespace
= False,
270 break_long_words
= False, break_on_hyphens
= False)
271 self
.__tags
= GroffTagReplacer()
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
)
280 def __format_option(self
, option
):
281 """Print a single option"""
282 if option
.section
and not len(option
.desc
):
285 print '.TP\n.B {0}\n.br'.format(option
.fullname
)
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
))
294 def format(self
, options
):
295 """Print a list of options"""
298 for option
in options
:
300 self
.__format_option(option
)
301 self
.format(option
.options
)
303 self
.__format_option(option
)
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()
316 for filename
in args
:
318 with
open(filename
, 'r') as file:
321 sys
.stderr
.write("Unable to open '{0}': {1}\n".format(filename
, e
.strerror
))
323 parser
.parse(sys
.stdin
)
325 options
= parser
.options
327 root
= parser
.get_option(opts
.root
)
329 options
= root
.options
331 if opts
.format
== "conf":
332 formatter
= ConfFormatter()
333 elif opts
.format
== "man":
334 formatter
= ManFormatter()
336 formatter
.format(options
)