1""" 2Custom configparser classes 3""" 4 5import re 6from configparser import * # pylint: disable=no-name-in-module,wildcard-import,unused-wildcard-import 7 8import salt.utils.stringutils 9 10try: 11 from collections import OrderedDict as _default_dict 12except ImportError: 13 # fallback for setup.py which hasn't yet built _collections 14 _default_dict = dict 15 16 17# pylint: disable=string-substitution-usage-error 18class GitConfigParser(RawConfigParser): 19 """ 20 Custom ConfigParser which reads and writes git config files. 21 22 READ A GIT CONFIG FILE INTO THE PARSER OBJECT 23 24 >>> import salt.utils.configparser 25 >>> conf = salt.utils.configparser.GitConfigParser() 26 >>> conf.read('/home/user/.git/config') 27 28 MAKE SOME CHANGES 29 30 >>> # Change user.email 31 >>> conf.set('user', 'email', 'myaddress@mydomain.tld') 32 >>> # Add another refspec to the "origin" remote's "fetch" multivar 33 >>> conf.set_multivar('remote "origin"', 'fetch', '+refs/tags/*:refs/tags/*') 34 35 WRITE THE CONFIG TO A FILEHANDLE 36 37 >>> import salt.utils.files 38 >>> with salt.utils.files.fopen('/home/user/.git/config', 'w') as fh: 39 ... conf.write(fh) 40 >>> 41 """ 42 43 DEFAULTSECT = "DEFAULT" 44 SPACEINDENT = " " * 8 45 46 # pylint: disable=useless-super-delegation 47 def __init__( 48 self, 49 defaults=None, 50 dict_type=_default_dict, 51 allow_no_value=True, 52 ): 53 """ 54 Changes default value for allow_no_value from False to True 55 """ 56 super().__init__(defaults, dict_type, allow_no_value) 57 58 # pylint: enable=useless-super-delegation 59 60 def _read(self, fp, fpname): 61 """ 62 Makes the following changes from the RawConfigParser: 63 64 1. Strip leading tabs from non-section-header lines. 65 2. Treat 8 spaces at the beginning of a line as a tab. 66 3. Treat lines beginning with a tab as options. 67 4. Drops support for continuation lines. 68 5. Multiple values for a given option are stored as a list. 69 6. Keys and values are decoded to the system encoding. 70 """ 71 cursect = None # None, or a dictionary 72 optname = None 73 lineno = 0 74 e = None # None, or an exception 75 while True: 76 line = salt.utils.stringutils.to_unicode(fp.readline()) 77 if not line: 78 break 79 lineno = lineno + 1 80 # comment or blank line? 81 if line.strip() == "" or line[0] in "#;": 82 continue 83 if line.split(None, 1)[0].lower() == "rem" and line[0] in "rR": 84 # no leading whitespace 85 continue 86 # Replace space indentation with a tab. Allows parser to work 87 # properly in cases where someone has edited the git config by hand 88 # and indented using spaces instead of tabs. 89 if line.startswith(self.SPACEINDENT): 90 line = "\t" + line[len(self.SPACEINDENT) :] 91 # is it a section header? 92 mo = self.SECTCRE.match(line) 93 if mo: 94 sectname = mo.group("header") 95 if sectname in self._sections: 96 cursect = self._sections[sectname] 97 elif sectname == self.DEFAULTSECT: 98 cursect = self._defaults 99 else: 100 cursect = self._dict() 101 self._sections[sectname] = cursect 102 # So sections can't start with a continuation line 103 optname = None 104 # no section header in the file? 105 elif cursect is None: 106 raise MissingSectionHeaderError( # pylint: disable=undefined-variable 107 salt.utils.stringutils.to_str(fpname), 108 lineno, 109 salt.utils.stringutils.to_str(line), 110 ) 111 # an option line? 112 else: 113 mo = self._optcre.match(line.lstrip()) 114 if mo: 115 optname, vi, optval = mo.group("option", "vi", "value") 116 optname = self.optionxform(optname.rstrip()) 117 if optval is None: 118 optval = "" 119 if optval: 120 if vi in ("=", ":") and ";" in optval: 121 # ';' is a comment delimiter only if it follows 122 # a spacing character 123 pos = optval.find(";") 124 if pos != -1 and optval[pos - 1].isspace(): 125 optval = optval[:pos] 126 optval = optval.strip() 127 # Empty strings should be considered as blank strings 128 if optval in ('""', "''"): 129 optval = "" 130 self._add_option(cursect, optname, optval) 131 else: 132 # a non-fatal parsing error occurred. set up the 133 # exception but keep going. the exception will be 134 # raised at the end of the file and will contain a 135 # list of all bogus lines 136 if not e: 137 e = ParsingError(fpname) # pylint: disable=undefined-variable 138 e.append(lineno, repr(line)) 139 # if any parsing errors occurred, raise an exception 140 if e: 141 raise e # pylint: disable=raising-bad-type 142 143 def _string_check(self, value, allow_list=False): 144 """ 145 Based on the string-checking code from the SafeConfigParser's set() 146 function, this enforces string values for config options. 147 """ 148 if self._optcre is self.OPTCRE or value: 149 is_list = isinstance(value, list) 150 if is_list and not allow_list: 151 raise TypeError( 152 "option value cannot be a list unless allow_list is True" 153 ) 154 elif not is_list: 155 value = [value] 156 if not all(isinstance(x, str) for x in value): 157 raise TypeError("option values must be strings") 158 159 def get(self, section, option, as_list=False): # pylint: disable=arguments-differ 160 """ 161 Adds an optional "as_list" argument to ensure a list is returned. This 162 is helpful when iterating over an option which may or may not be a 163 multivar. 164 """ 165 ret = super().get(section, option) 166 if as_list and not isinstance(ret, list): 167 ret = [ret] 168 return ret 169 170 def set(self, section, option, value=""): 171 """ 172 This is overridden from the RawConfigParser merely to change the 173 default value for the 'value' argument. 174 """ 175 self._string_check(value) 176 super().set(section, option, value) 177 178 def _add_option(self, sectdict, key, value): 179 if isinstance(value, list): 180 sectdict[key] = value 181 elif isinstance(value, str): 182 try: 183 sectdict[key].append(value) 184 except KeyError: 185 # Key not present, set it 186 sectdict[key] = value 187 except AttributeError: 188 # Key is present but the value is not a list. Make it into a list 189 # and then append to it. 190 sectdict[key] = [sectdict[key]] 191 sectdict[key].append(value) 192 else: 193 raise TypeError( 194 "Expected str or list for option value, got %s" % type(value).__name__ 195 ) 196 197 def set_multivar(self, section, option, value=""): 198 """ 199 This function is unique to the GitConfigParser. It will add another 200 value for the option if it already exists, converting the option's 201 value to a list if applicable. 202 203 If "value" is a list, then any existing values for the specified 204 section and option will be replaced with the list being passed. 205 """ 206 self._string_check(value, allow_list=True) 207 if not section or section == self.DEFAULTSECT: 208 sectdict = self._defaults 209 else: 210 try: 211 sectdict = self._sections[section] 212 except KeyError: 213 raise NoSectionError( # pylint: disable=undefined-variable 214 salt.utils.stringutils.to_str(section) 215 ) 216 key = self.optionxform(option) 217 self._add_option(sectdict, key, value) 218 219 def remove_option_regexp(self, section, option, expr): 220 """ 221 Remove an option with a value matching the expression. Works on single 222 values and multivars. 223 """ 224 if not section or section == self.DEFAULTSECT: 225 sectdict = self._defaults 226 else: 227 try: 228 sectdict = self._sections[section] 229 except KeyError: 230 raise NoSectionError( # pylint: disable=undefined-variable 231 salt.utils.stringutils.to_str(section) 232 ) 233 option = self.optionxform(option) 234 if option not in sectdict: 235 return False 236 regexp = re.compile(expr) 237 if isinstance(sectdict[option], list): 238 new_list = [x for x in sectdict[option] if not regexp.search(x)] 239 # Revert back to a list if we removed all but one item 240 if len(new_list) == 1: 241 new_list = new_list[0] 242 existed = new_list != sectdict[option] 243 if existed: 244 del sectdict[option] 245 sectdict[option] = new_list 246 del new_list 247 else: 248 existed = bool(regexp.search(sectdict[option])) 249 if existed: 250 del sectdict[option] 251 return existed 252 253 def write(self, fp_): # pylint: disable=arguments-differ 254 """ 255 Makes the following changes from the RawConfigParser: 256 257 1. Prepends options with a tab character. 258 2. Does not write a blank line between sections. 259 3. When an option's value is a list, a line for each option is written. 260 This allows us to support multivars like a remote's "fetch" option. 261 4. Drops support for continuation lines. 262 """ 263 convert = ( 264 salt.utils.stringutils.to_bytes 265 if "b" in fp_.mode 266 else salt.utils.stringutils.to_str 267 ) 268 if self._defaults: 269 fp_.write(convert("[%s]\n" % self.DEFAULTSECT)) 270 for (key, value) in self._defaults.items(): 271 value = salt.utils.stringutils.to_unicode(value).replace("\n", "\n\t") 272 fp_.write(convert("{} = {}\n".format(key, value))) 273 for section in self._sections: 274 fp_.write(convert("[%s]\n" % section)) 275 for (key, value) in self._sections[section].items(): 276 if (value is not None) or (self._optcre == self.OPTCRE): 277 if not isinstance(value, list): 278 value = [value] 279 for item in value: 280 fp_.write(convert("\t%s\n" % " = ".join((key, item)).rstrip())) 281