#!/bin/env python3 import configparser from configparser import RawConfigParser import argparse import re import sys class SystemdUnitParser(RawConfigParser): """ConfigParser allowing duplicate keys. Values are stored in a list""" def __init__(self): RawConfigParser.__init__(self) self.optionxform = lambda option: option # self._inline_comment_prefixes = ['#'] # self._comment_prefixes = ['#'] self._empty_lines_in_values = False def _read(self, fp, fpname): """Parse a sectioned configuration file. Each section in a configuration file contains a header, indicated by a name in square brackets (`[]'), plus key/value options, indicated by `name' and `value' delimited with a specific substring (`=' or `:' by default). Values can span multiple lines, as long as they are indented deeper than the first line of the value. Depending on the parser's mode, blank lines may be treated as parts of multiline values or ignored. Configuration files may include comments, prefixed by specific characters (`#' and `;' by default). Comments may appear on their own in an otherwise empty line or may be entered in lines holding values or section names. """ elements_added = set() cursect = None # None, or a dictionary sectname = None optname = None lineno = 0 indent_level = 0 e = None # None, or an exception for lineno, line in enumerate(fp, start=1): comment_start = sys.maxsize # strip inline comments inline_prefixes = {p: -1 for p in self._inline_comment_prefixes} while comment_start == sys.maxsize and inline_prefixes: next_prefixes = {} for prefix, index in inline_prefixes.items(): index = line.find(prefix, index + 1) if index == -1: continue next_prefixes[prefix] = index if index == 0 or (index > 0 and line[index - 1].isspace()): comment_start = min(comment_start, index) inline_prefixes = next_prefixes # strip full line comments for prefix in self._comment_prefixes: if line.strip().startswith(prefix): comment_start = 0 break if comment_start == sys.maxsize: comment_start = None value = line[:comment_start].strip() if not value: if self._empty_lines_in_values: # add empty line to the value, but only if there was no # comment on the line if (comment_start is None and cursect is not None and optname and cursect[optname] is not None): cursect[optname].append('') # newlines added at join else: # empty line marks end of value indent_level = sys.maxsize continue # continuation line? first_nonspace = self.NONSPACECRE.search(line) cur_indent_level = first_nonspace.start() if first_nonspace else 0 if (cursect is not None and optname and cur_indent_level > indent_level): cursect[optname].append(value) # a section header or option header? else: indent_level = cur_indent_level # is it a section header? mo = self.SECTCRE.match(value) if mo: sectname = mo.group('header') if sectname in self._sections: cursect = self._sections[sectname] elements_added.add(sectname) elif sectname == self.default_section: cursect = self._defaults else: cursect = self._dict() self._sections[sectname] = cursect self._proxies[sectname] = configparser.SectionProxy(self, sectname) elements_added.add(sectname) # So sections can't start with a continuation line optname = None # no section header in the file? elif cursect is None: raise configparser.MissingSectionHeaderError(fpname, lineno, line) # an option line? else: mo = self._optcre.match(value) if mo: optname, vi, optval = mo.group('option', 'vi', 'value') if not optname: e = self._handle_error(e, fpname, lineno, line) optname = self.optionxform(optname.rstrip()) elements_added.add((sectname, optname)) # This check is fine because the OPTCRE cannot # match if it would set optval to None if optval is not None: optval = optval.strip() # Check if this optname already exists if (optname in cursect) and (cursect[optname] is not None): # If it does, convert it to a tuple if it isn't already one if not isinstance(cursect[optname], tuple): cursect[optname] = tuple(cursect[optname]) cursect[optname] = cursect[optname] + tuple([optval]) else: cursect[optname] = [optval] else: # valueless option handling cursect[optname] = None else: # a non-fatal parsing error occurred. set up the # exception but keep going. the exception will be # raised at the end of the file and will contain a # list of all bogus lines e = self._handle_error(e, fpname, lineno, line) # if any parsing errors occurred, raise an exception if e: raise e self._join_multiline_values() def _validate_value_types(self, *, section="", option="", value=""): """Raises a TypeError for non-string values. The only legal non-string value if we allow valueless options is None, so we need to check if the value is a string if: - we do not allow valueless options, or - we allow valueless options but the value is not Noneconfigparser.RawConfigParser For compatibility reasons this method is not used in classic set() for RawConfigParsers. It is invoked in every case for mapping protocol access and in ConfigParser.set(). """ if not isinstance(section, str): raise TypeError("section names must be strings") if not isinstance(option, str): raise TypeError("option keys must be strings") if not self._allow_no_value or value: if not isinstance(value, str) and not isinstance(value, tuple): raise TypeError("option values must be strings or a tuple of strings") # Write out duplicate keys with their values def _write_section(self, fp, section_name, section_items, delimiter): """Write a single section to the specified `fp'.""" fp.write("[{}]\n".format(section_name)) for key, vals in section_items: vals = self._interpolation.before_write(self, section_name, key, vals) if not isinstance(vals, tuple): vals = tuple([vals]) for value in vals: if value is not None or not self._allow_no_value: value = delimiter + str(value).replace('\n', '\n\t') else: value = "" fp.write("{}{}\n".format(key, value)) fp.write("\n") # Default to not creating spaces around the delimiter def write(self, fp, space_around_delimiters=False): configparser.RawConfigParser.write(self, fp, space_around_delimiters) def edit(args): cfg = SystemdUnitParser() cfg.read(args.file) section_re = re.compile(args.section_re) option_re = re.compile(args.option_re) value_search_re = re.compile(args.value_search) print("running command: {0}".format(args.command)) for section_name in cfg.sections(): if section_re.match(section_name): for option in cfg.options(section_name): if option_re.match(option): value = cfg.get(section_name, option) if isinstance(value, tuple): value_list = value else: value_list = [value, ] new_value_list = [] for v in value_list: try: if value_search_re.match(v): new_value = value_search_re.sub(args.value_replace, v) print("{0} -> {1}".format(v, new_value)) else: new_value = v except TypeError as e: print("value '{0}' is giving us trouble".formt(str(v))) raise e new_value_list.append(new_value) cfg.set(section_name, option, tuple(new_value_list)) return cfg def add(args): cfg = SystemdUnitParser() cfg.read(args.file) cfg.set(args.section, args.option, args.value) return cfg def mutate(args): cfg = SystemdUnitParser() cfg.read(args.file) section_re = re.compile(args.section_re) option_re = re.compile(args.option_re) value_search_re = re.compile(args.value_search) value_backref = None print("running command: {0}".format(args.command)) for section_name in cfg.sections(): if section_re.match(section_name): for option in cfg.options(section_name): if option_re.match(option): value = cfg.get(section_name, option) if isinstance(value, tuple): value_list = value else: value_list = [value, ] for v in value_list: try: print(v) value_backref = value_search_re.search(v) if value_backref: print('got backreference {0}'.format(str(value_backref))) break except TypeError as e: print("value '{0}' is giving us trouble".formt(str(v))) raise e if value_backref: print(str(value_backref)) groups = value_backref.groups() mutated_value = args.value_replace_str.format(*groups) print(mutated_value) # print("{0} -> {1}".format(cfg.get(args.section, args.option), mutated_value)) cfg.set(args.section, args.option, mutated_value) return cfg else: return None if __name__ == '__main__': parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest='command') command_parser = subparsers.add_parser('edit') command_parser.add_argument('section_re') command_parser.add_argument('option_re') command_parser.add_argument('value_search') command_parser.add_argument('value_replace') command_parser = subparsers.add_parser('add') command_parser.add_argument('section') command_parser.add_argument('option') command_parser.add_argument('value') command_parser = subparsers.add_parser('mutate') command_parser.add_argument('section_re') command_parser.add_argument('option_re') command_parser.add_argument('value_search') command_parser.add_argument('section') command_parser.add_argument('option') command_parser.add_argument('value_replace_str') parser.add_argument('file') # parser.add_argument('out_file', default=None) args = parser.parse_args() cfg = None if args.command == 'edit': cfg = edit(args) elif args.command == 'add': cfg = add(args) elif args.command == 'mutate': cfg = mutate(args) if cfg: with open(args.file, 'w') as out_file: cfg.write(out_file)