# Disallow rm to remove *

Note: If you googled this by the title of this question, don't use this script unless you know what it is supposed to do.

This is a script in bash 3+ that I have used for long for preventing rm * and rm -rf * from accidentally invoked and removing important files by mistake. I put it in my ~/.bash_aliases.

alias rm='set -f;rm'
rm(){
if [[ "$-" == *i* ]] then if [ "$1" = "*" ] || [ "$2" = "*" ] || [ "$1" = "./*" ] || [ "$2" = "./*" ] then echo "Abort: refusing to remove *, please go to the parent folder and do rm <folder_name>/*" 1>&2 set +f return 1 fi fi set +f /bin/rm -i$@
}

set +f


I would like to know whether there are any vulnerabilities and whether it can be improved.

The vulnerability I see is that only the first two arguments are checked. You could check all of them:

rm() {
[[ $- == *i* ]] && for arg do if [[$arg = "*" || $arg = "./*" ]] then # abort fi done # do the rm  This kind of checking will miss other dangerous wildcards like ** or ?*. You can get safer checking by expanding * yourself, then see if the expanded arguments contain that same list of files: # alias not needed here; we want globs to be expanded rm() { declare -a star=(*) declare -a dotslashstar=(./*) if [[ "$@" == *"${star[@]}"* || "$@" == *"${dotslashstar[@]}"* ]] then # abort  ... but then you can't (for example) empty a directory full of temp files with rm *.tmp, if *.tmp and * match the same thing. • I love your idea of expanding * myself. – Weijun Zhou Mar 9 at 3:20 ### Double-quote variables used in command parameters This is a bug: /bin/rm -i$@


What will happen if you try to delete file a b (with space in the name)? Most likely this:

rm: a: No such file or directory
rm: b: No such file or directory


Always write "$@" instead of unquoted $@.

Unfortunately, as you pointed out in a comment, this will cause another problem: arguments containing globs will be taken literally. In short, it's difficult to have the cake and eat it too.

You could mitigate the problem by looping over the arguments, and if you detect a glob, then expand it yourself:

for arg; do
if [[ $arg == *[*?]* ]]; then expanded=($arg)
echo rm -i "${expanded[@]}" else echo rm -i "$arg"
fi
done


This still won't be perfect, because it doesn't handle the case when an argument contains both spaces and globs. A robust solution would take more effort, and not worth doing in Bash. (See this example delegating the hard work to Python.)

### Use command to bypass aliases

Don't worry about the absolute path of commands. Use command to bypass aliases:

command rm -i "$@"  ### Redundant file descriptors In echo "Abort: ..." 1>&2, the file descriptor 1 is redundant, you can safely omit it. ### Preserving the user's environment This is a minor nitpick. When the alias is executed, it will do set +f, regardless of whatever was the original setting in the shell, which may not be the same. This is really just a minor nitpick, for the record. I wouldn't care about this tiny impractical detail either. • I had an issue with patterns containing asterisk (e.g. rm -f .*.swp) and that's why I didn't quote $@. Your other advices are taken. – Weijun Zhou Mar 9 at 15:34
• @WeijunZhou That's a good point. See my updated answer. – janos Mar 10 at 7:21

## Don't call your function rm

If you start depending on this safety net, you'll eventually have an accident on a system with a standard rm (e.g. when you become root and find yourself using dash for admin tasks).

I'd suggest

weijun_rm() {
}

rm() {
return 1
}


This will train you not to use rm for deleting files. When your function isn't available and you have to use the real rm, you will be on alert and extra careful to check the arguments.

• Thank you for your suggestion. This is just an extra protection. I don't depend on this and I know what I am doing in most cases. Just in case I didn't get enough sleep and do sth silly ... – Weijun Zhou Mar 11 at 14:06
• I guess anyone can accidentally brush Enter when typing rm *~ or rm *.o or the like (I normally write echo, and only type the rm when I'm happy with the rest). But perhaps my advice might be useful for others who aren't so disciplined. – Toby Speight Mar 11 at 14:45

This is the version I am using after taking the advance of above answers. Only the core if is written.

if [[ $- == *i* ]] then set +f for arg in "$@"
do # We want to abort the whole command even if only some of the arguments contain dangerous patterns
if [[ "$arg" == "*" || "$arg" == "./*" ]] # Can be replaced by a custom program written in Python etc. to detect dangerous patterns
then
#abort
return 1
fi
done
declare -a all
for arg in "$@" do #We need to collect all arguments instead of rm-ing one by one otherwise rm -rf file does not work properly if [[ "$arg" == *[*?]* ]]
then
expanded=($arg) all+=("${expanded[@]}")
else
all+=("$arg") fi done command rm -i "${all[@]}"
...


# Restore options properly

Instead of set -f in our alias and unconditional set +f in the function, we can save the current shell options using set +o to print them as commands:

fun() {
restore=$(set +o) set -f # ... and any other option changes # and perform the task # finally (instead of set +f) eval "$restore"
}