#!/bin/bash
#                       /usr/local/bin/name_tidy
# https://crystalfaeries.net/posix/bin/name_tidy
# celeste:crystalfaery NAME_TIDY 2020-04-22 19:45:01+00:00
# 2012-06-28 10:23:31+00:00 This only handles $1, not $2-$n so please rewrite it soon :-)
# We tidy the names of our arguments, their contents, or current directory
# We SHOULD accept files as arguments versus demanding a whole directory
# We presume to do the current directory or the one named as argument (after optional recursion flag -r)
# 2011-10-08 19:14:20+00:00 had to hack the file renaming logic to accomodate the NTFS handling of case
# 2011-11-14 00:39:48+00:00 fixing bug with rsyncing an empty non cannonically named dir over its empty cannonically named empty dir.
# 2013-10-03 18:20:15+00:00 use "set -P" to avoid following symlinks
set -P	# do not follow symlinks (####### THIS DOES NOT WORK! WE DO FOLLOW SYMLINKS!	#######)
#		  ONLY UPON ACTIVATION OF A SWITCH OPTION SHOULD WE FOLLOW SYMLINKS	BUG!
# 2014-03-18 10:05:15+00:00 filter default directory files {HEADER,README}.{html,txt}
# 2015-08-02 11:19:52+00:00 we noticed we failed on a directory named "_".
# 2015-10-04 18:29:57+00:00 we are not yet handling names beginning with a "-". (hack added at end)
# 2016-07-27 11:46:53+00:00 we finally handle a Run Command file per directory to block name_tidy operations there!
#				(echo "no" > .name_tidy)	will block name_tidy
#	future option?		(echo "norecurse" > .name_tidy) could block recursion into subdirs
# We handle the usual documentation options for version and help, then exit.
# With an -e or --edit option we:
# handle an edit session for the    GLOBAL EXCLUDE file $HOME/.name_tidy_exclude.txt without a directory specified
# handle an edit session for the LOCAL RUN CONTROL file in a specified directory

let help=24	# line number of this line within file - 1

# process documentation options
case $# in
0)	:	#no-options
	;;
1)	# standard version or help request?
	case "${1}" in
	-v | --version )
		head -n 4 "${0}" | tail -n 1	1>&2	# version info
		exit
		;;
	-h | --help )
		head -n $help "${0}"	1>&2	# version info
		exit
		;;
	-e | --edit )
		shift	# thanks, we consume the option, then seek an argument:
		case "$#" in
		0 )	# no	${1} therefore edit GLOBAL CONTROL file
			# edit session for the    GLOBAL EXCLUDE file $HOME/.name_tidy_exclude.txt without a directory specified
			vi "${HOME}"/.name_tidy_exclude.txt
			exit $?
			;;
		1 )	#	${1} is the directory in which to edit the RUN CONTROL file
			# edit session for the LOCAL RUN CONTROL file in a specified directory
			cd -P	"${1}"	||	exit	$?"
			exit	255	#	DEBUGGING EXIT
			find	.name_tidy -links 1

cat	<<	EOF	>	.name_tidy
no

# DANGER, Danger, Will Robinson!
# DO NOT EDIT THIS FILE, as is:
# -- --- ----            __ __  

# If the file is a symlink, then
# remove the symlink and replace
# with a brand new text file.

# If the file has more than one link,
# unhardlink the file before editing.

# We shall create a bash script to
# manage these files in the future :-)

EOF

			&&	unhardlink	.name_tidy	\
			&&	vi	.name_tidy	# edit the file
			exit $?
			;;
		* )	
			;;
		esac
		exit $?
		;;
	* )	:	#no-op: handle below
		;;
	esac;;
*)	:	#no-op: handle below
	;;
esac

# Is this first execution for this user? We must default to safety...
if [ \!	-r					$HOME/.name_tidy_exclude.txt ]
then	# generate a list of system directories to NEVER name_tidy:
    if [ -d	/usr/share/rear/skel/default/ ]
    then
        cd	/usr/share/rear/skel/default/	&& \
        for	d	in $(find .	-mindepth 1 -maxdepth 1 -type d	| sort | sed 's/^\.//')
        do
	    echo $d >>	/tmp/exclude.$$.txt
        done
    fi
    pushd	/	1>/dev/null	2>/dev/null
    for		d	in $(find .	-mindepth 1 -maxdepth 1 -type d	| sort | sed 's/^\.//')
    do
	    echo $d >>	/tmp/exclude.$$.txt
    done
    popd		1>/dev/null	2>/dev/null
    sort -u		/tmp/exclude.$$.txt >	$HOME/.name_tidy_exclude.txt	\
    &&	rm		/tmp/exclude.$$.txt					\
    ||	exit	$?	# Fail Safe
fi
name_tidy_exclude="$HOME/.name_tidy_exclude.txt"	# System-Wide Exclude List for Executing User

let recurse=1			# false by default
if [ "${1}" ]			# any arguments on command line?
then
	if [ "${1}" == "-r" ]	# yes, "-r" tells us to recurse subdirectories
	then
		let recurse=0	# true  by option
		shift		# finished option
	fi
fi
if [ "${1}" ]			# any target directory arguments on command line?
then
	if [ -d "${1}" ]	# other arguments should be directories to process
	then
		cd -P "${1}"	|| exit -1	# make next directory to process be current working directory
		shift		# finished with argument
	else
		if [ -h "${1}" ]	# other arguments could be symlinks to directories to process
		then
			cd -P "${1}"	|| exit -1	# make next directory to process be current working directory
			shift		# finished with argument
		else
			echo	"$0: ERROR (neither dir nor symlink): ${1}."		1>&2
			exit -1		# better to die then do the wrong thing
		fi
	fi
else
	cd -P .			# force physical location
fi

directory="`pwd`"		# our physical location

#	is this directory excluded by our user's system-wide exclude list?
if [ -r "${name_tidy_exclude}" ]
then	# we have an exclude list
	if [	grep	"${directory}"	"${name_tidy_exclude}"	>&	/dev/null	]
	then	# we are NOT to name_tidy this directory (nor its subdirectories unless invoked below this level)
		echo ""									1>&2
		echo "NOT	tidying $directory due to global Exclude file"	1>&2
		exit	0	# gracefully decline to work here
	fi
fi

#	does this directory contain a "run control" file denying our tidy operation here?
if [ -r .name_tidy ]
then	# we have a "run control" file applicable to this directory
	RC="`head -n 1 .name_tidy`"	# as simple as we can make it (for now)
	if [ "${RC}" = "no" ]
	then	# we are NOT to name_tidy this directory (nor its subdirectories unless invoked below this level)
		echo ""									1>&2
		echo "NOT	tidying $directory due to local Run Control file"	1>&2
		exit	0	# gracefully decline to work here
	else
		echo	"	tidying $directory"					1>&2
	fi
fi

# start by expunging recursive directory hierarchies... callously discarding contents of insane structures...
no_recurse	# follow the White Rabbit, Neo	(DANGER! DANGER!, Will Robinson)

let loop=1		# start the loop
while [[ $loop -gt 0 ]] ; do
    let loop=0		# end the loop when we make no changes in a pass

    # start by fixing names that look like options to commands:
    for f in $(find . -maxdepth 1 -name '-*' 2>/dev/null | sed 's/^\.\/-*//g')
    do	# iterate
	nice ionice -c 3 rsync -auvzH ./-*"${f}" "${f}" && rm -rf ./-*"${f}" || echo "$0:${directory}/-*${f}"	1>&2
    done

    for i in *; do	# iterate through each item in current working directory
			# Files are renamed, though .* files are untouched
	if   [ "$i" == "gPodder" ]	# gPodder database
		then continue		# leave gPodder     files alone
	elif [ "$i" == "CACHEDIR.TAG" ]	# see /var/cache/CACHEDIR.TAG
		then continue		# leave CACHEDIR.TAG files alone
	elif [ "$i" == "HEADER.txt" ]	# default directory files
		then continue		# leave HEADER.html files alone
	elif [ "$i" == "HEADER.html" ]	# default directory files
		then continue		# leave HEADER.html files alone
	elif [ "$i" == "README.html" ]	# default directory files
		then continue		# leave README.html files alone
	elif [ "$i" == "README.txt" ]	# default directory files
		then continue		# leave README.html files alone
	elif [ "$i" == "README" ]	# default directory files
		then continue		# leave README files alone
	elif [ "$i" == "Incomplete Downloads" ]	# do not interfere with downloads currently in progress
		then continue		# leave Incomplete Downloads directory alone
	elif [ "$i" == "Converted" ]	# do not alter our Converted folder name
		then continue		# leave Converted directory alone
	elif [ "$i" == "Downloads" ]	# do not alter our Downloads
		then continue		# leave downloads directory alone
	elif [ "$i" == "Desktop" ]	# do not alter our Desktop directory
		then continue		# leave downloads directory alone
	elif [ "$i" == "Mail" ]		# do not alter our e-Mail directory
		then continue		# leave downloads directory alone
	elif [ "$i" == "Screencasts" ]	# do not alter our Screencasts directory
		then continue		# leave downloads directory alone
	elif [ "$i" == "*.part" ]	# do not interfere with downloads currently in progress
		then continue		# do not interrupt downloads currently in progress
	elif [ "$i" == "*.jpeg" ]	# we cannonicalize .jpeg to .jpg
		then
			ln "${i}"	$(echo "${i}" | sed 's/\.jpeg/.jpg/')	\
		&&	rm "${i}"	# BE VEWY VEWY CAREFUL OF HARDLINKS!
	fi
	if [ -a "$i" ] 
	then		# Accesible
		OLDNAME="$i"
		NEWNAME=$(echo "$i" | tr 'A-Z ' 'a-z_' | sed 's/"/_/g' | sed "s/'/_/g" | sed 's/`/_/g' | sed 's/(/_/g ; s/)/_/g ; s/_-_/-/g ; s/\.-\./-/g ; s/-_-/-/g ; s/-\.-/-/g ; s/^_//g ; s/__/_/g ; s/,/-/g ; s/!/./g ; s/\.\././g ; s/\.-/./g ; s/-_/-/g ; s/_-/-/g ; s/\[/_/g ; s/\]/_/g ; s/#/./g ; s/&/+/g ; s/^\.//g ; s/:/./g ; s/%20/_/g')
		if [ "$NEWNAME" == "" ]
		then NEWNAME="_"
		fi
		if [ "$OLDNAME" == "_" ]
		then NEWNAME="_"
		fi
		if [ "$NEWNAME" != "$OLDNAME" ]
		then
			let loop+=1		# we had to rename something and should loop because further tidy may be necessary
#			echo ""	1>&2		# a blank line between files renamed for easy reading
			if [ -d "$NEWNAME" ]
			then 								# we have at least one directory involved
				if [ -d "$OLDNAME" ]
				then							# rsync the directories
					rmdir "$OLDNAME"
					if [ $? -ne 0 ]
					then
						nice ionice -c 3 rsync -auvzH ./"$OLDNAME"/.??*	"$NEWNAME" && rm -rf ./"$OLDNAME"/.??* 2>/dev/null
						nice ionice -c 3 rsync -auvzH ./"$OLDNAME"/*	"$NEWNAME" && rm -rf ./"$OLDNAME"
					fi
				else							# file vs directory
					echo "In `pwd` I want to rename the file "$OLDNAME" over the "$NEWNAME" directory, which would be bad, so you fix it manually..."	1>&2
					echo "...it could be a collision of a directory and a symbolic link... in which case edit $0"	1>&2
					exit -2
				fi
			else								# good, no directory in the way
				if [ -f "$NEWNAME" ]					# do we already have a file by this name?
				then							# filename collision, let's update
					if [ "$NEWNAME" == "$(echo "$OLDNAME" | tr '[:upper:]' '[:lower:]')" ]
					then						# EEEK, name match requires special handling on NTFS
						mv		./"$OLDNAME"	./"$OLDNAME"-
						nice ionice -c 3 rsync -auvzH	./"$OLDNAME"-	"$NEWNAME" && rm -rf ./"$OLDNAME"-
					else						# Phew, no need to dance around Bill Gates steaming pile of excrement
						nice ionice -c 3 rsync -auvzH	./"$OLDNAME"	"$NEWNAME" && rm -rf ./"$OLDNAME"
					fi
				else
					mv -v -- ./"$OLDNAME" "$NEWNAME"		# no filename collision, just rename
				fi
			fi
		fi

		# Directories are recursed if we were invoked with option -r
		if [ $recurse = 0 ]
			then	# we were commanded to recurse
				if [ -d "$NEWNAME" ] 
				then	# directory
					"$0" -r "$NEWNAME"	# Follow the White Rabbit, Neo.
				fi
		fi

# This code stupidly preserves the (possibly) older version rather then overwriting it with (possibly) newer version
# We would prefer to rsync the new toxic name over the old detoxed name (doing what detox itself SHOULD do (at least optionally))
# followed by removal of the toxic name, but we're skipping the rsync step now,
# But it will take some thought to implement the improved version doing so...
# Even better but even more work, would be to write a shell script wrapper around detox itself to fix it,
# as I'm not about to go hack detox code in another language.
#		for f in $(/usr/bin/detox ${NEWNAME} |& grep "Cannot rename" | sed 's/^Cannot rename //;s/ .*$//'|sort -u)
#		do
#		    if [ -f "${f}" ]
#		    then
#			echo "DETOXifying	 ${f}" 1>&2
#			rm			"${f}" 1>&2
#		    fi
#		done

	fi
    done	# i
done	# loop until no changes necessary
exit 0	# Pau!

