#!/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_DOT_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.
# FREEDESKTOP_TRASHPATH is where the trash (for KDE/GNOME, after 2005) lives using the freedesktop.org standard: http://www.ramendik.ru/docs/trashspec.html
# KDE_TRASHCAN and GNOME_TRASHCAN are where the trash lives in older versions of KDE/GNOME.
TRASHCAN_LOCATIONS="$HOME/Desktop/Trash $HOME/Trash $HOME/Desktop/trash $HOME/trash $HOME/.trash"
FREEDESKTOP_TRASHPATH="$HOME/.local/share/Trash"
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)
	       -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
while getopts :t:fivh OPTION
do
        case "$OPTION" in
		t) TRASHCAN_PATH_SPECIFIED=true; TRASHCAN_PATH="$OPTARG";;
		f) FORCE=true;;
		i) INTERACTIVE=true;;
		v) VERBOSE=true;;
                h|\?) usage; exit 1 ;;
        esac
done
shift  $(($OPTIND-1))


# Process, and sanity-check options
if [ -z "$1" ]; then
	echo "cn: Error: no files were specified." >&2
	exit 1
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


# 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"
		FDO_DO_TRASHINFO=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 [ "$FDO_DO_TRASHINFO" == true -a ! -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


# 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 [ "$FDO_DO_TRASHINFO" == "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 [ "$FDO_DO_TRASHINFO" == "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 ?
#

