Tutorial :Safe rm -rf function in shell script



Question:

This question is similar to What is the safest way to empty a directory in *nix?

I'm writing bash script which defines several path constants and will use them for file and directory manipulation (copying, renaming and deleting). Often it will be necessary to do something like:

rm -rf "/${PATH1}"  rm -rf "${PATH2}/"*  

While developing this script I'd want to protect myself from mistyping names like PATH1 and PATH2 and avoid situations where they are expanded to empty string, thus resulting in wiping whole disk. I decided to create special wrapper:

rmrf() {      if [[ $1 =~ "regex" ]]; then          echo "Ignoring possibly unsafe path ${1}"          exit 1      fi        shopt -s dotglob      rm -rf -- $1      shopt -u dotglob  }  

Which will be called as:

rmrf "/${PATH1}"  rmrf "${PATH2}/"*  

Regex (or sed expression) should catch paths like "*", "/*", "/**/", "///*" etc. but allow paths like "dir", "/dir", "/dir1/dir2/", "/dir1/dir2/*". Also I don't know how to enable shell globbing in case like "/dir with space/*". Any ideas?

EDIT: this is what I came up with so far:

rmrf() {      local RES      local RMPATH="${1}"      SAFE=$(echo "${RMPATH}" | sed -r 's:^((\.?\*+/+)+.*|(/+\.?\*+)+.*|[\.\*/]+|.*/\.\*+)$::g')      if [ -z "${SAFE}" ]; then          echo "ERROR! Unsafe deletion of ${RMPATH}"          return 1      fi        shopt -s dotglob      if [ '*' == "${RMPATH: -1}" ]; then          echo rm -rf -- "${RMPATH/%\*/}"*          RES=$?      else          echo rm -rf -- "${RMPATH}"          RES=$?      fi      shopt -u dotglob        return $RES  }  

Intended use is (note an asterisk inside quotes):

rmrf "${SOMEPATH}"  rmrf "${SOMEPATH}/*"  

where $SOMEPATH is not system or /home directory (in my case all such operations are performed on filesystem mounted under /scratch directory).

CAVEATS:

  • not tested very well
  • not intended to use with paths possibly containing '..' or '.'
  • should not be used with user-supplied paths
  • rm -rf with asterisk probably can fail if there are too many files or directories inside $SOMEPATH (because of limited command line length) - this can be fixed with 'for' loop or 'find' command


Solution:1

I've found a big danger with rm in bash is that bash usually doesn't stop for errors. That means that:

cd $SOMEPATH  rm -rf *  

Is a very dangerous combination if the change directory fails. A safer way would be:

cd $SOMEPATH && rm -rf *  

Which will ensure the rf won't run unless you are really in $SOMEPATH. This doesn't protect you from a bad $SOMEPATH but it can be combined with the advice given by others to help make your script safer.

EDIT: @placeybordeaux makes a good point that if $SOMEPATH is undefined or empty cd doesn't treat it as an error and returns 0. In light of that this answer should be considered unsafe unless $SOMEPATH is validated as existing and non-empty first. I believe cd with no args should be an illegal command since at best is performs a no-op and at worse it can lead to unexpected behaviour but it is what it is.


Solution:2

There is a set -u bash directive that will cause exit, when uninitialized variable is used. I read about it here, with rm -rf as an example. I think that's what you're looking for. And here is set's manual.


Solution:3

I think "rm" command has a parameter to avoid the deleting of "/". Check it out.


Solution:4

I would recomend to use realpath(1) and not the command argument directly, so that you can avoid things like /A/B/../ or symbolic links.


Solution:5

Generally, when I'm developing a command with operations such as 'rm -fr' in it, I will neutralize the remove during development. One way of doing that is:

RMRF="echo rm -rf"  ...  $RMRF "/${PATH1}"  

This shows me what should be deleted - but does not delete it. I will do a manual clean up while things are under development - it is a small price to pay for not running the risk of screwing up everything.

The notation '"/${PATH1}"' is a little unusual; normally, you would ensure that PATH1 simply contains an absolute pathname.

Using the metacharacter with '"${PATH2}/"*' is unwise and unnecessary. The only difference between using that and using just '"${PATH2}"' is that if the directory specified by PATH2 contains any files or directories with names starting with dot, then those files or directories will not be removed. Such a design is unlikely and is rather fragile. It would be much simpler just to pass PATH2 and let the recursive remove do its job. Adding the trailing slash is not necessarily a bad idea; the system would have to ensure that $PATH2 contains a directory name, not just a file name, but the extra protection is rather minimal.

Using globbing with 'rm -fr' is usually a bad idea. You want to be precise and restrictive and limiting in what it does - to prevent accidents. Of course, you'd never run the command (shell script you are developing) as root while it is under development - that would be suicidal. Or, if root privileges are absolutely necessary, you neutralize the remove operation until you are confident it is bullet-proof.


Solution:6

Meanwhile I've found this perl project: http://code.google.com/p/safe-rm/


Solution:7

If it is possible, you should try and put everything into a folder with a hard-coded name which is unlikely to be found anywhere else on the filesystem, such as 'foofolder'. Then you can write your rmrf() function as:

rmrf() {      rm -rf "foofolder/$PATH1"      # or      rm -rf "$PATH1/foofolder"  }  

There is no way that function can delete anything but the files you want it to.


Solution:8

You may use

set -f    # cf. help set   

to disable filename generation (*).


Solution:9

You don't need to use regular expressions.
Just assign the directories you want to protect to a variable and then iterate over the variable. eg:

  protected_dirs="/ /bin /usr/bin /home $HOME"  for d in $protected_dirs; do      if [ "$1" = "$d" ]; then          rm=0          break;      fi  done  if [ ${rm:-1} -eq 1 ]; then      rm -rf $1  fi  


Solution:10

Add the following codes to your ~/.bashrc

# safe delete  move_to_trash () { now="$(date +%Y%m%d_%H%M%S)"; mv "$@" ~/.local/share/Trash/files/"$@_$now"; }  alias del='move_to_trash'    # safe rm  alias rmi='rm -i'  

Every time you need to rm something, first consider del, you can change the trash folder. If you do need to rm something, you could go to the trash folder and use rmi.

One small bug for del is that when del a folder, for example, my_folder, it should be del my_folder but not del my_folder/ since in order for possible later restore, I attach the time information in the end ("$@_$now"). For files, it works fine.


Note:If u also have question or solution just comment us below or mail us on toontricks1994@gmail.com
Previous
Next Post »