#!/usr/bin/env bash function mainScript() { local user local uid local finder_pid user=$(whoami) uid=$(id -u "$user") finder_pid=$(ps -u "$user" | grep /System/Library/CoreServices/Finder.app | grep -v grep | awk '{print $1}') if ${verbose}; then v="-v"; fi function have_scriptable_finder() { # Determine whether we can script the Finder or not # 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 } function realpath() { # 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). local successPath=true local path="$1" # make sure the string isn't empty as that implies something in further logic if [ -z "$path" ]; then successPath=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 successPath=false fi if ${success}; then # does the filename exist? if [[ ( -n "${file_basename}" ) && ( ! -e "${file_basename}" ) ]]; then successPath=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 ${successPath} } function listTrash() { local num_volumes local total_blocks local blocks local size num_volumes=0 total_blocks=0 notice "Listing items in Trash" # 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)." } 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 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 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 # 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 fi } function trashAFile() { # 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 success "'${userFile}' moved to trash" else warning "'${userFile}' not moved to trash" continue 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 } if ${list}; then listTrash; safeExit; fi if ${emptyTrash}; then emptyTheTrash; safeExit; fi if ${secureEmpty}; then secureEmptyTheTrash; safeExit; fi # Default behavior without flags is to delete a file trashAFile } function trapCleanup() { # trapCleanup Function # ----------------------------------- # Any actions that should be taken if the script is prematurely # exited. Always call this function at the top of your script. # ----------------------------------- echo "" # Delete temp files, if any if [ -d "${tmpDir}" ] ; then rm -r "${tmpDir}" fi die "Exit trapped." } function safeExit() { # safeExit # ----------------------------------- # Non destructive exit for when script exits naturally. # Usage: Add this function at the end of every script. # ----------------------------------- # Delete temp files, if any if [ -d "${tmpDir}" ] ; then rm -r "${tmpDir}" fi trap - INT TERM EXIT exit } # Set Flags # ----------------------------------- # Flags which can be overridden by user input. # Default values are below # ----------------------------------- list=false emptyTrash=false secureEmpty=false quiet=false printLog=false verbose=false force=false strict=false debug=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." } # Logging # ----------------------------------- # Log is only used when the '-l' flag is set. # # To never save a logfile change variable to '/dev/null' # Save to Desktop use: $HOME/Desktop/${scriptBasename}.log # Save to standard user log location use: $HOME/Library/Logs/${scriptBasename}.log # ----------------------------------- logFile="${HOME}/Library/Logs/${scriptBasename}.log" # 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. --log Print log to file -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 ;; --log) printLog=true ;; -q|--quiet) quiet=true ;; -s|--strict) strict=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+=("$@") # Logging and Colors # ----------------------------------------------------- # Here we set the colors for our script feedback. # Example usage: success "sometext" #------------------------------------------------------ # Set Colors bold=$(tput bold) reset=$(tput sgr0) purple=$(tput setaf 171) red=$(tput setaf 1) green=$(tput setaf 76) tan=$(tput setaf 3) blue=$(tput setaf 38) underline=$(tput sgr 0 1) function _alert() { if [ "${1}" = "emergency" ]; then local color="${bold}${red}"; fi if [ "${1}" = "error" ] || [ "${1}" = "warning" ]; then local color="${red}"; fi if [ "${1}" = "success" ]; then local color="${green}"; fi if [ "${1}" = "debug" ]; then local color="${purple}"; fi if [ "${1}" = "header" ]; then local color="${bold}""${tan}"; fi if [ "${1}" = "input" ]; then local color="${bold}"; printLog="0"; fi if [ "${1}" = "info" ] || [ "${1}" = "notice" ]; then local color=""; fi # Don't use colors on pipes or non-recognized terminals if [[ "${TERM}" != "xterm"* ]] || [ -t 1 ]; then color=""; reset=""; fi # Print to $logFile if ${printLog}; then echo -e "$(date +"%m-%d-%Y %r") $(printf "[%9s]" "${1}") ${_message}" >> "${logFile}"; fi # Print to console when script is not 'quiet' if ${quiet}; then return else echo -e "$(date +"%r") ${color}$(printf "[%9s]" "${1}") ${_message}${reset}"; fi } function die () { local _message="${*} Exiting."; echo "$(_alert emergency)"; safeExit;} function error () { local _message="${*}"; echo "$(_alert error)"; } function warning () { local _message="${*}"; echo "$(_alert warning)"; } function notice () { local _message="${*}"; echo "$(_alert notice)"; } function info () { local _message="${*}"; echo "$(_alert info)"; } function debug () { local _message="${*}"; echo "$(_alert debug)"; } function success () { local _message="${*}"; echo "$(_alert success)"; } function input() { local _message="${*}"; echo -n "$(_alert input)"; } function header() { local _message="========== ${*} ========== "; echo "$(_alert header)"; } # Log messages when verbose is set to "true" verbose() { if ${verbose}; then debug "$@"; fi } function seek_confirmation() { # echo "" input "$@" if [[ "${force}" == "1" ]]; then notice "Forcing confirmation with '--force' flag set" else read -p " (y/n) " -n 1 echo "" fi } function is_confirmed() { if [[ "${force}" == "1" ]]; then return 0 else if [[ "${REPLY}" =~ ^[Yy]$ ]]; then return 0 fi return 1 fi } function is_not_confirmed() { if [[ "${force}" == "1" ]]; then return 1 else if [[ "${REPLY}" =~ ^[Nn]$ ]]; then return 0 fi return 1 fi } # 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 # Run your script mainScript # Exit cleanly safeExit