#!/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=false printLog=false verbose=false force=false strict=false debug=false 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 local user=$(whoami) local uid=$(id -u "$user") local 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 local 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 #if osascript -e "tell app \"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 local 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 "${ }" in *.*) ext=true ;; esac # keep incrementing a number to append to the filename to mimic Finder local 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 MacOS trashing of files instead of tempting fate with ${bold}rm${reset}. Anything deleted with Trash will be moved to the native MacOS trash folder. This script: - Correctly handles ${bold}trashing files on other volumes${reset} - Uses the ${bold}same filename renaming scheme as Finder${reset} for duplicate file names - Can ${bold}list trash contents${reset} w/disk usage summary - ${bold}Empty trash${reset} (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=true ;; -q|--quiet) quiet=true ;; -d|--debug) debug=true;; --force) force=true ;; --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}; then set -x fi # Exit on empty variable if ${strict}; 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