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
59 """Representing a configuration option or described section in strongswan.conf"""
60 def __init__(self
, name
, default
= None, section
= False, commented
= False):
61 self
.name
= name
.split('.')[-1]
63 self
.default
= default
64 self
.section
= section
65 self
.commented
= commented
69 def __cmp__(self
, other
):
70 if self
.section
== other
.section
:
71 return cmp(self
.name
, other
.name
)
72 return 1 if self
.section
else -1
74 def add_paragraph(self
):
75 """Adds a new paragraph to the description"""
76 if len(self
.desc
) and len(self
.desc
[-1]):
80 """Adds a line to the last paragraph"""
81 if not len(self
.desc
):
82 self
.desc
.append(line
)
83 elif not len(self
.desc
[-1]):
86 self
.desc
[-1] += ' ' + line
88 def adopt(self
, other
):
89 """Adopts settings from other, which should be more recently parsed"""
90 self
.default
= other
.default
91 self
.commented
= other
.commented
92 self
.desc
= other
.desc
95 """Parses one or more files of configuration options"""
99 def parse(self
, file):
100 """Parses the given file and adds all options to the internal store"""
101 self
.__current
= None
103 self
.__parse_line(line
)
105 self
.__add_option(self
.__current
)
107 def __parse_line(self
, line
):
108 """Parses a single line"""
109 if re
.match(r
'^\s*#', line
):
112 m
= re
.match(r
'^(?P<name>\S+)\s*((?P<assign>:)?=\s*(?P<default>.+)?)?\s*$', line
)
115 self
.__add_option(self
.__current
)
116 self
.__current
= ConfigOption(m
.group('name'), m
.group('default'),
117 commented
= not m
.group('assign'))
120 m
= re
.match(r
'^(?P<name>\S+)\s*\{\s*(?P<comment>#)?\s*\}\s*$', line
)
123 self
.__add_option(self
.__current
)
124 self
.__current
= ConfigOption(m
.group('name'), section
= True,
125 commented
= m
.group('comment'))
127 # paragraph separator
128 m
= re
.match(r
'^\s*$', line
)
129 if m
and self
.__current
:
130 self
.__current
.add_paragraph()
132 m
= re
.match(r
'^\s+(?P<text>.+?)\s*$', line
)
133 if m
and self
.__current
:
134 self
.__current
.add(m
.group('text'))
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)
143 found
= next((x
for x
in parent
.options
if x
.name
== option
.name
144 and x
.section
== option
.section
), None)
148 parent
.options
.append(option
)
149 parent
.options
.sort()
151 def __get_option(self
, parts
, create
= False):
152 """Searches/Creates the option (section) based on a list of section names"""
154 options
= self
.options
157 fullname
+= '.' + name
if len(fullname
) else name
158 option
= next((x
for x
in options
if x
.name
== name
and x
.section
), None)
162 option
= ConfigOption(fullname
, section
= True)
163 options
.append(option
)
165 options
= option
.options
168 def get_option(self
, name
):
169 """Retrieves the option with the given name"""
170 return self
.__get_option(name
.split('.'))
173 """Replaces formatting tags in text"""
175 self
.__matcher_b
= self
.__create_matcher('**')
176 self
.__matcher_i
= self
.__create_matcher('_')
177 self
.__replacer
= None
179 def __create_matcher(self
, tag
):
181 return re
.compile(r
'''
182 (^|\s|(?P<brack>[(\[])) # prefix with optional opening bracket
183 (?P<tag>''' + tag
+ r
''') # start tag
184 (?P<text>\w|\S.*?\S) # text
185 ''' + tag
+ r
''' # end tag
186 (?P<punct>([.,!:)\]]|\(\d+\))*) # punctuation
187 (?=$|\s) # suffix (don't consume it so that subsequent tags can match)
188 ''', flags
= re
.DOTALL | re
.VERBOSE
)
190 def _create_replacer(self
):
192 punct
= m
.group('punct')
195 return '{0}{1}{2}'.format(m
.group(1), m
.group('text'), punct
)
198 def replace(self
, text
):
199 if not self
.__replacer
:
200 self
.__replacer
= self
._create_replacer()
201 text
= re
.sub(self
.__matcher_b
, self
.__replacer
, text
)
202 return re
.sub(self
.__matcher_i
, self
.__replacer
, text
)
204 class GroffTagReplacer(TagReplacer
):
205 def _create_replacer(self
):
207 nl
= '\n' if m
.group(1) else ''
208 format
= 'I' if m
.group('tag') == '_' else 'B'
209 brack
= m
.group('brack')
212 punct
= m
.group('punct')
215 text
= re
.sub(r
'[\r\n\t]', ' ', m
.group('text'))
216 return '{0}.R{1} "{2}" "{3}" "{4}"\n'.format(nl
, format
, brack
, text
, punct
)
220 """Formats options to a strongswan.conf snippet"""
223 self
.__wrapper
= TextWrapper(width
= 80, replace_whitespace
= True,
224 break_long_words
= False, break_on_hyphens
= False)
225 self
.__tags
= TagReplacer()
227 def __print_description(self
, opt
, indent
):
229 self
.__wrapper
.initial_indent
= '{0}# '.format(self
.__indent
* indent
)
230 self
.__wrapper
.subsequent_indent
= self
.__wrapper
.initial_indent
231 print format(self
.__wrapper
.fill(self
.__tags
.replace(opt
.desc
[0])))
233 def __print_option(self
, opt
, indent
, commented
):
234 """Print a single option with description and default value"""
235 comment
= "# " if commented
or opt
.commented
else ""
236 self
.__print_description(opt
, indent
)
238 print '{0}{1}{2} = {3}'.format(self
.__indent
* indent
, comment
, opt
.name
, opt
.default
)
240 print '{0}{1}{2} ='.format(self
.__indent
* indent
, comment
, opt
.name
)
243 def __print_section(self
, section
, indent
, commented
):
244 """Print a section with all options"""
245 comment
= "# " if commented
or section
.commented
else ""
246 self
.__print_description(section
, indent
)
247 print '{0}{1}{2} {{'.format(self
.__indent
* indent
, comment
, section
.name
)
249 for o
in section
.options
:
251 self
.__print_section(o
, indent
+ 1, section
.commented
)
253 self
.__print_option(o
, indent
+ 1, section
.commented
)
254 print '{0}{1}}}'.format(self
.__indent
* indent
, comment
)
257 def format(self
, options
):
258 """Print a list of options"""
261 for option
in options
:
263 self
.__print_section(option
, 0, False)
265 self
.__print_option(option
, 0, False)
268 """Formats a list of options into a groff snippet"""
270 self
.__wrapper
= TextWrapper(width
= 80, replace_whitespace
= False,
271 break_long_words
= False, break_on_hyphens
= False)
272 self
.__tags
= GroffTagReplacer()
274 def __groffize(self
, text
):
275 """Encode text as groff text"""
276 text
= self
.__tags
.replace(text
)
277 text
= re
.sub(r
'(?<!\\)-', r
'\\-', text
)
278 # remove any leading whitespace
279 return re
.sub(r
'^\s+', '', text
, flags
= re
.MULTILINE
)
281 def __format_option(self
, option
):
282 """Print a single option"""
283 if option
.section
and not len(option
.desc
):
286 print '.TP\n.B {0}\n.br'.format(option
.fullname
)
289 default
= option
.default
if option
.default
else ''
290 print '.BR {0} " [{1}]"'.format(option
.fullname
, default
)
291 for para
in option
.desc
if len(option
.desc
) < 2 else option
.desc
[1:]:
292 print self
.__groffize(self
.__wrapper
.fill(para
))
295 def format(self
, options
):
296 """Print a list of options"""
299 for option
in options
:
301 self
.__format_option(option
)
302 self
.format(option
.options
)
304 self
.__format_option(option
)
306 options
= OptionParser(usage
= "Usage: %prog [options] file1 file2\n\n"
307 "If no filenames are provided the input is read from stdin.")
308 options
.add_option("-f", "--format", dest
="format", type="choice", choices
=["conf", "man"],
309 help="output format: conf, man [default: %default]", default
="conf")
310 options
.add_option("-r", "--root", dest
="root", metavar
="NAME",
311 help="root section of which options are printed, "
312 "if not found everything is printed")
313 (opts
, args
) = options
.parse_args()
317 for filename
in args
:
319 with
open(filename
, 'r') as file:
322 sys
.stderr
.write("Unable to open '{0}': {1}\n".format(filename
, e
.strerror
))
324 parser
.parse(sys
.stdin
)
326 options
= parser
.options
328 root
= parser
.get_option(opts
.root
)
330 options
= root
.options
332 if opts
.format
== "conf":
333 formatter
= ConfFormatter()
334 elif opts
.format
== "man":
335 formatter
= ManFormatter()
337 formatter
.format(options
)