salt/utils/configparser.py
# -*- coding: utf-8 -*-
'''
Custom configparser classes
'''
# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals
import re
# Import Salt libs
import salt.utils.stringutils
# Import 3rd-party libs
from salt.ext import six
from salt.ext.six.moves.configparser import * # pylint: disable=no-name-in-module,wildcard-import
try:
from collections import OrderedDict as _default_dict
except ImportError:
# fallback for setup.py which hasn't yet built _collections
_default_dict = dict
# pylint: disable=string-substitution-usage-error
class GitConfigParser(RawConfigParser, object): # pylint: disable=undefined-variable
'''
Custom ConfigParser which reads and writes git config files.
READ A GIT CONFIG FILE INTO THE PARSER OBJECT
>>> import salt.utils.configparser
>>> conf = salt.utils.configparser.GitConfigParser()
>>> conf.read('/home/user/.git/config')
MAKE SOME CHANGES
>>> # Change user.email
>>> conf.set('user', 'email', 'myaddress@mydomain.tld')
>>> # Add another refspec to the "origin" remote's "fetch" multivar
>>> conf.set_multivar('remote "origin"', 'fetch', '+refs/tags/*:refs/tags/*')
WRITE THE CONFIG TO A FILEHANDLE
>>> import salt.utils.files
>>> with salt.utils.files.fopen('/home/user/.git/config', 'w') as fh:
... conf.write(fh)
>>>
'''
DEFAULTSECT = 'DEFAULT'
SPACEINDENT = ' ' * 8
def __init__(self, defaults=None, dict_type=_default_dict, allow_no_value=True):
'''
Changes default value for allow_no_value from False to True
'''
super(GitConfigParser, self).__init__(defaults, dict_type, allow_no_value)
def _read(self, fp, fpname):
'''
Makes the following changes from the RawConfigParser:
1. Strip leading tabs from non-section-header lines.
2. Treat 8 spaces at the beginning of a line as a tab.
3. Treat lines beginning with a tab as options.
4. Drops support for continuation lines.
5. Multiple values for a given option are stored as a list.
6. Keys and values are decoded to the system encoding.
'''
cursect = None # None, or a dictionary
optname = None
lineno = 0
e = None # None, or an exception
while True:
line = salt.utils.stringutils.to_unicode(fp.readline())
if not line:
break
lineno = lineno + 1
# comment or blank line?
if line.strip() == '' or line[0] in '#;':
continue
if line.split(None, 1)[0].lower() == 'rem' and line[0] in 'rR':
# no leading whitespace
continue
# Replace space indentation with a tab. Allows parser to work
# properly in cases where someone has edited the git config by hand
# and indented using spaces instead of tabs.
if line.startswith(self.SPACEINDENT):
line = '\t' + line[len(self.SPACEINDENT):]
# is it a section header?
mo = self.SECTCRE.match(line)
if mo:
sectname = mo.group('header')
if sectname in self._sections:
cursect = self._sections[sectname]
elif sectname == self.DEFAULTSECT:
cursect = self._defaults
else:
cursect = self._dict()
self._sections[sectname] = cursect
# So sections can't start with a continuation line
optname = None
# no section header in the file?
elif cursect is None:
raise MissingSectionHeaderError( # pylint: disable=undefined-variable
salt.utils.stringutils.to_str(fpname),
lineno,
salt.utils.stringutils.to_str(line))
# an option line?
else:
mo = self._optcre.match(line.lstrip())
if mo:
optname, vi, optval = mo.group('option', 'vi', 'value')
optname = self.optionxform(optname.rstrip())
if optval is None:
optval = ''
if optval:
if vi in ('=', ':') and ';' in optval:
# ';' is a comment delimiter only if it follows
# a spacing character
pos = optval.find(';')
if pos != -1 and optval[pos-1].isspace():
optval = optval[:pos]
optval = optval.strip()
# Empty strings should be considered as blank strings
if optval in ('""', "''"):
optval = ''
self._add_option(cursect, optname, optval)
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
if not e:
e = ParsingError(fpname) # pylint: disable=undefined-variable
e.append(lineno, repr(line))
# if any parsing errors occurred, raise an exception
if e:
raise e # pylint: disable=raising-bad-type
def _string_check(self, value, allow_list=False):
'''
Based on the string-checking code from the SafeConfigParser's set()
function, this enforces string values for config options.
'''
if self._optcre is self.OPTCRE or value:
is_list = isinstance(value, list)
if is_list and not allow_list:
raise TypeError('option value cannot be a list unless allow_list is True')
elif not is_list:
value = [value]
if not all(isinstance(x, six.string_types) for x in value):
raise TypeError('option values must be strings')
def get(self, section, option, as_list=False):
'''
Adds an optional "as_list" argument to ensure a list is returned. This
is helpful when iterating over an option which may or may not be a
multivar.
'''
ret = super(GitConfigParser, self).get(section, option)
if as_list and not isinstance(ret, list):
ret = [ret]
return ret
def set(self, section, option, value=''):
'''
This is overridden from the RawConfigParser merely to change the
default value for the 'value' argument.
'''
self._string_check(value)
super(GitConfigParser, self).set(section, option, value)
def _add_option(self, sectdict, key, value):
if isinstance(value, list):
sectdict[key] = value
elif isinstance(value, six.string_types):
try:
sectdict[key].append(value)
except KeyError:
# Key not present, set it
sectdict[key] = value
except AttributeError:
# Key is present but the value is not a list. Make it into a list
# and then append to it.
sectdict[key] = [sectdict[key]]
sectdict[key].append(value)
else:
raise TypeError('Expected str or list for option value, got %s' % type(value).__name__)
def set_multivar(self, section, option, value=''):
'''
This function is unique to the GitConfigParser. It will add another
value for the option if it already exists, converting the option's
value to a list if applicable.
If "value" is a list, then any existing values for the specified
section and option will be replaced with the list being passed.
'''
self._string_check(value, allow_list=True)
if not section or section == self.DEFAULTSECT:
sectdict = self._defaults
else:
try:
sectdict = self._sections[section]
except KeyError:
raise NoSectionError( # pylint: disable=undefined-variable
salt.utils.stringutils.to_str(section))
key = self.optionxform(option)
self._add_option(sectdict, key, value)
def remove_option_regexp(self, section, option, expr):
'''
Remove an option with a value matching the expression. Works on single
values and multivars.
'''
if not section or section == self.DEFAULTSECT:
sectdict = self._defaults
else:
try:
sectdict = self._sections[section]
except KeyError:
raise NoSectionError( # pylint: disable=undefined-variable
salt.utils.stringutils.to_str(section))
option = self.optionxform(option)
if option not in sectdict:
return False
regexp = re.compile(expr)
if isinstance(sectdict[option], list):
new_list = [x for x in sectdict[option] if not regexp.search(x)]
# Revert back to a list if we removed all but one item
if len(new_list) == 1:
new_list = new_list[0]
existed = new_list != sectdict[option]
if existed:
del sectdict[option]
sectdict[option] = new_list
del new_list
else:
existed = bool(regexp.search(sectdict[option]))
if existed:
del sectdict[option]
return existed
def write(self, fp_):
'''
Makes the following changes from the RawConfigParser:
1. Prepends options with a tab character.
2. Does not write a blank line between sections.
3. When an option's value is a list, a line for each option is written.
This allows us to support multivars like a remote's "fetch" option.
4. Drops support for continuation lines.
'''
convert = salt.utils.stringutils.to_bytes \
if 'b' in fp_.mode \
else salt.utils.stringutils.to_str
if self._defaults:
fp_.write(convert('[%s]\n' % self.DEFAULTSECT))
for (key, value) in six.iteritems(self._defaults):
value = salt.utils.stringutils.to_unicode(value).replace('\n', '\n\t')
fp_.write(convert('%s = %s\n' % (key, value)))
for section in self._sections:
fp_.write(convert('[%s]\n' % section))
for (key, value) in six.iteritems(self._sections[section]):
if (value is not None) or (self._optcre == self.OPTCRE):
if not isinstance(value, list):
value = [value]
for item in value:
fp_.write(convert('\t%s\n' % ' = '.join((key, item)).rstrip()))