]>
git.ipfire.org Git - thirdparty/strongswan.git/blob - conf/format-options.py
3 # Copyright (C) 2014-2017 Tobias Brunner
4 # HSR 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.
52 To add include statements to generated config files (ignored when generating
53 man pages) the following format can be used:
55 full.section.name.include files/to/include
56 Description of this include statement
58 Dots in section/option names may be escaped with a backslash. For instance,
59 with the following section description
61 charon.filelog./var/log/daemon\.log {}
62 Section to define logging into /var/log/daemon.log
64 /var/log/daemon.log will be the name of the last section.
69 from textwrap
import TextWrapper
70 from optparse
import OptionParser
71 from functools
import cmp_to_key
74 """Representing a configuration option or described section in strongswan.conf"""
75 def __init__(self
, path
, default
= None, section
= False, commented
= False, include
= False):
78 self
.fullname
= '.'.join(path
)
79 self
.default
= default
80 self
.section
= section
81 self
.commented
= commented
82 self
.include
= include
86 def __lt__(self
, other
):
87 return self
.name
< other
.name
89 def add_paragraph(self
):
90 """Adds a new paragraph to the description"""
91 if len(self
.desc
) and len(self
.desc
[-1]):
95 """Adds a line to the last paragraph"""
96 if not len(self
.desc
):
97 self
.desc
.append(line
)
98 elif not len(self
.desc
[-1]):
101 self
.desc
[-1] += ' ' + line
103 def adopt(self
, other
):
104 """Adopts settings from other, which should be more recently parsed"""
105 self
.default
= other
.default
106 self
.commented
= other
.commented
107 self
.desc
= other
.desc
111 # order options before sections and includes last
112 if a
.include
or b
.include
:
113 return a
.include
- b
.include
114 return a
.section
- b
.section
117 """Parses one or more files of configuration options"""
118 def __init__(self
, sort
= True):
122 def parse(self
, file):
123 """Parses the given file and adds all options to the internal store"""
124 self
.__current
= None
126 self
.__parse
_line
(line
)
128 self
.__add
_option
(self
.__current
)
130 def __parse_line(self
, line
):
131 """Parses a single line"""
132 if re
.match(r
'^\s*#', line
):
135 m
= re
.match(r
'^(?P<name>\S+)\s*((?P<assign>:)?=\s*(?P<default>.+)?)?\s*$', line
)
138 self
.__add
_option
(self
.__current
)
139 path
= self
.__split
_name
(m
.group('name'))
140 self
.__current
= ConfigOption(path
, m
.group('default'),
141 commented
= not m
.group('assign'))
144 m
= re
.match(r
'^(?P<name>\S+)\s*\{\s*(?P<comment>#)?\s*\}\s*$', line
)
147 self
.__add
_option
(self
.__current
)
148 path
= self
.__split
_name
(m
.group('name'))
149 self
.__current
= ConfigOption(path
, section
= True,
150 commented
= m
.group('comment'))
153 m
= re
.match(r
'^(?P<name>\S+\.include|include)\s+(?P<pattern>\S+)\s*$', line
)
156 self
.__add
_option
(self
.__current
)
157 path
= self
.__split
_name
(m
.group('name'))
158 self
.__current
= ConfigOption(path
, m
.group('pattern'), include
= True)
160 # paragraph separator
161 m
= re
.match(r
'^\s*$', line
)
162 if m
and self
.__current
:
163 self
.__current
.add_paragraph()
165 m
= re
.match(r
'^\s+(?P<text>.+?)\s*$', line
)
166 if m
and self
.__current
:
167 self
.__current
.add(m
.group('text'))
169 def __split_name(self
, name
):
170 """Split the given full name in a list of section/option names"""
171 return [x
.replace('\.', '.') for x
in re
.split(r
'(?<!\\)\.', name
)]
173 def __add_option(self
, option
):
174 """Adds the given option to the abstract storage"""
175 option
.desc
= [desc
for desc
in option
.desc
if len(desc
)]
176 parent
= self
.__get
_option
(option
.path
[:-1], True)
179 found
= next((x
for x
in parent
.options
if x
.name
== option
.name
180 and x
.section
== option
.section
), None)
184 parent
.options
.append(option
)
186 parent
.options
.sort()
188 def __get_option(self
, path
, create
= False):
189 """Searches/Creates the option (section) based on a list of section names"""
191 options
= self
.options
192 for i
, name
in enumerate(path
, 1):
193 option
= next((x
for x
in options
if x
.name
== name
and x
.section
), None)
197 option
= ConfigOption(path
[:i
], section
= True)
198 options
.append(option
)
201 options
= option
.options
204 def get_option(self
, name
):
205 """Retrieves the option with the given name"""
206 return self
.__get
_option
(self
.__split
_name
(name
))
209 """Replaces formatting tags in text"""
211 self
.__matcher
_b
= self
.__create
_matcher
('**')
212 self
.__matcher
_i
= self
.__create
_matcher
('_')
213 self
.__replacer
= None
215 def __create_matcher(self
, tag
):
217 return re
.compile(r
'''
218 (^|\s|(?P<brack>[(\[])) # prefix with optional opening bracket
219 (?P<tag>''' + tag
+ r
''') # start tag
220 (?P<text>\S|\S.*?\S) # text
221 ''' + tag
+ r
''' # end tag
222 (?P<punct>([.,!:)\]]|\(\d+\))*) # punctuation
223 (?=$|\s) # suffix (don't consume it so that subsequent tags can match)
224 ''', flags
= re
.DOTALL | re
.VERBOSE
)
226 def _create_replacer(self
):
228 punct
= m
.group('punct')
231 return '{0}{1}{2}'.format(m
.group(1), m
.group('text'), punct
)
234 def replace(self
, text
):
235 if not self
.__replacer
:
236 self
.__replacer
= self
._create
_replacer
()
237 text
= re
.sub(self
.__matcher
_b
, self
.__replacer
, text
)
238 return re
.sub(self
.__matcher
_i
, self
.__replacer
, text
)
240 class GroffTagReplacer(TagReplacer
):
241 def _create_replacer(self
):
243 nl
= '\n' if m
.group(1) else ''
244 format
= 'I' if m
.group('tag') == '_' else 'B'
245 brack
= m
.group('brack')
248 punct
= m
.group('punct')
251 text
= re
.sub(r
'[\r\n\t]', ' ', m
.group('text'))
252 return '{0}.R{1} "{2}" "{3}" "{4}"\n'.format(nl
, format
, brack
, text
, punct
)
256 """Formats options to a strongswan.conf snippet"""
259 self
.__wrapper
= TextWrapper(width
= 80, replace_whitespace
= True,
260 break_long_words
= False, break_on_hyphens
= False)
261 self
.__tags
= TagReplacer()
263 def __print_description(self
, opt
, indent
):
265 self
.__wrapper
.initial_indent
= '{0}# '.format(self
.__indent
* indent
)
266 self
.__wrapper
.subsequent_indent
= self
.__wrapper
.initial_indent
267 print(self
.__wrapper
.fill(self
.__tags
.replace(opt
.desc
[0])))
269 def __print_option(self
, opt
, indent
, commented
):
270 """Print a single option with description and default value"""
271 comment
= "# " if commented
or opt
.commented
else ""
272 self
.__print
_description
(opt
, indent
)
274 print('{0}{1} {2}'.format(self
.__indent
* indent
, opt
.name
, opt
.default
))
276 print('{0}{1}{2} = {3}'.format(self
.__indent
* indent
, comment
, opt
.name
, opt
.default
))
278 print('{0}{1}{2} ='.format(self
.__indent
* indent
, comment
, opt
.name
))
281 def __print_section(self
, section
, indent
, commented
):
282 """Print a section with all options"""
283 commented
= commented
or section
.commented
284 comment
= "# " if commented
else ""
285 self
.__print
_description
(section
, indent
)
286 print('{0}{1}{2} {{'.format(self
.__indent
* indent
, comment
, section
.name
))
288 for o
in sorted(section
.options
, key
=cmp_to_key(ConfigOption
.cmp)):
290 self
.__print
_section
(o
, indent
+ 1, commented
)
292 self
.__print
_option
(o
, indent
+ 1, commented
)
293 print('{0}{1}}}'.format(self
.__indent
* indent
, comment
))
296 def format(self
, options
):
297 """Print a list of options"""
300 for option
in sorted(options
, key
=cmp_to_key(ConfigOption
.cmp)):
302 self
.__print
_section
(option
, 0, False)
304 self
.__print
_option
(option
, 0, False)
307 """Formats a list of options into a groff snippet"""
309 self
.__wrapper
= TextWrapper(width
= 80, replace_whitespace
= False,
310 break_long_words
= False, break_on_hyphens
= False)
311 self
.__tags
= GroffTagReplacer()
313 def __groffize(self
, text
):
314 """Encode text as groff text"""
315 text
= self
.__tags
.replace(text
)
316 text
= re
.sub(r
'(?<!\\)-', r
'\\-', text
)
317 # remove any leading whitespace
318 return re
.sub(r
'^\s+', '', text
, flags
= re
.MULTILINE
)
320 def __format_option(self
, option
):
321 """Print a single option"""
322 if option
.section
and not len(option
.desc
):
327 print('.TP\n.B {0}\n.br'.format(option
.fullname
))
330 default
= option
.default
if option
.default
else ''
331 print('.BR {0} " [{1}]"'.format(option
.fullname
, default
))
332 for para
in option
.desc
if len(option
.desc
) < 2 else option
.desc
[1:]:
333 print(self
.__groffize
(self
.__wrapper
.fill(para
)))
336 def format(self
, options
):
337 """Print a list of options"""
340 for option
in options
:
342 self
.__format
_option
(option
)
343 self
.format(option
.options
)
345 self
.__format
_option
(option
)
347 options
= OptionParser(usage
= "Usage: %prog [options] file1 file2\n\n"
348 "If no filenames are provided the input is read from stdin.")
349 options
.add_option("-f", "--format", dest
="format", type="choice", choices
=["conf", "man"],
350 help="output format: conf, man [default: %default]", default
="conf")
351 options
.add_option("-r", "--root", dest
="root", metavar
="NAME",
352 help="root section of which options are printed, "
353 "if not found everything is printed")
354 options
.add_option("-n", "--nosort", action
="store_false", dest
="sort",
355 default
=True, help="do not sort sections alphabetically")
357 (opts
, args
) = options
.parse_args()
359 parser
= Parser(opts
.sort
)
361 for filename
in args
:
363 with
open(filename
, 'r') as file:
366 sys
.stderr
.write("Unable to open '{0}': {1}\n".format(filename
, e
.strerror
))
368 parser
.parse(sys
.stdin
)
370 options
= parser
.options
372 root
= parser
.get_option(opts
.root
)
374 options
= root
.options
376 if opts
.format
== "conf":
377 formatter
= ConfFormatter()
378 elif opts
.format
== "man":
379 formatter
= ManFormatter()
381 formatter
.format(options
)