From 69d18056c4ca1659d317135cf2a46078b25227bb Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Mon, 6 Apr 2020 23:17:08 -0700 Subject: [PATCH] Totally revamped and improved bashtrash. --- trash | 410 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 202 insertions(+), 208 deletions(-) diff --git a/trash b/trash index aa3a779..e4ed5b1 100755 --- a/trash +++ b/trash @@ -1,8 +1,9 @@ -#!/bin/bash +#!/bin/sh -# bashtrash - a bash script implementation of the FreeDesktop.org Trash -# Specification. +# shtrash - a pure POSIX shell script implementation of the +# FreeDesktop.org Trash Specification. +# Copyright (c) 2020, Bruce Hill # Copyright (c) 2009-2011, Robert Rothenberg # This program is free software; you can redistribute it and/or modify @@ -15,261 +16,254 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -version="0.4.0" +version="0.5.0" -progname=`basename $0` +progname="$(basename "$0")" +verbose_flag= +interactive_flag= +force_flag= -function show_usage { +fail() { + echo "$@" 1>&2 + exit 1 +} + +show_usage() { cat << EOU -Usage: ${progname} [OPTION]... FILE... +Usage: $progname [OPTION]... FILE... Move files into the trash. Options: - --version show program's version number and exit - -h show this help message and exit - -v explain what is being done - -i prompt before moving every file - -r, -R ignored (for compatability with rm) - -f ignore non-existent files, never prompt + -h, --help show this help message and exit + -V, --version show program's version number and exit + -v, --verbose explain what is being done + -i, --interactive prompt before moving every file + -f, --force ignore non-existent files, never prompt + -r, -R, --recursive ignored (for compatability with rm) + -u, --untrash restore file(s) from the trash + -e, --empty choose files to empty from the trash + -E, --empty-all empty all the files in the trash + -l, --list list files in the trash + -- Any arguments after '--' will be treated as filenames EOU } -function try_help { - echo "Try \`${progname} -h' for more information." 1>&2 - exit 1 -} - -# sed script to encode filenames - -sedscript='s/ /%20/g -s/!/%21/g -s/"/%22/g -s/\#/%23/g -s/\$/%24/g -s/\&/%26/g -s/'\''/%27/g -s/(/%28/g -s/)/%29/g -s/\*/%2a/g -s/+/%2b/g -s/,/%2c/g -s/-/%2d/g -s/:/%3a/g -s/;/%3b/g -s//%3e/g -s/?/%3f/g -s/@/%40/g -s/\[/%5b/g -s/\\/%5c/g -s/\]/%5d/g -s/\^/%5e/g -s/_/%5f/g -s/`/%60/g -s/{/%7b/g -s/|/%7c/g -s/}/%7d/g -s/~/%7e/g -s/ /%09/g' - -function url_encode { - echo $1 |sed -e "$sedscript" -} - -function get_trashdir { - mounts=`awk '{ print $2 }' /proc/mounts` - base=/ - - if [ "$EUID" != "0" ]; then - mounts="$HOME $mounts" - fi - - for i in $mounts - do - if [[ $1 =~ ^$i ]] - then - if [[ $i =~ ^$base ]] - then - base=$i - fi - fi - done - - if [ "$base" != "$HOME" ]; then - trashdir="$base/.Trash/$UID" - if [ ! -d "$trashdir" ]; then - trashdir="$base/.Trash-$UID" - fi - - mkdir -p "$trashdir" - if [ "$?" != "0" ]; then - base=$HOME - fi - fi - - if [ "$base" == "$HOME" ]; then - base=$XDG_DATA_HOME - if [ -z "$base" ]; then - base="$HOME/.local/share/" - fi - trashdir="$base/Trash" - fi - - echo $trashdir -} - -function can_trash { - filename="$1" - - if [ ! -e "$filename" ]; then - if [ "$interactive" != "force" ]; then - echo "${progname}: cannot move \`$filename' to trash: No such file or directory" 1>&2 - fi - echo 0 +confirm() { + [ "$force_flag" = "-f" ] && return 0 + if type ask >/dev/null; then + ask -n "$@" else - - type="file" - if [ -d "$filename" ]; then - type="directory" + # Get one character of input + tput civis >/dev/tty; + printf '\033[1m%s\033[0m' "$2" >/dev/tty; + stty -icanon -echo >/dev/tty 2>/dev/tty; + if [ "$(uname)" = "Darwin" ]; then + read -n 1 REPLY /dev/tty; + else + REPLY="$(dd bs=1 count=1 2>/dev/null &2 ; echo 0; exit 1 ;; - esac + stty icanon echo >/dev/tty 2>/dev/tty + tput cvvis >/dev/tty + [ "$REPLY" = "y" ] fi } -function init_trashdir { +has_arg() { + if type arg >/dev/null; then + arg "$@" >/dev/null + else + target="$1" + while [ $# -gt 0 ]; do + [ "$1" = "$target" ] && return 0 + [ "$1" = "--" ] && break + done + false + fi +} + +is_valid_trashdir() { + [ -d "$1" ] && expr "$(stat -c '%a' "$1")" : '^1[0-7]\{3\}' >/dev/null && ! [ -L "$1" ] +} + +get_trashdir() { + file="$(readlink -f -- "$1")" + if [ -e "$file" ]; then + file_base="$(df -h "$file" | awk 'NR==2 {print $NF}')" + else + file_base="$(awk -v f="$file" 'substr(f,1,length($2)+1)==($2 "/") && $2>best {best=$2} END {print best}' /proc/mounts)" + fi + home_base="$(df -h "$HOME" | awk 'NR==2 {print $NF}')" + if [ "$file_base" = "$home_base" ]; then + echo "$HOME/.Trash" + elif is_valid_trashdir "$file_base/.Trash"; then + echo "$file_base/.Trash" + else + echo "$file_base/.Trash-$(id -u)" + fi +} + +can_trash() { + filename="$1" + if [ ! -e "$filename" ]; then + if [ "$force_flag" != "-f" ]; then + fail "$progname: cannot move '$filename' to trash: No such file or directory" + fi + return 0 + fi + + if [ "$interactive_flag" = "-i" ]; then + [ -d "$filename" ] && type=directory || type="file" + confirm "$progname: move $type '$filename' to trash?" + fi +} + +init_trashdir() { trashdir=$1 - - mkdir -p "$trashdir/files" - if [ "$?" != "0" ]; then - echo "${progname}: unable to write to $trashdir" 1>&2 - exit 2 - fi - - mkdir -p "$trashdir/info" - if [ "$?" != "0" ]; then - echo "${progname}: unable to write to $trashdir" 1>&2 - exit 2 + if ! [ -d "$trashdir" ]; then + mkdir -m 1755 "$trashdir" || fail "Could not create trash directory" fi + mkdir -p "$trashdir/files" || fail "$progname: unable to write to $trashdir" + mkdir -p "$trashdir/info" || fail "$progname: unable to write to $trashdir" } -function trash_file { +path_from_trashinfo() { + #sed -n 's/^Path=//p' "$1" | php -r 'echo urldecode(fgets(STDIN));' + sed -n 's/^Path=//p' "$1" | perl -MURI::Escape -ne 'print uri_unescape $_' +} + +trashinfo_for_file() { + #php -r 'echo urlencode($argv[1]);' -- "$1" + cat < "$deletedinfo" < "$deletedinfo" || \ + fail "$progname: unable to create trash info for $filename at $deletedinfo" # Note that the trashinfo file will have the ownership and # permissions of the person who deleted the file, and not # necessarily of the original file. - mv $mv_opts "${filename}" "${deletedfile}" - if [ "$?" != "0" ]; then - echo "${progname}: unable to move ${filename} to ${deletedfile}" 1>&2 + if ! mv $verbose_flag -- "$filename" "$deletedfile"; then rm "$deletedinfo" - exit 2 + fail "$progname: unable to move $filename to $deletedfile" fi } -# Option handling +untrash_files() { + td="$(get_trashdir "$(readlink -f -- "${1:-$PWD}")")" + [ -d "$td/info" ] || return 1 + ls -A "$td"/info | \ + fzf -1 --prompt="Pick file to untrash: " --query="$@" --multi \ + --preview="sh -c 'f=\"$td/files/\${1%.trashinfo}\"; [ -d \"\$f\" ] && tree \"\$f\" || echo \"\$f\" && cat \"\$f\"' -- {}" | \ + while read -r info; do + orig="$(path_from_trashinfo "$td/info/$info")" + mv -i "$td/files/${info%.trashinfo}" "$orig" && rm -f "$td/info/$info" + done +} -function strip_quotes { - x="$1" - x="${x#\'}" - x="${x%\'}" - echo "${x}" +empty_files() { + td="$(get_trashdir "$(readlink -f -- "${1:-$PWD}")")" + if [ -d "$td" ] && [ -z "$(ls -A "$td/files")" ]; then + echo "Nothing in the trash!" + exit + fi + ls -A "$td"/info | \ + fzf -1 --prompt="Pick file(s) to permanently delete: " --query="$@" --multi \ + --preview="sh -c 'f=\"$td/files/\${1%.trashinfo}\"; [ -d \"\$f\" ] && tree \"\$f\" || echo \"\$f\" && cat \"\$f\"' -- {}" | \ + while read -r info; do + orig="$(path_from_trashinfo "$td/info/$info")" + rm -r $verbose_flag "$td/info/$info" "$td/files/${info%.trashinfo}" || fail "Could not remove file" + done + printf '\033[1mDeleted!\033[0m\n' +} + +empty_trash() { + td="$(get_trashdir "$(readlink -f -- "${1:-$PWD}")")" + if [ -d "$td" ] && [ -z "$(ls -A "$td/files")" ]; then + echo "Nothing in the trash!" + exit + fi + ( printf '\033[1mThe following files will be deleted:\033[0m\n' && + printf ' \033[33;1m%s\033[0m\n' "$td"/files/* && + printf '\033[1mThis will free up %s of space\033[0m\n' "$(du -h --summarize "$td/files" | cut -f1)") | more + confirm "Do you want to proceed?" || exit 1 + rm -r $verbose_flag "$td"/files/* "$td"/info/* || exit 1 + printf '\033[1mDeleted!\033[0m\n' +} + +list_trash() { + td="$(get_trashdir "$(readlink -f -- "${1:-$PWD}")")" + if [ -d "$td" ] && [ -z "$(ls -A "$td/files")" ]; then + echo "Nothing in the trash!" + exit + fi + ls -A "$td/files" } if [ $# -eq 0 ]; then - try_help + show_usage + fail "Try '$progname -h' for more information." fi -verbose=0 -interactive=never -filename= +(has_arg -v "$@" || has_arg --verbose "$@") && verbose_flag=-v +(has_arg -i "$@" || has_arg --interactive "$@") && interactive_flag=-i +(has_arg -f "$@" || has_arg --force "$@") && force_flag=-f +action=trash +(has_arg -h "$@" || has_arg --help "$@") && action=help +(has_arg -V "$@" || has_arg --version "$@") && action=version +(has_arg -l "$@" || has_arg --list "$@") && action=list +(has_arg -u "$@" || has_arg --untrash "$@") && action=untrash +(has_arg -e "$@" || has_arg --empty "$@") && action=emptyfiles +(has_arg -E "$@" || has_arg --empty-all "$@") && action=emptyall -while getopts hvirRf arg; do - case $arg in - h) show_usage; - exit 1 - ;; - i) interactive=always - ;; - v) verbose=1 - ;; - r|R) - ;; - f) interactive=force - ;; - [?]) try_help - exit 1 - ;; - esac +while a="$(expr ";$1" : "^;\(--\|-[VhvirRfueEl]\+\|--version\|--help\|--verbose\|--interactive\|--recursive\|--force\|--untrash\|--empty\|--empty-all\|--list\)$")"; do + shift + [ "$a" = "--" ] && break done -shift $(( OPTIND - 1)) - -for f in "$@"; do - - # get full pathname of file - - filename="$(readlink -f "${f}")" - - yes=`can_trash "$filename"` - if [ "$yes" != "0" ]; then - trash_file "$filename" - fi - -done - -exit 0 - - - - - +case "$action" in + help) show_usage ;; + version) echo "$version" ;; + list) list_trash ;; + untrash) untrash_files "$@" ;; + emptyfiles) empty_files "$@" ;; + emptyall) empty_trash "$@" ;; + trash) + for f; do + filename="$(readlink -f -- "$f")" + if ! [ -e "$filename" ]; then + [ "$force_flag" != "-f" ] || fail "File does not exist: $filename" + else + can_trash "$filename" && trash_file "$filename" || exit 1 + fi + done + ;; +esac