#!/usr/bin/env bash # ################################################## # # Taken from: https://github.com/morgant/tools-osx # # Trash allows trashing of files instead of tempting fate with rm. Correctly handles # trashing files on other volumes, uses the same filename renaming scheme as Finder # for duplicate file names, can list trash contents w/disk usage summary, and empty # trash (including securely) w/confirmation. Does not require Finder to be running. # version="1.0.0" # Sets version variable # scriptTemplateVersion="1.4.1" # Version of scriptTemplate.sh that this script is based on # # HISTORY: # # * 2015-06-20 - v1.0.0 - First Creation # # ################################################## # Provide a variable with the location of this script. scriptPath="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Source Scripting Utilities # ----------------------------------- # These shared utilities provide many functions which are needed to provide # the functionality in this boilerplate. This script will fail if they can # not be found. # ----------------------------------- utilsLocation="${scriptPath}/../lib/utils.sh" # Update this path to find the utilities. if [ -f "${utilsLocation}" ]; then source "${utilsLocation}" else echo "Please find the file util.sh and add a reference to it in this script. Exiting." exit 1 fi # trapCleanup Function # ----------------------------------- # Any actions that should be taken if the script is prematurely # exited. Always call this function at the top of your script. # ----------------------------------- function trapCleanup() { echo "" if is_dir "${tmpDir}"; then rm -r "${tmpDir}" fi die "Exit trapped." # Edit this if you like. } # Set Flags # ----------------------------------- # Flags which can be overridden by user input. # Default values are below # ----------------------------------- quiet=0 printLog=0 verbose=0 force=0 strict=0 debug=0 list=false emptyTrash=false secureEmpty=false args=() # Set Temp Directory # ----------------------------------- # Create temp directory with three random numbers and the process ID # in the name. This directory is removed automatically at exit. # ----------------------------------- tmpDir="/tmp/${scriptName}.$RANDOM.$RANDOM.$RANDOM.$$" (umask 077 && mkdir "${tmpDir}") || { die "Could not create temporary directory! Exiting." } # Check for Dependencies # ----------------------------------- # Arrays containing package dependencies needed to execute this script. # The script will fail if dependencies are not installed. For Mac users, # most dependencies can be installed automatically using the package # manager 'Homebrew'. Mac applications will be installed using # Homebrew Casks. Ruby and gems via RVM. # ----------------------------------- homebrewDependencies=() caskDependencies=() gemDependencies=() function mainScript() { ############## Begin Script Here ################### #################################################### # global variables user=$(whoami) uid=$(id -u "$user") finder_pid=$(ps -u "$user" | grep /System/Library/CoreServices/Finder.app | grep -v grep | awk '{print $1}') v='' # determine whether we can script the Finder or not function have_scriptable_finder() { # We must have a valid PID for Finder, plus we cannot be in `screen` (another thing that's broken) if [[ (${finder_pid} -gt 1) && ("$STY" == "") ]]; then true else false fi } ## ## Convert a relative path to an absolute path. ## ## From http://github.com/morgant/realpath ## ## @param string the string to converted from a relative path to an absolute path ## @returns Outputs the absolute path to STDOUT, returns 0 if successful or 1 if an error (esp. path not found). ## function realpath() { local success=true local path="$1" # make sure the string isn't empty as that implies something in further logic if [ -z "$path" ]; then success=false else # start with the file name (sans the trailing slash) path="${path%/}" # if we stripped off the trailing slash and were left with nothing, that means we're in the root directory if [ -z "$path" ]; then path="/" fi # get the basename of the file (ignoring '.' & '..', because they're really part of the path) local file_basename="${path##*/}" if [[ ( "$file_basename" = "." ) || ( "$file_basename" = ".." ) ]]; then file_basename="" fi # extracts the directory component of the full path, if it's empty then assume '.' (the current working directory) local directory="${path%$file_basename}" if [ -z "$directory" ]; then directory='.' fi # attempt to change to the directory if ! cd "$directory" &>/dev/null ; then success=false fi if $success; then # does the filename exist? if [[ ( -n "$file_basename" ) && ( ! -e "$file_basename" ) ]]; then success=false fi # get the absolute path of the current directory & change back to previous directory local abs_path="$(pwd -P)" cd "-" &>/dev/null # Append base filename to absolute path if [ "${abs_path}" = "/" ]; then abs_path="${abs_path}${file_basename}" else abs_path="${abs_path}/${file_basename}" fi # output the absolute path echo "$abs_path" fi fi $success } function listTrash() { num_volumes=0 total_blocks=0 # list file contents & calculate size for user's .Trash folder if find "/Users/${user}/.Trash" -depth 1 ! -depth 0; then num_volumes=$(( $num_volumes + 1 )) blocks=$(du -cs "/Users/${user}/.Trash" | tail -n 1 | cut -f 1) total_blocks=$(( $total_blocks + $blocks )) fi # list file contents & calculate size for volume-specific .Trashes folders for file in /Volumes/*; do if [ -d "$file" ]; then folder="${file}/.Trashes/${uid}" if [ -d "${folder}" ]; then if find "$folder" -depth 1 ! -depth 0; then num_volumes=$(( $num_volumes + 1 )) blocks=$(du -cs "$folder" | tail -n 1 | cut -f 1) total_blocks=$(( $total_blocks + $blocks )) fi fi fi done # convert blocks to human readable size size=0 if (( $total_blocks >= 2097152 )); then size=$(bc <<< "scale=2; $total_blocks / 2097152") size="${size}GB" elif (( $total_blocks >= 2048 )); then size=$(bc <<< "scale=2; $total_blocks / 2048") size="${size}MB" else size=$(bc <<< "scale=2; $total_blocks / 2") size="${size}K" fi info "$size across $num_volumes volume(s)." safeExit } function emptyTheTrash() { # determine if we can tell Finder to empty trash via AppleScript if have_scriptable_finder; then notice "Telling Finder to empty trash... " if /usr/bin/osascript -e "tell application \"Finder\" to empty trash" ; then success "Trash has been emptied." safeExit else die "Unable to empty trash." fi # if Finder isn't scriptable, we'll manually empty the trash ourselves else # confirm that the user wants to empty the trash seek_confirmation "Are you sure you want to empty the trash (this cannot be undone)?" if is_confirmed; then notice "Emptying trash..." # delete the contents of user's .Trash folder if [[ $verbose == "1" ]]; then v="-v"; fi find "/Users/${user}/.Trash" -depth 1 ! -depth 0 -print0 | xargs -0 rm $v -r # delete the contents of the volume-specific .Trashes folders for file in /Volumes/*; do if [ -d "$file" ]; then folder="${file}/.Trashes/${uid}" if [ -d "$folder" ]; then find "$folder" -depth 1 ! -depth 0 -print0 | xargs -0 rm $v -r fi fi done success "Trash has been emptied." fi safeExit fi } function secureEmptyTheTrash() { # determine if we can tell Finder to securely empty trash via AppleScript if have_scriptable_finder; then notice "Telling Finder to securely empty trash... " if /usr/bin/osascript -e "tell application \"Finder\" to empty trash with security" ; then success "Trash has been securely emptied." safeExit else die "Could not empty trash." fi # if Finder isn't scriptable, we'll manually empty the trash ourselves else if [[ $verbose == "1" ]]; then v="-v"; fi # confirm that the user wants to securely empty the trash seek_confirmation "Are you sure you want to securely empty the trash (this REALLY cannot be undone)?" if is_confirmed; then # securely delete the contents of user's .Trash folder find "/Users/${user}/.Trash" -depth 1 ! -depth 0 -print0 | xargs -0 srm $v -r # securely delete the contents of the volume-specific .Trashes folders for file in /Volumes/*; do if [ -d "$file" ]; then folder="${file}/.Trashes/${uid}" if [ -d "$folder" ]; then find "$folder" -depth 1 ! -depth 0 -print0 | xargs -0 srm $v -r fi fi done success "Trash has been securely emptied." fi safeExit fi } function trashAFile() { if [[ $verbose == "1" ]]; then v="-v"; fi # Iterate over all files passed by user for userFile in "${args[@]}"; do if [ ! -e "${userFile}" ]; then warning "$userFile: No such file or directory." continue fi # determine if we'll tell Finder to trash the file via AppleScript (very easy, plus free undo # support, but Finder must be running for the user and is DOES NOT work from within `screen`) if have_scriptable_finder; then # determine whether we have an absolute path name to the file or not if [ "${userFile:0:1}" = "/" ]; then file="$userFile" else # expand relative to absolute path verbose "Determining absolute path for '$userFile'... " file="$(realpath "$userFile")" if [ $? -ne 0 ]; then warning "Could not determine absolute path for '$userFile'!" fi fi verbose "Telling Finder to trash '$file'..." if /usr/bin/osascript -e "tell application \"Finder\" to delete POSIX file \"$file\"" &>/dev/null; then success "'$userFile' moved to trash" else warning "'$userFile' not moved to trash" safeExit fi # Finder isn't available for this user, so don't rely on it (we'll do all the dirty work ourselves) else trash="/Users/${user}/.Trash/" # create the trash folder if necessary if [ ! -d "$trash" ]; then mkdir $v "$trash" fi # move the file to the trash if [ ! -e "${trash}${userFile}" ]; then mv $v "$userFile" "$trash" else # determine if the filename has an extension ext=false case "$userFile" in *.*) ext=true ;; esac # keep incrementing a number to append to the filename to mimic Finder i=1 if $ext; then new="${trash}${userFile%%.*} ${i}.${userFile##*.}" else new="${trash}$userFile $i" fi while [ -e "$new" ]; do ((i=$i + 1)) if $ext; then new="${trash}${userFile%%.*} ${i}.${userFile##*.}" else new="${trash}${userFile} $i" fi done #move the file to the trash with the new name mv $v "${userFile}" "$new" fi fi done } # run functions if $list; then listTrash; fi if $emptyTrash; then emptyTheTrash; fi if $secureEmpty; then secureEmptyTheTrash; fi trashAFile #################################################### ############### End Script Here #################### } ############## Begin Options and Usage ################### # Print usage usage() { echo -n "${scriptName} [OPTION]... [FILE]... ${bold}Trash${reset} allows trashing of files instead of tempting fate with ${bold}rm${reset}. Correctly handles trashing files on other volumes, uses the same filename renaming scheme as Finder for duplicate file names, can list trash contents w/disk usage summary, and empty trash (including securely) w/confirmation. Does not require Finder to be running. ${bold}Options:${reset} -l , --list list trash contents -e, --empty empty trash contents -s, --secure secure empty trash contents --force Skip all user interaction. Implied 'Yes' to all actions. -q, --quiet Quiet (no output) -v, --verbose Output more information. (Items echoed to 'verbose') -d, --debug Runs script in BASH debug mode (set -x) -h, --help Display this help and exit --version Output version information and exit " } # Iterate over options breaking -ab into -a -b when needed and --foo=bar into # --foo bar optstring=h unset options while (($#)); do case $1 in # If option is of type -ab -[!-]?*) # Loop over each character starting with the second for ((i=1; i < ${#1}; i++)); do c=${1:i:1} # Add current char to options options+=("-$c") # If option takes a required argument, and it's not the last char make # the rest of the string its argument if [[ $optstring = *"$c:"* && ${1:i+1} ]]; then options+=("${1:i+1}") break fi done ;; # If option is of type --foo=bar --?*=*) options+=("${1%%=*}" "${1#*=}") ;; # add --endopts for -- --) options+=(--endopts) ;; # Otherwise, nothing special *) options+=("$1") ;; esac shift done set -- "${options[@]}" unset options # Print help if no arguments were passed. # Uncomment to force arguments when invoking the script [[ $# -eq 0 ]] && set -- "--help" # Read the options and set stuff while [[ $1 = -?* ]]; do case $1 in -h|--help) usage >&2; safeExit ;; --version) echo "$(basename $0) ${version}"; safeExit ;; -l|--list) list=true ;; -s|--secure) secureEmpty=true ;; -e|--empty) emptyTrash=true ;; -v|--verbose) verbose=1 ;; -q|--quiet) quiet=1 ;; -d|--debug) debug=1;; --force) force=1 ;; --endopts) shift; break ;; *) die "invalid option: '$1'." ;; esac shift done # Store the remaining part as arguments. args+=("$@") ############## End Options and Usage ################### # ############# ############# ############# # ## TIME TO RUN THE SCRIPT ## # ## ## # ## You shouldn't need to edit anything ## # ## beneath this line ## # ## ## # ############# ############# ############# # Trap bad exits with your cleanup function trap trapCleanup EXIT INT TERM # Set IFS to preferred implementation IFS=$'\n\t' # Exit on error. Append '||true' when you run the script if you expect an error. set -o errexit # Run in debug mode, if set if [ "${debug}" == "1" ]; then set -x fi # Exit on empty variable if [ "${strict}" == "1" ]; then set -o nounset fi # Bash will remember & return the highest exitcode in a chain of pipes. # This way you can catch the error in case mysqldump fails in `mysqldump |gzip`, for example. set -o pipefail # Invoke the checkDependenices function to test for Bash packages # checkDependencies # Run your script mainScript safeExit # Exit cleanly