Created
November 4, 2024 19:38
-
-
Save sampersand/e7415bd9c8b2dab02885b0bd3a3ffe12 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
# Safety first | |
set -o nounset | |
scriptname="$(basename -- "$0"; echo x)"; scriptname="${scriptname%?x}" | |
warn () { | |
msg="%s: $1"'\n' | |
shift | |
printf "$msg" "$scriptname" "$@" >&2 | |
} | |
shortusage () { cat; } <<EOS >&2 | |
usage: ${scriptname} [-h/--help] ... | |
${scriptname} [options] [--] source target | |
${scriptname} [options] [--] source ... directory | |
EOS | |
longusage () { shortusage; cat <<EOS; } >&2 | |
options: | |
-h, --help print help, then exit. | |
-i, --interactive ask to overwrite files that exist (disables -n) | |
-n, --not-interactive do not overwrite not overwrite existing files at all (disables -i); default | |
-r, --rename rename files to prevent conflicts mode | |
-R, --no-rename disables '-r' | |
-c, --clobber-empty overwrite empty files/folders | |
-C, --no-clobber-empty disables -'c' | |
returns: (for non-zero exit status, it's the last error status encountered.) | |
0 If everything was successful | |
2 There's a problem running 'mv' (or 'rm' when --clobber-empty is enabled) | |
3 If 'directory' is not actually a directory ((, or is not executable)) | |
4 A 'source' does not exist | |
5 The 'source' and the destination are identical | |
253 not enough args were given | |
254 If an unknown argument was given | |
255 When -h or --help supplied provided | |
Without any flags, this acts the same as 'mv -n' (or 'mv -i' if --interactive). | |
If the '--clobber-empty' flag is given, then attempting to overwrite empty | |
files/folders will succeed. (Note that this is susceptible to race conditions) | |
If the '--rename' flag is given, then files will first be moved via 'mv -n', | |
but if the target file exists, the the first 'num' where the path | |
'<dir>/<base> <num><ext>' does not exist will be used instead. | |
EOS | |
do_rename= | |
clobber_empty= | |
move_mode=-n | |
# Parse command line options | |
while [ "$#" -ne 0 ]; do | |
case "$1" in | |
--) shift; break ;; | |
-h) shortusage; exit 255 ;; | |
--help) longusage; exit 255 ;; | |
-r | --rename) do_rename=1 ;; | |
-R | --no-rename) do_rename= ;; | |
-c | --clobber-empty) clobber_empty=1 ;; | |
-C | --no-clobber-empty) clobber_empty= ;; | |
-n | --not-interactive) move_mode=-n ;; | |
-i | --interactive) move_mode=-i ;; | |
-??*) # Support `-abc` options | |
rest2="${1#-?}" | |
rest1="${1%"$rest2"}" | |
shift | |
set -- "${rest1}" "-${rest2}" "$@" | |
continue ;; | |
-?) warn 'unknown option: %s' "$1"; exit 254 ;; | |
*) break ;; | |
esac | |
shift | |
done | |
# If no arguments are given, then error out with the usage. | |
if [ "$#" -le 1 ]; then | |
shortusage | |
exit 253 | |
fi | |
try_move () { | |
# Resolve the paths to absolute values. | |
source="$1" | |
target="$2" | |
# If the thing we're trying to move does not exist, then print out an error | |
# and go to the next one | |
if [ ! -e "${source}" ]; then | |
warn 'cannot move %q: No such file or directory' "${source}" | |
return 4 | |
fi | |
# If the paths are identical, then just don't move anything. | |
if [ "${source}" -ef "${target}" ]; then | |
warn 'cannot move %q to %q: Paths are identical' "${source}" "${target}" | |
return 5 | |
fi | |
idx=1 | |
while | |
# If clobber empty is defined, then delete the target if it doesn't exist | |
# before we attempt to move our file. | |
if [ -n "${clobber_empty}" ]; then | |
# If the target is a file and it's empty, then forcibly remove it. | |
if [ -f "${target}" ] && [ ! -s "${target}" ]; then | |
command -p rm -f -- "${target}" || return 2 | |
# If the target is a folder, and there's nothing in it, then delete it. | |
elif [ -d "${target}" ] && ! ls -A1q -- "${target}" | grep -q .; then | |
command -p rm -fd -- "${target}" || return 2 | |
fi | |
fi | |
# Try to rename '$source' to '$target', making sure not to override | |
# '$target' if it exists. | |
command -p mv "${move_mode}" -- "${source}" "${target}" || return 2 | |
# Now, after moving it, does the source still exist? ... | |
[ -e "${source}" ] | |
do | |
# ... It does still exist! | |
# If we're not renaming files, and the original file still exists, then | |
# return an error. | |
[ -z "${do_rename}" ] && return 1 | |
# If we have an index of 1, that means this is our first time in | |
# the body. Setup all the required variables. | |
if [ "$idx" = 1 ]; then | |
base="$(basename -- "$2"; echo x)"; base="${base%?x}" | |
stem="${base%%.*}" | |
ext="${base#"$stem"}" | |
dir="$(dirname -- "$2"; echo x)"; dir="${dir%?x}" | |
root="$dir/$stem" | |
fi | |
# Now set the target and let's try again | |
target="$root $((idx=idx+1))$ext" | |
done | |
return 0 | |
} | |
# If two arguments are given, and the second isn't a directory, assume the first | |
# command form | |
if [ "$#" -eq 2 ] && [ ! -d "$2" ]; then | |
try_move "$1" "$2" | |
exit | |
fi | |
# The target folder is the last argument; make sure it exists. | |
target_folder="$(eval "echo \"\${$#}\"" && echo x)" | |
target_folder="${target_folder%?x}" | |
last_status=0 | |
while [ "$#" -gt 1 ]; do # Stop at 1, because the last argument is the folder. | |
base="$(basename -- "$1"; echo x)"; base="${base%?x}" | |
try_move "$1" "$target_folder/$base" || last_status=$? | |
shift | |
done | |
# Return the most recent failure | |
exit "${last_status}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment