#!/bin/bash
# A trashCaN' script to move files into the trashcan (as an alternative to manual use of 'rm').
# Use this always, instead of 'rm'. Then, your fingers won't get into dangerous habits!
# Copyright Richard Neill <cn_AT_REMOVE_ME_richardneill.org>, 2008-06-28, All rights reserved.
# This is Free Software, licensed under the GNU GPL version 3 or later, which may be found here:
http://www.gnu.org/licenses/gpl.html
# There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# Please see usage () below. Please also read the comments at the end, and the man page.
# For the latest version, see
www.richardneill.org/source.php
# ---- BEGIN CONFIGURATION SECTION ----
# TRASHCAN_LOCATIONS are the possible locations for a user's trashcan. cn will pick the first one of these that actually exists.
# Add extra paths here: entries are delimited by whitespace. IMPORTANT: None of these should contain a space, NOR a trailing slash.
TRASHCAN_LOCATIONS="$HOME/Desktop/Trash $HOME/Trash $HOME/Desktop/trash $HOME/trash $HOME/.trash"
# FREEDESKTOP_TRASHPATH is where the trash (for KDE/GNOME, after 2005) lives using the freedesktop.org standard:
http://www.ramendik.ru/docs/trashspec.html . This is usually just the
# default value, namely $HOME/.local/share/Trash, BUT if the environment var $XDG_DATA_HOME is set, use $XDG_DATA_HOME/Trash. See
http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
FREEDESKTOP_TRASHPATH_XDG_SUBDIR=Trash
FREEDESKTOP_TRASHPATH_DEFAULT="$HOME/.local/share/Trash"
# KDE_TRASHCAN and GNOME_TRASHCAN are where the trash lives in older versions of KDE/GNOME.
KDE_TRASHCAN_CONFIG_FILE=$HOME/.kde/share/config/kdeglobals
GNOME_TRASHCAN="$HOME/.Trash"
# This is the maximum number of numbered copies to store in trash before exiting with an error. Perhaps 10000?
# If this gets too high, then the performance of cn will get very slow, as it searches for the lowest non-existing $i in TRASH/file.~$i~
MAX_BACKUP_COPIES=10000
# ---- END CONFIGURATION SECTION ----
# Print the usage of cn.
function usage () {
cat <<-EOT >&2
cn moves files into the trashCaN. It is a safe replacement for 'rm'.
Note: cn is *recursive* over directories, (unlike rm). For more information, please see the man page.
Usage: cn [-t TRASHCAN_LOCATION ] [-v] [-i] [-f] [-h] FILE(s)_TO_BE_TRASHED
-t TRASHCAN_LOCATION specify the location of the trashcan
-v verbose mode: show details
-i interactive: ask for confirmation (default)
-f forcible (no confirmation, like rm -f)
-l list contents of trash
-z explain how to restore files from trash
-x empty trash
-h, --help show this help
Unless the trashcan location is specified with -t, cn will search for the trash directory thus:
- If KDE or GNOME are running, cn will first look for the respective KDE/GNOME trashcan.
- If KDE or GNOME are running, cn will first look for the respective KDE/GNOME trashcan.
- Otherwise, cn will try (in order): $TRASHCAN_LOCATIONS
EOT
}
# Test that there is at least one argument. If there is not, or the first one is --help or -help, then print the usage and exit.
# (Testing for --help is done here because bash's builtin getopts can't do long options.)
if [ -z "$1" ] || [ "$1" = "--help" ] || [ "$1" = "-help" ] ; then
usage
exit 1
fi
# Get options. Support -t,-f,-i,-v,-h,l,z,x
while getopts :t:fivhlzx OPTION
do
case "$OPTION" in
t) TRASHCAN_PATH_SPECIFIED=true; TRASHCAN_PATH="$OPTARG";;
f) FORCE=true;;
i) INTERACTIVE=true;;
v) VERBOSE=true;;
l) LIST=true;;
z) RESTORE=true;; # -z by analogy with Undo. -r is confusing: c.f. "rm -r".
x) EMPTY=true;;
h|\?) usage; exit 1 ;;
esac
done
shift $(($OPTIND-1))
# Process, and sanity-check options
if [ -z "$LIST" -a -z "$RESTORE" -a -z "$EMPTY" ] ;then
if [ -z "$1" ] ;then
echo "cn: Error: no files were specified." >&2 # Actually, this will get caught above, and show usage instead.
exit 1
fi
else
if [ -n "$1" ] ;then
echo "cn: Error: options -l, -x, and -z take no arguments." >&2
exit 1
fi
fi
if [ -n "$FORCE" -a -n "$INTERACTIVE" ];then
echo "cn: Error: options -f and -i conflict." >&2
exit 1
fi
if [ -n "$TRASHCAN_PATH_SPECIFIED" ]; then
# If trashcan path is specified, then use it. But if it begins with '-', then there's probably been an error in option processing
if [[ "$TRASHCAN_PATH" =~ ^- ]] ;then # =~ is bash's regex match operator; '^-' means "begins with '-' ".
echo "cn: Error TRASHCAN_LOCATION '$TRASHCAN_PATH' appears invalid. It looks like -t was followed by " >&2
echo "another option (beginning with a '-'), rather than the location of a trash directory." >&2
exit 1
else
TRASHCAN_LOCATIONS="$TRASHCAN_PATH"
fi
fi
# Test that $HOME is set - otherwise, we are in trouble, and $TRASHCAN_LOCATIONS will be wrong.
if [ -z "$HOME" ] ;then
echo 'cn: Error: $HOME is not set!' >&2
exit 1
fi
# If environment variable $XDG_DATA_HOME is set, then use this for the base directory of the FDO trashpath. Else, use $HOME/.local/share
# See:
http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html . Note: we test *whether* this path exists later, in order to see whether to use the FDO trash.
if [ -z "$XDG_DATA_HOME" ] ;then
FREEDESKTOP_TRASHPATH=$FREEDESKTOP_TRASHPATH_DEFAULT
else
FREEDESKTOP_TRASHPATH=$XDG_DATA_HOME/$FREEDESKTOP_TRASHPATH_XDG_SUBDIR
fi
# Unless -t is specified, find the trashcan location. If the Freedesktop.org trashcan exists, use that. Else, check whether the user is currently running either KDE or GNOME.
# If so, find their trashcan location and prepend it to the list. We have to do this dynamically, because KDE's trash-path isn't fixed, and we don't want to use the trashcan
# for the wrong desktop environment.
if [ -z "$TRASHCAN_PATH_SPECIFIED" ]; then
#Check whether the freedesktop.org trashcan exists. This is the most likely case.
if [ -d "$FREEDESKTOP_TRASHPATH" ];then
mkdir -p $FREEDESKTOP_TRASHPATH/{files,info} #Create the subdirectories {info,trash} if they are missing.
TRASHCAN_LOCATIONS="$FREEDESKTOP_TRASHPATH/files $TRASHCAN_LOCATIONS"
USING_FDO_TRASH=true
FREEDESKTOP_TRASHINFOPATH=$FREEDESKTOP_TRASHPATH/info # ~/.local/share/Trash/ contains files/ and info/
if [ -n "$VERBOSE" ];then
echo "cn: Freedesktop.org supported; trash directory is located at '$FREEDESKTOP_TRASHPATH'"
fi
#Otherwise, check if the user is currently running KDE. If so, get the trashcan path for KDE.
elif ps u -C kdeinit > /dev/null ;then
KDE_TRASHCAN=$(eval $(grep Trash $KDE_TRASHCAN_CONFIG_FILE) ; echo $Trash)
TRASHCAN_LOCATIONS="$KDE_TRASHCAN $TRASHCAN_LOCATIONS"
if [ -n "$VERBOSE" ];then
echo "cn: KDE is running; KDE's trash directory is located at '$KDE_TRASHCAN'"
fi
#Otherwise, check if the user is currently running GNOME. If so, get the trashcan path for GNOME.
elif ps u -C gnome-panel > /dev/null ;then
TRASHCAN_LOCATIONS="$GNOME_TRASHCAN $TRASHCAN_LOCATIONS"
if [ -n "$VERBOSE" ];then
echo "cn: GNOME is running; GNOME's trash directory is located at '$GNOME_TRASHCAN'"
fi
fi
fi
# Check each of the possible $TRASHCAN_LOCATIONS, pick the first of these that is a directory which actually exists.
# If they all fail, then TRASHCAN will be ''.
for TRASHCAN in $TRASHCAN_LOCATIONS; do
if [ -d "$TRASHCAN" ] ;then
break
else
TRASHCAN=''
fi
done
# If none of the possible locations for trashcan exist, exit.
if [ -z "$TRASHCAN" ]; then
echo "cn: Error: cannot find your trash directory. The following locations have been checked: " >&2
echo " $TRASHCAN_LOCATIONS" >&2
echo "Please create a trash directory in one of these locations, or alternatively, " >&2
echo "create a symbolic link from there to the actual location of your trash directory." >&2
exit 1
fi
# Check that $TRASHCAN is readable, writeable and executable.
if [ ! -r $TRASHCAN -o ! -w $TRASHCAN -o ! -x $TRASHCAN ]; then
echo "cn: Error: trash directory '$TRASHCAN' must be readable, writeable and executable." >&2
echo "Please change the permissions of '$TRASHCAN' to be at least 700." >&2
exit 1
fi
# Also check the trashinfo path, if appropriate.
if [ "$USING_FDO_TRASH" == true ] && [ ! -r $FREEDESKTOP_TRASHINFOPATH -o ! -w $FREEDESKTOP_TRASHINFOPATH -o ! -x $FREEDESKTOP_TRASHINFOPATH ]; then
echo "cn: Error: trash info directory '$FREEDESKTOP_TRASHINFOPATH' must be readable, writeable and executable." >&2
echo "Please change the permissions of '$FREEDESKTOP_TRASHINFOPATH' to be at least 700." >&2
exit 1
fi
if [ -n "$VERBOSE" ];then
echo "cn: Using trash directory '$TRASHCAN'" >&2
fi
# List files in trash?
if [ -n "$LIST" ]; then
echo -e "cn: listing contents of Trash directory, '$TRASHCAN/'...\n" >&2
if [ "$(/bin/ls -AH $TRASHCAN)" ] ; then
/bin/ls -lAH $TRASHCAN # Long format. Show hidden files too.
else
echo "[The trash is empty.]" >&2
fi
echo "" >&2
exit 0
fi
# Restore files from trash?
if [ -n "$RESTORE" ]; then
if [ "$USING_FDO_TRASH" == true ]; then # FDO system?
echo "cn: Your files can be manually restored with 'mv'. They are located in '$TRASHCAN'." >&2
echo "cn: Restore meta-data may be found for each file in its counterpart in '$FREEDESKTOP_TRASHINFOPATH' " >&2
echo "cn: Alternatively, visit 'trash:/' in the KDE or GNOME file browser." >&2
else
echo "cn: Your files can be manually restored with 'mv'. Unfortunately, there is no record of whence they originally came." >&2
echo "cn: The files are located in '$TRASHCAN'." >&2
fi
exit 0
fi
# Empty trash? Actually, this isn't exactly safe. So just give the user instructions!
# Easiest way: delete and re-create the directories (this catches hidden files.)
# Note: if the trashcan is actually a symlink, then we want to follow it first (so use readlink)
if [ -n "$EMPTY" ]; then
echo "cn: Warning: it's too dangerous to script emptying the trash, which permanently deletes files." >&2
echo "cn: Manually run the command below, if you're *really* sure." >&2
if [ "$USING_FDO_TRASH" == true ]; then # FDO system?
echo " rm -r $FREEDESKTOP_TRASHPATH ; mkdir -p $FREEDESKTOP_TRASHPATH/{files,info}" >&2
else
TRASHCAN=$(readlink -f $TRASHCAN); # If TRASHCAN is a symlink, must canonicalise it.
echo " rm -r $TRASHCAN ; mkdir $TRASHCAN" >&2 # Otherwise, we'll just mangle the link structure.
fi
exit 0
fi
# For each file/directory that is to be put in the trash, first check that it exists.
# This test uses -e, but broken (dangling) symbolic links are a special case.
# [There is no need to check if it is readable, or that the parent directory is writeable: mv will do that.]
for FILE in "$@" ;do
if [ ! -e "$FILE" -a ! -h "$FILE" ] ; then
echo "cn: Error: '$FILE' is not a valid file or directory." >&2
exit 1
else
TRASH_FILE_LIST=$TRASH_FILE_LIST"'$FILE' "
fi
done
# Ask the user (once) for confirmation. y|Y|yes => yes; anything else => no
# The -f option supresses this confirmation check; the -i option is the default anyway.
if [ -z "$FORCE" ]; then
echo -ne "cn: move to trash: ${TRASH_FILE_LIST}? [y/n]: " >&2
read RESPONSE
else
RESPONSE=y
fi
if [ "$RESPONSE" != "y" -a "$RESPONSE" != "Y" -a "$RESPONSE" != "yes" ]; then
echo "cn: not moved to trash: $TRASH_FILE_LIST" >&2
exit 1
else
# Now OK to move to trash.
# See note 1: this doesn't work: # /bin/mv -f --backup=numbered -- "$@" $TRASHCAN
# So, iterate over each file/directory and move it to trash. Create numbered backups in exactly the same
# way as mv does: [NEWEST] foobar foobar.~3~ foobar.~2~ foobar.~1~ [OLDEST]
for FILE in "$@" ;do
# Destination in trashcan
DEST="$TRASHCAN/`basename "$FILE"`"
DESTBAK_COMMENT=''
#See note 7. #if [ "$FILE" -ef "$DEST" ] ; then echo "Warning: file $FILE is already in the trash." >&2 ; break; fi
# If destination already exists, move it to the $DESTBAK, "$DEST.~$i~" for the smallest, non-existing $i.
if [ -e "$DEST" -o -h "$DEST" ] ;then
for ((i=1; i <= $MAX_BACKUP_COPIES; i++)); do
DESTBAK="$DEST.~$i~"
if [ ! -e "$DESTBAK" -a ! -h "$DESTBAK" ]; then
/bin/mv -i -- "$DEST" "$DESTBAK"
DESTBAK_COMMENT="; archived old '\$TRASH/$FILE' as '\$TRASH/$FILE.~$i~'"
#If we're using the FreeDesktop.org trashcan, then we also need to move around the corresponding .trashinfo files.
if [ "$USING_FDO_TRASH" == "true" ];then
TRASHINFO_FILE=$FREEDESKTOP_TRASHINFOPATH/`basename "$DEST"`.trashinfo
TRASHINFO_FILEBAK=$FREEDESKTOP_TRASHINFOPATH/`basename "$DEST"`.~$i~.trashinfo
/bin/mv -i -- "$TRASHINFO_FILE" "$TRASHINFO_FILEBAK"
DESTBAK_COMMENT="$DESTBAK_COMMENT; moved/edited old '$TRASHINFO_FILE' to '$TRASHINFO_FILEBAK'."
fi
break
fi
# Exit with error, if there are already too many numbered backups in the trashcan.
# Otherwise, the performance of cn could get really really slow.
if [ $i == $MAX_BACKUP_COPIES ] ;then
echo "cn: Error: there are already $MAX_BACKUP_COPIES incremental numbered backups of '$FILE' in the trashcan." >&2
echo "Please delete some and try again." >&2
exit 1
fi
done
fi
# If $FILE no longer exists, although it did exist before, then we know that $FILE was in the root of the trash directory.
if [ ! -e "$FILE" -a ! -h "$FILE" ];then
# In which case, it should not have been moved. Undo the previous move of $DEST -> $DESTBAK.
/bin/mv -i -- "$DESTBAK" "$DEST"
echo "cn: Warning: file '$FILE' is already in the trash." >&2
else
# Finally! Actually move the file/directory into the trash. Be verbose if required.
/bin/mv -i -- "$FILE" "$DEST"
if [ -n "$VERBOSE" ];then
echo "cn: moved '$FILE' to trash: '$DEST'$DESTBAK_COMMENT" >&2
fi
#If we're using the FreeDesktop.org trashcan, then we also need to create a .trashinfo file. See Note (9) for details.
#Without the .trashinfo file, konqueror trash:/ can't see the file. It also doesn't know where to restore it to.
if [ "$USING_FDO_TRASH" == "true" ];then
TRASHINFO_FILE=$FREEDESKTOP_TRASHINFOPATH/`basename "$DEST"`.trashinfo
if [[ "$FILE" =~ ^/ ]] ;then #Get full path of $FILE.
ORIG_FULLPATH="$FILE"
else
ORIG_FULLPATH=$PWD/"$FILE"
fi
DELETEION_DATETIME=`date +%Y-%m-%dT%H:%M:%S`
echo -e "[Trash Info]\nPath=$ORIG_FULLPATH\nDeletionDate=$DELETEION_DATETIME" > $TRASHINFO_FILE
if [ -n "$VERBOSE" ];then
echo "cn: created Freedesktop.org .trashinfo file: '$TRASHINFO_FILE'" >&2
fi
fi
# Did it work? (this could fail if, for example, the source file has the wrong permissions.)
if [ $? != 0 ]; then
ERROR_OCCURRED=true
fi
fi
done
fi
# Did any error ever occur with mv? If so, exit with status 1.
if [ -n "$ERROR_OCCURRED" ]; then
# Probably unnecessary to say this: mv will complain anyway.
# echo "cn: An error occurred." >&2
exit 1
fi
# We succeeded! Exit sucessfully, with status 0.
exit 0
#---------------------------------------------------------------------
# Notes:
#
# 1) This method would be ideal, but it fails due to a bug in mv:
# /bin/mv -f --backup=numbered -- "$@" $TRASHCAN
# This bug causes the --backup=numbered option to fail if the thing to be backed up is a directory, not a file.
# See GNU Core Utilities - Bugs / Bug Detail: 5477
# BUT NOTE: this is fixed in coreutils 6.0, as of 2006-08-26. So cn could now be simply replaced by a shell alias:
# alias cn='TRASHCAN=$HOME/.local/share/Trash/files/ ; /bin/mv -f --backup=numbered -t $TRASHCAN --'
#
# 2) This script contains lots of double-quotes for a good reason: some filenames may contain spaces.
#
# 3) The trash isn't automatically emptied (perhaps this is something for .bash_logout or cron?)
#
# 4) The -- option to mv tells it to expect no more options. Eg $@ containing '-f' would be a file, not an option. The -i option
# is unnecessary: we know that the destination shouldn't exist, but it's there for extra safety!
#
# 5) All output from this script is sent to stderr, not stdout.
#
# 6) We need to test for file non-existence with [ ! -e ] && [ ! -h ]. This is because a dangling symlink is deemed (by -e) not to exist,
# but we still need to process it as if it did. (The inverse of this, ie testing for file existence, is [ -e ] || [ -h ] )
#
# 7) If the file is *already* in the root of the trash directory, then we don't want to do anything. However, there is no
# easy way to test for this (we'd need a way to get the absolute path of a file, and there isn't one!!). Tried checking
# whether [ "$FILE" -ef "$DEST" ], but this can fail (if $HOME/A, $HOME/B, $HOME/trash/B are all hard linked, then
# `cn $HOME/B` will wrongly assume that $HOME/B is already in the trash). The best way is to check whether $FILE still
# exists before we move it; if not, we can conclude we are in the trash and move it back.
#
# 8) The trash directories of KDE and GNOME are different, and do not coincide. KDE's trashcan may be moved, and the location is stored
# as a preference; GNOME's trash is in $HOME/.Trash. (cn will try to pick sanely.)
# UPDATE: KDE and GNOME now both use the FreeDesktop.org specification, ~/.local/share/Trash/files/ for the trash.
#
# 9) To be properly compliant with the new FDO trash spec, we also need to create the info files too:
# ~/.local/share/Trash/
# files/foo
# info/foo.trashinfo
# where the .trashinfo file contains:
# [Trash Info]
# Path=/full/original/path/to/foo
# DeletionDate=2008-06-28T23:10:36
#
# 10) In KDE, we can now use the kfmclient command:
# kfmclient move "foo" "bar" trash:/
# also, in Konqueror, visit the URL trash:/
# So, you could just create a function in ~/.bashrc:
# function cn (){ kfmclient move "$@" trash:/ ;}
# However, it is KDE specific, and extremely slow, compared to this script, taking over 2 seconds on a decent machine!
# Furthermore, the error messages appear via kdialog, rather on the command line.
#
# 11) Possible improvements: (i)If the bug in note 1 is fixed, revert to that method. (ii)Better automatic detection of the
# trashcan's directory would be nice. (iii) cn could also be improved by re-writing it in C, ideally becoming part of the
# GNU coreutils package.
#
# 12) Can we use $DESKTOP instead of eg ps ux | grep kdeinit ?
#