xref: /illumos-gate/usr/src/test/util-tests/tests/rm/rm-test.ksh (revision 2aa8db5932a99c01d32f2aea7dbbf15b4898169b)
1#!/usr/bin/ksh
2#
3# This file and its contents are supplied under the terms of the
4# Common Development and Distribution License ("CDDL"), version 1.0.
5# You may only use this file in accordance with the terms of version
6# 1.0 of the CDDL.
7#
8# A full copy of the text of the CDDL should have accompanied this
9# source.  A copy of the CDDL is also available via the Internet at
10# http://www.illumos.org/license/CDDL.
11#
12
13#
14# Copyright 2026 Oxide Computer Company
15#
16
17#
18# Various tests for rm(1).
19#
20
21unalias -a
22set -o pipefail
23export LANG=C.UTF-8
24
25RM=${RM:-"/usr/bin/rm"}
26XRM=${XRM:-"/usr/xpg4/bin/rm"}
27
28rm_exit=0
29rm_arg0=$(basename $0)
30rm_work="/tmp/$rm_arg0.$$"
31
32function fatal
33{
34	typeset msg="$*"
35	echo "TEST FAILED: $msg" >&2
36	exit 1
37}
38
39function warn
40{
41	typeset msg="$*"
42	echo "TEST FAILED: $msg" >&2
43	rm_exit=1
44}
45
46function cleanup
47{
48	rm -rf $rm_work/
49}
50
51function setup
52{
53	mkdir "$rm_work" || fatal "failed to create directory $rm_work"
54	touch "$rm_work/file0" "$rm_work/file1" "$rm_work/file2" || \
55	    fatal "failed to make files"
56	mkdir "$rm_work/dir" || fatal "failed to create $rm_work/dir"
57	mkdir -p "$rm_work/a/b/c" || fatal "failed to create recursive dirs"
58	touch "$rm_work/a/1" "$rm_work/a/b/2" "$rm_work/a/b/c/3" || \
59	    fatal "failed to create files"
60	touch "$rm_work/noperm" || fatal "failed to make file"
61	chmod 0000 "$rm_work/noperm" || fatal "failed to chmod $rm_work/noperm"
62	mkdir "$rm_work/nodir"  || fatal "failed to make $rm_work/nodir"
63	chmod 0000 "$rm_work/nodir" || fatal "failed to chmod $rm_work/nodir"
64}
65
66#
67# Attempt an rm command that should fail.
68#
69function test_fail
70{
71	typeset desc="$1"
72	typeset exp="$2"
73	typeset out=
74	shift; shift
75
76	out=$($* 2>&1 1>/dev/null)
77	if (( $? == 0 )); then
78		warn "$desc: $* worked, but expected it to fail with $exp"
79		return
80	fi
81
82	if ! [[ "$out" =~ "$exp" ]]; then
83		warn "$desc: failed with $out, but wanted $exp"
84		return
85	fi
86
87	printf "TEST PASSED: %s\n" "$desc"
88}
89
90#
91# Verify all files listed have been removed from this world.
92#
93function check_gone
94{
95	typeset desc="$1"
96	typeset fail=0
97	typeset f
98	shift
99
100	for f in "$@"; do
101		if [[ -e $f ]]; then
102			warn "$desc: expected $f to be removed, but it still" \
103			    "exists"
104			fail=1
105		fi
106	done
107
108	return $fail
109}
110
111#
112# Variant of the above, but existence
113#
114function check_exists
115{
116	typeset desc="$1"
117	typeset fail=0
118	typeset f
119	shift
120
121	for f in "$@"; do
122		if ! [[ -e $f ]]; then
123			warn "$desc: expected $f to be present, but it was" \
124			    "removed"
125			fail=1
126		fi
127	done
128
129	return $fail
130}
131
132#
133# Run a basic rm command. It should pass. All arguments listed on the command line
134# should not exist, though some of them may never have. It should not require
135# input, but we redirect stdin to /dev/null regardless.
136#
137function test_rm
138{
139	typeset desc="$1"
140	typeset f
141	shift
142
143	if ! $* >/dev/null </dev/null; then
144		warn "$desc: rm failed, but expected success"
145		return
146	fi
147
148	#
149	# Remove any options.
150	#
151	shift
152	if [[ "$1" =~ -[A-Za-z]+ ]]; then
153		shift
154	fi
155
156	if ! check_gone "$desc" "$@"; then
157		return
158	fi
159
160	printf "TEST PASSED: %s\n" "$desc"
161}
162
163#
164# A variant of the above, but after executing all files should remain.
165#
166function test_remain
167{
168	typeset desc="$1"
169	shift
170
171	if ! $* >/dev/null 2>/dev/null </dev/null; then
172		warn "$desc: rm failed, but expected success"
173		return
174	fi
175
176	#
177	# Remove any options.
178	#
179	shift
180	if [[ "$1" =~ -[A-Za-z]+ ]]; then
181		shift
182	fi
183
184	if ! check_exists "$desc" "$@"; then
185		return
186	fi
187
188	printf "TEST PASSED: %s\n" "$desc"
189}
190
191#
192# Variant of the above where we say "yes".
193#
194function test_yes
195{
196	typeset desc="$1"
197	typeset ret
198	shift
199
200	#
201	# We need to disable pipefail briefly so the failure of the yes command
202	# doesn't lead rm to accidentally fail.
203	#
204	set +o pipefail
205	yes | $* >/dev/null 2>/dev/null
206	ret=$?
207	set -o pipefail
208	if (( ret != 0 )); then
209		warn "$desc: rm failed, but expected success"
210		return
211	fi
212
213	#
214	# Remove any options.
215	#
216	shift
217	if [[ "$1" =~ -[A-Za-z]+ ]]; then
218		shift
219	fi
220
221	if ! check_gone "$desc" "$@"; then
222		return
223	fi
224
225	printf "TEST PASSED: %s\n" "$desc"
226
227}
228
229#
230# Run an rm command that should produce verbose output. We employ sort here to
231# put the files in a deterministic order so that way if we ever end up with
232# multiple files in a directory we aren't subject to the dirent iteration order.
233#
234function test_verbose
235{
236	typeset desc="$1"
237	typeset exp="$2"
238	typeset out
239	shift; shift
240
241	out=$($* | sort)
242	if (( $? != 0 )); then
243		warn "$desc: rm failed, but expected success"
244		return
245	fi
246
247	shift
248	if [[ "$1" =~ -[A-Za-z]+ ]]; then
249		shift
250	fi
251
252	if ! check_gone "$desc" "$@"; then
253		return
254	fi
255
256	if [[ "$out" != "$exp" ]]; then
257		warn "expected verbose output $exp, found $out"
258		return
259	fi
260
261	printf "TEST PASSED: %s\n" "$desc"
262}
263
264trap cleanup EXIT
265
266#
267# Ensure that we're not running as uid 0 to minimize any chance of some of the
268# DAC privileges.
269#
270if (( $(id -u) == 0 )); then
271	echo "This test should be run as a non-privileged user like nobody." >&2
272	exit 1
273fi
274
275#
276# First test various failure scenarios.
277#
278for f in "$RM" "$XRM"; do
279	test_fail "$f with no arguments" "usage: rm" $f
280	test_fail "$f with bad arg -@" "illegal option -- @" $f -@
281	test_fail "$f on non-existent file fails" "No such file or directory" \
282	    $f "$rm_work/enoent"
283done
284
285#
286# Verify basic rm activity
287#
288for f in "$RM" "$XRM"; do
289	cleanup; setup
290	test_rm "$f basic file" $f "$rm_work/file0"
291	test_rm "$f multiple files" $f "$rm_work/file1" "$rm_work/file2"
292	test_rm "$f recursive dir" $f -r "$rm_work/a"
293	cleanup ; setup
294	test_rm "$f files and dirs" $f -r "$rm_work/a" "$rm_work/dir" \
295	    "$rm_work/file0" "$rm_work/file1" "$rm_work/file2"
296done
297
298#
299# Verify rm removes other operands even if one fails. Similarly, no error on -f.
300#
301for f in "$RM" "$XRM"; do
302	cleanup; setup
303	test_fail "$f fails with present/missing files" \
304	    "No such file or directory" $f "$rm_work/file0" "$rm_work/ENOENT" \
305	    "$rm_work/file1" "$rm_work/file2"
306	check_gone "$f removed other files after ENOENT" "$rm_work/file0" \
307	    "$rm_work/file1" "$rm_work/file2"
308	cleanup; setup
309	test_rm "$f -f passes with present/missing files" $f -f \
310	    "$rm_work/file0" "$rm_work/ENOENT" "$rm_work/file1" \
311	    "$rm_work/file2"
312done
313
314#
315# Test our behavior on directories: rm should fail on an empty directory or a
316# directory with files. Next, rm -d should work on an empty directory; however,
317# it should fail on a non-empty directory.
318#
319for f in "$RM" "$XRM"; do
320	cleanup; setup
321	test_fail "$f fails on empty directory" "is a directory" $f \
322	    "$rm_work/dir"
323	test_fail "$f fails on non-empty directory" "is a directory" $f \
324	    "$rm_work/a"
325	test_rm "$f -d removes empty directory" $f -d "$rm_work/dir"
326	test_fail "$f -d doesn't remove non-empty directory" \
327	    "Directory not empty" $f -d "$rm_work/a"
328	test_fail "$f -df doesn't remove non-empty directory" \
329	    "Directory not empty" $f -df "$rm_work/a"
330done
331
332#
333# rm -i doesn't remove files when told no.
334#
335for f in "$RM" "$XRM"; do
336	cleanup; setup
337	test_remain "$f -i answer no 1 file" $f -i "$rm_work/file0"
338	test_remain "$f -i answer no multi file" $f -i "$rm_work/file0" \
339	    "$rm_work/file1" "$rm_work/file2"
340	test_remain "$f -ri answer no" $f -ri "$rm_work/file0" "$rm_work/dir" \
341	    "$rm_work/a"
342done
343
344#
345# xpg4 rm has -i and -f as the last one wins. /usr/bin/rm has -i trump.
346#
347cleanup; setup
348test_remain "$RM -i overrides all -f" $RM -fif "$rm_work/file0"
349test_remain "$XRM last -if wins (-i)" $XRM -fifi "$rm_work/file0"
350test_rm "$XRM last -if wins (-f)" $XRM -fif "$rm_work/file0"
351
352#
353# Now, test cases where we say yes to -i and verify that everything is gone.
354#
355for f in "$RM" "$XRM"; do
356	cleanup; setup
357	test_yes "$f -i answer yes 1 file" $f -i "$rm_work/file0"
358	test_yes "$f -i answer yes two file" $f -i "$rm_work/file1" \
359	    "$rm_work/file2"
360	cleanup; setup
361	test_yes "$f -ri answer yes, multi" $f -ri "$rm_work/file1" \
362	    "$rm_work/file2" "$rm_work/dir" "$rm_work/a"
363done
364
365#
366# Now onto rm's behavior when there are no write permissions. rm with no flags
367# will prompt for this if we're on a tty, but not otherwise. -i will prompt. -f
368# will never prompt. Do the same with an empty dir.
369#
370for f in "$RM" "$XRM"; do
371	cleanup; setup
372	test_remain "$f -i no perms" $f -i "$rm_work/noperm"
373	test_rm "$f no perms (!tty)" $f "$rm_work/noperm"
374	cleanup; setup
375	test_yes "$f -i no perms (yes)" $f -i "$rm_work/noperm"
376	cleanup; setup
377	test_rm "$f -f no perms" $f -f "$rm_work/noperm"
378	cleanup; setup
379	test_remain "$f -di no perms dir" $f -di "$rm_work/nodir"
380	test_rm "$f -d no perms dir (!tty)" $f -d "$rm_work/nodir"
381	cleanup; setup
382	test_yes "$f -di no perms dir (yes)" $f -di "$rm_work/nodir"
383	cleanup; setup
384	test_rm "$f -df no perms dir" $f -df "$rm_work/nodir"
385
386done
387
388#
389# Finally, rm says it will fail and always print a message if it encounters a
390# directory without write permission in rm -rf.
391#
392for f in "$RM" "$XRM"; do
393	cleanup; setup
394	chmod 0000 "$rm_work/a/b" || fatal "failed to make chmod $rm_work/a/b"
395	test_fail "$f -rf non-empty non-writable dir" "Permission denied" \
396	    $f -rf "$rm_work/a"
397	chmod 755 "$rm_work/a/b" || fatal "failed to restore $rm_work/a/b"
398done
399
400#
401# Different tests for -v.
402#
403for f in "$RM" "$XRM"; do
404	cleanup; setup
405	test_verbose "$f -v single file" "$rm_work/file0" $f -v "$rm_work/file0"
406	exp=$(find $rm_work/file1 $rm_work/file2 | sort)
407	test_verbose "$f -v multiple files" "$exp" $f \
408	    -v "$rm_work/file2" "$rm_work/file1"
409	exp=$(find "$rm_work/noperm" "$rm_work/dir" "$rm_work/a" | sort)
410	test_verbose "$f -rvf multiple" "$exp" $f -rvf "$rm_work/noperm" \
411	    "$rm_work/dir" "$rm_work/a"
412done
413
414if (( rm_exit == 0 )); then
415	printf "All tests passed successfully\n"
416fi
417exit $rm_exit
418