]>
git.ipfire.org Git - thirdparty/strongswan.git/blob - conf/format-options.py
3 # Copyright (C) 2014-2019 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 argparse
import ArgumentParser
71 from functools
import cmp_to_key
, total_ordering
75 """Representing a configuration option or described section in strongswan.conf"""
76 def __init__(self
, path
, default
= None, section
= False, commented
= False, include
= False):
79 self
.fullname
= '.'.join(path
)
80 self
.default
= default
81 self
.section
= section
82 self
.commented
= commented
83 self
.include
= include
87 def __eq__(self
, other
):
88 return self
.name
== other
.name
90 def __lt__(self
, other
):
91 return self
.name
< other
.name
93 def add_paragraph(self
):
94 """Adds a new paragraph to the description"""
95 if len(self
.desc
) and len(self
.desc
[-1]):
99 """Adds a line to the last paragraph"""
100 if not len(self
.desc
):
101 self
.desc
.append(line
)
102 elif not len(self
.desc
[-1]):
105 self
.desc
[-1] += ' ' + line
107 def adopt(self
, other
):
108 """Adopts settings from other, which should be more recently parsed"""
109 self
.default
= other
.default
110 self
.commented
= other
.commented
111 self
.desc
= other
.desc
115 # order options before sections and includes last
116 if a
.include
or b
.include
:
117 return a
.include
- b
.include
118 return a
.section
- b
.section
121 """Parses one or more files of configuration options"""
122 def __init__(self
, sort
= True):
126 def parse(self
, file):
127 """Parses the given file and adds all options to the internal store"""
128 self
.__current
= None
130 self
.__parse
_line
(line
)
132 self
.__add
_option
(self
.__current
)
134 def __parse_line(self
, line
):
135 """Parses a single line"""
136 if re
.match(r
'^\s*#', line
):
139 m
= re
.match(r
'^(?P<name>\S+)\s*((?P<assign>:)?=\s*(?P<default>.+)?)?\s*$', line
)
142 self
.__add
_option
(self
.__current
)
143 path
= self
.__split
_name
(m
.group('name'))
144 self
.__current
= ConfigOption(path
, m
.group('default'),
145 commented
= not m
.group('assign'))
148 m
= re
.match(r
'^(?P<name>\S+)\s*\{\s*(?P<comment>#)?\s*\}\s*$', line
)
151 self
.__add
_option
(self
.__current
)
152 path
= self
.__split
_name
(m
.group('name'))
153 self
.__current
= ConfigOption(path
, section
= True,
154 commented
= m
.group('comment'))
157 m
= re
.match(r
'^(?P<name>\S+\.include|include)\s+(?P<pattern>\S+)\s*$', line
)
160 self
.__add
_option
(self
.__current
)
161 path
= self
.__split
_name
(m
.group('name'))
162 self
.__current
= ConfigOption(path
, m
.group('pattern'), include
= True)
164 # paragraph separator
165 m
= re
.match(r
'^\s*$', line
)
166 if m
and self
.__current
:
167 self
.__current
.add_paragraph()
169 m
= re
.match(r
'^\s+(?P<text>.+?)\s*$', line
)
170 if m
and self
.__current
:
171 self
.__current
.add(m
.group('text'))
173 def __split_name(self
, name
):
174 """Split the given full name in a list of section/option names"""
175 return [x
.replace('\.', '.') for x
in re
.split(r
'(?<!\\)\.', name
)]
177 def __add_option(self
, option
):
178 """Adds the given option to the abstract storage"""
179 option
.desc
= [desc
for desc
in option
.desc
if len(desc
)]
180 parent
= self
.__get
_option
(option
.path
[:-1], True)
183 found
= next((x
for x
in parent
.options
if x
.name
== option
.name
184 and x
.section
== option
.section
), None)
188 parent
.options
.append(option
)
190 parent
.options
.sort()
192 def __get_option(self
, path
, create
= False):
193 """Searches/Creates the option (section) based on a list of section names"""
195 options
= self
.options
196 for i
, name
in enumerate(path
, 1):
197 option
= next((x
for x
in options
if x
.name
== name
and x
.section
), None)
201 option
= ConfigOption(path
[:i
], section
= True)
202 options
.append(option
)
205 options
= option
.options
208 def get_option(self
, name
):
209 """Retrieves the option with the given name"""
210 return self
.__get
_option
(self
.__split
_name
(name
))
213 """Replaces formatting tags in text"""
215 self
.__matcher
_b
= self
.__create
_matcher
('**')
216 self
.__matcher
_i
= self
.__create
_matcher
('_')
217 self
.__replacer
= None
219 def __create_matcher(self
, tag
):
221 return re
.compile(r
'''
222 (^|\s|(?P<brack>[(\[])) # prefix with optional opening bracket
223 (?P<tag>''' + tag
+ r
''') # start tag
224 (?P<text>\S|\S.*?\S) # text
225 ''' + tag
+ r
''' # end tag
226 (?P<punct>([.,!:)\]]|\(\d+\))*) # punctuation
227 (?=$|\s) # suffix (don't consume it so that subsequent tags can match)
228 ''', flags
= re
.DOTALL | re
.VERBOSE
)
230 def _create_replacer(self
):
232 punct
= m
.group('punct')
235 return '{0}{1}{2}'.format(m
.group(1), m
.group('text'), punct
)
238 def replace(self
, text
):
239 if not self
.__replacer
:
240 self
.__replacer
= self
._create
_replacer
()
241 text
= re
.sub(self
.__matcher
_b
, self
.__replacer
, text
)
242 return re
.sub(self
.__matcher
_i
, self
.__replacer
, text
)
244 class GroffTagReplacer(TagReplacer
):
245 def _create_replacer(self
):
247 nl
= '\n' if m
.group(1) else ''
248 format
= 'I' if m
.group('tag') == '_' else 'B'
249 brack
= m
.group('brack')
252 punct
= m
.group('punct')
255 text
= re
.sub(r
'[\r\n\t]', ' ', m
.group('text'))
256 return '{0}.R{1} "{2}" "{3}" "{4}"\n'.format(nl
, format
, brack
, text
, punct
)
260 """Formats options to a strongswan.conf snippet"""
263 self
.__wrapper
= TextWrapper(width
= 80, replace_whitespace
= True,
264 break_long_words
= False, break_on_hyphens
= False)
265 self
.__tags
= TagReplacer()
267 def __print_description(self
, opt
, indent
):
269 self
.__wrapper
.initial_indent
= '{0}# '.format(self
.__indent
* indent
)
270 self
.__wrapper
.subsequent_indent
= self
.__wrapper
.initial_indent
271 print(self
.__wrapper
.fill(self
.__tags
.replace(opt
.desc
[0])))
273 def __print_option(self
, opt
, indent
, commented
):
274 """Print a single option with description and default value"""
275 comment
= "# " if commented
or opt
.commented
else ""
276 self
.__print
_description
(opt
, indent
)
278 print('{0}{1} {2}'.format(self
.__indent
* indent
, opt
.name
, opt
.default
))
280 print('{0}{1}{2} = {3}'.format(self
.__indent
* indent
, comment
, opt
.name
, opt
.default
))
282 print('{0}{1}{2} ='.format(self
.__indent
* indent
, comment
, opt
.name
))
285 def __print_section(self
, section
, indent
, commented
):
286 """Print a section with all options"""
287 commented
= commented
or section
.commented
288 comment
= "# " if commented
else ""
289 self
.__print
_description
(section
, indent
)
290 print('{0}{1}{2} {{'.format(self
.__indent
* indent
, comment
, section
.name
))
292 for o
in sorted(section
.options
, key
=cmp_to_key(ConfigOption
.cmp)):
294 self
.__print
_section
(o
, indent
+ 1, commented
)
296 self
.__print
_option
(o
, indent
+ 1, commented
)
297 print('{0}{1}}}'.format(self
.__indent
* indent
, comment
))
300 def format(self
, options
):
301 """Print a list of options"""
304 for option
in sorted(options
, key
=cmp_to_key(ConfigOption
.cmp)):
306 self
.__print
_section
(option
, 0, False)
308 self
.__print
_option
(option
, 0, False)
311 """Formats a list of options into a groff snippet"""
313 self
.__wrapper
= TextWrapper(width
= 80, replace_whitespace
= False,
314 break_long_words
= False, break_on_hyphens
= False)
315 self
.__tags
= GroffTagReplacer()
317 def __groffize(self
, text
):
318 """Encode text as groff text"""
319 text
= self
.__tags
.replace(text
)
320 text
= re
.sub(r
'(?<!\\)-', r
'\\-', text
)
321 # remove any leading whitespace
322 return re
.sub(r
'^\s+', '', text
, flags
= re
.MULTILINE
)
324 def __format_option(self
, option
):
325 """Print a single option"""
326 if option
.section
and not len(option
.desc
):
331 print('.TP\n.B {0}\n.br'.format(option
.fullname
))
334 default
= option
.default
if option
.default
else ''
335 print('.BR {0} " [{1}]"'.format(option
.fullname
, default
))
336 for para
in option
.desc
if len(option
.desc
) < 2 else option
.desc
[1:]:
337 print(self
.__groffize
(self
.__wrapper
.fill(para
)))
340 def format(self
, options
):
341 """Print a list of options"""
344 for option
in options
:
346 self
.__format
_option
(option
)
347 self
.format(option
.options
)
349 self
.__format
_option
(option
)
351 args
= ArgumentParser()
352 args
.add_argument('file', nargs
='*',
353 help="files to process, omit to read input from stdin")
354 args
.add_argument("-f", "--format", dest
="format", choices
=["conf", "man"],
355 help="output format (default: %(default)s)", default
="conf")
356 args
.add_argument("-r", "--root", dest
="root", metavar
="NAME",
357 help="root section of which options are printed; everything"
358 "is printed if not found")
359 args
.add_argument("-n", "--nosort", action
="store_false", dest
="sort",
360 default
=True, help="do not sort sections alphabetically")
362 opts
= args
.parse_args()
364 parser
= Parser(opts
.sort
)
366 for filename
in opts
.file:
368 with
open(filename
, 'r') as file:
371 sys
.stderr
.write("Unable to open '{0}': {1}\n".format(filename
, e
.strerror
))
373 parser
.parse(sys
.stdin
)
375 options
= parser
.options
377 root
= parser
.get_option(opts
.root
)
379 options
= root
.options
381 if opts
.format
== "conf":
382 formatter
= ConfFormatter()
383 elif opts
.format
== "man":
384 formatter
= ManFormatter()
386 formatter
.format(options
)