diff --git a/changelogs/fragments/replace-update-string-comparison-method-to-unicode.yml b/changelogs/fragments/replace-update-string-comparison-method-to-unicode.yml new file mode 100644 index 00000000000..d07f65f9891 --- /dev/null +++ b/changelogs/fragments/replace-update-string-comparison-method-to-unicode.yml @@ -0,0 +1,3 @@ +minor_changes: + - replace - read/write files in text-mode as unicode chars instead of as bytes + and switch regex matching to unicode chars instead of bytes. (https://github.com/ansible/ansible/pull/85785). diff --git a/lib/ansible/modules/replace.py b/lib/ansible/modules/replace.py index ebadb934914..f8dffd25c70 100644 --- a/lib/ansible/modules/replace.py +++ b/lib/ansible/modules/replace.py @@ -183,14 +183,14 @@ import os import re import tempfile -from ansible.module_utils.common.text.converters import to_text, to_bytes +from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.basic import AnsibleModule -def write_changes(module, contents, path): +def write_changes(module, contents, path, encoding='utf-8'): tmpfd, tmpfile = tempfile.mkstemp(dir=module.tmpdir) - with os.fdopen(tmpfd, 'wb') as f: + with os.fdopen(tmpfd, 'w', encoding=encoding) as f: f.write(contents) validate = module.params.get('validate', None) @@ -254,8 +254,8 @@ def main(): module.fail_json(rc=257, msg='Path %s does not exist !' % path) else: try: - with open(path, 'rb') as f: - contents = to_text(f.read(), errors='surrogate_or_strict', encoding=encoding) + with open(path, 'r', encoding=encoding) as f: + contents = f.read() except OSError as ex: raise Exception(f"Unable to read the contents of {path!r}.") from ex @@ -307,7 +307,7 @@ def main(): res_args['backup_file'] = module.backup_local(path) # We should always follow symlinks so that we change the real file path = os.path.realpath(path) - write_changes(module, to_bytes(result[0], encoding=encoding), path) + write_changes(module, result[0], path, encoding=encoding) res_args['msg'], res_args['changed'] = check_file_attrs(module, changed, msg) module.exit_json(**res_args) diff --git a/test/integration/targets/replace/tasks/main.yml b/test/integration/targets/replace/tasks/main.yml index af428ea998b..ab5f7782215 100644 --- a/test/integration/targets/replace/tasks/main.yml +++ b/test/integration/targets/replace/tasks/main.yml @@ -280,3 +280,68 @@ that: - replace_test9 is failure - replace_test9.msg.startswith("Unable to process replace") + +# test replace operation in non-utf8 encoded files + +- name: create test file encoded in cp273 + lineinfile: + path: "{{ remote_tmp_dir }}/test-encoding-file.txt" + create: True + line: "Test line encoded in cp273" + state: present + encoding: cp273 + +- name: stat the new test file + stat: + path: "{{ remote_tmp_dir }}/test-encoding-file.txt" + register: replace_test10 + +- name: validate test file content + assert: + that: + - replace_test10.stat.exists + - replace_test10.stat.checksum == 'e0bf2bdca94727d03483a9adab66543bcec4b99a' + +- name: replace only the 'e' in 'line' with a '3' using both the before and after options + replace: + path: "{{ remote_tmp_dir }}/test-encoding-file.txt" + before: 'encoded' + after: 'Test' + regexp: 'e' + replace: '3' + encoding: cp273 + register: replace_test11 + +- name: stat the modified file + stat: + path: "{{ remote_tmp_dir }}/test-encoding-file.txt" + register: replace_test12 + +- name: validate the test file content is modified as expected. + assert: + that: + - replace_test11.msg == "1 replacements made" + - replace_test12.stat.exists + - replace_test12.stat.checksum == '485e97f6e38e50680228b8d49e1eb23160f19fdc' + +- name: idempotence check - replace only the 'e' 'line' with a '3' again + replace: + path: "{{ remote_tmp_dir }}/test-encoding-file.txt" + before: 'encoded' + after: 'Test' + regexp: 'e' + replace: '3' + encoding: cp273 + register: replace_test13 + +- name: stat the un-modified file + stat: + path: "{{ remote_tmp_dir }}/test-encoding-file.txt" + register: replace_test14 + +- name: validate the test content is unchanged + assert: + that: + - replace_test13.changed == False + - replace_test14.stat.exists + - replace_test14.stat.checksum == '485e97f6e38e50680228b8d49e1eb23160f19fdc'