-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add New Module file_remove #11032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
felixfontein
merged 21 commits into
ansible-collections:main
from
shahargolshani:dev/file_remove
Nov 21, 2025
Merged
Add New Module file_remove #11032
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
2f79422
Add New Module file_remove
shahargolshani 31765e4
Add fixes from code review
shahargolshani e1154a4
Change file_type documentation
shahargolshani 296de23
Remove python to_native from the module
shahargolshani 49c8e5b
Remove redundant block/always cleanup
shahargolshani 0f0e2aa
Update plugins/modules/file_remove.py
shahargolshani d010bcb
Update plugins/modules/file_remove.py
shahargolshani e8eadc7
Update plugins/modules/file_remove.py
shahargolshani c9081e3
Update plugins/modules/file_remove.py
shahargolshani a670163
Update plugins/modules/file_remove.py
shahargolshani 139d9d7
Update plugins/modules/file_remove.py
shahargolshani 8f454da
Update plugins/modules/file_remove.py
shahargolshani b14fcb3
Update plugins/modules/file_remove.py
shahargolshani ce9cb03
Add more nox fixes to latest review
shahargolshani 674a3ee
Update plugins/modules/file_remove.py
shahargolshani 32dbf29
Update tests/integration/targets/file_remove/tasks/main.yml
shahargolshani 031aa1a
Fix EXAMPLES regex pattern
shahargolshani 193152e
Add warning when listed file was removed by other process during
shahargolshani ff3b80c
remove raise exception from find_matching_files;
shahargolshani f9b9534
Update plugins/modules/file_remove.py
shahargolshani ba06161
Update plugins/modules/file_remove.py
shahargolshani File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,290 @@ | ||
| #!/usr/bin/python | ||
|
|
||
| # Copyright (c) 2025, Shahar Golshani (@shahargolshani) | ||
| # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
|
|
||
| DOCUMENTATION = r""" | ||
| module: file_remove | ||
|
|
||
| short_description: Remove files matching a pattern from a directory | ||
|
|
||
| description: | ||
| - This module removes files from a specified directory that match a given pattern. | ||
| The pattern can include wildcards and regular expressions. | ||
| - By default, only files in the specified directory are removed (non-recursive). | ||
| Use the O(recursive) option to search and remove files in subdirectories. | ||
|
|
||
| version_added: "12.1.0" | ||
|
|
||
| author: | ||
| - Shahar Golshani (@shahargolshani) | ||
|
|
||
| extends_documentation_fragment: | ||
| - community.general.attributes | ||
|
|
||
| attributes: | ||
| check_mode: | ||
| support: full | ||
| diff_mode: | ||
| support: full | ||
|
|
||
| options: | ||
| path: | ||
| description: | ||
| - Path to the directory where files should be removed. | ||
| - This must be an existing directory. | ||
| type: path | ||
| required: true | ||
|
|
||
| pattern: | ||
| description: | ||
| - Pattern to match files for removal. | ||
| - Supports wildcards (V(*), V(?), V([seq]), V([!seq])) for glob-style matching. | ||
| - Use O(use_regex=true) to interpret this as a regular expression instead. | ||
| type: str | ||
| required: true | ||
|
|
||
| use_regex: | ||
| description: | ||
| - If V(true), O(pattern) is interpreted as a regular expression. | ||
| - If V(false), O(pattern) is interpreted as a glob-style wildcard pattern. | ||
| type: bool | ||
| default: false | ||
|
|
||
| recursive: | ||
| description: | ||
| - If V(true), search for files recursively in subdirectories. | ||
| - If V(false), only files in the specified directory are removed. | ||
| type: bool | ||
| default: false | ||
|
|
||
| file_type: | ||
| description: | ||
| - Type of files to remove. | ||
| type: str | ||
| choices: | ||
| file: remove only regular files. | ||
| link: remove only symbolic links. | ||
| any: remove both files and symbolic links. | ||
| default: file | ||
|
|
||
| notes: | ||
| - Directories are never removed by this module, only files and optionally symbolic links. | ||
| - This module will not follow symbolic links when O(recursive=true). | ||
| - Be careful with patterns that might match many files, especially with O(recursive=true). | ||
| """ | ||
|
|
||
| EXAMPLES = r""" | ||
| - name: Remove all log files from /var/log | ||
| community.general.file_remove: | ||
| path: /var/log | ||
| pattern: "*.log" | ||
|
|
||
| - name: Remove all temporary files recursively | ||
| community.general.file_remove: | ||
| path: /tmp/myapp | ||
| pattern: "*.tmp" | ||
| recursive: true | ||
|
|
||
| - name: Remove files matching a regex pattern | ||
| community.general.file_remove: | ||
| path: /data/backups | ||
| pattern: 'backup_[0-9]{8}\.tar\.gz' | ||
| use_regex: true | ||
|
|
||
| - name: Remove both files and symbolic links | ||
| community.general.file_remove: | ||
| path: /opt/app/cache | ||
| pattern: "cache_*" | ||
| file_type: any | ||
|
|
||
| - name: Remove all files starting with 'test_' (check mode) | ||
| community.general.file_remove: | ||
| path: /home/user/tests | ||
| pattern: "test_*" | ||
| check_mode: true | ||
| """ | ||
|
|
||
| RETURN = r""" | ||
| removed_files: | ||
| description: List of files that were removed. | ||
| type: list | ||
| elements: str | ||
| returned: always | ||
| sample: ['/var/log/app.log', '/var/log/error.log'] | ||
|
|
||
| files_count: | ||
| description: Number of files removed. | ||
| type: int | ||
| returned: always | ||
shahargolshani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| sample: 2 | ||
|
|
||
| path: | ||
| description: The directory path that was searched. | ||
| type: str | ||
| returned: always | ||
shahargolshani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| sample: /var/log | ||
| """ | ||
|
|
||
|
|
||
| import os | ||
| import re | ||
| import glob | ||
shahargolshani marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| from ansible.module_utils.basic import AnsibleModule | ||
|
|
||
|
|
||
| def find_matching_files(path, pattern, use_regex, recursive, file_type): | ||
shahargolshani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """Find all files matching the pattern in the given path.""" | ||
| matching_files = [] | ||
|
|
||
| if use_regex: | ||
| # Use regular expression matching | ||
| regex = re.compile(pattern) | ||
| if recursive: | ||
| for root, dirs, files in os.walk(path, followlinks=False): | ||
| for filename in files: | ||
| if regex.match(filename) or regex.search(filename): | ||
| full_path = os.path.join(root, filename) | ||
| if should_include_file(full_path, file_type): | ||
| matching_files.append(full_path) | ||
| else: | ||
| try: | ||
| for filename in os.listdir(path): | ||
| if regex.match(filename) or regex.search(filename): | ||
| full_path = os.path.join(path, filename) | ||
| if should_include_file(full_path, file_type): | ||
| matching_files.append(full_path) | ||
| except OSError as e: | ||
| raise AssertionError(f"Failed to list directory {path}: {e}") | ||
| else: | ||
| # Use glob pattern matching | ||
| if recursive: | ||
| glob_pattern = os.path.join(path, "**", pattern) | ||
| matching_files = [f for f in glob.glob(glob_pattern, recursive=True) if should_include_file(f, file_type)] | ||
| else: | ||
| glob_pattern = os.path.join(path, pattern) | ||
| matching_files = [f for f in glob.glob(glob_pattern) if should_include_file(f, file_type)] | ||
|
|
||
| return sorted(matching_files) | ||
|
|
||
|
|
||
| def should_include_file(file_path, file_type): | ||
shahargolshani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """Determine if a file should be included based on its type.""" | ||
| # Never include directories | ||
| if os.path.isdir(file_path): | ||
| return False | ||
|
|
||
| is_link = os.path.islink(file_path) | ||
| is_file = os.path.isfile(file_path) | ||
|
|
||
| if file_type == "file": | ||
| # Only regular files, not symlinks | ||
| return is_file and not is_link | ||
| elif file_type == "link": | ||
| # Only symbolic links | ||
| return is_link | ||
| elif file_type == "any": | ||
| # Both files and symlinks | ||
| return is_file or is_link | ||
|
|
||
| return False | ||
|
|
||
|
|
||
| def remove_files(module, files): | ||
shahargolshani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """Remove the specified files and return results.""" | ||
| removed_files = [] | ||
| failed_files = [] | ||
|
|
||
| for file_path in files: | ||
| try: | ||
| if module.check_mode: | ||
| # In check mode, just verify the file exists | ||
| if os.path.exists(file_path): | ||
| removed_files.append(file_path) | ||
| else: | ||
| # Actually remove the file | ||
| os.remove(file_path) | ||
| removed_files.append(file_path) | ||
| except OSError as e: | ||
| failed_files.append((file_path, str(e))) | ||
shahargolshani marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return removed_files, failed_files | ||
|
|
||
|
|
||
| def main(): | ||
shahargolshani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| module = AnsibleModule( | ||
| argument_spec=dict( | ||
| path=dict(type="path", required=True), | ||
| pattern=dict(type="str", required=True), | ||
| use_regex=dict(type="bool", default=False), | ||
| recursive=dict(type="bool", default=False), | ||
| file_type=dict(type="str", default="file", choices=["file", "link", "any"]), | ||
| ), | ||
| supports_check_mode=True, | ||
| ) | ||
|
|
||
| path = module.params["path"] | ||
| pattern = module.params["pattern"] | ||
| use_regex = module.params["use_regex"] | ||
| recursive = module.params["recursive"] | ||
| file_type = module.params["file_type"] | ||
shahargolshani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # Validate that the path exists and is a directory | ||
| if not os.path.exists(path): | ||
| module.fail_json(msg=f"Path does not exist: {path}") | ||
|
|
||
| if not os.path.isdir(path): | ||
| module.fail_json(msg=f"Path is not a directory: {path}") | ||
|
|
||
| # Validate regex pattern if use_regex is true | ||
| if use_regex: | ||
| try: | ||
| re.compile(pattern) | ||
| except re.error as e: | ||
| module.fail_json(msg=f"Invalid regular expression pattern: {e}") | ||
|
|
||
| # Find matching files | ||
| try: | ||
| matching_files = find_matching_files(path, pattern, use_regex, recursive, file_type) | ||
| except AssertionError as e: | ||
| module.fail_json(msg=str(e)) | ||
shahargolshani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # Prepare diff information | ||
| diff = dict(before=dict(files=matching_files), after=dict(files=[])) | ||
|
|
||
| # Remove the files | ||
| removed_files, failed_files = remove_files(module, matching_files) | ||
|
|
||
| # Prepare result | ||
| changed = len(removed_files) > 0 | ||
|
|
||
| result = dict( | ||
| changed=changed, | ||
| removed_files=removed_files, | ||
| files_count=len(removed_files), | ||
| path=path, | ||
| msg=f"Removed {len(removed_files)} file(s) matching pattern '{pattern}'", | ||
| ) | ||
|
|
||
| # Add diff if in diff mode | ||
| if module._diff: | ||
| result["diff"] = diff | ||
|
|
||
| # Report any failures | ||
| if failed_files: | ||
| failure_msg = "; ".join([f"{f}: {e}" for f, e in failed_files]) | ||
| module.fail_json( | ||
| msg=f"Failed to remove some files: {failure_msg}", | ||
| removed_files=removed_files, | ||
| failed_files=[f for f, e in failed_files], | ||
| ) | ||
|
|
||
| module.exit_json(**result) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Copyright (c) Ansible Project | ||
| # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||
|
|
||
| azp/posix/3 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| # Copyright (c) Ansible Project | ||
| # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||
|
|
||
| file_remove_testdir: "{{ remote_tmp_dir }}/file_remove_tests" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| # Copyright (c) Ansible Project | ||
| # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||
|
|
||
| dependencies: | ||
| - setup_remote_tmp_dir |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| --- | ||
| #################################################################### | ||
| # WARNING: These are designed specifically for Ansible tests # | ||
| # and should not be used as examples of how to write Ansible roles # | ||
| #################################################################### | ||
|
|
||
| # Test code for the file_remove module | ||
| # Copyright (c) Ansible Project | ||
| # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||
|
|
||
| - name: Ensure the test directory is absent before starting | ||
| ansible.builtin.file: | ||
| path: "{{ file_remove_testdir }}" | ||
| state: absent | ||
|
|
||
| - name: Create the test directory | ||
| ansible.builtin.file: | ||
| path: "{{ file_remove_testdir }}" | ||
| state: directory | ||
| mode: '0755' | ||
|
|
||
| - name: Include tasks to test error handling | ||
| include_tasks: test_errors.yml | ||
|
|
||
| - name: Include tasks to test glob pattern matching | ||
| include_tasks: test_glob.yml | ||
|
|
||
| - name: Include tasks to test regex pattern matching | ||
| include_tasks: test_regex.yml | ||
|
|
||
| - name: Include tasks to test recursive removal | ||
| include_tasks: test_recursive.yml | ||
|
|
||
| - name: Include tasks to test different file types | ||
| include_tasks: test_file_types.yml | ||
|
|
||
| - name: Include tasks to test check mode and diff mode | ||
| include_tasks: test_check_diff.yml | ||
shahargolshani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.