lineinfile - add search_string parameter for non-regexp searching (#70647)

* Add tests for search_string
* Improve examples
* Add changelog
This commit is contained in:
Jose Angel Munoz 2021-02-02 21:37:06 +01:00 committed by GitHub
parent 9a9272305a
commit 69631da889
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 579 additions and 19 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- lineinfile - add search_string parameter for non-regexp searching (https://github.com/ansible/ansible/issues/70470)

View file

@ -44,6 +44,17 @@ options:
type: str
aliases: [ regex ]
version_added: '1.7'
search_string:
description:
- The literal string to look for in every line of the file. This does not have to match the entire line.
- For C(state=present), the line to replace if the string is found in the file. Only the last line found will be replaced.
- For C(state=absent), the line(s) to remove if the string is in the line.
- If the literal expression is not matched, the line will be
added to the file in keeping with C(insertbefore) or C(insertafter)
settings.
- Mutually exclusive with C(backrefs) and C(regexp).
type: str
version_added: '2.11'
state:
description:
- Whether the line should be there or not.
@ -68,6 +79,7 @@ options:
does not match anywhere in the file, the file will be left unchanged.
- If the C(regexp) does match, the last matching line will be replaced by
the expanded line parameter.
- Mutually exclusive with C(search_string).
type: bool
default: no
version_added: "1.1"
@ -136,6 +148,7 @@ seealso:
author:
- Daniel Hokka Zakrissoni (@dhozac)
- Ahti Kitsik (@ahtik)
- Jose Angel Munoz (@imjoseangel)
'''
EXAMPLES = r'''
@ -161,6 +174,15 @@ EXAMPLES = r'''
group: root
mode: '0644'
- name: Replace a localhost entry searching for a literal string to avoid escaping
lineinfile:
path: /etc/hosts
search_string: '127.0.0.1'
line: 127.0.0.1 localhost
owner: root
group: root
mode: '0644'
- name: Ensure the default Apache port is 8080
ansible.builtin.lineinfile:
path: /etc/httpd/conf/httpd.conf
@ -168,6 +190,13 @@ EXAMPLES = r'''
insertafter: '^#Listen '
line: Listen 8080
- name: Ensure php extension matches new pattern
lineinfile:
path: /etc/httpd/conf/httpd.conf
search_string: '<FilesMatch ".php[45]?$">'
insertafter: '^\t<Location \/>\n'
line: ' <FilesMatch ".php[34]?$">'
- name: Ensure we have our own comment added to /etc/services
ansible.builtin.lineinfile:
path: /etc/services
@ -253,7 +282,7 @@ def check_file_attrs(module, changed, message, diff):
return message, changed
def present(module, dest, regexp, line, insertafter, insertbefore, create,
def present(module, dest, regexp, search_string, line, insertafter, insertbefore, create,
backup, backrefs, firstmatch):
diff = {'before': '',
@ -301,8 +330,8 @@ def present(module, dest, regexp, line, insertafter, insertbefore, create,
# "If regular expressions are passed to both regexp and
# insertafter, insertafter is only honored if no match for regexp is found."
# Therefore:
# 1. regexp was found -> ignore insertafter, replace the founded line
# 2. regexp was not found -> insert the line after 'insertafter' or 'insertbefore' line
# 1. regexp or search_string was found -> ignore insertafter, replace the founded line
# 2. regexp or search_string was not found -> insert the line after 'insertafter' or 'insertbefore' line
# Given the above:
# 1. First check that there is no match for regexp:
@ -315,7 +344,17 @@ def present(module, dest, regexp, line, insertafter, insertbefore, create,
if firstmatch:
break
# 2. When no match found on the previous step,
# 2. Second check that there is no match for search_string:
if search_string is not None:
for lineno, b_cur_line in enumerate(b_lines):
match_found = to_bytes(search_string, errors='surrogate_or_strict') in b_cur_line
if match_found:
index[0] = lineno
match = match_found
if firstmatch:
break
# 3. When no match found on the previous step,
# parse for searching insertafter/insertbefore:
if not match:
for lineno, b_cur_line in enumerate(b_lines):
@ -350,9 +389,9 @@ def present(module, dest, regexp, line, insertafter, insertbefore, create,
if not b_new_line.endswith(b_linesep):
b_new_line += b_linesep
# If no regexp was given and no line match is found anywhere in the file,
# If no regexp or search_string was given and no line match is found anywhere in the file,
# insert the line appropriately if using insertbefore or insertafter
if regexp is None and match is None and not exact_line_match:
if (regexp, search_string, match) == (None, None, None) and not exact_line_match:
# Insert lines
if insertafter and insertafter != 'EOF':
@ -428,7 +467,7 @@ def present(module, dest, regexp, line, insertafter, insertbefore, create,
msg = 'line added'
changed = True
# insert matched, but not the regexp
# insert matched, but not the regexp or search_string
else:
b_lines.insert(index[1], b_line + b_linesep)
msg = 'line added'
@ -456,7 +495,7 @@ def present(module, dest, regexp, line, insertafter, insertbefore, create,
module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=difflist)
def absent(module, dest, regexp, line, backup):
def absent(module, dest, regexp, search_string, line, backup):
b_dest = to_bytes(dest, errors='surrogate_or_strict')
if not os.path.exists(b_dest):
@ -483,6 +522,8 @@ def absent(module, dest, regexp, line, backup):
def matcher(b_cur_line):
if regexp is not None:
match_found = bre_c.search(b_cur_line)
elif search_string is not None:
match_found = to_bytes(search_string, errors='surrogate_or_strict') in b_cur_line
else:
match_found = b_line == b_cur_line.rstrip(b'\r\n')
if match_found:
@ -521,6 +562,7 @@ def main():
path=dict(type='path', required=True, aliases=['dest', 'destfile', 'name']),
state=dict(type='str', default='present', choices=['absent', 'present']),
regexp=dict(type='str', aliases=['regex']),
search_string=dict(type='str'),
line=dict(type='str', aliases=['value']),
insertafter=dict(type='str'),
insertbefore=dict(type='str'),
@ -530,7 +572,8 @@ def main():
firstmatch=dict(type='bool', default=False),
validate=dict(type='str'),
),
mutually_exclusive=[['insertbefore', 'insertafter']],
mutually_exclusive=[
['insertbefore', 'insertafter'], ['regexp', 'search_string'], ['backrefs', 'search_string']],
add_file_common_args=True,
supports_check_mode=True,
)
@ -542,13 +585,17 @@ def main():
path = params['path']
firstmatch = params['firstmatch']
regexp = params['regexp']
search_string = params['search_string']
line = params['line']
if '' in [regexp, search_string]:
msg = ("The %s is an empty string, which will match every line in the file. "
"This may have unintended consequences, such as replacing the last line in the file rather than appending.")
param_name = 'search string'
if regexp == '':
module.warn(
"The regular expression is an empty string, which will match every line in the file. "
"This may have unintended consequences, such as replacing the last line in the file rather than appending. "
"If this is desired, use '^' to match every line in the file and avoid this warning.")
param_name = 'regular expression'
msg += " If this is desired, use '^' to match every line in the file and avoid this warning."
module.warn(msg % param_name)
b_path = to_bytes(path, errors='surrogate_or_strict')
if os.path.isdir(b_path):
@ -567,13 +614,13 @@ def main():
if ins_bef is None and ins_aft is None:
ins_aft = 'EOF'
present(module, path, regexp, line,
present(module, path, regexp, search_string, line,
ins_aft, ins_bef, create, backup, backrefs, firstmatch)
else:
if regexp is None and line is None:
module.fail_json(msg='one of line or regexp is required with state=absent')
if (regexp, search_string, line) == (None, None, None):
module.fail_json(msg='one of line, search_string, or regexp is required with state=absent')
absent(module, path, regexp, line, backup)
absent(module, path, regexp, search_string, line, backup)
if __name__ == '__main__':

View file

@ -0,0 +1,5 @@
[section_one]
[section_two]
[section_three]

View file

@ -0,0 +1,5 @@
This is line 1
This is line 2
(\\w)(\\s+)([\\.,])
This is line 4
<FilesMatch ".py[45]?$">

View file

@ -0,0 +1,4 @@
#!/bin/sh
case "`uname`" in
Darwin*) if [ -z "$JAVA_HOME" ] ; then

View file

@ -253,6 +253,8 @@
that:
- "result.stat.checksum == 'ab56c210ea82839a54487464800fed4878cb2608'"
- import_tasks: test_string01.yml
- name: use create=yes
lineinfile:
dest: "{{ output_dir }}/new_test.txt"
@ -806,7 +808,8 @@
- oneline_insbefore_test2 is not changed
- oneline_insbefore_file.stat.checksum == '4dca56d05a21f0d018cd311f43e134e4501cf6d9'
###################################################################
- import_tasks: test_string02.yml
# Issue 29443
# When using an empty regexp, replace the last line (since it matches every line)
# but also provide a warning.
@ -848,6 +851,49 @@
This may have unintended consequences, such as replacing the last line in the file rather than appending.
If this is desired, use '^' to match every line in the file and avoid this warning.
###################################################################
# When using an empty search string, replace the last line (since it matches every line)
# but also provide a warning.
- name: Deploy the test file for lineinfile
copy:
src: teststring.txt
dest: "{{ output_dir }}/teststring.txt"
register: result
- name: Assert that the test file was deployed
assert:
that:
- result is changed
- result.checksum == '481c2b73fe062390afdd294063a4f8285d69ac85'
- result.state == 'file'
- name: Insert a line in the file using an empty string as a search string
lineinfile:
path: "{{ output_dir }}/teststring.txt"
search_string: ''
line: This is line 6
register: insert_empty_literal
- name: Stat the file
stat:
path: "{{ output_dir }}/teststring.txt"
register: result
- name: Assert that the file contents match what is expected and a warning was displayed
assert:
that:
- insert_empty_literal is changed
- warning_message in insert_empty_literal.warnings
- result.stat.checksum == 'eaa79f878557d4bd8d96787a850526a0facab342'
vars:
warning_message: >-
The search string is an empty string, which will match every line in the file.
This may have unintended consequences, such as replacing the last line in the file rather than appending.
- name: meta
meta: end_play
###################################################################
## Issue #58923
## Using firstmatch with insertafter and ensure multiple lines are not inserted
@ -1126,6 +1172,120 @@
- insertbefore_test5 is not changed
- insertbefore_test5_file.stat.checksum == '3c6630b9d44f561ea9ad999be56a7504cadc12f7'
########################################################################################
# Same tests for literal
# Test insertafter with literal
- name: Deploy the test file
copy:
src: teststring_58923.txt
dest: "{{ output_dir }}/teststring_58923.txt"
register: initial_file
- name: Assert that the test file was deployed
assert:
that:
- initial_file is changed
- initial_file.checksum == 'b6379ba43261c451a62102acb2c7f438a177c66e'
- initial_file.state == 'file'
# Regarding the documentation:
# If the search string is passed to both search_string and
# insertafter, insertafter is only honored if no match for search_string is found.
# Therefore,
# when search_string expressions are passed to both search_string and insertafter, then:
# 1. search_string was found -> ignore insertafter, replace the founded line
# 2. search_string was not found -> insert the line after 'insertafter' line
# literal is not present in the file, so the line must be inserted after ^#!/bin/sh
- name: Add the line using firstmatch, regexp, and insertafter
lineinfile:
path: "{{ output_dir }}/teststring_58923.txt"
insertafter: '^#!/bin/sh'
search_string: export FISHEYE_OPTS
firstmatch: true
line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
register: insertafter_test1
- name: Stat the file
stat:
path: "{{ output_dir }}/teststring_58923.txt"
register: insertafter_test1_file
- name: Add the line using firstmatch, literal, and insertafter again
lineinfile:
path: "{{ output_dir }}/teststring_58923.txt"
insertafter: '^#!/bin/sh'
search_string: export FISHEYE_OPTS
firstmatch: true
line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
register: insertafter_test2
# Check of the prev step.
# We tried to add the same line with the same playbook,
# so nothing has been added:
- name: Stat the file again
stat:
path: "{{ output_dir }}/teststring_58923.txt"
register: insertafter_test2_file
- name: Assert insertafter tests gave the expected results
assert:
that:
- insertafter_test1 is changed
- insertafter_test1_file.stat.checksum == '9232aed6fe88714964d9e29d13e42cd782070b08'
- insertafter_test2 is not changed
- insertafter_test2_file.stat.checksum == '9232aed6fe88714964d9e29d13e42cd782070b08'
# Test insertbefore with literal
- name: Deploy the test file
copy:
src: teststring_58923.txt
dest: "{{ output_dir }}/teststring_58923.txt"
register: initial_file
- name: Assert that the test file was deployed
assert:
that:
- initial_file is changed
- initial_file.checksum == 'b6379ba43261c451a62102acb2c7f438a177c66e'
- initial_file.state == 'file'
- name: Add the line using literal, firstmatch, and insertbefore
lineinfile:
path: "{{ output_dir }}/teststring_58923.txt"
insertbefore: '^#!/bin/sh'
search_string: export FISHEYE_OPTS
firstmatch: true
line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
register: insertbefore_test1
- name: Stat the file
stat:
path: "{{ output_dir }}/teststring_58923.txt"
register: insertbefore_test1_file
- name: Add the line using literal, firstmatch, and insertbefore again
lineinfile:
path: "{{ output_dir }}/teststring_58923.txt"
insertbefore: '^#!/bin/sh'
search_string: export FISHEYE_OPTS
firstmatch: true
line: export FISHEYE_OPTS="-Xmx4096m -Xms2048m"
register: insertbefore_test2
- name: Stat the file again
stat:
path: "{{ output_dir }}/teststring_58923.txt"
register: insertbefore_test2_file
- name: Assert insertbefore with literal tests gave the expected results
assert:
that:
- insertbefore_test1 is changed
- insertbefore_test1_file.stat.checksum == '3c6630b9d44f561ea9ad999be56a7504cadc12f7'
- insertbefore_test2 is not changed
- insertbefore_test2_file.stat.checksum == '3c6630b9d44f561ea9ad999be56a7504cadc12f7'
# Test inserting a line at the end of the file using regexp with insertafter
# https://github.com/ansible/ansible/issues/63684
@ -1155,3 +1315,32 @@
- testend1 is changed
- testend2 is changed
- testend_file.stat.checksum == 'ef36116966836ce04f6b249fd1837706acae4e19'
# Test inserting a line at the end of the file using search_string with insertafter
- name: Create a file by inserting a line
lineinfile:
path: "{{ output_dir }}/testendliteral.txt"
create: yes
line: testline
register: testend1
- name: Insert a line at the end of the file
lineinfile:
path: "{{ output_dir }}/testendliteral.txt"
insertafter: testline
search_string: line at the end
line: line at the end
register: testend2
- name: Stat the file
stat:
path: "{{ output_dir }}/testendliteral.txt"
register: testend_file
- name: Assert inserting at the end gave the expected results.
assert:
that:
- testend1 is changed
- testend2 is changed
- testend_file.stat.checksum == 'ef36116966836ce04f6b249fd1837706acae4e19'

View file

@ -0,0 +1,142 @@
---
###################################################################
# 1st search_string tests
- name: deploy the test file for lineinfile string
copy:
src: teststring.txt
dest: "{{ output_dir }}/teststring.txt"
register: result
- name: assert that the test file was deployed
assert:
that:
- result is changed
- "result.checksum == '481c2b73fe062390afdd294063a4f8285d69ac85'"
- "result.state == 'file'"
- name: insert a line at the beginning of the file, and back it up
lineinfile:
dest: "{{ output_dir }}/teststring.txt"
state: present
line: "New line at the beginning"
insertbefore: "BOF"
backup: yes
register: result1
- name: insert a line at the beginning of the file again
lineinfile:
dest: "{{ output_dir }}/teststring.txt"
state: present
line: "New line at the beginning"
insertbefore: "BOF"
register: result2
- name: Replace a line using string
lineinfile:
dest: "{{ output_dir }}/teststring.txt"
state: present
line: "Thi$ i^ [ine 3"
search_string: (\\w)(\\s+)([\\.,])
register: backrefs_result1
- name: Replace a line again using string
lineinfile:
dest: "{{ output_dir }}/teststring.txt"
state: present
line: "Thi$ i^ [ine 3"
search_string: (\\w)(\\s+)([\\.,])
register: backrefs_result2
- command: cat {{ output_dir }}/teststring.txt
- name: assert that the line with backrefs was changed
assert:
that:
- backrefs_result1 is changed
- backrefs_result2 is not changed
- "backrefs_result1.msg == 'line replaced'"
- name: stat the test after the backref line was replaced
stat:
path: "{{ output_dir }}/teststring.txt"
register: result
- name: assert test checksum matches after backref line was replaced
assert:
that:
- "result.stat.checksum == '8084519b53e268920a46592a112297715951f167'"
- name: remove the middle line using string
lineinfile:
dest: "{{ output_dir }}/teststring.txt"
state: absent
search_string: "Thi$ i^ [ine 3"
register: result
- name: assert that the line was removed
assert:
that:
- result is changed
- "result.msg == '1 line(s) removed'"
- name: stat the test after the middle line was removed
stat:
path: "{{ output_dir }}/teststring.txt"
register: result
- name: assert test checksum matches after the middle line was removed
assert:
that:
- "result.stat.checksum == '89919ef2ef91e48ad02e0ca2bcb76dfc2a86d516'"
- name: run a validation script that succeeds using string
lineinfile:
dest: "{{ output_dir }}/teststring.txt"
state: absent
search_string: <FilesMatch ".py[45]?$">
validate: "true %s"
register: result
- name: assert that the file validated after removing a line
assert:
that:
- result is changed
- "result.msg == '1 line(s) removed'"
- name: stat the test after the validation succeeded
stat:
path: "{{ output_dir }}/teststring.txt"
register: result
- name: assert test checksum matches after the validation succeeded
assert:
that:
- "result.stat.checksum == 'ba9600b34febbc88bfb3ca99cd6b57f1010c19a4'"
- name: run a validation script that fails using string
lineinfile:
dest: "{{ output_dir }}/teststring.txt"
state: absent
search_string: "This is line 1"
validate: "/bin/false %s"
register: result
ignore_errors: yes
- name: assert that the validate failed
assert:
that:
- "result.failed == true"
- name: stat the test after the validation failed
stat:
path: "{{ output_dir }}/teststring.txt"
register: result
- name: assert test checksum matches the previous after the validation failed
assert:
that:
- "result.stat.checksum == 'ba9600b34febbc88bfb3ca99cd6b57f1010c19a4'"
# End of string tests
###################################################################

View file

@ -0,0 +1,166 @@
---
###################################################################
# 2nd search_string tests
- name: Deploy the teststring.conf file
copy:
src: teststring.conf
dest: "{{ output_dir }}/teststring.conf"
register: result
- name: Assert that the teststring.conf file was deployed
assert:
that:
- result is changed
- result.checksum == '6037f13e419b132eb3fd20a89e60c6c87a6add38'
- result.state == 'file'
# Test instertafter
- name: Insert lines after with string
lineinfile:
path: "{{ output_dir }}/teststring.conf"
search_string: "{{ item.regexp }}"
line: "{{ item.line }}"
insertafter: "{{ item.after }}"
with_items: "{{ test_befaf_regexp }}"
register: _multitest_5
- name: Do the same thing again and check for changes
lineinfile:
path: "{{ output_dir }}/teststring.conf"
search_string: "{{ item.regexp }}"
line: "{{ item.line }}"
insertafter: "{{ item.after }}"
with_items: "{{ test_befaf_regexp }}"
register: _multitest_6
- name: Assert that the file was changed the first time but not the second time
assert:
that:
- item.0 is changed
- item.1 is not changed
with_together:
- "{{ _multitest_5.results }}"
- "{{ _multitest_6.results }}"
- name: Stat the file
stat:
path: "{{ output_dir }}/teststring.conf"
register: result
- name: Assert that the file contents match what is expected
assert:
that:
- result.stat.checksum == '06e2c456e5028dd7bcd0b117b5927a1139458c82'
- name: Do the same thing a third time without string and check for changes
lineinfile:
path: "{{ output_dir }}/teststring.conf"
line: "{{ item.line }}"
insertafter: "{{ item.after }}"
with_items: "{{ test_befaf_regexp }}"
register: _multitest_7
- name: Stat the file
stat:
path: "{{ output_dir }}/teststring.conf"
register: result
- name: Assert that the file was changed when no string was provided
assert:
that:
- item is not changed
with_items: "{{ _multitest_7.results }}"
- name: Stat the file
stat:
path: "{{ output_dir }}/teststring.conf"
register: result
- name: Assert that the file contents match what is expected
assert:
that:
- result.stat.checksum == '06e2c456e5028dd7bcd0b117b5927a1139458c82'
# Test insertbefore
- name: Deploy the test.conf file
copy:
src: teststring.conf
dest: "{{ output_dir }}/teststring.conf"
register: result
- name: Assert that the teststring.conf file was deployed
assert:
that:
- result is changed
- result.checksum == '6037f13e419b132eb3fd20a89e60c6c87a6add38'
- result.state == 'file'
- name: Insert lines before with string
lineinfile:
path: "{{ output_dir }}/teststring.conf"
search_string: "{{ item.regexp }}"
line: "{{ item.line }}"
insertbefore: "{{ item.before }}"
with_items: "{{ test_befaf_regexp }}"
register: _multitest_8
- name: Do the same thing again and check for changes
lineinfile:
path: "{{ output_dir }}/teststring.conf"
search_string: "{{ item.regexp }}"
line: "{{ item.line }}"
insertbefore: "{{ item.before }}"
with_items: "{{ test_befaf_regexp }}"
register: _multitest_9
- name: Assert that the file was changed the first time but not the second time
assert:
that:
- item.0 is changed
- item.1 is not changed
with_together:
- "{{ _multitest_8.results }}"
- "{{ _multitest_9.results }}"
- name: Stat the file
stat:
path: "{{ output_dir }}/teststring.conf"
register: result
- name: Assert that the file contents match what is expected
assert:
that:
- result.stat.checksum == 'c3be9438a07c44d4c256cebfcdbca15a15b1db91'
- name: Do the same thing a third time without string and check for changes
lineinfile:
path: "{{ output_dir }}/teststring.conf"
line: "{{ item.line }}"
insertbefore: "{{ item.before }}"
with_items: "{{ test_befaf_regexp }}"
register: _multitest_10
- name: Stat the file
stat:
path: "{{ output_dir }}/teststring.conf"
register: result
- name: Assert that the file was changed when no string was provided
assert:
that:
- item is not changed
with_items: "{{ _multitest_10.results }}"
- name: Stat the file
stat:
path: "{{ output_dir }}/teststring.conf"
register: result
- name: Assert that the file contents match what is expected
assert:
that:
- result.stat.checksum == 'c3be9438a07c44d4c256cebfcdbca15a15b1db91'
# End of string tests
###################################################################