Annotation of embedaddon/strongswan/conf/format-options.py, revision 1.1.1.1
1.1 misho 1: #!/usr/bin/env python
2: #
3: # Copyright (C) 2014-2019 Tobias Brunner
4: # HSR 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: To add include statements to generated config files (ignored when generating
53: man pages) the following format can be used:
54:
55: full.section.name.include files/to/include
56: Description of this include statement
57:
58: Dots in section/option names may be escaped with a backslash. For instance,
59: with the following section description
60:
61: charon.filelog./var/log/daemon\.log {}
62: Section to define logging into /var/log/daemon.log
63:
64: /var/log/daemon.log will be the name of the last section.
65: """
66:
67: import sys
68: import re
69: from textwrap import TextWrapper
70: from argparse import ArgumentParser
71: from functools import cmp_to_key, total_ordering
72:
73: @total_ordering
74: class ConfigOption:
75: """Representing a configuration option or described section in strongswan.conf"""
76: def __init__(self, path, default = None, section = False, commented = False, include = False):
77: self.path = path
78: self.name = path[-1]
79: self.fullname = '.'.join(path)
80: self.default = default
81: self.section = section
82: self.commented = commented
83: self.include = include
84: self.desc = []
85: self.options = []
86:
87: def __eq__(self, other):
88: return self.name == other.name
89:
90: def __lt__(self, other):
91: return self.name < other.name
92:
93: def add_paragraph(self):
94: """Adds a new paragraph to the description"""
95: if len(self.desc) and len(self.desc[-1]):
96: self.desc.append("")
97:
98: def add(self, line):
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]):
103: self.desc[-1] = line
104: else:
105: self.desc[-1] += ' ' + line
106:
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
112:
113: @staticmethod
114: def cmp(a, b):
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
119:
120: class Parser:
121: """Parses one or more files of configuration options"""
122: def __init__(self, sort = True):
123: self.options = []
124: self.sort = sort
125:
126: def parse(self, file):
127: """Parses the given file and adds all options to the internal store"""
128: self.__current = None
129: for line in file:
130: self.__parse_line(line)
131: if self.__current:
132: self.__add_option(self.__current)
133:
134: def __parse_line(self, line):
135: """Parses a single line"""
136: if re.match(r'^\s*#', line):
137: return
138: # option definition
139: m = re.match(r'^(?P<name>\S+)\s*((?P<assign>:)?=\s*(?P<default>.+)?)?\s*$', line)
140: if m:
141: if self.__current:
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'))
146: return
147: # section definition
148: m = re.match(r'^(?P<name>\S+)\s*\{\s*(?P<comment>#)?\s*\}\s*$', line)
149: if m:
150: if self.__current:
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'))
155: return
156: # include definition
157: m = re.match(r'^(?P<name>\S+\.include|include)\s+(?P<pattern>\S+)\s*$', line)
158: if m:
159: if self.__current:
160: self.__add_option(self.__current)
161: path = self.__split_name(m.group('name'))
162: self.__current = ConfigOption(path, m.group('pattern'), include = True)
163: return
164: # paragraph separator
165: m = re.match(r'^\s*$', line)
166: if m and self.__current:
167: self.__current.add_paragraph()
168: # description line
169: m = re.match(r'^\s+(?P<text>.+?)\s*$', line)
170: if m and self.__current:
171: self.__current.add(m.group('text'))
172:
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)]
176:
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)
181: if not parent:
182: parent = self
183: found = next((x for x in parent.options if x.name == option.name
184: and x.section == option.section), None)
185: if found:
186: found.adopt(option)
187: else:
188: parent.options.append(option)
189: if self.sort:
190: parent.options.sort()
191:
192: def __get_option(self, path, create = False):
193: """Searches/Creates the option (section) based on a list of section names"""
194: option = None
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)
198: if not option:
199: if not create:
200: break
201: option = ConfigOption(path[:i], section = True)
202: options.append(option)
203: if self.sort:
204: options.sort()
205: options = option.options
206: return option
207:
208: def get_option(self, name):
209: """Retrieves the option with the given name"""
210: return self.__get_option(self.__split_name(name))
211:
212: class TagReplacer:
213: """Replaces formatting tags in text"""
214: def __init__(self):
215: self.__matcher_b = self.__create_matcher('**')
216: self.__matcher_i = self.__create_matcher('_')
217: self.__replacer = None
218:
219: def __create_matcher(self, tag):
220: tag = re.escape(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)
229:
230: def _create_replacer(self):
231: def replacer(m):
232: punct = m.group('punct')
233: if not punct:
234: punct = ''
235: return '{0}{1}{2}'.format(m.group(1), m.group('text'), punct)
236: return replacer
237:
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)
243:
244: class GroffTagReplacer(TagReplacer):
245: def _create_replacer(self):
246: def replacer(m):
247: nl = '\n' if m.group(1) else ''
248: format = 'I' if m.group('tag') == '_' else 'B'
249: brack = m.group('brack')
250: if not brack:
251: brack = ''
252: punct = m.group('punct')
253: if not punct:
254: 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)
257: return replacer
258:
259: class ConfFormatter:
260: """Formats options to a strongswan.conf snippet"""
261: def __init__(self):
262: self.__indent = ' '
263: self.__wrapper = TextWrapper(width = 80, replace_whitespace = True,
264: break_long_words = False, break_on_hyphens = False)
265: self.__tags = TagReplacer()
266:
267: def __print_description(self, opt, indent):
268: if len(opt.desc):
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])))
272:
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)
277: if opt.include:
278: print('{0}{1} {2}'.format(self.__indent * indent, opt.name, opt.default))
279: elif opt.default:
280: print('{0}{1}{2} = {3}'.format(self.__indent * indent, comment, opt.name, opt.default))
281: else:
282: print('{0}{1}{2} ='.format(self.__indent * indent, comment, opt.name))
283: print('')
284:
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))
291: print('')
292: for o in sorted(section.options, key=cmp_to_key(ConfigOption.cmp)):
293: if o.section:
294: self.__print_section(o, indent + 1, commented)
295: else:
296: self.__print_option(o, indent + 1, commented)
297: print('{0}{1}}}'.format(self.__indent * indent, comment))
298: print('')
299:
300: def format(self, options):
301: """Print a list of options"""
302: if not options:
303: return
304: for option in sorted(options, key=cmp_to_key(ConfigOption.cmp)):
305: if option.section:
306: self.__print_section(option, 0, False)
307: else:
308: self.__print_option(option, 0, False)
309:
310: class ManFormatter:
311: """Formats a list of options into a groff snippet"""
312: def __init__(self):
313: self.__wrapper = TextWrapper(width = 80, replace_whitespace = False,
314: break_long_words = False, break_on_hyphens = False)
315: self.__tags = GroffTagReplacer()
316:
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)
323:
324: def __format_option(self, option):
325: """Print a single option"""
326: if option.section and not len(option.desc):
327: return
328: if option.include:
329: return
330: if option.section:
331: print('.TP\n.B {0}\n.br'.format(option.fullname))
332: else:
333: print('.TP')
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)))
338: print('')
339:
340: def format(self, options):
341: """Print a list of options"""
342: if not options:
343: return
344: for option in options:
345: if option.section:
346: self.__format_option(option)
347: self.format(option.options)
348: else:
349: self.__format_option(option)
350:
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")
361:
362: opts = args.parse_args()
363:
364: parser = Parser(opts.sort)
365: if len(opts.file):
366: for filename in opts.file:
367: try:
368: with open(filename, 'r') as file:
369: parser.parse(file)
370: except IOError as e:
371: sys.stderr.write("Unable to open '{0}': {1}\n".format(filename, e.strerror))
372: else:
373: parser.parse(sys.stdin)
374:
375: options = parser.options
376: if (opts.root):
377: root = parser.get_option(opts.root)
378: if root:
379: options = root.options
380:
381: if opts.format == "conf":
382: formatter = ConfFormatter()
383: elif opts.format == "man":
384: formatter = ManFormatter()
385:
386: formatter.format(options)
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>