1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
|
#!/bin/sh
# Plugin to find/remove duplicate files for Clifm
#
# Usage: fdups.sh [DIR]
#
# Description: List non-empty duplicate files (based on size and followed
# by MD5) in DIR (current directory if omitted) and allow the user to remove
# one or more of them
#
# Dependencies:
# find md5sum sort uniq xargs sed stat (on Linux/Haiku)
# gfind md5 sort guniq xargs sed stat (on BSD/MacOS/SunOS)
#
# On BSD/MacOS/SunOS you need to install both coreutils (for guniq)
# and findutils (for gfind)
#
# Notes:
# If the file size exceeds SIZE_DIGITS digits the file will be misplaced.
# (12 digits amount to sizes up to 931GiB)
#
# Based on https://github.com/jarun/nnn/blob/master/plugins/dups and modified
# to fit our needs
#
# Authors: syssyphus, KlzXS, leo-arch, danfe
# License: GPL3
me="clifm"
OS="$(uname)"
case "$OS" in
Linux|Haiku)
FIND="find"
MD5="md5sum"
UNIQ="uniq"
STAT="stat -c %Y"
;;
FreeBSD|NetBSD|OpenBSD|DragonFly|Darwin|SunOS)
FIND="gfind"
if [ "$OS" = "NetBSD" ]; then
MD5="md5 -n"
else
MD5="md5 -r"
fi
UNIQ="guniq"
STAT="stat -f %m"
;;
*)
printf "%s: This plugin is not supported on $OS\n" "$me" >&2
exit 1
esac
no_dep=0
if ! type "$FIND" > /dev/null 2>&1; then
printf "%s: %s: command not found\n" "$me" "$FIND" >&2; no_dep=1
elif ! type "${MD5%% *}" > /dev/null 2>&1; then
printf "%s: %s: command not found\n" "$me" "${MD5%% *}" >&2; no_dep=1
elif ! type sort > /dev/null 2>&1; then
printf "%s: sort: command not found\n" "$me" >&2; no_dep=1
elif ! type "$UNIQ" > /dev/null 2>&1; then
printf "%s: %s: command not found\n" "$me" "$UNIQ" >&2; no_dep=1
elif ! type xargs > /dev/null 2>&1; then
printf "%s: xargs: command not found\n" "$me" >&2; no_dep=1
elif ! type sed > /dev/null 2>&1; then
printf "%s: sed: command not found\n" "$me" >&2; no_dep=1
elif ! type "${STAT%% *}" > /dev/null 2>&1; then
printf "%s: %s: command not found\n" "$me" "${STAT%% *}" >&2; no_dep=1
fi
[ "$no_dep" = 1 ] && exit 127
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
name="${CLIFM_PLUGIN_NAME:-$(basename "$0")}"
printf "List non-empty duplicated files in DIR (current directory \
if omitted) and allow the user to selectively delete them\n"
printf "\n\x1b[1mUSAGE\x1b[0m\n %s [DIR]\n" "$name"
exit 0
fi
if [ -n "$1" ] && ! [ -d "$1" ]; then
printf "%s: %s: Not a directory\n" "$me" "$1" >&2
exit 1
fi
dir="${1:-.}"
# The find command below fails when file names contain single quotes
# Let's warn the user
SQF="$($FIND "$dir" -type f -name "*'*")"
if [ -n "$SQF" ]; then
#if $FIND "$dir" -type f -name "*'*"; then
printf "Warning: Some files in this directory contain single quotes in their names.\n\
Rename them or they will be ignored.\n\n\
TIP: You can use the 'br' command to rename them in bulk:\n\
s *\\\'*\n\
br sel\n\n\
Ignore these files and continue? [y/N] "
read -r answer
if [ "$answer" != "y" ] && [ "$answer" != "Y" ]; then
exit 0
fi
echo ""
fi
_EDITOR="${EDITOR:-nano}"
TMP_DIR="${TMPDIR:-/tmp}"
tmp_file=$(mktemp "$TMP_DIR/.${me}XXXXXX")
size_digits=12
printf "\
## This is a list of all duplicates found (if empty, just exit).
## Comment out the files you want to remove (lines starting with double number
## sign (##) are ignored.
## Save and close this file to remove commented files (deletion approval will
## be asked before removing files)
## Files can be removed either forcefully or interactively.\n
" > "$tmp_file"
# shellcheck disable=SC2016
$FIND "$dir" -size +0 -type f -printf "%${size_digits}s %p\n" | sort -rn | $UNIQ -w"${size_digits}" -D \
| sed -e "s/^ \{0,12\}\([0-9]\{0,12\}\) \(.*\)\$/printf '%s %s\\\n' \"\$($MD5 '\2')\" 'd\1'/" \
| tr '\n' '\0' | xargs -0 -n1 -r sh -c 2>/dev/null | sort | { $UNIQ -w32 --all-repeated=separate; echo; } \
| sed -ne 'h
s/^\(.\{32\}\).* d\([0-9]*\)$/## md5sum: \1 size: \2 bytes/p
g
:loop
N
/.*\n$/!b loop
p' \
| sed -e 's/^.\{32\} *\(.*\) d[0-9]*$/\1/' >> "$tmp_file"
time_pre="$($STAT "$tmp_file")"
"$_EDITOR" "$tmp_file"
time_post="$($STAT "$tmp_file")"
if [ "$time_pre" = "$time_post" ]; then
printf "%s: Nothing to do\n" "$me"
exit 0
fi
printf "Note: If you answer is yes, you will be given the option to remove them interactively\n\
Remove commented files? [y/N] "
read -r answer
if [ "$answer" = "y" ] || [ "$answer" = "Y" ]; then
sedcmd="/^##.*/d; /^[^#].*/d; /^$/d; s/^# *\(.*\)$/\1/"
else
exit 0
fi
printf "Remove files forcefully or interactively? [f/I] "
read -r force
if [ "$force" = "f" ] || [ "$force" = "F" ]; then
#shellcheck disable=SC2016
sed -e "$sedcmd" "$tmp_file" | tr '\n' '\0' | xargs -0 -r sh -c 'rm -f "$0" "$@" </dev/tty'
else
#shellcheck disable=SC2016
sed -e "$sedcmd" "$tmp_file" | tr '\n' '\0' | xargs -0 -r sh -c 'rm -i "$0" "$@" </dev/tty'
fi
rm -- "$tmp_file"
exit 0
|