Tux

...making Linux just a little more fun!

2-cent tip: A safer 'rm'

Ben Okopnik [ben at linuxgazette.net]


Sun, 16 May 2010 08:49:40 -0400

----- Forwarded message from "Silas S. Brown" <ssb22 at cam.ac.uk> -----

If you've ever tried to delete Emacs backup files with

rm *~

(i.e. remove anything ending with ~), but you accidentally hit Enter before the ~ and did "rm *", you might want to put this in your .bashrc and .bash_profile :

function rm () { if test "a $*" == "a $(echo *)"; then echo "If you really meant that, say -f" else /bin/rm $@; fi }

That way, typing "rm *" will give you a message telling you to use the -f flag if you really meant it, but any other rm command will work. (The "a" in the test is to ensure that any options for "rm" are not read as options for "test".)

(It's also possible to alias rm to rm -i, but that's more annoying as it prompts EVERY time, which is likely to make you habitually type -f and that could be a bad thing.)

Silas

----- End forwarded message -----


Top    Back


Thomas Adam [thomas at xteddy.org]


Sun, 16 May 2010 13:55:36 +0100

On Sun, May 16, 2010 at 08:49:40AM -0400, Ben Okopnik wrote:

> ----- Forwarded message from "Silas S. Brown" <ssb22 at cam.ac.uk> -----
> 
> If you've ever tried to delete Emacs backup files with
> 
> rm *~
> 
> (i.e. remove anything ending with ~), but you
> accidentally hit Enter before the ~ and did "rm *",
> you might want to put this in your .bashrc and .bash_profile :
> 
> function rm () {
>   if test "a $*" == "a $(echo *)"; then
>     echo "If you really meant that, say -f"
>   else /bin/rm $@; fi }

Yet another reason why using zsh is preferred. :)

[n6tadam at shuttle][foo]% rm *
zsh: sure you want to delete all the files in /tmp/foo [yn]? 

-- Thomas Adam

-- "It was the cruelest game I've ever played and it's played inside my head." -- "Hush The Warmth", Gorky's Zygotic Mynci.


Top    Back


Ben Okopnik [ben at linuxgazette.net]


Sun, 16 May 2010 10:13:15 -0400

On Sun, May 16, 2010 at 01:55:36PM +0100, Thomas Adam wrote:

> On Sun, May 16, 2010 at 08:49:40AM -0400, Ben Okopnik wrote:
> > ----- Forwarded message from "Silas S. Brown" <ssb22 at cam.ac.uk> -----
> > 
> > If you've ever tried to delete Emacs backup files with
> > 
> > rm *~
> > 
> > (i.e. remove anything ending with ~), but you
> > accidentally hit Enter before the ~ and did "rm *",
> > you might want to put this in your .bashrc and .bash_profile :
> > 
> > function rm () {
> >   if test "a $*" == "a $(echo *)"; then
> >     echo "If you really meant that, say -f"
> >   else /bin/rm $@; fi }
> 
> Yet another reason why using zsh is preferred.  :)

Well, "preferred" is too strong of a term (unless you qualify it as "preferred by me"); the numbers certainly don't support you. :)

> ``
> [n6tadam at shuttle][foo]% rm *
> zsh: sure you want to delete all the files in /tmp/foo [yn]? 
> ''

Nice. Bash isn't far behind, though:

ben at Jotunheim:~$ alias rm="rm -I"
ben at Jotunheim:~$ rm *
rm: remove all arguments?

-- * Ben Okopnik * Editor-in-Chief, Linux Gazette * http://LinuxGazette.NET *


Top    Back


Ben Okopnik [ben at linuxgazette.net]


Sun, 16 May 2010 10:22:43 -0400

On Sun, May 16, 2010 at 08:49:40AM -0400, Benjamin Okopnik wrote:

> ----- Forwarded message from "Silas S. Brown" <ssb22 at cam.ac.uk> -----
> 
> If you've ever tried to delete Emacs backup files with
> 
> rm *~
> 
> (i.e. remove anything ending with ~), but you
> accidentally hit Enter before the ~ and did "rm *",
> you might want to put this in your .bashrc and .bash_profile :
> 
> function rm () {
>   if test "a $*" == "a $(echo *)"; then
>     echo "If you really meant that, say -f"
>   else /bin/rm $@; fi }

(Note: $@ is the same thing as $* until you put double quotes around it; see "man bash".)

> That way, typing "rm *" will give you a message
> telling you to use the -f flag if you really meant it,
> but any other rm command will work.
> (The "a" in the test is to ensure that any
> options for "rm" are not read as options for "test".)

Unfortunately, mistyping the above as "rm * ~" would still blow away everything. Aside from the 'rm -I' option that I mentioned, you could possibly script around it this way:

function rm () {
    if [ "${#*}" -ge `/bin/ls|/usr/bin/wc -l` ]
    then
        echo "To delete everything, use 'rm -f *'."
    else
        /bin/rm "$@"
    fi
}

This counts the number of entries in the current directory and compares it to the number of entries on the 'rm' command line. If the first is the same or greater than the second, it spits out the message; otherwise, it passes the list to the actual "rm" program.

-- * Ben Okopnik * Editor-in-Chief, Linux Gazette * http://LinuxGazette.NET *


Top    Back


Henry Grebler [henrygrebler at optusnet.com.au]


Tue, 18 May 2010 19:06:46 +1000

-->----- Forwarded message from "Silas S. Brown" <ssb22 at cam.ac.uk> ----- --> -->If you've ever tried to delete Emacs backup files with --> -->rm *~ --> -->(i.e. remove anything ending with ~), but you -->accidentally hit Enter before the ~ and did "rm *", -->you might want to put this in your .bashrc and .bash_profile :

<snip>

I enclose my solution to the problem. It will come as no surprise to those who know me that it is overkill. Nonetheless, I have used it for some time and have found it quite satisfactory. It deletes emacs backup files anywhere in the tree under the current working directory.

Further, because I have an interest in forensic analysis, I have not deleted the original incarnation (which, of course, is not executed). Anyone adopting (some of) my solution can delete the "exit" and all that follows.

It could have been created as a function, but perhaps back then I did not know about or understand functions.

The shell script is invoked from an alias:

alias cleanup $HOME/scripts/cleanup.sh

------------------------------------------------------------------------
#!/bin/sh
#	cleanup.sh - remove all backup files (*~ .*~)
 
#   2 Jun 05  Henry Grebler    Use printf rather than echo. Change default to
#				yes.
#   9 Jul 93  Henry Grebler    First cut.
#=============================================================================#
 
	PATH=/usr/5bin:$PATH
 
	matches=`find . '(' -name \.\*~ -o -name \*~ ')' -print`
	if [ "$matches" = "" ]
	then
		echo No files to cleanup.
		exit
	fi
	for i in $matches
	do
		echo $i
	done
	echo 'Use n, N or ^C for no'
	printf 'The above files will be deleted. Ok? [y] '
	read ans
	case "$ans" in
		n*|N*)		echo "Operation abandoned"
				exit
				;;
	esac
	rm $matches
	echo Done
 
exit
#  9 Jul 93 V2	Call find once (instead of 4 times).
#		Don't do anything if no matches.
 
#	    V1	First cut.
	PATH=/usr/5bin:$PATH
 
	find . -name \.\*~ -print | more
	find . -name \*~ -print | more
	echo 'The above files will be deleted. Ok? [n] \c'
	read ans
	if [ "$ans" != "y" ]
	then
		echo "Operation abandoned"
		exit
	fi
	find . -name \.\*~ -print -exec rm {} \;
	find . -name \*~ -print -exec rm {} \;
	echo Done
 
------------------------------------------------------------------------


Top    Back


Thomas Adam [thomas at xteddy.org]


Tue, 18 May 2010 10:46:26 +0100

On Tue, May 18, 2010 at 07:06:46PM +1000, Henry Grebler wrote:

> 	matches=`find . '(' -name \.\*~ -o -name \*~ ')' -print`
> 	if [ "$matches" = "" ]
> 	then
> 		echo No files to cleanup.
> 		exit
> 	fi
> 	for i in $matches
> 	do
> 		echo $i
> 	done
> 	echo 'Use n, N or ^C for no'
> 	printf 'The above files will be deleted. Ok? [y] '
> 	read ans
> 	case "$ans" in
> 		n*|N*)		echo "Operation abandoned"
> 				exit
> 				;;
> 	esac
> 	rm $matches
^^^^^^^^^^^

Ah. That's no good. What if $matches has special characters? Without quoting it, $matches is guaranteed to undergo word-splitting. You mean of course:

rm "$matches"

... and the same goes for any variable really. Use *more* quotes. Always!

Also, there's a more efficient way of doing this in general -- rather than relying on quoting, just get find to NUL-terminate the files.

find . \( -name '*~' -o -name '.*~' \) -print0 | xargs -0 rm

-- Thomas Adam


Top    Back


Henry Grebler [henrygrebler at optusnet.com.au]


Tue, 18 May 2010 23:25:40 +1000

>> 	rm $matches
>    ^^^^^^^^^^^
>
>Ah.  That's no good.  What if $matches has special characters?   Without
>quoting it, $matches is guaranteed to undergo word-splitting.  You mean of
>course:
>
>``
>rm "$matches"
>''
>
>... and the same goes for any variable really.  Use *more* quotes.
>Always!

1. Your suggestion won't work:

	touch /tmp/qqqq /tmp/qqqr /tmp/qqqs 
	find /tmp | grep qqq
/tmp/qqqq
/tmp/qqqr
/tmp/qqqs
	aaa=`find /tmp | grep qqq`
	echo $aaa
/tmp/qqqq /tmp/qqqr /tmp/qqqs
	rm "$aaa"
rm: /tmp/qqqq
/tmp/qqqr
/tmp/qqqs: No such file or directory

2. "cleanup" is for me. It is used to clean up files I have been editing with emacs. Such files NEVER contain special characters. (Why would they?)

I cannot recall any occasion when cleanup.sh has caused me grief.

>Also, there's a more efficient way of doing this in general -- rather than
>relying on quoting, just get find to NUL-terminate the files.
>
>``
>find . \( -name '*~' -o -name '.*~' \) -print0 | xargs -0 rm
>''

The above deletes without prompting.

I'm an extremely cautious person. Before I delete, I display the files that will be deleted. Before the delete takes place, I have to confirm. (I also wear a belt and braces :-).

But I understand that my way is not to everyone's taste.

BTW, efficiency is not really an issue. The big gain for me in efficiency was going from 4 finds to one. I only found that out when I invoked "cleanup" on a very big directory tree. After that, the object of the exercise (for me) was to not inadvertently delete the wrong things. Hence the checking. And hence, originally, the default on the prompt to NOT delete. I had to enter exactly "y" for the delete to occur.

I felt quite brave when I changed the default from "do not delete" to "delete" - after 8 years!


Top    Back


Ben Okopnik [ben at linuxgazette.net]


Tue, 18 May 2010 11:15:40 -0400

On Tue, May 18, 2010 at 11:25:40PM +1000, Henry Grebler wrote:

> Thomas Adam wrote:
> 
> -->> 	rm $matches
> -->    ^^^^^^^^^^^
> -->
> -->Ah.  That's no good.  What if $matches has special characters?   Without
> -->quoting it, $matches is guaranteed to undergo word-splitting.  You mean of
> -->course:
> -->
> -->``
> -->rm "$matches"
> -->''
> -->
> -->... and the same goes for any variable really.  Use *more* quotes.
> -->Always!

Whoops. Except, maybe, in this case.

In general, I agree with Thomas: most shell scripts are missing a significant number of critically-necessary quotes, making them quite fragile. However, in the above situation, $match contains multiple strings - and given the way that shells generally concatenate strings, you already have a problem. Consider:

ben at Jotunheim:/tmp/x$ touch a b c\ d
ben at Jotunheim:/tmp/x$ files=`find -type f`
ben at Jotunheim:/tmp/x$ echo $files
./c d ./b ./a

At this point, quotes will not help a whole lot.

> 2. "cleanup" is for me. It is used to clean up files I have been
> editing with emacs. Such files   NEVER   contain special characters.
> (Why would they?)
> 
> I cannot recall any occasion when cleanup.sh has caused me grief.
Well, wait a minute. Since we're doing this in TAG, which is intended to be exposed to a rather broad readership, that's not a reasonable standard. At that point, we might as well go back and say "well, I always double-check my typing before hitting 'Enter', so this tip isn't even worth talking about!" But that's not the case here; what we're trying to do is inform that readership about the issues that exist as well as the broader issues around them - so saying "it works for me" is insufficient.

Between you and Thomas, you've exposed a rather important problem that comes up in shell scripting - and it would be a good idea to show how to deal with that problem.

The solution that Thomas proposed - the null-terminated '-print0' and "xargs" - is a pretty good one, but somewhat limited in that you can't break into that loop (e.g., to print the names of the files, as you do). As a result, showing a listing of all the files takes an extra 'find' command (instead of reusing the results.) Yours handles the above by storing a list of files - but it's not very friendly (i.e., will fail) with odd filenames. So, we need something that will slide between those two.

How about this:

#!/bin/bash
# Created by Ben Okopnik on Tue May 18 10:52:20 EDT 2010
 
files=$( find $PWD \( -name '*~' -o -name '.*~' \) )
 
read -p "Enter 'y' to delete the following files:
$files
"
 
echo $REPLY|grep -qi '^y$'
[ "$?" -eq 0 ] || exit
 
# Change the IFS to a single '\n'
IFS='
'
 
for f in $files
do
    rm "$f"
done
> I'm an extremely cautious person. Before I delete, I display the files
> that will be deleted. Before the delete takes place, I have to confirm.
> (I also wear a belt and braces :-).

That's a good idea, in general. I like using "echo" instead of "rm" or even instead of "mv" in my scripts while I'm testing them; it's prevented much suffering.

-- * Ben Okopnik * Editor-in-Chief, Linux Gazette * http://LinuxGazette.NET *


Top    Back