Skip to content

Commit 5b8e67c

Browse files
Add New Module file_remove
1 parent 980e8e2 commit 5b8e67c

File tree

12 files changed

+1005
-0
lines changed

12 files changed

+1005
-0
lines changed

.github/BOTMETA.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,8 @@ files:
592592
$modules/filesystem.py:
593593
labels: filesystem
594594
maintainers: pilou- abulimov quidame
595+
$modules/file_remove.py:
596+
maintainers: shahargolshani
595597
$modules/flatpak.py:
596598
maintainers: $team_flatpak
597599
$modules/flatpak_remote.py:

plugins/modules/file_remove.py

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
# SPDX-License-Identifier: GPL-3.0-or-later
6+
7+
from __future__ import absolute_import, division, print_function
8+
__metaclass__ = type
9+
10+
11+
DOCUMENTATION = r"""
12+
module: file_remove
13+
14+
short_description: Remove files matching a pattern from a directory
15+
16+
description:
17+
- This module removes files from a specified directory that match a given pattern.
18+
- The pattern can include wildcards and regular expressions.
19+
- By default, only files in the specified directory are removed (non-recursive).
20+
- Use the O(recursive) option to search and remove files in subdirectories.
21+
22+
author:
23+
- Shahar Golshani (@shahargolshani)
24+
25+
attributes:
26+
check_mode:
27+
description: Can run in check_mode and return changed status without modifying the target.
28+
support: full
29+
diff_mode:
30+
description: Will return details on what has changed (or possibly needs changing in check_mode).
31+
support: full
32+
33+
options:
34+
path:
35+
description:
36+
- Path to the directory where files should be removed.
37+
- This must be an existing directory.
38+
type: path
39+
required: true
40+
41+
pattern:
42+
description:
43+
- Pattern to match files for removal.
44+
- Supports wildcards (*, ?, [seq], [!seq]) for glob-style matching.
45+
- Use O(use_regex=true) to interpret this as a regular expression instead.
46+
type: str
47+
required: true
48+
49+
use_regex:
50+
description:
51+
- If V(true), O(pattern) is interpreted as a regular expression.
52+
- If V(false), O(pattern) is interpreted as a glob-style wildcard pattern.
53+
type: bool
54+
default: false
55+
56+
recursive:
57+
description:
58+
- If V(true), search for files recursively in subdirectories.
59+
- If V(false), only files in the specified directory are removed.
60+
type: bool
61+
default: false
62+
63+
file_type:
64+
description:
65+
- Type of files to remove.
66+
- V(file) - remove only regular files.
67+
- V(link) - remove only symbolic links.
68+
- V(any) - remove both files and symbolic links.
69+
type: str
70+
choices: ['file', 'link', 'any']
71+
default: file
72+
73+
notes:
74+
- Directories are never removed by this module, only files and optionally symbolic links.
75+
- This module will not follow symbolic links when O(recursive=true).
76+
- Be careful with patterns that might match many files, especially with O(recursive=true).
77+
"""
78+
79+
EXAMPLES = r"""
80+
- name: Remove all log files from /var/log
81+
community.general.file_remove:
82+
path: /var/log
83+
pattern: "*.log"
84+
85+
- name: Remove all temporary files recursively
86+
community.general.file_remove:
87+
path: /tmp/myapp
88+
pattern: "*.tmp"
89+
recursive: true
90+
91+
- name: Remove files matching a regex pattern
92+
community.general.file_remove:
93+
path: /data/backups
94+
pattern: 'backup_[0-9]{8}\.tar\.gz'
95+
use_regex: true
96+
97+
- name: Remove both files and symbolic links
98+
community.general.file_remove:
99+
path: /opt/app/cache
100+
pattern: "cache_*"
101+
file_type: any
102+
103+
- name: Remove all files starting with 'test_' (check mode)
104+
community.general.file_remove:
105+
path: /home/user/tests
106+
pattern: "test_*"
107+
check_mode: true
108+
"""
109+
110+
RETURN = r"""
111+
removed_files:
112+
description: List of files that were removed.
113+
type: list
114+
elements: str
115+
returned: always
116+
sample: ['/var/log/app.log', '/var/log/error.log']
117+
118+
files_count:
119+
description: Number of files removed.
120+
type: int
121+
returned: always
122+
sample: 2
123+
124+
msg:
125+
description: Status message.
126+
type: str
127+
returned: always
128+
sample: "Removed 2 files matching pattern '*.log'"
129+
130+
path:
131+
description: The directory path that was searched.
132+
type: str
133+
returned: always
134+
sample: /var/log
135+
"""
136+
137+
138+
import os
139+
import re
140+
import glob
141+
from ansible.module_utils.basic import AnsibleModule
142+
from ansible.module_utils.common.text.converters import to_native
143+
144+
145+
def find_matching_files(path, pattern, use_regex, recursive, file_type):
146+
"""Find all files matching the pattern in the given path."""
147+
matching_files = []
148+
149+
if use_regex:
150+
# Use regular expression matching
151+
regex = re.compile(pattern)
152+
if recursive:
153+
for root, dirs, files in os.walk(path, followlinks=False):
154+
for filename in files:
155+
if regex.match(filename) or regex.search(filename):
156+
full_path = os.path.join(root, filename)
157+
if should_include_file(full_path, file_type):
158+
matching_files.append(full_path)
159+
else:
160+
try:
161+
for filename in os.listdir(path):
162+
if regex.match(filename) or regex.search(filename):
163+
full_path = os.path.join(path, filename)
164+
if should_include_file(full_path, file_type):
165+
matching_files.append(full_path)
166+
except OSError as e:
167+
raise AssertionError(f"Failed to list directory {path}: {to_native(e)}")
168+
else:
169+
# Use glob pattern matching
170+
if recursive:
171+
glob_pattern = os.path.join(path, '**', pattern)
172+
matching_files = [
173+
f for f in glob.glob(glob_pattern, recursive=True)
174+
if should_include_file(f, file_type)
175+
]
176+
else:
177+
glob_pattern = os.path.join(path, pattern)
178+
matching_files = [
179+
f for f in glob.glob(glob_pattern)
180+
if should_include_file(f, file_type)
181+
]
182+
183+
return sorted(matching_files)
184+
185+
186+
def should_include_file(file_path, file_type):
187+
"""Determine if a file should be included based on its type."""
188+
# Never include directories
189+
if os.path.isdir(file_path):
190+
return False
191+
192+
is_link = os.path.islink(file_path)
193+
is_file = os.path.isfile(file_path)
194+
195+
if file_type == 'file':
196+
# Only regular files, not symlinks
197+
return is_file and not is_link
198+
elif file_type == 'link':
199+
# Only symbolic links
200+
return is_link
201+
elif file_type == 'any':
202+
# Both files and symlinks
203+
return is_file or is_link
204+
205+
return False
206+
207+
208+
def remove_files(module, files):
209+
"""Remove the specified files and return results."""
210+
removed_files = []
211+
failed_files = []
212+
213+
for file_path in files:
214+
try:
215+
if module.check_mode:
216+
# In check mode, just verify the file exists
217+
if os.path.exists(file_path):
218+
removed_files.append(file_path)
219+
else:
220+
# Actually remove the file
221+
os.remove(file_path)
222+
removed_files.append(file_path)
223+
except OSError as e:
224+
failed_files.append((file_path, to_native(e)))
225+
226+
return removed_files, failed_files
227+
228+
229+
def main():
230+
module = AnsibleModule(
231+
argument_spec=dict(
232+
path=dict(type='path', required=True),
233+
pattern=dict(type='str', required=True),
234+
use_regex=dict(type='bool', default=False),
235+
recursive=dict(type='bool', default=False),
236+
file_type=dict(type='str', default='file', choices=['file', 'link', 'any']),
237+
),
238+
supports_check_mode=True,
239+
)
240+
241+
path = module.params['path']
242+
pattern = module.params['pattern']
243+
use_regex = module.params['use_regex']
244+
recursive = module.params['recursive']
245+
file_type = module.params['file_type']
246+
247+
# Validate that the path exists and is a directory
248+
if not os.path.exists(path):
249+
module.fail_json(msg=f"Path does not exist: {path}")
250+
251+
if not os.path.isdir(path):
252+
module.fail_json(msg=f"Path is not a directory: {path}")
253+
254+
# Validate regex pattern if use_regex is true
255+
if use_regex:
256+
try:
257+
re.compile(pattern)
258+
except re.error as e:
259+
module.fail_json(msg=f"Invalid regular expression pattern: {to_native(e)}")
260+
261+
# Find matching files
262+
try:
263+
matching_files = find_matching_files(path, pattern, use_regex, recursive, file_type)
264+
except AssertionError as e:
265+
module.fail_json(msg=to_native(e))
266+
267+
# Prepare diff information
268+
diff = dict(
269+
before=dict(files=matching_files),
270+
after=dict(files=[])
271+
)
272+
273+
# Remove the files
274+
removed_files, failed_files = remove_files(module, matching_files)
275+
276+
# Prepare result
277+
changed = len(removed_files) > 0
278+
279+
result = dict(
280+
changed=changed,
281+
removed_files=removed_files,
282+
files_count=len(removed_files),
283+
path=path,
284+
msg=f"Removed {len(removed_files)} file(s) matching pattern '{pattern}'",
285+
)
286+
287+
# Add diff if in diff mode
288+
if module._diff:
289+
result['diff'] = diff
290+
291+
# Report any failures
292+
if failed_files:
293+
failure_msg = "; ".join([f"{f}: {e}" for f, e in failed_files])
294+
module.fail_json(
295+
msg=f"Failed to remove some files: {failure_msg}",
296+
removed_files=removed_files,
297+
failed_files=[f for f, e in failed_files]
298+
)
299+
300+
module.exit_json(**result)
301+
302+
303+
if __name__ == '__main__':
304+
main()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copyright (c) Ansible Project
2+
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
azp/posix/3
6+
destructive
7+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
# Copyright (c) Ansible Project
3+
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
4+
# SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
file_remove_testdir: "{{ remote_tmp_dir }}/file_remove_tests"
7+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
# Copyright (c) Ansible Project
3+
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
4+
# SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
dependencies:
7+
- setup_remote_tmp_dir
8+
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
####################################################################
3+
# WARNING: These are designed specifically for Ansible tests #
4+
# and should not be used as examples of how to write Ansible roles #
5+
####################################################################
6+
7+
# Test code for the file_remove module
8+
# Copyright (c) Ansible Project
9+
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
10+
# SPDX-License-Identifier: GPL-3.0-or-later
11+
12+
- name: Ensure the test directory is absent before starting
13+
ansible.builtin.file:
14+
path: "{{ file_remove_testdir }}"
15+
state: absent
16+
17+
- name: Create the test directory
18+
ansible.builtin.file:
19+
path: "{{ file_remove_testdir }}"
20+
state: directory
21+
mode: '0755'
22+
23+
- name: Run all tests and clean up afterwards
24+
block:
25+
- name: Include tasks to test error handling
26+
include_tasks: test_errors.yml
27+
28+
- name: Include tasks to test glob pattern matching
29+
include_tasks: test_glob.yml
30+
31+
- name: Include tasks to test regex pattern matching
32+
include_tasks: test_regex.yml
33+
34+
- name: Include tasks to test recursive removal
35+
include_tasks: test_recursive.yml
36+
37+
- name: Include tasks to test different file types
38+
include_tasks: test_file_types.yml
39+
40+
- name: Include tasks to test check mode and diff mode
41+
include_tasks: test_check_diff.yml
42+
43+
always:
44+
- name: Remove test directory
45+
ansible.builtin.file:
46+
path: "{{ file_remove_testdir }}"
47+
state: absent
48+

0 commit comments

Comments
 (0)