Repository: ansible/ansible
Pull request: replace - fixed combined before and after usage
##### SUMMARY

Combined usage of `before` and `after` options should select a subsection after the `after`, and before the `before`. Currently, it does the opposite. (Singular use of each option works as expected, though docs were poorly written.)

Additionally, subsection replacement should use start and end indices of original match, instead of a string replacement (in case the subsection occurs multiple times in the source file).

Updated the docs and examples accordingly. Added integration tests.

Fixes #31354
Fixes #31891

##### ISSUE TYPE

 - Bugfix Pull Request

##### COMPONENT NAME

`replace` module

##### ANSIBLE VERSION

```
ansible 2.5.0 (31354-replace-before-after-bug f2cf84b564) last updated 2017/10/08 16:36:54 (GMT +000)
  config file = None
  configured module search path = [u'/home/ubuntu/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /vagrant/lib/ansible
  executable location = /vagrant/bin/ansible
  python version = 2.7.12 (default, Nov 19 2016, 06:48:10) [GCC 5.4.0 20160609]
```

Problem statement:
replace module before/after wrong way around
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
files/replace module

##### ANSIBLE VERSION

```
2.4.0.0
```

##### CONFIGURATION

N/A
##### OS / ENVIRONMENT

N/A

##### SUMMARY

Simply try to use the replace module with either "before:" or "after:" alone, and for the opposite behaviour with both "before:" and "after:" together.  I would assume from the confusing description that begin/after values represent the "pre-string" and "post-string" to select the text to apply the regexp to.  If this is correct then the module seems to be incorrect for the standalone cases:

Current module code provided below shows (I believe) the code for the issue, which locates the "%s" incorrectly related to subsection for "before:" and "after:" standalone cases.

ansible/modules/files/replace.py:

    if params['after'] and params['before']:
        pattern = u'%s(?P<subsection>.*?)%s' % (params['before'], params['after'])
    elif params['after']:
        pattern = u'%s(?P<subsection>.*)' % params['after']
    elif params['before']:
        pattern = u'(?P<subsection>.*)%s' % params['before']

    if pattern:
        section_re = re.compile(pattern, re.DOTALL)
        match = re.search(section_re, contents)
        if match:
            section = match.group('subsection')




replace module before/after selection not honoured


##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME

files/replace module

##### ANSIBLE VERSION

```
2.4.0.0
```

##### CONFIGURATION
N/A

##### OS / ENVIRONMENT
N/A

##### SUMMARY
when using before/after to select text to apply the replace to, replace still acts on whole file

##### STEPS TO REPRODUCE

Look at the code, eg try before/after patterns that exist in file which regexp that appears outside the intended text selection area  :-)


ansible/modules/files/replace.py:

```
if result[1] > 0 and section != result[0]: 
     if pattern: 
         result = (contents.replace(section, result[0]), result[1])
```

Should  be "section.replace" rather than "contents.replace"

Hints from issue discussion:
cc @EvanK
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
The `before`/`after` options were added in [cd39ff9](https://github.com/ansible/ansible/commit/cd39ff9996e5449aa5e777537edf0b17897fc94d) by @j-carl, and definitely should have been tested from the start.

The standalone options work as I would have expected them to work (and as the original contributed examples showed), but **the docs for them do contradict that**. And the combination of both does indeed swap the logic. I'm not sure the best solution as far as breaking backward compatibility, because either one or the other will be broken.

No matter what the fix, I will get some integration tests hammered out for the `replace` module, so we know all the moving pieces are working as expected going forward.
We _could_ do something clever like match a subsection between `after` and `before` (as I would logically expect them to work), and if that fails, match the other way around for backward compatibility:

```diff
diff --git a/lib/ansible/modules/files/replace.py b/lib/ansible/modules/files/replace.py
index f2bf6da..964a649 100644
--- a/lib/ansible/modules/files/replace.py
+++ b/lib/ansible/modules/files/replace.py
@@ -229,7 +229,8 @@ def main():
 
     pattern = u''
     if params['after'] and params['before']:
-        pattern = u'%s(?P<subsection>.*?)%s' % (params['before'], params['after'])
+        pattern = u'%s(?P<subsection>.*?)%s' % (params['after'], params['before'])
+        swapped_pattern = u'%s(?P<subsection>.*?)%s' % (params['before'], params['after'])
     elif params['after']:
         pattern = u'%s(?P<subsection>.*)' % params['after']
     elif params['before']:
@@ -241,9 +242,15 @@ def main():
         if match:
             section = match.group('subsection')
         else:
-            res_args['msg'] = 'Pattern for before/after params did not match the given file: %s' % pattern
-            res_args['changed'] = False
-            module.exit_json(**res_args)
+            section_re = re.compile(swapped_pattern, re.DOTALL)
+            match = re.search(section_re, contents)
+            if match:
+                pattern = swapped_pattern
+                section = match.group('subsection')
+            else:
+                res_args['msg'] = 'Pattern for before/after params did not match the given file: %s' % pattern
+                res_args['changed'] = False
+                module.exit_json(**res_args)
     else:
         section = contents
```

> In case the swapped pattern matches as a fallback, we could return a deprecation warning, and then remove this fancy workaround in a later version?

The other alternative is to patch the module and docs to work as expected, **and break any existing usage of `before`/`after` in combination**.
There is a further issue with the replacement code, and I'm feeling lazy to create new issue.  The string replace acts on the whole file content it seems to me.  

`    if result[1] > 0 and section != result[0]:
        if pattern:
            result = (contents.replace(section, result[0]), result[1])
`

Should that not be the following


`    if result[1] > 0 and section != result[0]:
        if pattern:
            result = (section.replace(section, result[0]), result[1])
`

BTW, I would avoid any funny logic to try to detect the intended action based on file content.
The code you're referring to could probably be refactored for clarity, but it does seem to be working as expected
I had thought this might end up in 2.5 but it seems not :-(
Do we have an expected timeframe for this to be fixed in a release?
cc @EvanK
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
If the `before`/`after` are present, [we do the replacements against a subsection](https://github.com/ansible/ansible/blob/eb899c9bcdabb14b6e95f1c9fca0c604967459ab/lib/ansible/modules/files/replace.py#L251), and then [we swap out the unmodified subsection within the content with the replaced subsection](https://github.com/ansible/ansible/blob/eb899c9bcdabb14b6e95f1c9fca0c604967459ab/lib/ansible/modules/files/replace.py#L255)...so this is working as expected.

@ansibot notabug
Ah, sorry my suggestion makes no sense.  Now I see what you are trying to do.  Alas, it does not work because that `replace` method call is searching for text that could pretty much be anything at all.  I.e. the string `section` is the subsection text that sits between/after/before as defined by the requested before/after patterns, but that same resulting text is arbitrary and could easily appear anywhere in the original file _outside_ of the subsection.  This becomes more and more likely to give invalid replacements as the subsection text shortens or is a common string etc, with an example worst case of say a simple new line or a space char. 

How about instead of using `replace` method, use something like the following which simply rebuilds the string based on the match start and end indexes returned from `re.search`.

```
if result[1] > 0 and section != result[0]:        
    if pattern:
        result = (contents[0:match.start('subsection')] + result[0] + contents[match.end('subsection'):], result[1])
```


Ah, I understand now...that's a pretty far edge case, so I've never stumbled on it before.

I can bang out an integration test for it, and work on a fix from there
For a far edge case it seemed to hit most cases I tried :-)

I guess its very likely when using replace to _remove_ arbitrary text from a certain section of a file, and then on a subsequent run that section will be clear of text and thus the replace method will match everywhere.
The replacement approach of the modified subsection _is_ faulty, and your suggestion to use the matches' indices is a solid idea 👍

I'm currently figuring whether to roll it into #31452 or make a separate PR.

That said, it is beyond the scope of the module _and entirely up to the user_ to ensure the idempotence of whatever replacements are being made. Specifically, it is in the user's best interest to make not only the `regex`/`replace`, but the subsection between `before`/`after`, **as explicit and unlikely to be duplicated** within the file as possible -- even on additional runs of the task.
I had thought this might end up in 2.5 but it seems not :-(
I don't understand why this issue is closed.

Commit message: Cleaned up a rebase flub

Patch to review:
diff --git a/lib/ansible/modules/files/replace.py b/lib/ansible/modules/files/replace.py
index c816d64de13c8e..8b5c0ef4a9047d 100644
--- a/lib/ansible/modules/files/replace.py
+++ b/lib/ansible/modules/files/replace.py
@@ -57,21 +57,25 @@
       - The string to replace regexp matches.
       - May contain backreferences that will get expanded with the regexp capture groups if the regexp matches.
       - If not set, matches are removed entirely.
+      - Backreferences can be used ambiguously like C(\\1), or explicitly
+        like C(\\g<1>).
     type: str
   after:
     description:
-      - If specified, the line after the replace/remove will start.
+      - If specified, only content after this match will be replaced/removed.
       - Can be used in combination with C(before).
       - Uses Python regular expressions; see
         U(http://docs.python.org/2/library/re.html).
+      - Uses DOTALL, which means the C(.) special character I(can match newlines).
     type: str
     version_added: "2.4"
   before:
     description:
-      - If specified, the line before the replace/remove will occur.
+      - If specified, only content before this match will be replaced/removed.
       - Can be used in combination with C(after).
       - Uses Python regular expressions; see
         U(http://docs.python.org/2/library/re.html).
+      - Uses DOTALL, which means the C(.) special character I(can match newlines).
     type: str
     version_added: "2.4"
   backup:
@@ -92,63 +96,76 @@
     version_added: "2.4"
 notes:
   - As of Ansible 2.3, the I(dest) option has been changed to I(path) as default, but I(dest) still works as well.
+  - As of Ansible 2.x, the combined use of I(before) and I(after) options was changed significantly. See
+    U(https://github.com/ansible/ansible/issues/31354) for details.
   - Option I(follow) has been removed in Ansible 2.5, because this module modifies the contents of the file so I(follow=no) doesn't make sense.
 '''
 
 EXAMPLES = r'''
-# Before Ansible 2.3, option 'dest', 'destfile' or 'name' was used instead of 'path'
-- replace:
+- name: Before Ansible 2.3, option 'dest', 'destfile' or 'name' was used instead of 'path'
+  replace:
     path: /etc/hosts
     regexp: '(\s+)old\.host\.name(\s+.*)?$'
     replace: '\1new.host.name\2'
-    backup: yes
 
 - name: Replace after the expression till the end of the file (requires Ansible >= 2.4)
   replace:
-    path: /etc/hosts
-    regexp: '(\s+)old\.host\.name(\s+.*)?$'
-    replace: '\1new.host.name\2'
-    after: Start after line.*
-    backup: yes
+    path: /etc/apache2/sites-available/default.conf
+    after: 'NameVirtualHost [*]'
+    regexp: '^(.+)$'
+    replace: '# \1'
 
 - name: Replace before the expression till the begin of the file (requires Ansible >= 2.4)
   replace:
-    path: /etc/hosts
-    regexp: '(\s+)old\.host\.name(\s+.*)?$'
-    replace: '\1new.host.name\2'
-    before: 'Start before line.*'
-    backup: yes
+    path: /etc/apache2/sites-available/default.conf
+    before: '# live site config'
+    regexp: '^(.+)$'
+    replace: '# \1'
 
+# Previous to Ansible 2.x, using before and after in combination switches their logic (see ansible/ansible #31354)
 - name: Replace between the expressions (requires Ansible >= 2.4)
   replace:
     path: /etc/hosts
-    regexp: '(\s+)old\.host\.name(\s+.*)?$'
-    replace: '\1new.host.name\2'
-    after: 'Start after line.*'
-    before: 'Start before line.*'
-    backup: yes
+    after: '<VirtualHost [*]>'
+    before: '</VirtualHost>'
+    regexp: '^(.+)$'
+    replace: '# \1'
 
-- replace:
+- name: Supports common file attributes
+  replace:
     path: /home/jdoe/.ssh/known_hosts
     regexp: '^old\.host\.name[^\n]*\n'
     owner: jdoe
     group: jdoe
     mode: '0644'
 
-- replace:
+- name: Supports a validate command
+  replace:
     path: /etc/apache/ports
     regexp: '^(NameVirtualHost|Listen)\s+80\s*$'
     replace: '\1 127.0.0.1:8080'
     validate: '/usr/sbin/apache2ctl -f %s -t'
 
 - name: Short form task (in ansible 2+) necessitates backslash-escaped sequences
-  replace: dest=/etc/hosts regexp='\\b(localhost)(\\d*)\\b' replace='\\1\\2.localdomain\\2 \\1\\2'
+  replace: path=/etc/hosts regexp='\\b(localhost)(\\d*)\\b' replace='\\1\\2.localdomain\\2 \\1\\2'
 
 - name: Long form task does not
   replace:
-    dest: /etc/hosts
+    path: /etc/hosts
     regexp: '\b(localhost)(\d*)\b'
     replace: '\1\2.localdomain\2 \1\2'
+
+- name: Explicitly specifying positional matched groups in replacement
+  replace:
+    path: /etc/ssh/sshd_config
+    regexp: '^(ListenAddress[ ]+)[^\n]+$'
+    replace: '\g<1>0.0.0.0'
+
+- name: Explicitly specifying named matched groups
+  replace:
+    path: /etc/ssh/sshd_config
+    regexp: '^(?P<dctv>ListenAddress[ ]+)(?P<host>[^\n]+)$'
+    replace: '#\g<dctv>\g<host>\n\g<dctv>0.0.0.0'
 '''
 
 import os
@@ -231,7 +248,7 @@ def main():
 
     pattern = u''
     if params['after'] and params['before']:
-        pattern = u'%s(?P<subsection>.*?)%s' % (params['before'], params['after'])
+        pattern = u'%s(?P<subsection>.*?)%s' % (params['after'], params['before'])
     elif params['after']:
         pattern = u'%s(?P<subsection>.*)' % params['after']
     elif params['before']:
@@ -242,6 +259,7 @@ def main():
         match = re.search(section_re, contents)
         if match:
             section = match.group('subsection')
+            indices = [match.start('subsection'), match.end('subsection')]
         else:
             res_args['msg'] = 'Pattern for before/after params did not match the given file: %s' % pattern
             res_args['changed'] = False
@@ -254,7 +272,7 @@ def main():
 
     if result[1] > 0 and section != result[0]:
         if pattern:
-            result = (contents.replace(section, result[0]), result[1])
+            result = (contents[:indices[0]] + result[0] + contents[indices[1]:], result[1])
         msg = '%s replacements made' % result[1]
         changed = True
         if module._diff:
diff --git a/test/integration/targets/replace/aliases b/test/integration/targets/replace/aliases
new file mode 100644
index 00000000000000..a6dafcf8cd8b03
--- /dev/null
+++ b/test/integration/targets/replace/aliases
@@ -0,0 +1 @@
+shippable/posix/group1
diff --git a/test/integration/targets/replace/meta/main.yml b/test/integration/targets/replace/meta/main.yml
new file mode 100644
index 00000000000000..07faa217762603
--- /dev/null
+++ b/test/integration/targets/replace/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+  - prepare_tests
diff --git a/test/integration/targets/replace/tasks/main.yml b/test/integration/targets/replace/tasks/main.yml
new file mode 100644
index 00000000000000..24146ff31b4ac5
--- /dev/null
+++ b/test/integration/targets/replace/tasks/main.yml
@@ -0,0 +1,265 @@
+# setup
+- set_fact: output_dir_test={{output_dir}}/test_replace
+
+- name: make sure our testing sub-directory does not exist
+  file: path="{{ output_dir_test }}" state=absent
+
+- name: create our testing sub-directory
+  file: path="{{ output_dir_test }}" state=directory
+
+# tests
+- name: create test files
+  copy:
+      content: |-
+                The quick brown fox jumps over the lazy dog.
+                We promptly judged antique ivory buckles for the next prize.
+                Jinxed wizards pluck ivy from the big quilt.
+                Jaded zombies acted quaintly but kept driving their oxen forward.
+      dest: "{{ output_dir_test }}/pangrams.{{ item }}.txt"
+  with_sequence: start=0 end=6 format=%02x #increment as needed
+
+
+## test `before` option
+- name: remove all spaces before "quilt"
+  replace:
+    path: "{{ output_dir_test }}/pangrams.00.txt"
+    before: 'quilt'
+    regexp: ' '
+  register: replace_test0
+
+- command: "cat {{ output_dir_test }}/pangrams.00.txt"
+  register: replace_cat0
+
+- name: validate before assertions
+  assert:
+    that:
+      - replace_test0 is successful
+      - replace_test0 is changed
+      - replace_cat0.stdout_lines[0] == 'Thequickbrownfoxjumpsoverthelazydog.'
+      - replace_cat0.stdout_lines[-1] == 'Jaded zombies acted quaintly but kept driving their oxen forward.'
+
+
+## test `after` option
+- name: remove all spaces after "promptly"
+  replace:
+    path: "{{ output_dir_test }}/pangrams.01.txt"
+    after: 'promptly'
+    regexp: ' '
+  register: replace_test1
+
+- command: "cat {{ output_dir_test }}/pangrams.01.txt"
+  register: replace_cat1
+
+- name: validate after assertions
+  assert:
+    that:
+      - replace_test1 is successful
+      - replace_test1 is changed
+      - replace_cat1.stdout_lines[0] == 'The quick brown fox jumps over the lazy dog.'
+      - replace_cat1.stdout_lines[-1] == 'Jadedzombiesactedquaintlybutkeptdrivingtheiroxenforward.'
+
+
+## test combined `before` and `after` options
+- name: before "promptly" but after "quilt", replace every "e" with a "3"
+  replace:
+    path: "{{ output_dir_test }}/pangrams.02.txt"
+    before: 'promptly'
+    after: 'quilt'
+    regexp: 'e'
+    replace: '3'
+  register: replace_test2
+
+- name: validate after+before assertions
+  assert:
+    that:
+      - replace_test2 is successful
+      - not replace_test2 is changed
+      - replace_test2.msg.startswith("Pattern for before/after params did not match the given file")
+
+- name: before "quilt" but after "promptly", replace every "e" with a "3"
+  replace:
+    path: "{{ output_dir_test }}/pangrams.03.txt"
+    before: 'quilt'
+    after: 'promptly'
+    regexp: 'e'
+    replace: '3'
+  register: replace_test3
+
+- command: "cat {{ output_dir_test }}/pangrams.03.txt"
+  register: replace_cat3
+
+- name: validate before+after assertions
+  assert:
+    that:
+      - replace_test3 is successful
+      - replace_test3 is changed
+      - replace_cat3.stdout_lines[1] == 'We promptly judg3d antiqu3 ivory buckl3s for th3 n3xt priz3.'
+
+
+## test ^$ behavior in MULTILINE, and . behavior in absense of DOTALL
+- name: quote everything between bof and eof
+  replace:
+    path: "{{ output_dir_test }}/pangrams.04.txt"
+    regexp: ^([\S\s]+)$
+    replace: '"\1"'
+  register: replace_test4_0
+
+- command: "cat {{ output_dir_test }}/pangrams.04.txt"
+  register: replace_cat4_0
+
+- name: quote everything between bol and eol
+  replace:
+    path: "{{ output_dir_test }}/pangrams.04.txt"
+    regexp: ^(.+)$
+    replace: '"\1"'
+  register: replace_test4_1
+
+- command: "cat {{ output_dir_test }}/pangrams.04.txt"
+  register: replace_cat4_1
+
+- name: validate before+after assertions
+  assert:
+    that:
+      - replace_test4_0 is successful
+      - replace_test4_0 is changed
+      - replace_test4_1 is successful
+      - replace_test4_1 is changed
+      - replace_cat4_0.stdout_lines[0] == '"The quick brown fox jumps over the lazy dog.'
+      - replace_cat4_0.stdout_lines[-1] == 'Jaded zombies acted quaintly but kept driving their oxen forward."'
+      - replace_cat4_1.stdout_lines[0] == '""The quick brown fox jumps over the lazy dog."'
+      - replace_cat4_1.stdout_lines[-1] == '"Jaded zombies acted quaintly but kept driving their oxen forward.""'
+
+
+## test \b escaping in short and long form
+- name: short form with unescaped word boundaries
+  replace: path="{{ output_dir_test }}/pangrams.05.txt" regexp='\b(.+)\b' replace='"\1"'
+  register: replace_test5_0
+
+- name: short form with escaped word boundaries
+  replace: path="{{ output_dir_test }}/pangrams.05.txt" regexp='\\b(.+)\\b' replace='"\1"'
+  register: replace_test5_1
+
+- command: "cat {{ output_dir_test }}/pangrams.05.txt"
+  register: replace_cat5_1
+
+- name: long form with unescaped word boundaries
+  replace:
+    path: "{{ output_dir_test }}/pangrams.05.txt"
+    regexp: '\b(.+)\b'
+    replace: '"\1"'
+  register: replace_test5_2
+
+- command: "cat {{ output_dir_test }}/pangrams.05.txt"
+  register: replace_cat5_2
+
+- name: long form with escaped word boundaries
+  replace:
+    path: "{{ output_dir_test }}/pangrams.05.txt"
+    regexp: '\\b(.+)\\b'
+    replace: '"\1"'
+  register: replace_test5_3
+
+- name: validate before+after assertions
+  assert:
+    that:
+      - not replace_test5_0 is changed
+      - replace_test5_1 is changed
+      - replace_test5_2 is changed
+      - not replace_test5_3 is changed
+      - replace_cat5_1.stdout_lines[0] == '"The quick brown fox jumps over the lazy dog".'
+      - replace_cat5_1.stdout_lines[-1] == '"Jaded zombies acted quaintly but kept driving their oxen forward".'
+      - replace_cat5_2.stdout_lines[0] == '""The quick brown fox jumps over the lazy dog"".'
+      - replace_cat5_2.stdout_lines[-1] == '""Jaded zombies acted quaintly but kept driving their oxen forward"".'
+
+
+## test backup behaviors
+- name: replacement with backup
+  replace:
+    path: "{{ output_dir_test }}/pangrams.06.txt"
+    regexp: ^(.+)$
+    replace: '"\1"'
+    backup: true
+  register: replace_test6
+
+- command: "cat {{ output_dir_test }}/pangrams.06.txt"
+  register: replace_cat6_0
+
+- command: "cat {{ replace_test6.backup_file }}"
+  register: replace_cat6_1
+
+- name: validate backup
+  assert:
+    that:
+      - replace_test6 is successful
+      - replace_test6 is changed
+      - replace_test6.backup_file is search('/pangrams.06.txt.')
+      - replace_cat6_0.stdout != replace_cat6_1.stdout
+
+
+## test filesystem failures
+- name: fail on directory
+  replace:
+    path: "{{ output_dir_test }}"
+    regexp: ^(.+)$
+  register: replace_test7_1
+  ignore_errors: true
+
+- name: fail on missing file
+  replace:
+    path: "{{ output_dir_test }}/missing_file.txt"
+    regexp: ^(.+)$
+  register: replace_test7_2
+  ignore_errors: true
+
+- name: validate backup
+  assert:
+    that:
+      - replace_test7_1 is failure
+      - replace_test7_2 is failure
+      - replace_test7_1.msg.endswith(" is a directory !")
+      - replace_test7_2.msg.endswith(" does not exist !")
+
+
+## test subsection replacement when before/after potentially match more than once
+- name: test file for subsection replacement gone awry
+  copy:
+      content: |-
+                # start of group
+                0.0.0.0
+                127.0.0.1
+                127.0.1.1
+                # end of group
+
+                # start of group
+                0.0.0.0
+                127.0.0.1
+                127.0.1.1
+                # end of group
+
+                # start of group
+                0.0.0.0
+                127.0.0.1
+                127.0.1.1
+                # end of group
+      dest: "{{ output_dir_test }}/addresses.txt"
+
+- name: subsection madness
+  replace:
+    path: "{{ output_dir_test }}/addresses.txt"
+    after: '# start of group'
+    before: '# end of group'
+    regexp: '0'
+    replace: '9'
+  register: replace_test8
+
+- command: "cat {{ output_dir_test }}/addresses.txt"
+  register: replace_cat8
+
+- name: validate before+after assertions
+  assert:
+    that:
+      - replace_test8 is successful
+      - replace_test8 is changed
+      - replace_cat8.stdout_lines[1] == "9.9.9.9"
+      - replace_cat8.stdout_lines[7] == "0.0.0.0"
+      - replace_cat8.stdout_lines[13] == "0.0.0.0"


Code review comments:
diff_hunk: @@ -57,21 +57,25 @@
       - The string to replace regexp matches.
       - May contain backreferences that will get expanded with the regexp capture groups if the regexp matches.
       - If not set, matches are removed entirely.
+      - Backreferences can be used ambiguously like C(\\1), or explicitly
+        like C(\\g<1>).
path: lib/ansible/modules/files/replace.py
text: @author:
I'm not certain if the backslash escaping here is necessary (eg `C(\\1)` versus `C(\1)`)

@user1:
No, backslash escaping is not needed here.