diff --git a/bin/trash b/bin/trash new file mode 100755 index 0000000..b5aeec0 --- /dev/null +++ b/bin/trash @@ -0,0 +1,495 @@ +#!/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=true +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 + verbose "listing trash contents" + + # 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 \ No newline at end of file