ProgrammingThis forum is for all programming questions.
The question does not have to be directly related to Linux and any language is fair game.
Notices
Welcome to LinuxQuestions.org, a friendly and active Linux Community.
You are currently viewing LQ as a guest. By joining our community you will have the ability to post topics, receive our newsletter, use the advanced search, subscribe to threads and access many other special features. Registration is quick, simple and absolutely free. Join our community today!
Note that registered members see fewer ads, and ContentLink is completely disabled once you log in.
If you have any problems with the registration process or your account login, please contact us. If you need to reset your password, click here.
Having a problem logging in? Please visit this page to clear all LQ-related cookies.
Get a virtual cloud desktop with the Linux distro that you want in less than five minutes with Shells! With over 10 pre-installed distros to choose from, the worry-free installation life is here! Whether you are a digital nomad or just looking for flexibility, Shells can put your Linux machine on the device that you want to use.
Exclusive for LQ members, get up to 45% off per month. Click here for more info.
The very first bash script I ever wrote was an attempt to create a "rmdir" program that protected me from accidentally deleting subfiles and directories. Well, what I created worked, but it's far from ideal.
So taking advantage of the experience I've gained over the last few years, I've spent the last few days writing a new version of it that's much smoother to use and adds a few new features, particularly protected directories.
I'd appreciate it if you all could look it over and kindly point out anything that needs fixing or could be improved. And feel free to use it or take advantage of it yourselves as well, of course. Thanks.
Code:
#!/bin/bash
## Ths script attempts to provide a safe and easy-to-use rmdir (remove
## directory) function. In addition to bash it requires rm, cat, find,
## grep, readlink, sort, and optionally tree. All but grep and tree
## should be in the standard coreutilities package.
#### DOCUMENTATION ####
# (uses "here" documents to print everything
# between the "EndOfBlock" strings):
# This message will be printed if improper arguments are used.
USAGESIMPLE=$(cat <<EndOfBlock
Usage: $0 [-f|-h|-u] directory1 [directory2 ...]
"$0 -h" for detailed help.
EndOfBlock)
# This message will be printed if the -h option is used.
USAGEFULL=$(cat <<EndOfBlock
This script is designed to create a safer way to remove directories.
It will first test for the existence of files or subdirectories inside the
directory , and if any are found it will ask you what you want to do.
Usage: $0 [-f|-h|-u] directory1 [directory2 directory3 ...]
-f Force deletion of all listed directories
(and their contents) without confirmation.
Protected directories will be ignored.
-h Display this usage dialog.
-u Display a list of currently protected directories.
Protected directories:
You can designate a list of directories that will never be
deleted by this script. Simply add the directory names you
want protected to one of the files "/etc/protecteddirs.txt"
or "~/.protecteddirs.txt", with one directory path per line.
(You can change the names and locations of the files by altering
the variables "\$GLOBALFILE" and "\$USERFILE" in the script.)
This script will read the contents of these two files, if they
exist, each time it is run, and will refuse to delete any names
listed in them.
A directory path sent to the script may also match a substring
of a protected path, so in addition to protecting the stated
directory, it will also refuse to delete all parent directories
as well. e.g. if "/home/user/dir1/dir2" is protected, then
attempting to delete "/home/user/dir1" will also fail.
You can display a list of all currently protected directories
with the "-u" option.
Note that this feature is only a convenience function designed
to prevent careless mistakes. It will not completely protect
your files from deletion and is not guaranteed to work in all
cases.
The file listing dialog ("tree" option):
You will have the option to list all the files and subdirectories
found inside the directory to be deleted (if any). If your
system has the "tree" application installed it will use it to
display them in a nicely formatted directory tree. Otherwise it
will display a simple list of files using the find command.
EndOfBlock)
##### Script environment variable settings ####
ERROR_NOARGS=64 # No command arguments were given.
ERROR_USERCANCEL=66 # User canceled the operation.
IFS='
' # Set internal field separator to newline only
# to handle dirnames with spaces in them.
FORCED=0 # Initial setting for "force" option.
GLOBALFILE="/etc/protecteddirs.txt" # Location of the global protected dirs file.
USERFILE="$HOME/.protecteddirs.txt" # Location of the user protected dirs file.
#### Build a list of protected directories from the ####
#### "GLOBALFILE" and "USERFILE" files ####
#### if they exist. ####
PROTECTED=$(
for LINE in $(cat "$GLOBALFILE" "$USERFILE" 2>/dev/null|sort -u); do
readlink -f "$LINE"
done
)
#### Test for existence of command options and arguments. ####
# Show usage message if nothing found, or if "-h" is found.
# Display all protected directories if "-u" is found.
# Set $FORCED variable to 1 if "-f" is found and shift the remaining
# arguments.
if [ -z $1 ]; then
echo
echo "No command arguments found."
echo "You must supply at least one directory name."
echo "$USAGESIMPLE"
echo
exit $ERROR_NOARGS
elif [ "$1" = "-h" ]; then
echo "$USAGEFULL"
echo
exit 0
elif [ "$1" = "-u" ]; then
echo "Directories protected from deletion:"
echo
echo -e "${PROTECTED:-No protected directories found.}"
echo
exit 0
elif [ "$1" = "-f" ]; then
FORCED=1
shift
if [ -z $1 ]; then
echo
echo "No directories specified."
echo "You must supply at least one directory name."
echo "$USAGESIMPLE"
echo
exit $ERROR_NOARGS
fi
fi
#### Main operation ####
#
# For each directory name fed to the command:
# 1: Check if it's a directory. Skip it if not.
# 2: Check if it matches the protected list. Skip it if it does.
# 3: If FORCED flag is on, delete the directory.
# else:
# 4a: If directory is empty, ask delete: yes/no/exit
# 4b: If directory is non-empty, ask delete: yes/no/show/exit
# Loop to next argument
#
#### ============== ####
echo "A Safer Remove Directory Script"
echo
for ENTRY in $@; do
ENTRYFULL="$(readlink -f "$ENTRY")" # Get the absolute path for use in
# file operations, while $ENTRY
# will be used for most displays.
echo -e "Processing: \"${ENTRYFULL:=$ENTRY}\"" # substitution necessary
# if dirname doesn't exist.
if [ ! -d "$ENTRYFULL" ]; then
echo "Not a directory. Skipping."
echo
continue
elif [[ "${PROTECTED:-null}" =~ $ENTRYFULL ]]; then
echo "Matches a protected list entry. Skipping."
echo
continue
elif [ $FORCED = 1 ]; then
if rm -rf "$ENTRYFULL" ; then
echo "Directory deleted, including possible contents."
echo -e "\(Forced deletion\)"
echo
else
echo -e "Something went wrong.\nCheck output of rm for details"
echo
fi
continue
elif [[ $FORCED = 0 && $(find "$ENTRYFULL" -type d -prune -empty) ]];
then
while true; do
echo
echo "Directory is empty. Do you wish to delete it?"
echo
echo -e "\t[1|y] Delete [2|n] Don't delete [3|x] Exit script"
echo -ne "\tSelect option: "
read ANSWER
case $ANSWER in
"y"|"1") if rm -rf "$ENTRYFULL" ; then
echo "Empty directory $ENTRY deleted."
echo
else
echo "Something went wrong."
echo "Check output of rm for details"
echo
fi
break
;;
"n"|"2") echo "Directory $ENTRY not deleted."
break
;;
"x"|"3") echo "Exiting script" ; exit $ERROR_USERCANCEL
;;
* ) echo "Invalid choice. Try again"
;;
esac
done
echo
elif [[ $FORCED -eq 0 && ! $(find "$ENTRYFULL" -type d -prune -empty) ]];
then
while true; do
echo
echo "WARNING! Directory contains other files or folders!"
echo "Do you wish to delete it?"
echo
echo -e "\t[1|y] Delete\t\t[2|n] Don't delete"
echo -e "\t[3|s] Show files\t[4|x] Exit script"
echo -ne "\tSelect option: "
read ANSWER
case $ANSWER in
"y"|"1") if rm -rf "$ENTRYFULL" ; then
echo "Directory $ENTRY and its contents deleted."
echo
else
echo "Something went wrong."
echo "Check output of rm for details"
echo
fi
break
;;
"n"|"2") echo "Directory $ENTRY not deleted."
break
;;
"s"|"3") echo "Displaying contents of $ENTRY:"
echo "======================="
echo
if [ -e /usr/bin/tree ]; then
tree -C --noreport "$ENTRYFULL"
echo
else
find "$ENTRYFULL" -printf "%p\n"
echo
fi
echo "======================="
;;
"x"|"4") echo "Exiting script" ; exit $ERROR_USERCANCEL
;;
* ) echo "Invalid choice. Try again"
;;
esac
done
echo
fi
done
exit 0
### TODO: Add colors to output.
Edit: Updated version created. See below.
Last edited by David the H.; 06-05-2009 at 11:52 AM.
So this isn't really the response you asked for, but isn't what you describe ("a... program that protected me from accidentally deleting subfiles and directories") exactly how rmdir works?
The very first bash script I ever wrote was an attempt to create a "rmdir" program that protected me from accidentally deleting subfiles and directories. Well, what I created worked, but it's far from ideal.
Not to disrespect your script but why try to reinvent the wheel when there's already a solution that works (libtrash, see Freshmeat)?
Well, when I first my first version way back when, I didn't even know that the gnu version of rmdir even existed. I just wanted a way to do it that was easier and safer than using 'rm -rf' all the time. But I've never really liked the official verison much in any case. It basically just refuses to do anything if the directory isn't empty, and that's it.
My script, OTOH, checks for the existence of subfiles, then asks you if you want to delete it all, cancel, or display a list of the files, so you can decide what you want to do from there. There's also a "force" option, so you can bypass the questioning if you want. And as I mentioned, I've also implemented a protected list, where you can specify directories that it will NEVER delete.
In short, it gives you a lot more control over the operation.
edit:
I took a look at libtrash once a while back too. But I don't want a trash can system either, just a simple extra layer of safety over the operation.
In any case, I wasn't asking for comments on why I wrote it, just to point out anything I can improve in what I did.
Last edited by David the H.; 05-22-2009 at 03:12 PM.
It seems to me there are a lot of unnecessary indirection and unnecessarily large variables. For example, instead of creating a large string called USAGEFULL, which is to be echoed upon encounter of a help flag, just put it in a function, which you call.
for LINE in $(sort -u "$GLOBALFILE" "$USERFILE"); do
will create a temporary variable array-like object over which you will loop. It is unknown how big this can be, and possibly you might have funny side-effects (for example). If you pipe to a “while read” construct the data will be read as it is output from the sort command, and pipe buffering will take care of the potential problem (as an upside, you won’t have to change IFS, as read will only strip away leading whitespace).
So, stylistically (as well as for efficiency), you might want to change a lot of these sorts of constructs:
Code:
foo=$(bar)
…
# later on
…
echo $foo
into function calls (especially if the value is only used once).
Also, as you stated above, the script is not an equivalent of rmdir, but it does have some of the same functionality. You might want to isolate the portion that duplicates rmdir from that which is for protected files.
Also, be aware of portability (though this may not be as important in this situation). Using bash != having a GNU userspace. There are certain options which are available in GNU versions of utilities that aren’t available (or have different names) on other implementations (e.g., the printf flag on find). If you want to be safe, use no more than is specified in POSIX (though you can safely use “builtins” such as echo as long as you specify the bash shell).
Sorry for not getting back for a few days. I had a busy week.
Thanks a lot for the suggestions. My main reason for asking was to improve my scripting ability. I'm trying to pay attention to efficiency and readability in what I write.
As for the comments:
The reason I didn't use...
Code:
for LINE in $(sort -u "$GLOBALFILE" "$USERFILE"); do
...or the like is that sort fails with an error message if either of the files is non-existent or unset. It refuses to output anything if it can't find all the files. The only easy way I found to get around it was to cat the text and pipe any errors away. If you know a better way to handle the errors, please let me know.
osor, that's a good point about the usage variables. I hadn't thought about how they are always created, even if not needed. My main intention with the "here" statements was to make the documentation easily readable within the script itself, as well as when executing. I'll try moving them into functions, though I'll probably keep them pretty much as-is otherwise.
I don't think I'll need to worry about overtaxing the for loop when reading the protected files though. I can't imagine them containing more than a few dozen lines in any reasonable usage scenario. But I may try out the while loop anyway just to see how it works. I generally prefer changing the IFS in any case though; it always seems to make things easier in the long run.
My biggest worry about the "protected" files is that I don't really like the idea of having to read them every time the program is run. I thought about using an environmental variable instead, but to my mind that's a bit harder to manage, as you'd have to mess with .bashrc and/or manipulate the string manually. It's much easier to edit a text file, so I went that way.
Quote:
You might want to isolate the portion that duplicates rmdir from that which is for protected files.
Not sure exactly what you mean here. I debated for a long time about exactly how to break up the process flow, but I finally decided that the simplest way to do it was to just run a single loop that checks all cases at once and does whatever's necessary. One loop handles everything.
Finally, no, I don't think portability is a big issue here. I did give it some consideration, but I doubt I'll ever need to run this on any system that doesn't have the basic gnu utilities. I did try to use external tools as little as possible though.
Of course if anyone else wants to use what I wrote they may have to take it into account.
Last edited by David the H.; 05-31-2009 at 11:38 AM.
Well, I've updated things a bit based on the suggestions I got. I moved the documentation into a function, and cleaned things up several places. I tried out some of the other things mentioned, but in the end decided that there wasn't that much need for major changes.
In particular I failed to find many places where functions would be really useful (other than the one above. To my mind functions are mostly for use when you need to do the same operation over and over, or conversely if the operation if only called under certain conditions and you don't want to load up the code with a lot of nested branches and stuff. But since most of what my script does is controlled through if-then branching anyway I see functions as being mostly superfluous. (Hmm...perhaps if I restructured the whole thing to be function oriented instead....)
I did change the part that tests for command line arguments from an if to a case statement. I don't know why I didn't do that in the first place, as I usually prefer using case statements for this kind of thing anyway. I think they're often cleaner and probably more efficient overall if there's only a single input to test for conditions.
Other than that I mostly just updated the output formatting. In particular I added color code escape sequences so that the output is more readable, and altered some of the wording.
@bigearsbilly: I tried using select at first, but I didn't like the looks of the output menus. Maybe it could be made to look better, but it's easier just to do it manually. I also decided getopts was unnecessary because the three flags I use are all independent of each other and should never need to be combined. Two of them are simple print-it-and-close options, and only the force flag requires additional inputs, all easily tested against. I may try it out in a future tweak though.
Here's my updated attempt:
Code:
#!/bin/bash
###########################################################################
## ##
## Ths script attempts to provide a safe and easy-to-use rmdir (remove ##
## directory) function. In addition to bash it requires rm, cat, find, ##
## grep, readlink, sort, and optionally tree. All but grep and tree ##
## should be in the standard coreutilities package. ##
## ##
###########################################################################
################### DOCUMENTATION ###########################
#############################################################
# (a function that uses "here" documents to print everything
# between the "EndOfBlock" strings):
# There are two levels, a simple usage message and a longer help document.
# The function must be called with an argument with a value of 1 or 2
# in order to determine which message gets printed.
##--------------begin documentation function---------------##
#############################################################
function print_documentation(){
if [ $1 -eq 1 ]; then
# This message will be printed if improper arguments are used.
USAGESIMPLE=$(cat <<EndOfBlock
Usage: $0 [-f|-h|-u] directory1 [directory2 ...]
${CYAN}"$0 -h" for detailed help.${UNSET}
EndOfBlock)
echo -e "${CYAN}You must supply at least one directory name.${UNSET}"
echo -e "$USAGESIMPLE"
echo
# This message will be printed if the -h option is used.
elif [ $1 -eq 2 ]; then
USAGEFULL=$(cat <<EndOfBlock
${CYAN}This script is designed to create a safer way to remove directories.
It will first test for the existence of files or subdirectories inside the
directory , and if any are found it will ask you what you want to do.$UNSET
Usage: $0 [-f|-h|-u] directory1 [directory2 directory3 ...]
${GREEN}
-f Force deletion of all listed directories
(and their contents) without confirmation.
Protected directories will be ignored.
-h Display this usage dialog.
-u Display a list of currently protected directories.
${CYAN}Protected directories:${UNSET}
You can designate a list of directories that will never be
deleted by this script. Simply add the directory names you
want protected to one of the files "/etc/protecteddirs.txt"
or "~/.protecteddirs.txt", with one directory path per line.
(You can change the names and locations of the files by altering
the variables "\$GLOBALFILE" and "\$USERFILE" in the script.)
This script will read the contents of these two files, if they
exist, each time it is run, and will refuse to delete any names
listed in them.
A directory path sent to the script may also match a substring
of a protected path, so in addition to protecting the stated
directory, it will also refuse to delete all parent directories
as well. e.g. if "/home/user/dir1/dir2" is protected, then
attempting to delete "/home/user/dir1" will also fail.
You can display a list of all currently protected directories
with the "-u" option.
Note that this feature is only a convenience function designed
to prevent careless mistakes. It will not completely protect
your files from deletion and is not guaranteed to work in all
cases.
${CYAN}The file listing dialog ("tree" option):${UNSET}
You will have the option to list all the files and subdirectories
found inside the directory to be deleted (if any). If your
system has the "tree" application installed it will use it to
display them in a nicely formatted directory tree. Otherwise it
will display a simple list of files using the find command.
EndOfBlock)
echo -e "$USAGEFULL"
echo
echo
fi
}
##--------------end documentation function----------------##
############################################################
##### Internal script environment variable settings ####
########################################################
GLOBALFILE="/etc/protecteddirs.txt" # Location of the global protected dirs file.
USERFILE="$HOME/.protecteddirs.txt" # Location of the user protected dirs file.
ERROR_NOARGS=64 # No command arguments were given.
ERROR_USERCANCEL=66 # User canceled the operation.
IFS='
' # Set internal field separator to newline only
# to handle dirnames with spaces in them.
FORCED=0 # Initial setting for "force" option.
#Define some color escape sequences for use in the output display.
RED='\e[0;31m'
WHITE='\e[1;37m'
BLUE='\e[1;34m'
GREEN='\e[0;32m'
CYAN='\e[0;36m'
UNSET='\e[0m'
#### Build a list of protected directories from the ########
#### "GLOBALFILE" and "USERFILE" textfiles if they exist.###
############################################################
PROTECTED=$(
for LINE in $(cat "$GLOBALFILE" "$USERFILE" 2>/dev/null|sort -u); do
readlink -f "$LINE"
done
)
#### Test for existence of command options and arguments. ####
##############################################################
# Show usage message if nothing found, or if "-h" is found.
# Display all protected directories if "-u" is found.
# Set $FORCED variable to 1 if "-f" is found and shift the remaining
# arguments.
case $1 in
#If no arguments given:
"") echo -e ${RED}
echo -e "No command arguments given.${UNSET}"
print_documentation 1 # Call documentation function with arg "1".
exit $ERROR_NOARGS
;;
#'-h' Print full documentation
-h) print_documentation 2 # Call documentation function with arg "2".
exit 0
;;
#'-u' List protected directories
-u) echo
echo -e "${CYAN}Directories protected from deletion:${UNSET}"
echo
echo -e "${PROTECTED:-No protected directories found.}"
echo
exit 0
;;
#'-f' Set forced flag
-f) FORCED=1
shift
if [ -z $1 ]; then #check for existence of a second argument.
echo -e ${RED}
echo -e "No directories specified.${UNSET}"
print_documentation 1
exit $ERROR_NOARGS
fi
esac
####--------------- Main operation loop------------------ ####
##############################################################
#
# For each directory name fed to the command:
#
# 1: Check if it's a directory. Skip it if not.
# 2: Check if it matches the protected list. Skip it if it does.
# 3: If FORCED flag is on, delete the directory.
# else:
# 4a: If directory is empty, ask delete: yes/no/exit
# 4b: If directory is non-empty, ask delete: yes/no/show/exit
# Loop to next argument
#
###############################################################
# Display welcome line.
echo
echo -e "${WHITE}A Safer Remove Directory Script"
echo -e "-------------------------------${UNSET}"
echo
# Start the main loop
for ENTRY in $@; do
ENTRYFULL="$(readlink -f "$ENTRY")" # Get the absolute path for use in
# file operations, while $ENTRY
# will be used for most displays.
# Display the filename being currently worked on.
echo -e "Processing: ${BLUE}\"${ENTRYFULL:=$ENTRY}\"${UNSET}"
# The substitution is necessary if dirname doesn't exist.
# Function for testing if directory has contents. To be used later. #
##-------------------------------------------------------------------##
function empty_test() {
test $(find ${ENTRYFULL:-$ENTRY} -type d -prune -empty)
echo $?
}
## End function ---------------------------------------------------##
#########################
### Start the checks ####
#########################
# Check if entry is a directory or not:
if [ ! -d "$ENTRYFULL" ]; then
echo -e " \"$ENTRY\" is not a directory (or doesn't exist)."
echo -e " ${CYAN}Skipping.${UNSET}"
echo
continue
# Check if entry is protected or not:
elif [[ "${PROTECTED:-null}" =~ $ENTRYFULL ]]; then
echo -e " \"$ENTRY\" matches a protected list entry."
echo -e " ${CYAN}Skipping.${UNSET}"
echo
continue
# Delete without question if forced flag is set:
elif [ $FORCED = 1 ]; then
if rm -rf "$ENTRYFULL" ; then
echo " \"$ENTRY\"" Deleted.
echo -e " ${CYAN}Forced deletion of directory and any contents.${UNSET}"
echo
else
echo -e "${RED}Something went wrong.\nCheck output of rm for details${UNSET}"
echo
fi
continue
# Otherwise check if empty or not:
# If empty:
elif [[ $FORCED -eq 0 && $(empty_test) -eq 0 ]]; then
while true; do
echo -e " ${CYAN}Directory is empty. Do you wish to delete it?"
echo
echo -e "${GREEN}\t[1|y] Delete [2|n] Don't delete [3|x] Exit script"
echo -ne "${BLUE}\tSelect option: ${UNSET}"
read ANSWER
case $ANSWER in
"y"|"1") if rm -rf "$ENTRYFULL" ; then
echo
echo -e " Empty directory \"$ENTRY\" deleted."
echo
else
echo
echo -e "${RED}Something went wrong."
echo -e "Check output of rm for details${UNSET}"
echo
fi
break
;;
"n"|"2") echo
echo -e " Directory \"$ENTRY\" left untouched."
echo -e " ${CYAN}Delete action canceled by user.${UNSET}"
break
;;
"x"|"3") echo
echo -e "${RED}Exiting script.${UNSET}"
exit $ERROR_USERCANCEL
;;
* ) echo
echo -e "${RED}Invalid choice. Try again.${UNSET}"
;;
esac
done
echo
elif [[ $FORCED -eq 0 && $(empty_test) -ne 0 ]]; then
while true; do
echo
echo -e "${RED}##### WARNING! ${CYAN}Directory \"$ENTRY\" contains other files or folders! ${RED}#####"
echo -e " ${CYAN}Do you wish to delete it?${UNSET}"
echo
echo -e "\t${GREEN}[1|y] Delete\t\t[2|n] Don't delete"
echo -e "\t[3|s] Show files\t[4|x] Exit script"
echo -ne "\t${BLUE}Select option: ${UNSET}"
read ANSWER
case $ANSWER in
"y"|"1") if rm -rf "$ENTRYFULL" ; then
echo
echo -e " ${CYAN}Directory \"$ENTRY\" and its contents deleted.${UNSET}"
echo
else
echo
echo -e "${RED}Something went wrong."
echo - "Check output of rm for details${UNSET}"
echo
fi
break
;;
"n"|"2") echo
echo -e " Directory \"$ENTRY\" left untouched."
echo -e " ${CYAN}Delete action canceled by user.${UNSET}"
break
;;
"s"|"3")
#small function for printing variable-length underline strings.
#prints $1 number of spaces, then subsitutes them with equal signs.
function varline() {
printf -v line "%$1s" ""
printf "%s" "${line// /=}"
}
echo
echo -e " Displaying contents of \"$ENTRY\":"
echo -e " ==========================$(varline ${#ENTRY})"
echo
if [ -e /usr/bin/tree ]; then
tree -C --noreport "$ENTRYFULL"
echo
else
find "$ENTRYFULL" -printf "%p\n"
echo
fi
echo -e " ==========================$(varline ${#ENTRY})"
;;
"x"|"4") echo
echo -e "${RED}Exiting script${UNSET}"
exit $ERROR_USERCANCEL
;;
* ) echo
echo -e "${RED}Invalid choice. Try again.${UNSET}"
;;
esac
done
echo
fi
done
echo -e "${BLUE}All entries processed. Goodbye!${UNSET}"
exit 0
LinuxQuestions.org is looking for people interested in writing
Editorials, Articles, Reviews, and more. If you'd like to contribute
content, let us know.