Skip to content
Merged
Show file tree
Hide file tree
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 Nov 5, 2025
31765e4
Add fixes from code review
shahargolshani Nov 6, 2025
e1154a4
Change file_type documentation
shahargolshani Nov 6, 2025
296de23
Remove python to_native from the module
shahargolshani Nov 7, 2025
49c8e5b
Remove redundant block/always cleanup
shahargolshani Nov 7, 2025
0f0e2aa
Update plugins/modules/file_remove.py
shahargolshani Nov 16, 2025
d010bcb
Update plugins/modules/file_remove.py
shahargolshani Nov 16, 2025
e8eadc7
Update plugins/modules/file_remove.py
shahargolshani Nov 16, 2025
c9081e3
Update plugins/modules/file_remove.py
shahargolshani Nov 16, 2025
a670163
Update plugins/modules/file_remove.py
shahargolshani Nov 16, 2025
139d9d7
Update plugins/modules/file_remove.py
shahargolshani Nov 16, 2025
8f454da
Update plugins/modules/file_remove.py
shahargolshani Nov 16, 2025
b14fcb3
Update plugins/modules/file_remove.py
shahargolshani Nov 16, 2025
ce9cb03
Add more nox fixes to latest review
shahargolshani Nov 16, 2025
674a3ee
Update plugins/modules/file_remove.py
shahargolshani Nov 16, 2025
32dbf29
Update tests/integration/targets/file_remove/tasks/main.yml
shahargolshani Nov 16, 2025
031aa1a
Fix EXAMPLES regex pattern
shahargolshani Nov 17, 2025
193152e
Add warning when listed file was removed by other process during
shahargolshani Nov 18, 2025
ff3b80c
remove raise exception from find_matching_files;
shahargolshani Nov 18, 2025
f9b9534
Update plugins/modules/file_remove.py
shahargolshani Nov 20, 2025
ba06161
Update plugins/modules/file_remove.py
shahargolshani Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/BOTMETA.yml
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,8 @@ files:
$modules/filesystem.py:
labels: filesystem
maintainers: pilou- abulimov quidame
$modules/file_remove.py:
maintainers: shahargolshani
$modules/flatpak.py:
maintainers: $team_flatpak
$modules/flatpak_remote.py:
Expand Down
290 changes: 290 additions & 0 deletions plugins/modules/file_remove.py
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
sample: 2

path:
description: The directory path that was searched.
type: str
returned: always
sample: /var/log
"""


import os
import re
import glob
from ansible.module_utils.basic import AnsibleModule


def find_matching_files(path, pattern, use_regex, recursive, file_type):
"""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):
"""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):
"""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)))

return removed_files, failed_files


def main():
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"]

# 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))

# 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()
5 changes: 5 additions & 0 deletions tests/integration/targets/file_remove/aliases
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
6 changes: 6 additions & 0 deletions tests/integration/targets/file_remove/defaults/main.yml
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"
7 changes: 7 additions & 0 deletions tests/integration/targets/file_remove/meta/main.yml
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
39 changes: 39 additions & 0 deletions tests/integration/targets/file_remove/tasks/main.yml
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
Loading