Repository: ansible/ansible

Issue:
lineinfile with state=absent will delete all lines matching regexp
##### ISSUE TYPE
 - Documentation Report

Note: I'm not sure whether this is desired behavior or a bug. Depending on this, this is a documentation report or a bug report.

##### COMPONENT NAME
lineinfile

##### ANSIBLE VERSION
```
ansible 2.3.0.0
  config file = /Users/dimitrijer/.ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.13 (default, Dec 20 2016, 10:00:44) [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)]
```

##### CONFIGURATION
fact_caching enabled:
```
fact_caching = jsonfile
fact_caching_connection = /Users/dimitrijer/.ansible/facts_cache.json
fact_caching_timeout = 86400
```

##### OS / ENVIRONMENT
N/A

##### SUMMARY
lineinfile module deletes *all* matching lines when `regexp` and `state=absent` are specified. Documentation hints that `regexp` should remove _the_ matching line (a single line):
> For state=absent, the pattern of the line to remove. Uses Python regular expressions; see http://docs.python.org/2/library/re.html.

##### STEPS TO REPRODUCE
Let's say I wanted to clear the last line matching `1` from file with the following contents (`/etc/nginx/conf.d/test.conf`):
```
2
3
1
1
5
1
6
```

This is how my task looks like:
```yaml
  - name: clear default nginx vhost
    lineinfile:
      dest: /etc/nginx/conf.d/test.conf
      regexp: "1"
      state: absent
    notify:
      - reload nginx
```

##### EXPECTED RESULTS
I was expecting `lineinfile` module to only delete the last matched line in file. 

##### ACTUAL RESULTS
Instead, it deleted all matching lines. Resulting file looks like this:
```
2
3
5
6
```

 I was actually able to clear entire file by specifying `.*` as regexp.
lineinfile with state=absent will delete all lines matching regexp
##### ISSUE TYPE
 - Documentation Report

Note: I'm not sure whether this is desired behavior or a bug. Depending on this, this is a documentation report or a bug report.

##### COMPONENT NAME
lineinfile

##### ANSIBLE VERSION
```
ansible 2.3.0.0
  config file = /Users/dimitrijer/.ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.13 (default, Dec 20 2016, 10:00:44) [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)]
```

##### CONFIGURATION
fact_caching enabled:
```
fact_caching = jsonfile
fact_caching_connection = /Users/dimitrijer/.ansible/facts_cache.json
fact_caching_timeout = 86400
```

##### OS / ENVIRONMENT
N/A

##### SUMMARY
lineinfile module deletes *all* matching lines when `regexp` and `state=absent` are specified. Documentation hints that `regexp` should remove _the_ matching line (a single line):
> For state=absent, the pattern of the line to remove. Uses Python regular expressions; see http://docs.python.org/2/library/re.html.

##### STEPS TO REPRODUCE
Let's say I wanted to clear the last line matching `1` from file with the following contents (`/etc/nginx/conf.d/test.conf`):
```
2
3
1
1
5
1
6
```

This is how my task looks like:
```yaml
  - name: clear default nginx vhost
    lineinfile:
      dest: /etc/nginx/conf.d/test.conf
      regexp: "1"
      state: absent
    notify:
      - reload nginx
```

##### EXPECTED RESULTS
I was expecting `lineinfile` module to only delete the last matched line in file. 

##### ACTUAL RESULTS
Instead, it deleted all matching lines. Resulting file looks like this:
```
2
3
5
6
```

 I was actually able to clear entire file by specifying `.*` as regexp.


Discussion:
I think this is working as per documentation 

```
For `state=present', the pattern to replace if found; only the last line found will be replaced. For `state=absent', the pattern of the line to remove.
```
I was confused by `the` part:
> ...the pattern of the line to remove...

Think it would be clearer if it read:
> ...the pattern of lines to remove...
I think this is working as per documentation 

```
For `state=present', the pattern to replace if found; only the last line found will be replaced. For `state=absent', the pattern of the line to remove.
```
I was confused by `the` part:
> ...the pattern of the line to remove...

Think it would be clearer if it read:
> ...the pattern of lines to remove...

Resolving patch:
diff --git a/lib/ansible/modules/files/lineinfile.py b/lib/ansible/modules/files/lineinfile.py
--- a/lib/ansible/modules/files/lineinfile.py
+++ b/lib/ansible/modules/files/lineinfile.py
@@ -55,7 +55,7 @@
     description:
       - The regular expression to look for in every line of the file. For
         C(state=present), the pattern to replace if found; only the last line
-        found will be replaced. For C(state=absent), the pattern of the line
+        found will be replaced. For C(state=absent), the pattern of the line(s)
         to remove.  Uses Python regular expressions; see
         U(http://docs.python.org/2/library/re.html).
   state:
@@ -216,8 +216,8 @@ def write_changes(module, b_lines, dest):
                                  'rc:%s error:%s' % (rc, err))
     if valid:
         module.atomic_move(tmpfile,
-                to_native(os.path.realpath(to_bytes(dest, errors='surrogate_or_strict')), errors='surrogate_or_strict'),
-            unsafe_writes=module.params['unsafe_writes'])
+                           to_native(os.path.realpath(to_bytes(dest, errors='surrogate_or_strict')), errors='surrogate_or_strict'),
+                           unsafe_writes=module.params['unsafe_writes'])
 
 
 def check_file_attrs(module, changed, message, diff):
@@ -379,6 +379,7 @@ def absent(module, dest, regexp, line, backup):
     found = []
 
     b_line = to_bytes(line, errors='surrogate_or_strict')
+
     def matcher(b_cur_line):
         if regexp is not None:
             match_found = bre_c.search(b_cur_line)


Test patch:
diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt
--- a/test/sanity/pep8/legacy-files.txt
+++ b/test/sanity/pep8/legacy-files.txt
@@ -418,7 +418,6 @@ lib/ansible/modules/files/copy.py
 lib/ansible/modules/files/find.py
 lib/ansible/modules/files/ini_file.py
 lib/ansible/modules/files/iso_extract.py
-lib/ansible/modules/files/lineinfile.py
 lib/ansible/modules/files/patch.py
 lib/ansible/modules/files/replace.py
 lib/ansible/modules/files/stat.py