diff options
author | Andrew Shadura <andrew@shadura.me> | 2015-08-20 15:58:26 +0200 |
---|---|---|
committer | Andrew Shadura <andrew@shadura.me> | 2015-08-20 15:58:26 +0200 |
commit | ff1408420159488a106492ccd11dd234967029b6 (patch) | |
tree | 473420cee1c5229a427ec4cafead1aa6c0a26800 /reconfigure/parsers |
Imported Upstream version 0.1.29
Diffstat (limited to 'reconfigure/parsers')
-rw-r--r-- | reconfigure/parsers/__init__.py | 25 | ||||
-rw-r--r-- | reconfigure/parsers/base.py | 18 | ||||
-rw-r--r-- | reconfigure/parsers/bind9.py | 20 | ||||
-rw-r--r-- | reconfigure/parsers/crontab.py | 82 | ||||
-rw-r--r-- | reconfigure/parsers/exports.py | 47 | ||||
-rw-r--r-- | reconfigure/parsers/ini.py | 70 | ||||
-rw-r--r-- | reconfigure/parsers/iniparse/__init__.py | 25 | ||||
-rw-r--r-- | reconfigure/parsers/iniparse/compat.py | 343 | ||||
-rw-r--r-- | reconfigure/parsers/iniparse/config.py | 293 | ||||
-rw-r--r-- | reconfigure/parsers/iniparse/ini.py | 642 | ||||
-rw-r--r-- | reconfigure/parsers/iniparse/utils.py | 47 | ||||
-rw-r--r-- | reconfigure/parsers/iptables.py | 72 | ||||
-rw-r--r-- | reconfigure/parsers/jsonparser.py | 35 | ||||
-rw-r--r-- | reconfigure/parsers/nginx.py | 87 | ||||
-rw-r--r-- | reconfigure/parsers/nsd.py | 50 | ||||
-rw-r--r-- | reconfigure/parsers/squid.py | 53 | ||||
-rw-r--r-- | reconfigure/parsers/ssv.py | 72 |
17 files changed, 1981 insertions, 0 deletions
diff --git a/reconfigure/parsers/__init__.py b/reconfigure/parsers/__init__.py new file mode 100644 index 0000000..8de7aeb --- /dev/null +++ b/reconfigure/parsers/__init__.py @@ -0,0 +1,25 @@ +from base import BaseParser +from bind9 import BIND9Parser +from exports import ExportsParser +from ini import IniFileParser +from iptables import IPTablesParser +from jsonparser import JsonParser +from nginx import NginxParser +from nsd import NSDParser +from ssv import SSVParser +from squid import SquidParser +from crontab import CrontabParser + +__all__ = [ + 'BaseParser', + 'BIND9Parser', + 'CrontabParser', + 'ExportsParser', + 'IniFileParser', + 'IPTablesParser', + 'JsonParser', + 'NginxParser', + 'NSDParser', + 'SSVParser', + 'SquidParser', +] diff --git a/reconfigure/parsers/base.py b/reconfigure/parsers/base.py new file mode 100644 index 0000000..321fe1b --- /dev/null +++ b/reconfigure/parsers/base.py @@ -0,0 +1,18 @@ +class BaseParser (object): # pragma: no cover + """ + A base parser class + """ + + def parse(self, content): + """ + :param content: string config content + :returns: a :class:`reconfigure.nodes.Node` tree + """ + return None + + def stringify(self, tree): + """ + :param tree: a :class:`reconfigure.nodes.Node` tree + :returns: string config content + """ + return None diff --git a/reconfigure/parsers/bind9.py b/reconfigure/parsers/bind9.py new file mode 100644 index 0000000..d5c8e01 --- /dev/null +++ b/reconfigure/parsers/bind9.py @@ -0,0 +1,20 @@ +from reconfigure.nodes import * +from reconfigure.parsers.nginx import NginxParser + + +class BIND9Parser (NginxParser): + """ + A parser for named.conf + """ + + tokens = [ + (r"[\w_]+\s*?.*?{", lambda s, t: ('section_start', t)), + (r"[\w\d_:]+?.*?;", lambda s, t: ('option', t)), + (r"\s", lambda s, t: 'whitespace'), + (r"$^", lambda s, t: 'newline'), + (r"\#.*?\n", lambda s, t: ('comment', t)), + (r"//.*?\n", lambda s, t: ('comment', t)), + (r"/\*.*?\*/", lambda s, t: ('comment', t)), + (r"\};", lambda s, t: 'section_end'), + ] + token_section_end = '};' diff --git a/reconfigure/parsers/crontab.py b/reconfigure/parsers/crontab.py new file mode 100644 index 0000000..88dadba --- /dev/null +++ b/reconfigure/parsers/crontab.py @@ -0,0 +1,82 @@ +from reconfigure.nodes import RootNode, Node, PropertyNode +from reconfigure.parsers import BaseParser + + +class CrontabParser(BaseParser): + + def __init__(self, remove_comments=False): + self.remove_comments = remove_comments + + def parse(self, content): + root = RootNode() + lines = [l.strip() for l in content.splitlines() if l] + comment = None + for line in lines: + if line.startswith('#'): + comment = '\n'.join([comment, line]) if comment else line[1:] + continue + elif line.startswith('@'): + special, command = line.split(' ', 1) + node = Node('special_task', comment=comment) + node.append(PropertyNode('special', special)) + node.append(PropertyNode('command', command)) + + else: + split_line = line.split(' ', 5) + if len(split_line) <= 3 and '=' in line: + name, value = [n.strip() for n in line.split('=')] + if not name: + continue + node = Node('env_setting', comment=comment) + node.append(PropertyNode('name', name)) + node.append(PropertyNode('value', value)) + elif len(split_line) == 6: + node = Node('normal_task', comment=comment) + node.append(PropertyNode('minute', split_line[0])) + node.append(PropertyNode('hour', split_line[1])) + node.append(PropertyNode('day_of_month', split_line[2])) + node.append(PropertyNode('month', split_line[3])) + node.append(PropertyNode('day_of_week', split_line[4])) + node.append(PropertyNode('command', split_line[5])) + else: + continue + root.append(node) + comment = None + root.comment = comment + return root + + def stringify(self, tree): + result_lines = [] + stringify_func = { + 'special_task': self.stringify_special_task, + 'env_setting': self.stringify_env_setting, + 'normal_task': self.stringify_normal_task, + } + for node in tree: + if isinstance(node, Node): + string_line = stringify_func.get(node.name, lambda x: '')(node) + if node.comment: + result_lines.append('#' + node.comment) + result_lines.append(string_line) + return '\n'.join([line for line in result_lines if line]) + + def stringify_special_task(self, node): + special_node = node.get('special') + command_node = node.get('command') + if isinstance(special_node, PropertyNode) and isinstance(command_node, PropertyNode): + return ' '.join([special_node.value, command_node.value]) + return '' + + def stringify_env_setting(self, node): + name = node.get('name') + value = node.get('value') + if isinstance(name, PropertyNode) and isinstance(value, PropertyNode): + return ' = '.join([name.value, value.value]) + return '' + + def stringify_normal_task(self, node): + if all([isinstance(child, PropertyNode) for child in node.children]): + values_list = [str(pr_node.value).strip() for pr_node in node.children if pr_node.value] + if len(values_list) == 6: + return ' '.join(values_list) + return '' diff --git a/reconfigure/parsers/exports.py b/reconfigure/parsers/exports.py new file mode 100644 index 0000000..1941f61 --- /dev/null +++ b/reconfigure/parsers/exports.py @@ -0,0 +1,47 @@ +from reconfigure.nodes import * +from reconfigure.parsers import BaseParser +from reconfigure.parsers.ssv import SSVParser + + +class ExportsParser (BaseParser): + """ + A parser for NFS' /etc/exports + """ + + def __init__(self, *args, **kwargs): + BaseParser.__init__(self, *args, **kwargs) + self.inner = SSVParser(continuation='\\') + + def parse(self, content): + tree = self.inner.parse(content) + root = RootNode() + for export in tree: + export_node = Node(export[0].get('value').value) + export_node.comment = export.comment + clients_node = Node('clients') + export_node.append(clients_node) + root.append(export_node) + + for client in export[1:]: + s = client.get('value').value + name = s.split('(')[0] + options = '' + if '(' in s: + options = s.split('(', 1)[1].rstrip(')') + client_node = Node(name) + client_node.set_property('options', options) + clients_node.append(client_node) + return root + + def stringify(self, tree): + root = RootNode() + for export in tree: + export_node = Node('line', comment=export.comment) + export_node.append(Node('token', PropertyNode('value', export.name))) + for client in export['clients']: + s = client.name + if client['options'].value: + s += '(%s)' % client['options'].value + export_node.append(Node('token', PropertyNode('value', s))) + root.append(export_node) + return self.inner.stringify(root) diff --git a/reconfigure/parsers/ini.py b/reconfigure/parsers/ini.py new file mode 100644 index 0000000..dbd0c7c --- /dev/null +++ b/reconfigure/parsers/ini.py @@ -0,0 +1,70 @@ +from reconfigure.nodes import * +from reconfigure.parsers import BaseParser +from iniparse import INIConfig +from StringIO import StringIO + + +class IniFileParser (BaseParser): + """ + A parser for standard ``.ini`` config files. + + :param sectionless: if ``True``, allows a section-less attributes appear in the beginning of file + """ + + def __init__(self, sectionless=False, nullsection='__default__'): + self.sectionless = sectionless + self.nullsection = nullsection + + def _get_comment(self, container): + c = container.contents[0].comment + return c.strip() if c else None + + def _set_comment(self, container, comment): + if comment: + container.contents[0].comment = comment + container.contents[0].comment_separator = ';' + + def parse(self, content): + content = '\n'.join(filter(None, [x.strip() for x in content.splitlines()])) + if self.sectionless: + content = '[' + self.nullsection + ']\n' + content + data = StringIO(content) + cp = INIConfig(data, optionxformvalue=lambda x: x) + + root = RootNode() + for section in cp: + name = section + if self.sectionless and section == self.nullsection: + name = None + section_node = Node(name) + section_node.comment = self._get_comment(cp[section]._lines[0]) + for option in cp[section]: + if option in cp[section]._options: + node = PropertyNode(option, cp[section][option]) + node.comment = self._get_comment(cp[section]._options[option]) + section_node.children.append(node) + root.children.append(section_node) + return root + + def stringify(self, tree): + cp = INIConfig() + + for section in tree.children: + if self.sectionless and section.name is None: + sectionname = self.nullsection + else: + sectionname = section.name + cp._new_namespace(sectionname) + for option in section.children: + if not isinstance(option, PropertyNode): + raise TypeError('Third level nodes should be PropertyNodes') + cp[sectionname][option.name] = option.value + if option.comment: + self._set_comment(cp[sectionname]._options[option.name], option.comment) + if hasattr(cp[sectionname], '_lines'): + self._set_comment(cp[sectionname]._lines[0], section.comment) + + data = str(cp) + '\n' + if self.sectionless: + data = data.replace('[' + self.nullsection + ']\n', '') + return data diff --git a/reconfigure/parsers/iniparse/__init__.py b/reconfigure/parsers/iniparse/__init__.py new file mode 100644 index 0000000..618bd20 --- /dev/null +++ b/reconfigure/parsers/iniparse/__init__.py @@ -0,0 +1,25 @@ +# Copyright (c) 2001, 2002, 2003 Python Software Foundation +# Copyright (c) 2004-2008 Paramjit Oberoi <param.cs.wisc.edu> +# Copyright (c) 2007 Tim Lauridsen <tla@rasmil.dk> +# All Rights Reserved. See LICENSE-PSF & LICENSE for details. + +from ini import INIConfig, change_comment_syntax +from config import BasicConfig, ConfigNamespace +from compat import RawConfigParser, ConfigParser, SafeConfigParser +from utils import tidy + +from ConfigParser import DuplicateSectionError, \ + NoSectionError, NoOptionError, \ + InterpolationMissingOptionError, \ + InterpolationDepthError, \ + InterpolationSyntaxError, \ + DEFAULTSECT, MAX_INTERPOLATION_DEPTH + +__all__ = [ + 'BasicConfig', 'ConfigNamespace', + 'INIConfig', 'tidy', 'change_comment_syntax', + 'RawConfigParser', 'ConfigParser', 'SafeConfigParser', + 'DuplicateSectionError', 'NoSectionError', 'NoOptionError', + 'InterpolationMissingOptionError', 'InterpolationDepthError', + 'InterpolationSyntaxError', 'DEFAULTSECT', 'MAX_INTERPOLATION_DEPTH', +]
\ No newline at end of file diff --git a/reconfigure/parsers/iniparse/compat.py b/reconfigure/parsers/iniparse/compat.py new file mode 100644 index 0000000..17c4f67 --- /dev/null +++ b/reconfigure/parsers/iniparse/compat.py @@ -0,0 +1,343 @@ +# Copyright (c) 2001, 2002, 2003 Python Software Foundation +# Copyright (c) 2004-2008 Paramjit Oberoi <param.cs.wisc.edu> +# All Rights Reserved. See LICENSE-PSF & LICENSE for details. + +"""Compatibility interfaces for ConfigParser + +Interfaces of ConfigParser, RawConfigParser and SafeConfigParser +should be completely identical to the Python standard library +versions. Tested with the unit tests included with Python-2.3.4 + +The underlying INIConfig object can be accessed as cfg.data +""" + +import re +from ConfigParser import DuplicateSectionError, \ + NoSectionError, NoOptionError, \ + InterpolationMissingOptionError, \ + InterpolationDepthError, \ + InterpolationSyntaxError, \ + DEFAULTSECT, MAX_INTERPOLATION_DEPTH + +# These are imported only for compatiability. +# The code below does not reference them directly. +from ConfigParser import Error, InterpolationError, \ + MissingSectionHeaderError, ParsingError + +import ini + +class RawConfigParser(object): + def __init__(self, defaults=None, dict_type=dict): + if dict_type != dict: + raise ValueError('Custom dict types not supported') + self.data = ini.INIConfig(defaults=defaults, optionxformsource=self) + + def optionxform(self, optionstr): + return optionstr.lower() + + def defaults(self): + d = {} + secobj = self.data._defaults + for name in secobj._options: + d[name] = secobj._compat_get(name) + return d + + def sections(self): + """Return a list of section names, excluding [DEFAULT]""" + return list(self.data) + + def add_section(self, section): + """Create a new section in the configuration. + + Raise DuplicateSectionError if a section by the specified name + already exists. Raise ValueError if name is DEFAULT or any of + its case-insensitive variants. + """ + # The default section is the only one that gets the case-insensitive + # treatment - so it is special-cased here. + if section.lower() == "default": + raise ValueError, 'Invalid section name: %s' % section + + if self.has_section(section): + raise DuplicateSectionError(section) + else: + self.data._new_namespace(section) + + def has_section(self, section): + """Indicate whether the named section is present in the configuration. + + The DEFAULT section is not acknowledged. + """ + return (section in self.data) + + def options(self, section): + """Return a list of option names for the given section name.""" + if section in self.data: + return list(self.data[section]) + else: + raise NoSectionError(section) + + def read(self, filenames): + """Read and parse a filename or a list of filenames. + + Files that cannot be opened are silently ignored; this is + designed so that you can specify a list of potential + configuration file locations (e.g. current directory, user's + home directory, systemwide directory), and all existing + configuration files in the list will be read. A single + filename may also be given. + """ + files_read = [] + if isinstance(filenames, basestring): + filenames = [filenames] + for filename in filenames: + try: + fp = open(filename) + except IOError: + continue + files_read.append(filename) + self.data._readfp(fp) + fp.close() + return files_read + + def readfp(self, fp, filename=None): + """Like read() but the argument must be a file-like object. + + The `fp' argument must have a `readline' method. Optional + second argument is the `filename', which if not given, is + taken from fp.name. If fp has no `name' attribute, `<???>' is + used. + """ + self.data._readfp(fp) + + def get(self, section, option, vars=None): + if not self.has_section(section): + raise NoSectionError(section) + if vars is not None and option in vars: + value = vars[option] + + sec = self.data[section] + if option in sec: + return sec._compat_get(option) + else: + raise NoOptionError(option, section) + + def items(self, section): + if section in self.data: + ans = [] + for opt in self.data[section]: + ans.append((opt, self.get(section, opt))) + return ans + else: + raise NoSectionError(section) + + def getint(self, section, option): + return int(self.get(section, option)) + + def getfloat(self, section, option): + return float(self.get(section, option)) + + _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, + '0': False, 'no': False, 'false': False, 'off': False} + + def getboolean(self, section, option): + v = self.get(section, option) + if v.lower() not in self._boolean_states: + raise ValueError, 'Not a boolean: %s' % v + return self._boolean_states[v.lower()] + + def has_option(self, section, option): + """Check for the existence of a given option in a given section.""" + if section in self.data: + sec = self.data[section] + else: + raise NoSectionError(section) + return (option in sec) + + def set(self, section, option, value): + """Set an option.""" + if section in self.data: + self.data[section][option] = value + else: + raise NoSectionError(section) + + def write(self, fp): + """Write an .ini-format representation of the configuration state.""" + fp.write(str(self.data)) + + def remove_option(self, section, option): + """Remove an option.""" + if section in self.data: + sec = self.data[section] + else: + raise NoSectionError(section) + if option in sec: + del sec[option] + return 1 + else: + return 0 + + def remove_section(self, section): + """Remove a file section.""" + if not self.has_section(section): + return False + del self.data[section] + return True + + +class ConfigDict(object): + """Present a dict interface to a ini section.""" + + def __init__(self, cfg, section, vars): + self.cfg = cfg + self.section = section + self.vars = vars + + def __getitem__(self, key): + try: + return RawConfigParser.get(self.cfg, self.section, key, self.vars) + except (NoOptionError, NoSectionError): + raise KeyError(key) + + +class ConfigParser(RawConfigParser): + + def get(self, section, option, raw=False, vars=None): + """Get an option value for a given section. + + All % interpolations are expanded in the return values, based on the + defaults passed into the constructor, unless the optional argument + `raw' is true. Additional substitutions may be provided using the + `vars' argument, which must be a dictionary whose contents overrides + any pre-existing defaults. + + The section DEFAULT is special. + """ + if section != DEFAULTSECT and not self.has_section(section): + raise NoSectionError(section) + + option = self.optionxform(option) + value = RawConfigParser.get(self, section, option, vars) + + if raw: + return value + else: + d = ConfigDict(self, section, vars) + return self._interpolate(section, option, value, d) + + def _interpolate(self, section, option, rawval, vars): + # do the string interpolation + value = rawval + depth = MAX_INTERPOLATION_DEPTH + while depth: # Loop through this until it's done + depth -= 1 + if "%(" in value: + try: + value = value % vars + except KeyError, e: + raise InterpolationMissingOptionError( + option, section, rawval, e.args[0]) + else: + break + if value.find("%(") != -1: + raise InterpolationDepthError(option, section, rawval) + return value + + def items(self, section, raw=False, vars=None): + """Return a list of tuples with (name, value) for each option + in the section. + + All % interpolations are expanded in the return values, based on the + defaults passed into the constructor, unless the optional argument + `raw' is true. Additional substitutions may be provided using the + `vars' argument, which must be a dictionary whose contents overrides + any pre-existing defaults. + + The section DEFAULT is special. + """ + if section != DEFAULTSECT and not self.has_section(section): + raise NoSectionError(section) + if vars is None: + options = list(self.data[section]) + else: + options = [] + for x in self.data[section]: + if x not in vars: + options.append(x) + options.extend(vars.keys()) + + if "__name__" in options: + options.remove("__name__") + + d = ConfigDict(self, section, vars) + if raw: + return [(option, d[option]) + for option in options] + else: + return [(option, self._interpolate(section, option, d[option], d)) + for option in options] + + +class SafeConfigParser(ConfigParser): + _interpvar_re = re.compile(r"%\(([^)]+)\)s") + _badpercent_re = re.compile(r"%[^%]|%$") + + def set(self, section, option, value): + if not isinstance(value, basestring): + raise TypeError("option values must be strings") + # check for bad percent signs: + # first, replace all "good" interpolations + tmp_value = self._interpvar_re.sub('', value) + # then, check if there's a lone percent sign left + m = self._badpercent_re.search(tmp_value) + if m: + raise ValueError("invalid interpolation syntax in %r at " + "position %d" % (value, m.start())) + + ConfigParser.set(self, section, option, value) + + def _interpolate(self, section, option, rawval, vars): + # do the string interpolation + L = [] + self._interpolate_some(option, L, rawval, section, vars, 1) + return ''.join(L) + + _interpvar_match = re.compile(r"%\(([^)]+)\)s").match + + def _interpolate_some(self, option, accum, rest, section, map, depth): + if depth > MAX_INTERPOLATION_DEPTH: + raise InterpolationDepthError(option, section, rest) + while rest: + p = rest.find("%") + if p < 0: + accum.append(rest) + return + if p > 0: + accum.append(rest[:p]) + rest = rest[p:] + # p is no longer used + c = rest[1:2] + if c == "%": + accum.append("%") + rest = rest[2:] + elif c == "(": + m = self._interpvar_match(rest) + if m is None: + raise InterpolationSyntaxError(option, section, + "bad interpolation variable reference %r" % rest) + var = m.group(1) + rest = rest[m.end():] + try: + v = map[var] + except KeyError: + raise InterpolationMissingOptionError( + option, section, rest, var) + if "%" in v: + self._interpolate_some(option, accum, v, + section, map, depth + 1) + else: + accum.append(v) + else: + raise InterpolationSyntaxError( + option, section, + "'%' must be followed by '%' or '(', found: " + repr(rest))
\ No newline at end of file diff --git a/reconfigure/parsers/iniparse/config.py b/reconfigure/parsers/iniparse/config.py new file mode 100644 index 0000000..d007f16 --- /dev/null +++ b/reconfigure/parsers/iniparse/config.py @@ -0,0 +1,293 @@ +class ConfigNamespace(object): + """Abstract class representing the interface of Config objects. + + A ConfigNamespace is a collection of names mapped to values, where + the values may be nested namespaces. Values can be accessed via + container notation - obj[key] - or via dotted notation - obj.key. + Both these access methods are equivalent. + + To minimize name conflicts between namespace keys and class members, + the number of class members should be minimized, and the names of + all class members should start with an underscore. + + Subclasses must implement the methods for container-like access, + and this class will automatically provide dotted access. + + """ + + # Methods that must be implemented by subclasses + + def _getitem(self, key): + return NotImplementedError(key) + + def __setitem__(self, key, value): + raise NotImplementedError(key, value) + + def __delitem__(self, key): + raise NotImplementedError(key) + + def __iter__(self): + return NotImplementedError() + + def _new_namespace(self, name): + raise NotImplementedError(name) + + def __contains__(self, key): + try: + self._getitem(key) + except KeyError: + return False + return True + + # Machinery for converting dotted access into container access, + # and automatically creating new sections/namespaces. + # + # To distinguish between accesses of class members and namespace + # keys, we first call object.__getattribute__(). If that succeeds, + # the name is assumed to be a class member. Otherwise it is + # treated as a namespace key. + # + # Therefore, member variables should be defined in the class, + # not just in the __init__() function. See BasicNamespace for + # an example. + + def __getitem__(self, key): + try: + return self._getitem(key) + except KeyError: + return Undefined(key, self) + + def __getattr__(self, name): + try: + return self._getitem(name) + except KeyError: + if name.startswith('__') and name.endswith('__'): + raise AttributeError + return Undefined(name, self) + + def __setattr__(self, name, value): + try: + object.__getattribute__(self, name) + object.__setattr__(self, name, value) + except AttributeError: + self.__setitem__(name, value) + + def __delattr__(self, name): + try: + object.__getattribute__(self, name) + object.__delattr__(self, name) + except AttributeError: + self.__delitem__(name) + + # During unpickling, Python checks if the class has a __setstate__ + # method. But, the data dicts have not been initialised yet, which + # leads to _getitem and hence __getattr__ raising an exception. So + # we explicitly impement default __setstate__ behavior. + def __setstate__(self, state): + self.__dict__.update(state) + +class Undefined(object): + """Helper class used to hold undefined names until assignment. + + This class helps create any undefined subsections when an + assignment is made to a nested value. For example, if the + statement is "cfg.a.b.c = 42", but "cfg.a.b" does not exist yet. + """ + + def __init__(self, name, namespace): + object.__setattr__(self, 'name', name) + object.__setattr__(self, 'namespace', namespace) + + def __setattr__(self, name, value): + obj = self.namespace._new_namespace(self.name) + obj[name] = value + + def __setitem__(self, name, value): + obj = self.namespace._new_namespace(self.name) + obj[name] = value + + +# ---- Basic implementation of a ConfigNamespace + +class BasicConfig(ConfigNamespace): + """Represents a hierarchical collection of named values. + + Values are added using dotted notation: + + >>> n = BasicConfig() + >>> n.x = 7 + >>> n.name.first = 'paramjit' + >>> n.name.last = 'oberoi' + + ...and accessed the same way, or with [...]: + + >>> n.x + 7 + >>> n.name.first + 'paramjit' + >>> n.name.last + 'oberoi' + >>> n['x'] + 7 + >>> n['name']['first'] + 'paramjit' + + Iterating over the namespace object returns the keys: + + >>> l = list(n) + >>> l.sort() + >>> l + ['name', 'x'] + + Values can be deleted using 'del' and printed using 'print'. + + >>> n.aaa = 42 + >>> del n.x + >>> print n + aaa = 42 + name.first = paramjit + name.last = oberoi + + Nested namepsaces are also namespaces: + + >>> isinstance(n.name, ConfigNamespace) + True + >>> print n.name + first = paramjit + last = oberoi + >>> sorted(list(n.name)) + ['first', 'last'] + + Finally, values can be read from a file as follows: + + >>> from StringIO import StringIO + >>> sio = StringIO(''' + ... # comment + ... ui.height = 100 + ... ui.width = 150 + ... complexity = medium + ... have_python + ... data.secret.password = goodness=gracious me + ... ''') + >>> n = BasicConfig() + >>> n._readfp(sio) + >>> print n + complexity = medium + data.secret.password = goodness=gracious me + have_python + ui.height = 100 + ui.width = 150 + """ + + # this makes sure that __setattr__ knows this is not a namespace key + _data = None + + def __init__(self): + self._data = {} + + def _getitem(self, key): + return self._data[key] + + def __setitem__(self, key, value): + self._data[key] = value + + def __delitem__(self, key): + del self._data[key] + + def __iter__(self): + return iter(self._data) + + def __str__(self, prefix=''): + lines = [] + keys = self._data.keys() + keys.sort() + for name in keys: + value = self._data[name] + if isinstance(value, ConfigNamespace): + lines.append(value.__str__(prefix='%s%s.' % (prefix,name))) + else: + if value is None: + lines.append('%s%s' % (prefix, name)) + else: + lines.append('%s%s = %s' % (prefix, name, value)) + return '\n'.join(lines) + + def _new_namespace(self, name): + obj = BasicConfig() + self._data[name] = obj + return obj + + def _readfp(self, fp): + while True: + line = fp.readline() + if not line: + break + + line = line.strip() + if not line: continue + if line[0] == '#': continue + data = line.split('=', 1) + if len(data) == 1: + name = line + value = None + else: + name = data[0].strip() + value = data[1].strip() + name_components = name.split('.') + ns = self + for n in name_components[:-1]: + if n in ns: + ns = ns[n] + if not isinstance(ns, ConfigNamespace): + raise TypeError('value-namespace conflict', n) + else: + ns = ns._new_namespace(n) + ns[name_components[-1]] = value + + +# ---- Utility functions + +def update_config(target, source): + """Imports values from source into target. + + Recursively walks the <source> ConfigNamespace and inserts values + into the <target> ConfigNamespace. For example: + + >>> n = BasicConfig() + >>> n.playlist.expand_playlist = True + >>> n.ui.display_clock = True + >>> n.ui.display_qlength = True + >>> n.ui.width = 150 + >>> print n + playlist.expand_playlist = True + ui.display_clock = True + ui.display_qlength = True + ui.width = 150 + + >>> from iniparse import ini + >>> i = ini.INIConfig() + >>> update_config(i, n) + >>> print i + [playlist] + expand_playlist = True + <BLANKLINE> + [ui] + display_clock = True + display_qlength = True + width = 150 + + """ + for name in source: + value = source[name] + if isinstance(value, ConfigNamespace): + if name in target: + myns = target[name] + if not isinstance(myns, ConfigNamespace): + raise TypeError('value-namespace conflict') + else: + myns = target._new_namespace(name) + update_config(myns, value) + else: + target[name] = value + + diff --git a/reconfigure/parsers/iniparse/ini.py b/reconfigure/parsers/iniparse/ini.py new file mode 100644 index 0000000..7881fd2 --- /dev/null +++ b/reconfigure/parsers/iniparse/ini.py @@ -0,0 +1,642 @@ +"""Access and/or modify INI files + +* Compatiable with ConfigParser +* Preserves order of sections & options +* Preserves comments/blank lines/etc +* More conveninet access to data + +Example: + + >>> from StringIO import StringIO + >>> sio = StringIO('''# configure foo-application + ... [foo] + ... bar1 = qualia + ... bar2 = 1977 + ... [foo-ext] + ... special = 1''') + + >>> cfg = INIConfig(sio) + >>> print cfg.foo.bar1 + qualia + >>> print cfg['foo-ext'].special + 1 + >>> cfg.foo.newopt = 'hi!' + >>> cfg.baz.enabled = 0 + + >>> print cfg + # configure foo-application + [foo] + bar1 = qualia + bar2 = 1977 + newopt = hi! + [foo-ext] + special = 1 + <BLANKLINE> + [baz] + enabled = 0 + +""" + +# An ini parser that supports ordered sections/options +# Also supports updates, while preserving structure +# Backward-compatiable with ConfigParser + +import re +from ConfigParser import DEFAULTSECT, ParsingError, MissingSectionHeaderError + +import config + +class LineType(object): + line = None + + def __init__(self, line=None): + if line is not None: + self.line = line.strip('\n') + + # Return the original line for unmodified objects + # Otherwise construct using the current attribute values + def __str__(self): + if self.line is not None: + return self.line + else: + return self.to_string() + + # If an attribute is modified after initialization + # set line to None since it is no longer accurate. + def __setattr__(self, name, value): + if hasattr(self,name): + self.__dict__['line'] = None + self.__dict__[name] = value + + def to_string(self): + raise Exception('This method must be overridden in derived classes') + + +class SectionLine(LineType): + regex = re.compile(r'^\[' + r'(?P<name>[^]]+)' + r'\]\s*' + r'((?P<csep>;|#)(?P<comment>.*))?$') + + def __init__(self, name, comment=None, comment_separator=None, + comment_offset=-1, line=None): + super(SectionLine, self).__init__(line) + self.name = name + self.comment = comment + self.comment_separator = comment_separator + self.comment_offset = comment_offset + + def to_string(self): + out = '[' + self.name + ']' + if self.comment is not None: + # try to preserve indentation of comments + out = (out+' ').ljust(self.comment_offset) + out = out + self.comment_separator + self.comment + return out + + def parse(cls, line): + m = cls.regex.match(line.rstrip()) + if m is None: + return None + return cls(m.group('name'), m.group('comment'), + m.group('csep'), m.start('csep'), + line) + parse = classmethod(parse) + + +class OptionLine(LineType): + def __init__(self, name, value, separator='=', comment=None, + comment_separator=None, comment_offset=-1, line=None): + super(OptionLine, self).__init__(line) + self.name = name + self.value = value + self.separator = separator + self.comment = comment + self.comment_separator = comment_separator + self.comment_offset = comment_offset + + def to_string(self): + out = '%s%s%s' % (self.name, self.separator, self.value) + if self.comment is not None: + # try to preserve indentation of comments + out = (out+' ').ljust(self.comment_offset) + out = out + self.comment_separator + self.comment + return out + + regex = re.compile(r'^(?P<name>[^:=\s[][^:=]*)' + r'(?P<sep>[:=]\s*)' + r'(?P<value>.*)$') + + def parse(cls, line): + m = cls.regex.match(line.rstrip()) + if m is None: + return None + + name = m.group('name').rstrip() + value = m.group('value') + sep = m.group('name')[len(name):] + m.group('sep') + + # comments are not detected in the regex because + # ensuring total compatibility with ConfigParser + # requires that: + # option = value ;comment // value=='value' + # option = value;1 ;comment // value=='value;1 ;comment' + # + # Doing this in a regex would be complicated. I + # think this is a bug. The whole issue of how to + # include ';' in the value needs to be addressed. + # Also, '#' doesn't mark comments in options... + + coff = value.find(';') + if coff != -1 and value[coff-1].isspace(): + comment = value[coff+1:] + csep = value[coff] + value = value[:coff].rstrip() + coff = m.start('value') + coff + else: + comment = None + csep = None + coff = -1 + + return cls(name, value, sep, comment, csep, coff, line) + parse = classmethod(parse) + + +def change_comment_syntax(comment_chars='%;#', allow_rem=False): + comment_chars = re.sub(r'([\]\-\^])', r'\\\1', comment_chars) + regex = r'^(?P<csep>[%s]' % comment_chars + if allow_rem: + regex += '|[rR][eE][mM]' + regex += r')(?P<comment>.*)$' + CommentLine.regex = re.compile(regex) + +class CommentLine(LineType): + regex = re.compile(r'^(?P<csep>[;#]|[rR][eE][mM])' + r'(?P<comment>.*)$') + + def __init__(self, comment='', separator='#', line=None): + super(CommentLine, self).__init__(line) + self.comment = comment + self.separator = separator + + def to_string(self): + return self.separator + self.comment + + def parse(cls, line): + m = cls.regex.match(line.rstrip()) + if m is None: + return None + return cls(m.group('comment'), m.group('csep'), line) + parse = classmethod(parse) + + +class EmptyLine(LineType): + # could make this a singleton + def to_string(self): + return '' + + value = property(lambda _: '') + + def parse(cls, line): + if line.strip(): return None + return cls(line) + parse = classmethod(parse) + + +class ContinuationLine(LineType): + regex = re.compile(r'^\s+(?P<value>.*)$') + + def __init__(self, value, value_offset=None, line=None): + super(ContinuationLine, self).__init__(line) + self.value = value + if value_offset is None: + value_offset = 8 + self.value_offset = value_offset + + def to_string(self): + return ' '*self.value_offset + self.value + + def parse(cls, line): + m = cls.regex.match(line.rstrip()) + if m is None: + return None + return cls(m.group('value'), m.start('value'), line) + parse = classmethod(parse) + + +class LineContainer(object): + def __init__(self, d=None): + self.contents = [] + self.orgvalue = None + if d: + if isinstance(d, list): self.extend(d) + else: self.add(d) + + def add(self, x): + self.contents.append(x) + + def extend(self, x): + for i in x: self.add(i) + + def get_name(self): + return self.contents[0].name + + def set_name(self, data): + self.contents[0].name = data + + def get_value(self): + if self.orgvalue is not None: + return self.orgvalue + elif len(self.contents) == 1: + return self.contents[0].value + else: + return '\n'.join([('%s' % x.value) for x in self.contents + if not isinstance(x, CommentLine)]) + + def set_value(self, data): + self.orgvalue = data + lines = ('%s' % data).split('\n') + + # If there is an existing ContinuationLine, use its offset + value_offset = None + for v in self.contents: + if isinstance(v, ContinuationLine): + value_offset = v.value_offset + break + + # Rebuild contents list, preserving initial OptionLine + self.contents = self.contents[0:1] + self.contents[0].value = lines[0] + del lines[0] + for line in lines: + if line.strip(): + self.add(ContinuationLine(line, value_offset)) + else: + self.add(EmptyLine()) + + name = property(get_name, set_name) + value = property(get_value, set_value) + + def __str__(self): + s = [x.__str__() for x in self.contents] + return '\n'.join(s) + + def finditer(self, key): + for x in self.contents[::-1]: + if hasattr(x, 'name') and x.name==key: + yield x + + def find(self, key): + for x in self.finditer(key): + return x + raise KeyError(key) + + +def _make_xform_property(myattrname, srcattrname=None): + private_attrname = myattrname + 'value' + private_srcname = myattrname + 'source' + if srcattrname is None: + srcattrname = myattrname + + def getfn(self): + srcobj = getattr(self, private_srcname) + if srcobj is not None: + return getattr(srcobj, srcattrname) + else: + return getattr(self, private_attrname) + + def setfn(self, value): + srcobj = getattr(self, private_srcname) + if srcobj is not None: + setattr(srcobj, srcattrname, value) + else: + setattr(self, private_attrname, value) + + return property(getfn, setfn) + + +class INISection(config.ConfigNamespace): + _lines = None + _options = None + _defaults = None + _optionxformvalue = None + _optionxformsource = None + _compat_skip_empty_lines = set() + def __init__(self, lineobj, defaults = None, + optionxformvalue=None, optionxformsource=None): + self._lines = [lineobj] + self._defaults = defaults + self._optionxformvalue = optionxformvalue + self._optionxformsource = optionxformsource + self._options = {} + + _optionxform = _make_xform_property('_optionxform') + + def _compat_get(self, key): + # identical to __getitem__ except that _compat_XXX + # is checked for backward-compatible handling + if key == '__name__': + return self._lines[-1].name + if self._optionxform: key = self._optionxform(key) + try: + value = self._options[key].value + del_empty = key in self._compat_skip_empty_lines + except KeyError: + if self._defaults and key in self._defaults._options: + value = self._defaults._options[key].value + del_empty = key in self._defaults._compat_skip_empty_lines + else: + raise + if del_empty: + value = re.sub('\n+', '\n', value) + return value + + def _getitem(self, key): + if key == '__name__': + return self._lines[-1].name + if self._optionxform: key = self._optionxform(key) + try: + return self._options[key].value + except KeyError: + if self._defaults and key in self._defaults._options: + return self._defaults._options[key].value + else: + raise + + def __setitem__(self, key, value): + if self._optionxform: xkey = self._optionxform(key) + else: xkey = key + if xkey in self._compat_skip_empty_lines: + self._compat_skip_empty_lines.remove(xkey) + if xkey not in self._options: + # create a dummy object - value may have multiple lines + obj = LineContainer(OptionLine(key, '')) + self._lines[-1].add(obj) + self._options[xkey] = obj + # the set_value() function in LineContainer + # automatically handles multi-line values + self._options[xkey].value = value + + def __delitem__(self, key): + if self._optionxform: key = self._optionxform(key) + if key in self._compat_skip_empty_lines: + self._compat_skip_empty_lines.remove(key) + for l in self._lines: + remaining = [] + for o in l.contents: + if isinstance(o, LineContainer): + n = o.name + if self._optionxform: n = self._optionxform(n) + if key != n: remaining.append(o) + else: + remaining.append(o) + l.contents = remaining + del self._options[key] + + def __iter__(self): + d = set() + for l in self._lines: + for x in l.contents: + if isinstance(x, LineContainer): + if self._optionxform: + ans = self._optionxform(x.name) + else: + ans = x.name + if ans not in d: + yield ans + d.add(ans) + if self._defaults: + for x in self._defaults: + if x not in d: + yield x + d.add(x) + + def _new_namespace(self, name): + raise Exception('No sub-sections allowed', name) + + +def make_comment(line): + return CommentLine(line.rstrip('\n')) + + +def readline_iterator(f): + """iterate over a file by only using the file object's readline method""" + + have_newline = False + while True: + line = f.readline() + + if not line: + if have_newline: + yield "" + return + + if line.endswith('\n'): + have_newline = True + else: + have_newline = False + + yield line + + +def lower(x): + return x.lower() + + +class INIConfig(config.ConfigNamespace): + _data = None + _sections = None + _defaults = None + _optionxformvalue = None + _optionxformsource = None + _sectionxformvalue = None + _sectionxformsource = None + _parse_exc = None + _bom = False + def __init__(self, fp=None, defaults=None, parse_exc=True, + optionxformvalue=lower, optionxformsource=None, + sectionxformvalue=None, sectionxformsource=None): + self._data = LineContainer() + self._parse_exc = parse_exc + self._optionxformvalue = optionxformvalue + self._optionxformsource = optionxformsource + self._sectionxformvalue = sectionxformvalue + self._sectionxformsource = sectionxformsource + self._sections = {} + if defaults is None: defaults = {} + self._defaults = INISection(LineContainer(), optionxformsource=self) + for name, value in defaults.iteritems(): + self._defaults[name] = value + if fp is not None: + self._readfp(fp) + + _optionxform = _make_xform_property('_optionxform', 'optionxform') + _sectionxform = _make_xform_property('_sectionxform', 'optionxform') + + def _getitem(self, key): + if key == DEFAULTSECT: + return self._defaults + if self._sectionxform: key = self._sectionxform(key) + return self._sections[key] + + def __setitem__(self, key, value): + raise Exception('Values must be inside sections', key, value) + + def __delitem__(self, key): + if self._sectionxform: key = self._sectionxform(key) + for line in self._sections[key]._lines: + self._data.contents.remove(line) + del self._sections[key] + + def __iter__(self): + d = set() + d.add(DEFAULTSECT) + for x in self._data.contents: + if isinstance(x, LineContainer): + if x.name not in d: + yield x.name + d.add(x.name) + + def _new_namespace(self, name): + if self._data.contents: + self._data.add(EmptyLine()) + obj = LineContainer(SectionLine(name)) + self._data.add(obj) + if self._sectionxform: name = self._sectionxform(name) + if name in self._sections: + ns = self._sections[name] + ns._lines.append(obj) + else: + ns = INISection(obj, defaults=self._defaults, + optionxformsource=self) + self._sections[name] = ns + return ns + + def __str__(self): + if self._bom: + fmt = u'\ufeff%s' + else: + fmt = '%s' + return fmt % self._data.__str__() + + __unicode__ = __str__ + + _line_types = [EmptyLine, CommentLine, + SectionLine, OptionLine, + ContinuationLine] + + def _parse(self, line): + for linetype in self._line_types: + lineobj = linetype.parse(line) + if lineobj: + return lineobj + else: + # can't parse line + return None + + def _readfp(self, fp): + cur_section = None + cur_option = None + cur_section_name = None + cur_option_name = None + pending_lines = [] + pending_empty_lines = False + try: + fname = fp.name + except AttributeError: + fname = '<???>' + linecount = 0 + exc = None + line = None + + for line in readline_iterator(fp): + # Check for BOM on first line + if linecount == 0 and isinstance(line, unicode): + if line[0] == u'\ufeff': + line = line[1:] + self._bom = True + + lineobj = self._parse(line) + linecount += 1 + + if not cur_section and not isinstance(lineobj, + (CommentLine, EmptyLine, SectionLine)): + if self._parse_exc: + raise MissingSectionHeaderError(fname, linecount, line) + else: + lineobj = make_comment(line) + + if lineobj is None: + if self._parse_exc: + if exc is None: exc = ParsingError(fname) + exc.append(linecount, line) + lineobj = make_comment(line) + + if isinstance(lineobj, ContinuationLine): + if cur_option: + if pending_lines: + cur_option.extend(pending_lines) + pending_lines = [] + if pending_empty_lines: + optobj._compat_skip_empty_lines.add(cur_option_name) + pending_empty_lines = False + cur_option.add(lineobj) + else: + # illegal continuation line - convert to comment + if self._parse_exc: + if exc is None: exc = ParsingError(fname) + exc.append(linecount, line) + lineobj = make_comment(line) + + if isinstance(lineobj, OptionLine): + if pending_lines: + cur_section.extend(pending_lines) + pending_lines = [] + pending_empty_lines = False + cur_option = LineContainer(lineobj) + cur_section.add(cur_option) + if self._optionxform: + cur_option_name = self._optionxform(cur_option.name) + else: + cur_option_name = cur_option.name + if cur_section_name == DEFAULTSECT: + optobj = self._defaults + else: + optobj = self._sections[cur_section_name] + optobj._options[cur_option_name] = cur_option + + if isinstance(lineobj, SectionLine): + self._data.extend(pending_lines) + pending_lines = [] + pending_empty_lines = False + cur_section = LineContainer(lineobj) + self._data.add(cur_section) + cur_option = None + cur_option_name = None + if cur_section.name == DEFAULTSECT: + self._defaults._lines.append(cur_section) + cur_section_name = DEFAULTSECT + else: + if self._sectionxform: + cur_section_name = self._sectionxform(cur_section.name) + else: + cur_section_name = cur_section.name + if cur_section_name not in self._sections: + self._sections[cur_section_name] = \ + INISection(cur_section, defaults=self._defaults, + optionxformsource=self) + else: + self._sections[cur_section_name]._lines.append(cur_section) + + if isinstance(lineobj, (CommentLine, EmptyLine)): + pending_lines.append(lineobj) + if isinstance(lineobj, EmptyLine): + pending_empty_lines = True + + self._data.extend(pending_lines) + if line and line[-1]=='\n': + self._data.add(EmptyLine()) + + if exc: + raise exc + diff --git a/reconfigure/parsers/iniparse/utils.py b/reconfigure/parsers/iniparse/utils.py new file mode 100644 index 0000000..9cb7488 --- /dev/null +++ b/reconfigure/parsers/iniparse/utils.py @@ -0,0 +1,47 @@ +import compat +from ini import LineContainer, EmptyLine + +def tidy(cfg): + """Clean up blank lines. + + This functions makes the configuration look clean and + handwritten - consecutive empty lines and empty lines at + the start of the file are removed, and one is guaranteed + to be at the end of the file. + """ + + if isinstance(cfg, compat.RawConfigParser): + cfg = cfg.data + cont = cfg._data.contents + i = 1 + while i < len(cont): + if isinstance(cont[i], LineContainer): + tidy_section(cont[i]) + i += 1 + elif (isinstance(cont[i-1], EmptyLine) and + isinstance(cont[i], EmptyLine)): + del cont[i] + else: + i += 1 + + # Remove empty first line + if cont and isinstance(cont[0], EmptyLine): + del cont[0] + + # Ensure a last line + if cont and not isinstance(cont[-1], EmptyLine): + cont.append(EmptyLine()) + +def tidy_section(lc): + cont = lc.contents + i = 1 + while i < len(cont): + if (isinstance(cont[i-1], EmptyLine) and + isinstance(cont[i], EmptyLine)): + del cont[i] + else: + i += 1 + + # Remove empty first line + if len(cont) > 1 and isinstance(cont[1], EmptyLine): + del cont[1]
\ No newline at end of file diff --git a/reconfigure/parsers/iptables.py b/reconfigure/parsers/iptables.py new file mode 100644 index 0000000..46edf60 --- /dev/null +++ b/reconfigure/parsers/iptables.py @@ -0,0 +1,72 @@ +from reconfigure.nodes import * +from reconfigure.parsers import BaseParser + + +class IPTablesParser (BaseParser): + """ + A parser for ``iptables`` configuration as produced by ``iptables-save`` + """ + + def parse(self, content): + content = filter(None, [x.strip() for x in content.splitlines() if not x.startswith('#')]) + root = RootNode() + cur_table = None + chains = {} + for l in content: + if l.startswith('*'): + cur_table = Node(l[1:]) + chains = {} + root.append(cur_table) + elif l.startswith(':'): + name = l[1:].split()[0] + node = Node(name) + node.set_property('default', l.split()[1]) + chains[name] = node + cur_table.append(node) + else: + comment = None + if '#' in l: + l, comment = l.split('#') + comment = comment.strip() + tokens = l.split() + if tokens[0] == '-A': + tokens.pop(0) + node = Node('append') + node.comment = comment + chain = tokens.pop(0) + chains[chain].append(node) + while tokens: + token = tokens.pop(0) + option = Node('option') + option.set_property('negative', token == '!') + if token == '!': + token = tokens.pop(0) + option.set_property('name', token.strip('-')) + while tokens and not tokens[0].startswith('-') and tokens[0] != '!': + option.append(Node('argument', PropertyNode('value', tokens.pop(0)))) + node.append(option) + + return root + + def stringify(self, tree): + data = '' + for table in tree.children: + data += '*%s\n' % table.name + for chain in table.children: + data += ':%s %s [0:0]\n' % (chain.name, chain.get('default').value) + for chain in table.children: + for item in chain.children: + if item.name == 'append': + data += '-A %s %s%s\n' % ( + chain.name, + ' '.join( + ('! ' if o.get('negative').value else '') + + ('--' if len(o.get('name').value) > 1 else '-') + o.get('name').value + ' ' + + ' '.join(a.get('value').value for a in o.children if a.name == 'argument') + for o in item.children + if o.name == 'option' + ), + ' # %s' % item.comment if item.comment else '' + ) + data += 'COMMIT\n' + return data diff --git a/reconfigure/parsers/jsonparser.py b/reconfigure/parsers/jsonparser.py new file mode 100644 index 0000000..c1eaee6 --- /dev/null +++ b/reconfigure/parsers/jsonparser.py @@ -0,0 +1,35 @@ +from reconfigure.nodes import * +from reconfigure.parsers import BaseParser +import json + + +class JsonParser (BaseParser): + """ + A parser for JSON files (using ``json`` module) + """ + + def parse(self, content): + node = RootNode() + self.load_node_rec(node, json.loads(content)) + return node + + def load_node_rec(self, node, json): + for k, v in json.iteritems(): + if isinstance(v, dict): + child = Node(k) + node.children.append(child) + self.load_node_rec(child, v) + else: + node.children.append(PropertyNode(k, v)) + + def stringify(self, tree): + return json.dumps(self.save_node_rec(tree), indent=4) + + def save_node_rec(self, node): + r = {} + for child in node.children: + if isinstance(child, PropertyNode): + r[child.name] = child.value + else: + r[child.name] = self.save_node_rec(child) + return r diff --git a/reconfigure/parsers/nginx.py b/reconfigure/parsers/nginx.py new file mode 100644 index 0000000..661cfcd --- /dev/null +++ b/reconfigure/parsers/nginx.py @@ -0,0 +1,87 @@ +from reconfigure.nodes import * +from reconfigure.parsers import BaseParser +import re + + +class NginxParser (BaseParser): + """ + A parser for nginx configs + """ + + tokens = [ + (r"[\w_]+?.+?;", lambda s, t: ('option', t)), + (r"\s", lambda s, t: 'whitespace'), + (r"$^", lambda s, t: 'newline'), + (r"\#.*?\n", lambda s, t: ('comment', t)), + (r"[\w_]+\s*?.*?{", lambda s, t: ('section_start', t)), + (r"\}", lambda s, t: 'section_end'), + ] + token_comment = '#' + token_section_end = '}' + + def parse(self, content): + scanner = re.Scanner(self.tokens) + tokens, remainder = scanner.scan(' '.join(filter(None, content.split(' ')))) + if remainder: + raise Exception('Invalid tokens: %s' % remainder) + + node = RootNode() + node.parameter = None + node_stack = [] + next_comment = None + + while len(tokens) > 0: + token = tokens[0] + tokens = tokens[1:] + if token in ['whitespace', 'newline']: + continue + if token == 'section_end': + node = node_stack.pop() + if token[0] == 'comment': + if not next_comment: + next_comment = '' + else: + next_comment += '\n' + next_comment += token[1].strip('#/*').strip() + if token[0] == 'option': + if ' ' in token[1]: + k, v = token[1].split(None, 1) + else: + v = token[1] + k = '' + prop = PropertyNode(k.strip(), v[:-1].strip()) + prop.comment = next_comment + next_comment = None + node.children.append(prop) + if token[0] == 'section_start': + line = token[1][:-1].strip().split(None, 1) + [None] + section = Node(line[0]) + section.parameter = line[1] + section.comment = next_comment + next_comment = None + node_stack += [node] + node.children.append(section) + node = section + + return node + + def stringify(self, tree): + return ''.join(self.stringify_rec(node) for node in tree.children) + + def stringify_rec(self, node): + if isinstance(node, PropertyNode): + if node.name: + s = '%s %s;\n' % (node.name, node.value) + else: + s = '%s;\n' % (node.value) + elif isinstance(node, IncludeNode): + s = 'include %s;\n' % (node.files) + else: + result = '\n%s %s {\n' % (node.name, node.parameter or '') + for child in node.children: + result += '\n'.join('\t' + x for x in self.stringify_rec(child).splitlines()) + '\n' + result += self.token_section_end + '\n' + s = result + if node.comment: + s = ''.join(self.token_comment + ' %s\n' % l for l in node.comment.splitlines()) + s + return s diff --git a/reconfigure/parsers/nsd.py b/reconfigure/parsers/nsd.py new file mode 100644 index 0000000..a1c0522 --- /dev/null +++ b/reconfigure/parsers/nsd.py @@ -0,0 +1,50 @@ +from reconfigure.nodes import * +from reconfigure.parsers import BaseParser + + +class NSDParser (BaseParser): + """ + A parser for NSD DNS server nsd.conf file + """ + + def parse(self, content): + lines = content.splitlines() + root = RootNode() + last_comment = None + node = root + for line in lines: + line = line.strip() + if line: + if line.startswith('#'): + c = line.strip('#').strip() + if last_comment: + last_comment += '\n' + c + else: + last_comment = c + continue + + key, value = line.split(':') + value = value.strip() + key = key.strip() + if key in ['server', 'zone', 'key']: + node = Node(key, comment=last_comment) + root.append(node) + else: + node.append(PropertyNode(key, value, comment=last_comment)) + last_comment = None + return root + + def stringify_comment(self, line, comment): + if comment: + return ''.join('# %s\n' % x for x in comment.splitlines()) + line + return line + + def stringify(self, tree): + r = '' + for node in tree.children: + r += self.stringify_comment(node.name + ':', node.comment) + '\n' + for subnode in node.children: + l = '%s: %s' % (subnode.name, subnode.value) + r += self.stringify_comment(l, subnode.comment) + '\n' + r += '\n' + return r diff --git a/reconfigure/parsers/squid.py b/reconfigure/parsers/squid.py new file mode 100644 index 0000000..b9b4f97 --- /dev/null +++ b/reconfigure/parsers/squid.py @@ -0,0 +1,53 @@ +from reconfigure.nodes import * +from reconfigure.parsers import BaseParser + + +class SquidParser (BaseParser): + """ + A parser for Squid configs + """ + + def parse(self, content): + lines = filter(None, [x.strip() for x in content.splitlines()]) + root = RootNode() + last_comment = None + for line in lines: + line = line.strip() + if line.startswith('#'): + c = line.strip('#').strip() + if last_comment: + last_comment += '\n' + c + else: + last_comment = c + continue + if len(line) == 0: + continue + tokens = line.split() + node = Node('line', Node('arguments')) + if last_comment: + node.comment = last_comment + last_comment = None + + index = 0 + for token in tokens: + if token.startswith('#'): + node.comment = ' '.join(tokens[tokens.index(token):])[1:].strip() + break + if index == 0: + node.set_property('name', token) + else: + node.get('arguments').set_property(str(index), token) + index += 1 + root.append(node) + return root + + def stringify(self, tree): + r = '' + for node in tree.children: + if node.comment and '\n' in node.comment: + r += ''.join('%s %s\n' % ('#', x) for x in node.comment.splitlines()) + r += node.get('name').value + ' ' + ' '.join(x.value for x in node.get('arguments').children) + if node.comment and not '\n' in node.comment: + r += ' # %s' % node.comment + r += '\n' + return r diff --git a/reconfigure/parsers/ssv.py b/reconfigure/parsers/ssv.py new file mode 100644 index 0000000..2119a4a --- /dev/null +++ b/reconfigure/parsers/ssv.py @@ -0,0 +1,72 @@ +from reconfigure.nodes import * +from reconfigure.parsers import BaseParser + + +class SSVParser (BaseParser): + """ + A parser for files containing space-separated value (notably, ``/etc/fstab`` and friends) + + :param separator: separator character, defaults to whitespace + :param maxsplit: max number of tokens per line, defaults to infinity + :param comment: character denoting comments + :param continuation: line continuation character, None to disable + """ + + def __init__(self, separator=None, maxsplit=-1, comment='#', continuation=None, *args, **kwargs): + self.separator = separator + self.maxsplit = maxsplit + self.comment = comment + self.continuation = continuation + BaseParser.__init__(self, *args, **kwargs) + + def parse(self, content): + rawlines = content.splitlines() + lines = [] + while rawlines: + l = rawlines.pop(0).strip() + while self.continuation and rawlines and l.endswith(self.continuation): + l = l[:-len(self.continuation)] + l += rawlines.pop(0) + lines.append(l) + root = RootNode() + last_comment = None + for line in lines: + line = line.strip() + if line: + if line.startswith(self.comment): + c = line.strip(self.comment).strip() + if last_comment: + last_comment += '\n' + c + else: + last_comment = c + continue + if len(line) == 0: + continue + tokens = line.split(self.separator, self.maxsplit) + node = Node('line') + if last_comment: + node.comment = last_comment + last_comment = None + for token in tokens: + if token.startswith(self.comment): + node.comment = ' '.join(tokens[tokens.index(token):])[1:].strip() + break + node.append(Node( + name='token', + children=[ + PropertyNode(name='value', value=token) + ] + )) + root.append(node) + return root + + def stringify(self, tree): + r = '' + for node in tree.children: + if node.comment and '\n' in node.comment: + r += ''.join('%s %s\n' % (self.comment, x) for x in node.comment.splitlines()) + r += (self.separator or '\t').join(x.get('value').value for x in node.children) + if node.comment and not '\n' in node.comment: + r += ' # %s' % node.comment + r += '\n' + return r |