diff --git a/lib/ansible/plugins/lookup/first_found.py b/lib/ansible/plugins/lookup/first_found.py index ef4b045649..a6fb1b7b8f 100644 --- a/lib/ansible/plugins/lookup/first_found.py +++ b/lib/ansible/plugins/lookup/first_found.py @@ -44,6 +44,17 @@ EXAMPLES = """ - "bar.txt" # will be looked in files/ dir relative to role and/or play - "/path/to/biz.txt" +- name: | + include tasks only if files exist. Note the use of query() to return + a blank list for the loop if no files are found. + import_tasks: '{{ item }}' + vars: + params: + files: + - path/tasks.yaml + - path/other_tasks.yaml + loop: "{{ q('first_found', params, errors='ignore') }}" + - name: | copy first existing file found to /some/file, looking in relative directories from where the task is defined and @@ -166,5 +177,5 @@ class LookupModule(LookupBase): return [path] if skip: return [] - raise AnsibleLookupError("No file was found when using first_found. Use the 'skip: true' option to allow this task to be skipped if no " + raise AnsibleLookupError("No file was found when using first_found. Use errors='ignore' to allow this task to be skipped if no " "files are found") diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py index 2d168e48f3..f88b7165db 100644 --- a/lib/ansible/template/__init__.py +++ b/lib/ansible/template/__init__.py @@ -706,7 +706,7 @@ class Templar: display.display(msg, log_only=True) else: raise AnsibleError(to_native(msg)) - ran = None + ran = [] if wantlist else None if ran and not allow_unsafe: if wantlist: diff --git a/test/integration/targets/iterators/tasks/main.yml b/test/integration/targets/iterators/tasks/main.yml index 917d97357c..c7438ed338 100644 --- a/test/integration/targets/iterators/tasks/main.yml +++ b/test/integration/targets/iterators/tasks/main.yml @@ -270,3 +270,59 @@ - "b__ == 'flattened'" - "c__ == 'flattened'" - "d__ == 'flattened'" + + +# q(FIRST_FOUND) +- name: test q(first_found) with no files produces empty list + set_fact: + first_found_var: "{{ q('first_found', params, errors='ignore') }}" + vars: + params: + files: "not_a_file.yaml" + +- name: verify q(first_found) result + assert: + that: + - "first_found_var == []" + +- name: test lookup(first_found) with no files produces empty string + set_fact: + first_found_var: "{{ lookup('first_found', params, errors='ignore') }}" + vars: + params: + files: "not_a_file.yaml" + +- name: verify lookup(first_found) result + assert: + that: + - "first_found_var == ''" + +# NOTE: skip: True deprecated e17a2b502d6601be53c60d7ba1c627df419460c9, remove 2.12 +- name: test first_found with no matches and skip=True does nothing + set_fact: "this_not_set={{ item }}" + vars: + params: + files: + - not/a/file.yaml + - another/non/file.yaml + skip: True + loop: "{{ q('first_found', params) }}" + +- name: verify skip + assert: + that: + - "this_not_set is not defined" + +- name: test first_found with no matches and errors='ignore' skips in a loop + set_fact: "this_not_set={{ item }}" + vars: + params: + files: + - not/a/file.yaml + - another/non/file.yaml + loop: "{{ query('first_found', params, errors='ignore') }}" + +- name: verify errors=ignore + assert: + that: + - "this_not_set is not defined"