Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
#!/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)