Path: blob/develop/awscli/customizations/configure/writer.py
1567 views
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.1#2# Licensed under the Apache License, Version 2.0 (the "License"). You3# may not use this file except in compliance with the License. A copy of4# the License is located at5#6# http://aws.amazon.com/apache2.0/7#8# or in the "license" file accompanying this file. This file is9# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF10# ANY KIND, either express or implied. See the License for the specific11# language governing permissions and limitations under the License.12import os13import re1415from . import SectionNotFoundError161718class ConfigFileWriter(object):19SECTION_REGEX = re.compile(r'^\s*\[(?P<header>[^]]+)\]')20OPTION_REGEX = re.compile(21r'(?P<option>[^:=][^:=]*)'22r'\s*(?P<vi>[:=])\s*'23r'(?P<value>.*)$'24)2526def update_config(self, new_values, config_filename):27"""Update config file with new values.2829This method will update a section in a config file with30new key value pairs.3132This method provides a few conveniences:3334* If the ``config_filename`` does not exist, it will35be created. Any parent directories will also be created36if necessary.37* If the section to update does not exist, it will be created.38* Any existing lines that are specified by ``new_values``39**will not be touched**. This ensures that commented out40values are left unaltered.4142:type new_values: dict43:param new_values: The values to update. There is a special44key ``__section__``, that specifies what section in the INI45file to update. If this key is not present, then the46``default`` section will be updated with the new values.4748:type config_filename: str49:param config_filename: The config filename where values will be50written.5152"""53section_name = new_values.pop('__section__', 'default')54if not os.path.isfile(config_filename):55self._create_file(config_filename)56self._write_new_section(section_name, new_values, config_filename)57return58with open(config_filename, 'r') as f:59contents = f.readlines()60# We can only update a single section at a time so we first need61# to find the section in question62try:63self._update_section_contents(contents, section_name, new_values)64with open(config_filename, 'w') as f:65f.write(''.join(contents))66except SectionNotFoundError:67self._write_new_section(section_name, new_values, config_filename)6869def _create_file(self, config_filename):70# Create the file as well as the parent dir if needed.71dirname = os.path.split(config_filename)[0]72if not os.path.isdir(dirname):73os.makedirs(dirname)74with os.fdopen(os.open(config_filename,75os.O_WRONLY | os.O_CREAT, 0o600), 'w'):76pass7778def _check_file_needs_newline(self, filename):79# check if the last byte is a newline80with open(filename, 'rb') as f:81# check if the file is empty82f.seek(0, os.SEEK_END)83if not f.tell():84return False85f.seek(-1, os.SEEK_END)86last = f.read()87return last != b'\n'8889def _write_new_section(self, section_name, new_values, config_filename):90needs_newline = self._check_file_needs_newline(config_filename)91with open(config_filename, 'a') as f:92if needs_newline:93f.write('\n')94f.write('[%s]\n' % section_name)95contents = []96self._insert_new_values(line_number=0,97contents=contents,98new_values=new_values)99f.write(''.join(contents))100101def _find_section_start(self, contents, section_name):102for i in range(len(contents)):103line = contents[i]104if line.strip().startswith(('#', ';')):105# This is a comment, so we can safely ignore this line.106continue107match = self.SECTION_REGEX.search(line)108if match is not None and self._matches_section(match,109section_name):110return i111raise SectionNotFoundError(section_name)112113def _update_section_contents(self, contents, section_name, new_values):114# First, find the line where the section_name is defined.115# This will be the value of i.116new_values = new_values.copy()117# ``contents`` is a list of file line contents.118section_start_line_num = self._find_section_start(contents,119section_name)120# If we get here, then we've found the section. We now need121# to figure out if we're updating a value or adding a new value.122# There's 2 cases. Either we're setting a normal scalar value123# of, we're setting a nested value.124last_matching_line = section_start_line_num125j = last_matching_line + 1126while j < len(contents):127line = contents[j]128if self.SECTION_REGEX.search(line) is not None:129# We've hit a new section which means the config key is130# not in the section. We need to add it here.131self._insert_new_values(line_number=last_matching_line,132contents=contents,133new_values=new_values)134return135match = self.OPTION_REGEX.search(line)136if match is not None:137last_matching_line = j138key_name = match.group(1).strip()139if key_name in new_values:140# We've found the line that defines the option name.141# if the value is not a dict, then we can write the line142# out now.143if not isinstance(new_values[key_name], dict):144option_value = new_values[key_name]145new_line = '%s = %s\n' % (key_name, option_value)146contents[j] = new_line147del new_values[key_name]148else:149j = self._update_subattributes(150j, contents, new_values[key_name],151len(match.group(1)) - len(match.group(1).lstrip()))152return153j += 1154155if new_values:156if not contents[-1].endswith('\n'):157contents.append('\n')158self._insert_new_values(line_number=last_matching_line + 1,159contents=contents,160new_values=new_values)161162def _update_subattributes(self, index, contents, values, starting_indent):163index += 1164for i in range(index, len(contents)):165line = contents[i]166match = self.OPTION_REGEX.search(line)167if match is not None:168current_indent = len(169match.group(1)) - len(match.group(1).lstrip())170key_name = match.group(1).strip()171if key_name in values:172option_value = values[key_name]173new_line = '%s%s = %s\n' % (' ' * current_indent,174key_name, option_value)175contents[i] = new_line176del values[key_name]177if starting_indent == current_indent or \178self.SECTION_REGEX.search(line) is not None:179# We've arrived at the starting indent level so we can just180# write out all the values now.181self._insert_new_values(i - 1, contents, values, ' ')182break183else:184if starting_indent != current_indent:185# The option is the last option in the file186self._insert_new_values(i, contents, values, ' ')187return i188189def _insert_new_values(self, line_number, contents, new_values, indent=''):190new_contents = []191for key, value in list(new_values.items()):192if isinstance(value, dict):193subindent = indent + ' '194new_contents.append('%s%s =\n' % (indent, key))195for subkey, subval in list(value.items()):196new_contents.append('%s%s = %s\n' % (subindent, subkey,197subval))198else:199new_contents.append('%s%s = %s\n' % (indent, key, value))200del new_values[key]201contents.insert(line_number + 1, ''.join(new_contents))202203def _matches_section(self, match, section_name):204parts = section_name.split(' ')205unquoted_match = match.group(0) == '[%s]' % section_name206if len(parts) > 1:207quoted_match = match.group(0) == '[%s "%s"]' % (208parts[0], ' '.join(parts[1:]))209return unquoted_match or quoted_match210return unquoted_match211212213