From b1afbc092b4e53bd24e556cd8ed68bfa321b6542 Mon Sep 17 00:00:00 2001 From: Kranthi Boyapati Date: Sun, 23 Aug 2026 01:33:05 -0700 Subject: [PATCH] Fix UnboundLocalError in nested config updates _update_subattributes() read ``current_indent`` on every line but only assigned it when OPTION_REGEX matched. Any non-option line directly after a nested key -- a comment, a blank line, or a section header -- was therefore read before assignment and raised UnboundLocalError. ``i`` had the same problem when the nested key was the last line in the file, leaving the for-else branch with no loop variable. Classify each line explicitly rather than carrying the indent of the previous match forward, and seed ``i`` so an empty loop range takes the same append path as running off the end of the file. Appending to a block at the end of a file with no trailing newline joins two lines together, so add the missing newline first. Without this the change above turns a crash into a silently corrupted config file. Fixes #10348 --- .../next-release/bugfix-configure-10348.json | 5 ++ awscli/customizations/configure/writer.py | 25 ++++--- .../customizations/configure/test_writer.py | 72 +++++++++++++++++++ 3 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 .changes/next-release/bugfix-configure-10348.json diff --git a/.changes/next-release/bugfix-configure-10348.json b/.changes/next-release/bugfix-configure-10348.json new file mode 100644 index 000000000000..e36e96862708 --- /dev/null +++ b/.changes/next-release/bugfix-configure-10348.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "configure", + "description": "Fixed an ``UnboundLocalError`` crash in ``aws configure set`` when a nested config block such as ``s3 =`` is empty, or is immediately followed by a comment, a blank line, or a new section header. Fixes `#10348 `__." +} diff --git a/awscli/customizations/configure/writer.py b/awscli/customizations/configure/writer.py index 83825a53dab6..7ac4f8862e4a 100644 --- a/awscli/customizations/configure/writer.py +++ b/awscli/customizations/configure/writer.py @@ -215,6 +215,8 @@ def _update_section_contents(self, contents, section_name, new_values): def _update_subattributes(self, index, contents, values, starting_indent): index += 1 + # Seeded in case there are no lines after the nested option at all. + i = index - 1 for i in range(index, len(contents)): line = contents[i] match = self.OPTION_REGEX.search(line) @@ -228,16 +230,21 @@ def _update_subattributes(self, index, contents, values, starting_indent): key_name, option_value) contents[i] = new_line del values[key_name] - if starting_indent == current_indent or \ - self.SECTION_REGEX.search(line) is not None: - # We've arrived at the starting indent level so we can just - # write out all the values now. - self._insert_new_values(i - 1, contents, values, ' ') - break + if current_indent != starting_indent: + # We're still inside the nested block. + continue + elif self.SECTION_REGEX.search(line) is None: + # Comments, blank lines and the like don't end the block. + continue + # We've arrived at the starting indent level so we can just + # write out all the values now. + self._insert_new_values(i - 1, contents, values, ' ') + break else: - if starting_indent != current_indent: - # The option is the last option in the file - self._insert_new_values(i, contents, values, ' ') + # The option is the last option in the file + if values and not contents[-1].endswith('\n'): + contents[-1] += '\n' + self._insert_new_values(i, contents, values, ' ') return i def _insert_new_values(self, line_number, contents, new_values, indent=''): diff --git a/tests/unit/customizations/configure/test_writer.py b/tests/unit/customizations/configure/test_writer.py index 9c85516762ef..8a7a0d4e33f0 100644 --- a/tests/unit/customizations/configure/test_writer.py +++ b/tests/unit/customizations/configure/test_writer.py @@ -326,6 +326,78 @@ def test_updated_nested_attribute_new_section(self): '[profile foo]\n' 'foo = bar\n') + def test_add_to_empty_nested_at_end_of_file(self): + original = ( + '[default]\n' + 's3 =\n' + ) + self.assert_update_config( + original, {'__section__': 'default', + 's3': {'signature_version': 'newval'}}, + '[default]\n' + 's3 =\n' + ' signature_version = newval\n') + + def test_add_to_nested_with_comment_after_nested_key(self): + original = ( + '[default]\n' + 's3 =\n' + '# a comment\n' + ' other = foo\n' + ) + self.assert_update_config( + original, {'__section__': 'default', + 's3': {'signature_version': 'newval'}}, + '[default]\n' + 's3 =\n' + '# a comment\n' + ' other = foo\n' + ' signature_version = newval\n') + + def test_add_to_nested_with_new_section_after_nested_key(self): + original = ( + '[default]\n' + 's3 =\n' + '[profile foo]\n' + 'foo = bar\n' + ) + self.assert_update_config( + original, {'__section__': 'default', + 's3': {'signature_version': 'newval'}}, + '[default]\n' + 's3 =\n' + ' signature_version = newval\n' + '[profile foo]\n' + 'foo = bar\n') + + def test_add_to_nested_with_blank_line_after_nested_key(self): + original = ( + '[default]\n' + 's3 =\n' + '\n' + ' other = foo\n' + ) + self.assert_update_config( + original, {'__section__': 'default', + 's3': {'signature_version': 'newval'}}, + '[default]\n' + 's3 =\n' + '\n' + ' other = foo\n' + ' signature_version = newval\n') + + def test_add_to_empty_nested_without_trailing_newline(self): + original = ( + '[default]\n' + 's3 =' + ) + self.assert_update_config( + original, {'__section__': 'default', + 's3': {'signature_version': 'newval'}}, + '[default]\n' + 's3 =\n' + ' signature_version = newval\n') + def test_update_nested_attr_no_prior_nesting(self): original = ( '[default]\n'