Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/customizations/configure/writer.py
1567 views
1
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"). You
4
# may not use this file except in compliance with the License. A copy of
5
# the License is located at
6
#
7
# http://aws.amazon.com/apache2.0/
8
#
9
# or in the "license" file accompanying this file. This file is
10
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
# ANY KIND, either express or implied. See the License for the specific
12
# language governing permissions and limitations under the License.
13
import os
14
import re
15
16
from . import SectionNotFoundError
17
18
19
class ConfigFileWriter(object):
20
SECTION_REGEX = re.compile(r'^\s*\[(?P<header>[^]]+)\]')
21
OPTION_REGEX = re.compile(
22
r'(?P<option>[^:=][^:=]*)'
23
r'\s*(?P<vi>[:=])\s*'
24
r'(?P<value>.*)$'
25
)
26
27
def update_config(self, new_values, config_filename):
28
"""Update config file with new values.
29
30
This method will update a section in a config file with
31
new key value pairs.
32
33
This method provides a few conveniences:
34
35
* If the ``config_filename`` does not exist, it will
36
be created. Any parent directories will also be created
37
if necessary.
38
* If the section to update does not exist, it will be created.
39
* Any existing lines that are specified by ``new_values``
40
**will not be touched**. This ensures that commented out
41
values are left unaltered.
42
43
:type new_values: dict
44
:param new_values: The values to update. There is a special
45
key ``__section__``, that specifies what section in the INI
46
file to update. If this key is not present, then the
47
``default`` section will be updated with the new values.
48
49
:type config_filename: str
50
:param config_filename: The config filename where values will be
51
written.
52
53
"""
54
section_name = new_values.pop('__section__', 'default')
55
if not os.path.isfile(config_filename):
56
self._create_file(config_filename)
57
self._write_new_section(section_name, new_values, config_filename)
58
return
59
with open(config_filename, 'r') as f:
60
contents = f.readlines()
61
# We can only update a single section at a time so we first need
62
# to find the section in question
63
try:
64
self._update_section_contents(contents, section_name, new_values)
65
with open(config_filename, 'w') as f:
66
f.write(''.join(contents))
67
except SectionNotFoundError:
68
self._write_new_section(section_name, new_values, config_filename)
69
70
def _create_file(self, config_filename):
71
# Create the file as well as the parent dir if needed.
72
dirname = os.path.split(config_filename)[0]
73
if not os.path.isdir(dirname):
74
os.makedirs(dirname)
75
with os.fdopen(os.open(config_filename,
76
os.O_WRONLY | os.O_CREAT, 0o600), 'w'):
77
pass
78
79
def _check_file_needs_newline(self, filename):
80
# check if the last byte is a newline
81
with open(filename, 'rb') as f:
82
# check if the file is empty
83
f.seek(0, os.SEEK_END)
84
if not f.tell():
85
return False
86
f.seek(-1, os.SEEK_END)
87
last = f.read()
88
return last != b'\n'
89
90
def _write_new_section(self, section_name, new_values, config_filename):
91
needs_newline = self._check_file_needs_newline(config_filename)
92
with open(config_filename, 'a') as f:
93
if needs_newline:
94
f.write('\n')
95
f.write('[%s]\n' % section_name)
96
contents = []
97
self._insert_new_values(line_number=0,
98
contents=contents,
99
new_values=new_values)
100
f.write(''.join(contents))
101
102
def _find_section_start(self, contents, section_name):
103
for i in range(len(contents)):
104
line = contents[i]
105
if line.strip().startswith(('#', ';')):
106
# This is a comment, so we can safely ignore this line.
107
continue
108
match = self.SECTION_REGEX.search(line)
109
if match is not None and self._matches_section(match,
110
section_name):
111
return i
112
raise SectionNotFoundError(section_name)
113
114
def _update_section_contents(self, contents, section_name, new_values):
115
# First, find the line where the section_name is defined.
116
# This will be the value of i.
117
new_values = new_values.copy()
118
# ``contents`` is a list of file line contents.
119
section_start_line_num = self._find_section_start(contents,
120
section_name)
121
# If we get here, then we've found the section. We now need
122
# to figure out if we're updating a value or adding a new value.
123
# There's 2 cases. Either we're setting a normal scalar value
124
# of, we're setting a nested value.
125
last_matching_line = section_start_line_num
126
j = last_matching_line + 1
127
while j < len(contents):
128
line = contents[j]
129
if self.SECTION_REGEX.search(line) is not None:
130
# We've hit a new section which means the config key is
131
# not in the section. We need to add it here.
132
self._insert_new_values(line_number=last_matching_line,
133
contents=contents,
134
new_values=new_values)
135
return
136
match = self.OPTION_REGEX.search(line)
137
if match is not None:
138
last_matching_line = j
139
key_name = match.group(1).strip()
140
if key_name in new_values:
141
# We've found the line that defines the option name.
142
# if the value is not a dict, then we can write the line
143
# out now.
144
if not isinstance(new_values[key_name], dict):
145
option_value = new_values[key_name]
146
new_line = '%s = %s\n' % (key_name, option_value)
147
contents[j] = new_line
148
del new_values[key_name]
149
else:
150
j = self._update_subattributes(
151
j, contents, new_values[key_name],
152
len(match.group(1)) - len(match.group(1).lstrip()))
153
return
154
j += 1
155
156
if new_values:
157
if not contents[-1].endswith('\n'):
158
contents.append('\n')
159
self._insert_new_values(line_number=last_matching_line + 1,
160
contents=contents,
161
new_values=new_values)
162
163
def _update_subattributes(self, index, contents, values, starting_indent):
164
index += 1
165
for i in range(index, len(contents)):
166
line = contents[i]
167
match = self.OPTION_REGEX.search(line)
168
if match is not None:
169
current_indent = len(
170
match.group(1)) - len(match.group(1).lstrip())
171
key_name = match.group(1).strip()
172
if key_name in values:
173
option_value = values[key_name]
174
new_line = '%s%s = %s\n' % (' ' * current_indent,
175
key_name, option_value)
176
contents[i] = new_line
177
del values[key_name]
178
if starting_indent == current_indent or \
179
self.SECTION_REGEX.search(line) is not None:
180
# We've arrived at the starting indent level so we can just
181
# write out all the values now.
182
self._insert_new_values(i - 1, contents, values, ' ')
183
break
184
else:
185
if starting_indent != current_indent:
186
# The option is the last option in the file
187
self._insert_new_values(i, contents, values, ' ')
188
return i
189
190
def _insert_new_values(self, line_number, contents, new_values, indent=''):
191
new_contents = []
192
for key, value in list(new_values.items()):
193
if isinstance(value, dict):
194
subindent = indent + ' '
195
new_contents.append('%s%s =\n' % (indent, key))
196
for subkey, subval in list(value.items()):
197
new_contents.append('%s%s = %s\n' % (subindent, subkey,
198
subval))
199
else:
200
new_contents.append('%s%s = %s\n' % (indent, key, value))
201
del new_values[key]
202
contents.insert(line_number + 1, ''.join(new_contents))
203
204
def _matches_section(self, match, section_name):
205
parts = section_name.split(' ')
206
unquoted_match = match.group(0) == '[%s]' % section_name
207
if len(parts) > 1:
208
quoted_match = match.group(0) == '[%s "%s"]' % (
209
parts[0], ' '.join(parts[1:]))
210
return unquoted_match or quoted_match
211
return unquoted_match
212
213